Fix tabbing in eveapi.py
authorRyan Campbell <campbellr@gmail.com>
Thu, 30 Dec 2010 05:17:02 +0000 (22:17 -0700)
committerRyan Campbell <campbellr@gmail.com>
Thu, 30 Dec 2010 05:17:02 +0000 (22:17 -0700)
package/src/eveapi/eveapi.py

index 4633c14..ffed8bd 100644 (file)
@@ -27,7 +27,7 @@
 #-----------------------------------------------------------------------------\r
 # Version: 1.1.1 - 10 Januari 2010\r
 # - Fixed bug that causes nested tags to not appear in rows of rowsets created\r
-#      from normal Elements. This should fix the corp.MemberSecurity method,\r
+#   from normal Elements. This should fix the corp.MemberSecurity method,\r
 #   which now returns all data for members. [jehed]\r
 #\r
 # Version: 1.1.0 - 15 Januari 2009\r
@@ -89,100 +89,100 @@ proxy = None
 #-----------------------------------------------------------------------------\r
 \r
 class Error(StandardError):\r
-       def __init__(self, code, message):\r
-               self.code = code\r
-               self.args = (message.rstrip("."),)\r
+    def __init__(self, code, message):\r
+        self.code = code\r
+        self.args = (message.rstrip("."),)\r
 \r
 \r
 def EVEAPIConnection(url="api.eve-online.com", cacheHandler=None, proxy=None):\r
-       # Creates an API object through which you can call remote functions.\r
-       #\r
-       # The following optional arguments may be provided:\r
-       #\r
-       # url - root location of the EVEAPI server\r
-       #\r
-       # proxy - (host,port) specifying a proxy server through which to request\r
-       #         the API pages. Specifying a proxy overrides default proxy.\r
-       #\r
-       # cacheHandler - an object which must support the following interface:\r
-       #\r
-       #      retrieve(host, path, params)\r
-       #\r
-       #          Called when eveapi wants to fetch a document.\r
-       #          host is the address of the server, path is the full path to\r
-       #          the requested document, and params is a dict containing the\r
-       #          parameters passed to this api call (userID, apiKey etc).\r
-       #          The method MUST return one of the following types:\r
-       #\r
-       #           None - if your cache did not contain this entry\r
-       #           str/unicode - eveapi will parse this as XML\r
-       #           Element - previously stored object as provided to store()\r
-       #           file-like object - eveapi will read() XML from the stream.\r
-       #\r
-       #      store(host, path, params, doc, obj)\r
-       #\r
-       #          Called when eveapi wants you to cache this item.\r
-       #          You can use obj to get the info about the object (cachedUntil\r
-       #          and currentTime, etc) doc is the XML document the object\r
-       #          was generated from. It's generally best to cache the XML, not\r
-       #          the object, unless you pickle the object. Note that this method\r
-       #          will only be called if you returned None in the retrieve() for\r
-       #          this object.\r
-       #\r
-\r
-       if url.lower().startswith("http://"):\r
-               url = url[7:]\r
-\r
-       if "/" in url:\r
-               url, path = url.split("/", 1)\r
-       else:\r
-               path = ""\r
-\r
-       ctx = _RootContext(None, path, {}, {})\r
-       ctx._handler = cacheHandler\r
-       ctx._host = url\r
-       ctx._proxy = proxy or globals()["proxy"]\r
-       return ctx\r
+    # Creates an API object through which you can call remote functions.\r
+    #\r
+    # The following optional arguments may be provided:\r
+    #\r
+    # url - root location of the EVEAPI server\r
+    #\r
+    # proxy - (host,port) specifying a proxy server through which to request\r
+    #         the API pages. Specifying a proxy overrides default proxy.\r
+    #\r
+    # cacheHandler - an object which must support the following interface:\r
+    #\r
+    #      retrieve(host, path, params)\r
+    #\r
+    #          Called when eveapi wants to fetch a document.\r
+    #          host is the address of the server, path is the full path to\r
+    #          the requested document, and params is a dict containing the\r
+    #          parameters passed to this api call (userID, apiKey etc).\r
+    #          The method MUST return one of the following types:\r
+    #\r
+    #           None - if your cache did not contain this entry\r
+    #           str/unicode - eveapi will parse this as XML\r
+    #           Element - previously stored object as provided to store()\r
+    #           file-like object - eveapi will read() XML from the stream.\r
+    #\r
+    #      store(host, path, params, doc, obj)\r
+    #\r
+    #          Called when eveapi wants you to cache this item.\r
+    #          You can use obj to get the info about the object (cachedUntil\r
+    #          and currentTime, etc) doc is the XML document the object\r
+    #          was generated from. It's generally best to cache the XML, not\r
+    #          the object, unless you pickle the object. Note that this method\r
+    #          will only be called if you returned None in the retrieve() for\r
+    #          this object.\r
+    #\r
+\r
+    if url.lower().startswith("http://"):\r
+        url = url[7:]\r
+\r
+    if "/" in url:\r
+        url, path = url.split("/", 1)\r
+    else:\r
+        path = ""\r
+\r
+    ctx = _RootContext(None, path, {}, {})\r
+    ctx._handler = cacheHandler\r
+    ctx._host = url\r
+    ctx._proxy = proxy or globals()["proxy"]\r
+    return ctx\r
 \r
 \r
 def ParseXML(file_or_string):\r
-       try:\r
-               return _ParseXML(file_or_string, False, None)\r
-       except TypeError:\r
-               raise TypeError("XML data must be provided as string or file-like object")\r
+    try:\r
+        return _ParseXML(file_or_string, False, None)\r
+    except TypeError:\r
+        raise TypeError("XML data must be provided as string or file-like object")\r
 \r
 \r
 def _ParseXML(response, fromContext, storeFunc):\r
-       # pre/post-process XML or Element data\r
+    # pre/post-process XML or Element data\r
 \r
-       if fromContext and isinstance(response, Element):\r
-               obj = response\r
-       elif type(response) in (str, unicode):\r
-               obj = _Parser().Parse(response, False)\r
-       elif hasattr(response, "read"):\r
-               obj = _Parser().Parse(response, True)\r
-       else:\r
-               raise TypeError("retrieve method must return None, string, file-like object or an Element instance")\r
+    if fromContext and isinstance(response, Element):\r
+        obj = response\r
+    elif type(response) in (str, unicode):\r
+        obj = _Parser().Parse(response, False)\r
+    elif hasattr(response, "read"):\r
+        obj = _Parser().Parse(response, True)\r
+    else:\r
+        raise TypeError("retrieve method must return None, string, file-like object or an Element instance")\r
 \r
-       error = getattr(obj, "error", False)\r
-       if error:\r
-               raise Error(error.code, error.data)\r
+    error = getattr(obj, "error", False)\r
+    if error:\r
+        raise Error(error.code, error.data)\r
 \r
-       result = getattr(obj, "result", False)\r
-       if not result:\r
-               raise RuntimeError("API object does not contain result")\r
+    result = getattr(obj, "result", False)\r
+    if not result:\r
+        raise RuntimeError("API object does not contain result")\r
 \r
-       if fromContext and storeFunc:\r
-               # call the cache handler to store this object\r
-               storeFunc(obj)\r
+    if fromContext and storeFunc:\r
+        # call the cache handler to store this object\r
+        storeFunc(obj)\r
 \r
-       # make metadata available to caller somehow\r
-       result._meta = obj\r
+    # make metadata available to caller somehow\r
+    result._meta = obj\r
 \r
-       return result\r
+    return result\r
 \r
 \r
-       \r
+    \r
 \r
 \r
 #-----------------------------------------------------------------------------\r
@@ -194,116 +194,116 @@ _unspecified = []
 \r
 class _Context(object):\r
 \r
-       def __init__(self, root, path, parentDict, newKeywords=None):\r
-               self._root = root or self\r
-               self._path = path\r
-               if newKeywords:\r
-                       if parentDict:\r
-                               self.parameters = parentDict.copy()\r
-                       else:\r
-                               self.parameters = {}\r
-                       self.parameters.update(newKeywords)\r
-               else:\r
-                       self.parameters = parentDict or {}\r
-\r
-       def context(self, *args, **kw):\r
-               if kw or args:\r
-                       path = self._path\r
-                       if args:\r
-                               path += "/" + "/".join(args)\r
-                       return self.__class__(self._root, path, self.parameters, kw)\r
-               else:\r
-                       return self\r
-\r
-       def __getattr__(self, this):\r
-               # perform arcane attribute majick trick\r
-               return _Context(self._root, self._path + "/" + this, self.parameters)\r
-\r
-       def __call__(self, **kw):\r
-               if kw:\r
-                       # specified keywords override contextual ones\r
-                       for k, v in self.parameters.iteritems():\r
-                               if k not in kw:\r
-                                       kw[k] = v\r
-               else:\r
-                       # no keywords provided, just update with contextual ones.\r
-                       kw.update(self.parameters)\r
-\r
-               # now let the root context handle it further\r
-               return self._root(self._path, **kw)\r
+    def __init__(self, root, path, parentDict, newKeywords=None):\r
+        self._root = root or self\r
+        self._path = path\r
+        if newKeywords:\r
+            if parentDict:\r
+                self.parameters = parentDict.copy()\r
+            else:\r
+                self.parameters = {}\r
+            self.parameters.update(newKeywords)\r
+        else:\r
+            self.parameters = parentDict or {}\r
+\r
+    def context(self, *args, **kw):\r
+        if kw or args:\r
+            path = self._path\r
+            if args:\r
+                path += "/" + "/".join(args)\r
+            return self.__class__(self._root, path, self.parameters, kw)\r
+        else:\r
+            return self\r
+\r
+    def __getattr__(self, this):\r
+        # perform arcane attribute majick trick\r
+        return _Context(self._root, self._path + "/" + this, self.parameters)\r
+\r
+    def __call__(self, **kw):\r
+        if kw:\r
+            # specified keywords override contextual ones\r
+            for k, v in self.parameters.iteritems():\r
+                if k not in kw:\r
+                    kw[k] = v\r
+        else:\r
+            # no keywords provided, just update with contextual ones.\r
+            kw.update(self.parameters)\r
+\r
+        # now let the root context handle it further\r
+        return self._root(self._path, **kw)\r
 \r
 \r
 class _AuthContext(_Context):\r
 \r
-       def character(self, characterID):\r
-               # returns a copy of this connection object but for every call made\r
-               # through it, it will add the folder "/char" to the url, and the\r
-               # characterID to the parameters passed.\r
-               return _Context(self._root, self._path + "/char", self.parameters, {"characterID":characterID})\r
+    def character(self, characterID):\r
+        # returns a copy of this connection object but for every call made\r
+        # through it, it will add the folder "/char" to the url, and the\r
+        # characterID to the parameters passed.\r
+        return _Context(self._root, self._path + "/char", self.parameters, {"characterID":characterID})\r
 \r
-       def corporation(self, characterID):\r
-               # same as character except for the folder "/corp"\r
-               return _Context(self._root, self._path + "/corp", self.parameters, {"characterID":characterID})\r
+    def corporation(self, characterID):\r
+        # same as character except for the folder "/corp"\r
+        return _Context(self._root, self._path + "/corp", self.parameters, {"characterID":characterID})\r
 \r
 \r
 class _RootContext(_Context):\r
 \r
-       def auth(self, userID=None, apiKey=None):\r
-               # returns a copy of this object but for every call made through it, the\r
-               # userID and apiKey will be added to the API request.\r
-               if userID and apiKey:\r
-                       return _AuthContext(self._root, self._path, self.parameters, {"userID":userID, "apiKey":apiKey})\r
-               raise ValueError("Must specify userID and apiKey")\r
-\r
-       def setcachehandler(self, handler):\r
-               self._root._handler = handler\r
-\r
-       def __call__(self, path, **kw):\r
-               # convert list type arguments to something the API likes\r
-               for k, v in kw.iteritems():\r
-                       if isinstance(v, _listtypes):\r
-                               kw[k] = ','.join(map(str, list(v)))\r
-\r
-               cache = self._root._handler\r
-\r
-               # now send the request\r
-               path += ".xml.aspx"\r
-\r
-               if cache:\r
-                       response = cache.retrieve(self._host, path, kw)\r
-               else:\r
-                       response = None\r
-\r
-               if response is None:\r
-                       if self._proxy is None:\r
-                               http = httplib.HTTPConnection(self._host)\r
-                               if kw:\r
-                                       http.request("POST", path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"})\r
-                               else:\r
-                                       http.request("GET", path)\r
-                       else:\r
-                               http = httplib.HTTPConnection(*self._proxy)\r
-                               if kw:\r
-                                       http.request("POST", 'http://'+self._host+path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"})\r
-                               else:\r
-                                       http.request("GET", 'http://'+self._host+path)\r
-\r
-                       response = http.getresponse()\r
-                       if response.status != 200:\r
-                               if response.status == httplib.NOT_FOUND:\r
-                                       raise AttributeError("'%s' not available on API server (404 Not Found)" % path)\r
-                               else:\r
-                                       raise RuntimeError("'%s' request failed (%d %s)" % (path, response.status, response.reason))\r
-\r
-                       if cache:\r
-                               store = True\r
-                               response = response.read()\r
-                       else:\r
-                               store = False\r
-               else:\r
-                       store = False\r
-\r
-               return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj)))\r
+    def auth(self, userID=None, apiKey=None):\r
+        # returns a copy of this object but for every call made through it, the\r
+        # userID and apiKey will be added to the API request.\r
+        if userID and apiKey:\r
+            return _AuthContext(self._root, self._path, self.parameters, {"userID":userID, "apiKey":apiKey})\r
+        raise ValueError("Must specify userID and apiKey")\r
+\r
+    def setcachehandler(self, handler):\r
+        self._root._handler = handler\r
+\r
+    def __call__(self, path, **kw):\r
+        # convert list type arguments to something the API likes\r
+        for k, v in kw.iteritems():\r
+            if isinstance(v, _listtypes):\r
+                kw[k] = ','.join(map(str, list(v)))\r
+\r
+        cache = self._root._handler\r
+\r
+        # now send the request\r
+        path += ".xml.aspx"\r
+\r
+        if cache:\r
+            response = cache.retrieve(self._host, path, kw)\r
+        else:\r
+            response = None\r
+\r
+        if response is None:\r
+            if self._proxy is None:\r
+                http = httplib.HTTPConnection(self._host)\r
+                if kw:\r
+                    http.request("POST", path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"})\r
+                else:\r
+                    http.request("GET", path)\r
+            else:\r
+                http = httplib.HTTPConnection(*self._proxy)\r
+                if kw:\r
+                    http.request("POST", 'http://'+self._host+path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"})\r
+                else:\r
+                    http.request("GET", 'http://'+self._host+path)\r
+\r
+            response = http.getresponse()\r
+            if response.status != 200:\r
+                if response.status == httplib.NOT_FOUND:\r
+                    raise AttributeError("'%s' not available on API server (404 Not Found)" % path)\r
+                else:\r
+                    raise RuntimeError("'%s' request failed (%d %s)" % (path, response.status, response.reason))\r
+\r
+            if cache:\r
+                store = True\r
+                response = response.read()\r
+            else:\r
+                store = False\r
+        else:\r
+            store = False\r
+\r
+        return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj)))\r
 \r
 \r
 #-----------------------------------------------------------------------------\r
@@ -311,193 +311,193 @@ class _RootContext(_Context):
 #-----------------------------------------------------------------------------\r
 \r
 def _autocast(s):\r
-       # attempts to cast an XML string to the most probable type.\r
-       try:\r
-               if s.strip("-").isdigit():\r
-                       return int(s)\r
-       except ValueError:\r
-               pass\r
-\r
-       try:\r
-               return float(s)\r
-       except ValueError:\r
-               pass\r
-\r
-       if len(s) == 19 and s[10] == ' ':\r
-               # it could be a date string\r
-               try:\r
-                       return max(0, int(timegm(strptime(s, "%Y-%m-%d %H:%M:%S"))))\r
-               except OverflowError:\r
-                       pass\r
-               except ValueError:\r
-                       pass\r
-\r
-       # couldn't cast. return string unchanged.\r
-       return s\r
+    # attempts to cast an XML string to the most probable type.\r
+    try:\r
+        if s.strip("-").isdigit():\r
+            return int(s)\r
+    except ValueError:\r
+        pass\r
+\r
+    try:\r
+        return float(s)\r
+    except ValueError:\r
+        pass\r
+\r
+    if len(s) == 19 and s[10] == ' ':\r
+        # it could be a date string\r
+        try:\r
+            return max(0, int(timegm(strptime(s, "%Y-%m-%d %H:%M:%S"))))\r
+        except OverflowError:\r
+            pass\r
+        except ValueError:\r
+            pass\r
+\r
+    # couldn't cast. return string unchanged.\r
+    return s\r
 \r
 \r
 class _Parser(object):\r
 \r
-       def Parse(self, data, isStream=False):\r
-               self.container = self.root = None\r
-               p = expat.ParserCreate()\r
-               p.StartElementHandler = self.tag_start\r
-               p.CharacterDataHandler = self.tag_cdata\r
-               p.EndElementHandler = self.tag_end\r
-               p.ordered_attributes = True\r
-               p.buffer_text = True\r
-\r
-               if isStream:\r
-                       p.ParseFile(data)\r
-               else:\r
-                       p.Parse(data, True)\r
-               return self.root\r
-               \r
-\r
-       def tag_start(self, name, attributes):\r
-               # <hack>\r
-               # If there's a colon in the tag name, cut off the name from the colon\r
-               # onward. This is a workaround to make certain bugged XML responses\r
-               # (such as eve/CharacterID.xml.aspx) work.\r
-               if ":" in name:\r
-                       name = name[:name.index(":")]\r
-               # </hack>\r
-\r
-               if name == "rowset":\r
-                       # for rowsets, use the given name\r
-                       try:\r
-                               columns = attributes[attributes.index('columns')+1].split(",")\r
-                       except ValueError:\r
-                               # rowset did not have columns tag set (this is a bug in API)\r
-                               # columns will be extracted from first row instead.\r
-                               columns = []\r
-\r
-                       try:\r
-                               priKey = attributes[attributes.index('key')+1]\r
-                               this = IndexRowset(cols=columns, key=priKey)\r
-                       except ValueError:\r
-                               this = Rowset(cols=columns)\r
-\r
-\r
-                       this._name = attributes[attributes.index('name')+1]\r
-                       this.__catch = "row" # tag to auto-add to rowset.\r
-               else:\r
-                       this = Element()\r
-                       this._name = name\r
-\r
-               this.__parent = self.container\r
-\r
-               if self.root is None:\r
-                       # We're at the root. The first tag has to be "eveapi" or we can't\r
-                       # really assume the rest of the xml is going to be what we expect.\r
-                       if name != "eveapi":\r
-                               raise RuntimeError("Invalid API response")\r
-                       self.root = this\r
-\r
-               if isinstance(self.container, Rowset) and (self.container.__catch == this._name):\r
-                       # check for missing columns attribute (see above)\r
-                       if not self.container._cols:\r
-                               self.container._cols = attributes[0::2]\r
-\r
-                       self.container.append([_autocast(attributes[i]) for i in range(1, len(attributes), 2)])\r
-                       this._isrow = True\r
-                       this._attributes = this._attributes2 = None\r
-               else:\r
-                       this._isrow = False\r
-                       this._attributes = attributes\r
-                       this._attributes2 = []\r
-       \r
-               self.container = this\r
-\r
-\r
-       def tag_cdata(self, data):\r
-               if data == "\r\n" or data.strip() != data:\r
-                       return\r
-\r
-               this = self.container\r
-               data = _autocast(data)\r
-\r
-               if this._attributes:\r
-                       # this tag has attributes, so we can't simply assign the cdata\r
-                       # as an attribute to the parent tag, as we'll lose the current\r
-                       # tag's attributes then. instead, we'll assign the data as\r
-                       # attribute of this tag.\r
-                       this.data = data\r
-               else:\r
-                       # this was a simple <tag>data</tag> without attributes.\r
-                       # we won't be doing anything with this actual tag so we can just\r
-                       # bind it to its parent (done by __tag_end)\r
-                       setattr(this.__parent, this._name, data)\r
-\r
-\r
-       def tag_end(self, name):\r
-               this = self.container\r
-               if this is self.root:\r
-                       del this._attributes\r
-                       #this.__dict__.pop("_attributes", None)\r
-                       return\r
-\r
-               # we're done with current tag, so we can pop it off. This means that\r
-               # self.container will now point to the container of element 'this'.\r
-               self.container = this.__parent\r
-               del this.__parent\r
-\r
-               attributes = this.__dict__.pop("_attributes")\r
-               attributes2 = this.__dict__.pop("_attributes2")\r
-               if attributes is None:\r
-                       # already processed this tag's closure early, in tag_start()\r
-                       return\r
-\r
-               if self.container._isrow:\r
-                       # Special case here. tags inside a row! Such tags have to be\r
-                       # added as attributes of the row.\r
-                       parent = self.container.__parent\r
-\r
-                       # get the row line for this element from its parent rowset\r
-                       _row = parent._rows[-1]\r
-\r
-                       # add this tag's value to the end of the row\r
-                       _row.append(getattr(self.container, this._name, this))\r
-\r
-                       # fix columns if neccessary.\r
-                       if len(parent._cols) < len(_row):\r
-                               parent._cols.append(this._name)\r
-               else:\r
-                       # see if there's already an attribute with this name (this shouldn't\r
-                       # really happen, but it doesn't hurt to handle this case!\r
-                       sibling = getattr(self.container, this._name, None)\r
-                       if sibling is None:\r
-                               self.container._attributes2.append(this._name)\r
-                               setattr(self.container, this._name, this)\r
-                       # Note: there aren't supposed to be any NON-rowset tags containing\r
-                       # multiples of some tag or attribute. Code below handles this case.\r
-                       elif isinstance(sibling, Rowset):\r
-                               # its doppelganger is a rowset, append this as a row to that.\r
-                               row = [_autocast(attributes[i]) for i in range(1, len(attributes), 2)]\r
-                               row.extend([getattr(this, col) for col in attributes2])\r
-                               sibling.append(row)\r
-                       elif isinstance(sibling, Element):\r
-                               # parent attribute is an element. This means we're dealing\r
-                               # with multiple of the same sub-tag. Change the attribute\r
-                               # into a Rowset, adding the sibling element and this one.\r
-                               rs = Rowset()\r
-                               rs.__catch = rs._name = this._name\r
-                               row = [_autocast(attributes[i]) for i in range(1, len(attributes), 2)]+[getattr(this, col) for col in attributes2]\r
-                               rs.append(row)\r
-                               row = [getattr(sibling, attributes[i]) for i in range(0, len(attributes), 2)]+[getattr(sibling, col) for col in attributes2]\r
-                               rs.append(row)\r
-                               rs._cols = [attributes[i] for i in range(0, len(attributes), 2)]+[col for col in attributes2]\r
-                               setattr(self.container, this._name, rs)\r
-                       else:\r
-                               # something else must have set this attribute already.\r
-                               # (typically the <tag>data</tag> case in tag_data())\r
-                               pass\r
-\r
-               # Now fix up the attributes and be done with it.\r
-               for i in range(1, len(attributes), 2):\r
-                       this.__dict__[attributes[i-1]] = _autocast(attributes[i])\r
-\r
-               return\r
+    def Parse(self, data, isStream=False):\r
+        self.container = self.root = None\r
+        p = expat.ParserCreate()\r
+        p.StartElementHandler = self.tag_start\r
+        p.CharacterDataHandler = self.tag_cdata\r
+        p.EndElementHandler = self.tag_end\r
+        p.ordered_attributes = True\r
+        p.buffer_text = True\r
+\r
+        if isStream:\r
+            p.ParseFile(data)\r
+        else:\r
+            p.Parse(data, True)\r
+        return self.root\r
+        \r
+\r
+    def tag_start(self, name, attributes):\r
+        # <hack>\r
+        # If there's a colon in the tag name, cut off the name from the colon\r
+        # onward. This is a workaround to make certain bugged XML responses\r
+        # (such as eve/CharacterID.xml.aspx) work.\r
+        if ":" in name:\r
+            name = name[:name.index(":")]\r
+        # </hack>\r
+\r
+        if name == "rowset":\r
+            # for rowsets, use the given name\r
+            try:\r
+                columns = attributes[attributes.index('columns')+1].split(",")\r
+            except ValueError:\r
+                # rowset did not have columns tag set (this is a bug in API)\r
+                # columns will be extracted from first row instead.\r
+                columns = []\r
+\r
+            try:\r
+                priKey = attributes[attributes.index('key')+1]\r
+                this = IndexRowset(cols=columns, key=priKey)\r
+            except ValueError:\r
+                this = Rowset(cols=columns)\r
+\r
+\r
+            this._name = attributes[attributes.index('name')+1]\r
+            this.__catch = "row" # tag to auto-add to rowset.\r
+        else:\r
+            this = Element()\r
+            this._name = name\r
+\r
+        this.__parent = self.container\r
+\r
+        if self.root is None:\r
+            # We're at the root. The first tag has to be "eveapi" or we can't\r
+            # really assume the rest of the xml is going to be what we expect.\r
+            if name != "eveapi":\r
+                raise RuntimeError("Invalid API response")\r
+            self.root = this\r
+\r
+        if isinstance(self.container, Rowset) and (self.container.__catch == this._name):\r
+            # check for missing columns attribute (see above)\r
+            if not self.container._cols:\r
+                self.container._cols = attributes[0::2]\r
+\r
+            self.container.append([_autocast(attributes[i]) for i in range(1, len(attributes), 2)])\r
+            this._isrow = True\r
+            this._attributes = this._attributes2 = None\r
+        else:\r
+            this._isrow = False\r
+            this._attributes = attributes\r
+            this._attributes2 = []\r
+    \r
+        self.container = this\r
+\r
+\r
+    def tag_cdata(self, data):\r
+        if data == "\r\n" or data.strip() != data:\r
+            return\r
+\r
+        this = self.container\r
+        data = _autocast(data)\r
+\r
+        if this._attributes:\r
+            # this tag has attributes, so we can't simply assign the cdata\r
+            # as an attribute to the parent tag, as we'll lose the current\r
+            # tag's attributes then. instead, we'll assign the data as\r
+            # attribute of this tag.\r
+            this.data = data\r
+        else:\r
+            # this was a simple <tag>data</tag> without attributes.\r
+            # we won't be doing anything with this actual tag so we can just\r
+            # bind it to its parent (done by __tag_end)\r
+            setattr(this.__parent, this._name, data)\r
+\r
+\r
+    def tag_end(self, name):\r
+        this = self.container\r
+        if this is self.root:\r
+            del this._attributes\r
+            #this.__dict__.pop("_attributes", None)\r
+            return\r
+\r
+        # we're done with current tag, so we can pop it off. This means that\r
+        # self.container will now point to the container of element 'this'.\r
+        self.container = this.__parent\r
+        del this.__parent\r
+\r
+        attributes = this.__dict__.pop("_attributes")\r
+        attributes2 = this.__dict__.pop("_attributes2")\r
+        if attributes is None:\r
+            # already processed this tag's closure early, in tag_start()\r
+            return\r
+\r
+        if self.container._isrow:\r
+            # Special case here. tags inside a row! Such tags have to be\r
+            # added as attributes of the row.\r
+            parent = self.container.__parent\r
+\r
+            # get the row line for this element from its parent rowset\r
+            _row = parent._rows[-1]\r
+\r
+            # add this tag's value to the end of the row\r
+            _row.append(getattr(self.container, this._name, this))\r
+\r
+            # fix columns if neccessary.\r
+            if len(parent._cols) < len(_row):\r
+                parent._cols.append(this._name)\r
+        else:\r
+            # see if there's already an attribute with this name (this shouldn't\r
+            # really happen, but it doesn't hurt to handle this case!\r
+            sibling = getattr(self.container, this._name, None)\r
+            if sibling is None:\r
+                self.container._attributes2.append(this._name)\r
+                setattr(self.container, this._name, this)\r
+            # Note: there aren't supposed to be any NON-rowset tags containing\r
+            # multiples of some tag or attribute. Code below handles this case.\r
+            elif isinstance(sibling, Rowset):\r
+                # its doppelganger is a rowset, append this as a row to that.\r
+                row = [_autocast(attributes[i]) for i in range(1, len(attributes), 2)]\r
+                row.extend([getattr(this, col) for col in attributes2])\r
+                sibling.append(row)\r
+            elif isinstance(sibling, Element):\r
+                # parent attribute is an element. This means we're dealing\r
+                # with multiple of the same sub-tag. Change the attribute\r
+                # into a Rowset, adding the sibling element and this one.\r
+                rs = Rowset()\r
+                rs.__catch = rs._name = this._name\r
+                row = [_autocast(attributes[i]) for i in range(1, len(attributes), 2)]+[getattr(this, col) for col in attributes2]\r
+                rs.append(row)\r
+                row = [getattr(sibling, attributes[i]) for i in range(0, len(attributes), 2)]+[getattr(sibling, col) for col in attributes2]\r
+                rs.append(row)\r
+                rs._cols = [attributes[i] for i in range(0, len(attributes), 2)]+[col for col in attributes2]\r
+                setattr(self.container, this._name, rs)\r
+            else:\r
+                # something else must have set this attribute already.\r
+                # (typically the <tag>data</tag> case in tag_data())\r
+                pass\r
+\r
+        # Now fix up the attributes and be done with it.\r
+        for i in range(1, len(attributes), 2):\r
+            this.__dict__[attributes[i-1]] = _autocast(attributes[i])\r
+\r
+        return\r
 \r
 \r
 \r
@@ -513,275 +513,275 @@ class _Parser(object):
 #-----------------------------------------------------------------------------\r
 \r
 class Element(object):\r
-       # Element is a namespace for attributes and nested tags\r
-       def __str__(self):\r
-               return "<Element '%s'>" % self._name\r
+    # Element is a namespace for attributes and nested tags\r
+    def __str__(self):\r
+        return "<Element '%s'>" % self._name\r
 \r
 \r
 class Row(object):\r
-       # A Row is a single database record associated with a Rowset.\r
-       # The fields in the record are accessed as attributes by their respective\r
-       # column name.\r
-       #\r
-       # To conserve resources, Row objects are only created on-demand. This is\r
-       # typically done by Rowsets (e.g. when iterating over the rowset).\r
-       \r
-       def __init__(self, cols=None, row=None):\r
-               self._cols = cols or []\r
-               self._row = row or []\r
+    # A Row is a single database record associated with a Rowset.\r
+    # The fields in the record are accessed as attributes by their respective\r
+    # column name.\r
+    #\r
+    # To conserve resources, Row objects are only created on-demand. This is\r
+    # typically done by Rowsets (e.g. when iterating over the rowset).\r
+    \r
+    def __init__(self, cols=None, row=None):\r
+        self._cols = cols or []\r
+        self._row = row or []\r
 \r
-       def __nonzero__(self):\r
-               return True\r
+    def __nonzero__(self):\r
+        return True\r
 \r
-       def __ne__(self, other):\r
-               return self.__cmp__(other)\r
+    def __ne__(self, other):\r
+        return self.__cmp__(other)\r
 \r
-       def __eq__(self, other):\r
-               return self.__cmp__(other) == 0\r
+    def __eq__(self, other):\r
+        return self.__cmp__(other) == 0\r
 \r
-       def __cmp__(self, other):\r
-               if type(other) != type(self):\r
-                       raise TypeError("Incompatible comparison type")\r
-               return cmp(self._cols, other._cols) or cmp(self._row, other._row)\r
+    def __cmp__(self, other):\r
+        if type(other) != type(self):\r
+            raise TypeError("Incompatible comparison type")\r
+        return cmp(self._cols, other._cols) or cmp(self._row, other._row)\r
 \r
-       def __getattr__(self, this):\r
-               try:\r
-                       return self._row[self._cols.index(this)]\r
-               except:\r
-                       raise AttributeError, this\r
+    def __getattr__(self, this):\r
+        try:\r
+            return self._row[self._cols.index(this)]\r
+        except:\r
+            raise AttributeError, this\r
 \r
-       def __getitem__(self, this):\r
-               return self._row[self._cols.index(this)]\r
+    def __getitem__(self, this):\r
+        return self._row[self._cols.index(this)]\r
 \r
-       def __str__(self):\r
-               return "Row(" + ','.join(map(lambda k, v: "%s:%s" % (str(k), str(v)), self._cols, self._row)) + ")"\r
+    def __str__(self):\r
+        return "Row(" + ','.join(map(lambda k, v: "%s:%s" % (str(k), str(v)), self._cols, self._row)) + ")"\r
 \r
 \r
 class Rowset(object):\r
-       # Rowsets are collections of Row objects.\r
-       #\r
-       # Rowsets support most of the list interface:\r
-       #   iteration, indexing and slicing\r
-       #\r
-       # As well as the following methods: \r
-       #\r
-       #   IndexedBy(column)\r
-       #     Returns an IndexRowset keyed on given column. Requires the column to\r
-       #     be usable as primary key.\r
-       #\r
-       #   GroupedBy(column)\r
-       #     Returns a FilterRowset keyed on given column. FilterRowset objects\r
-       #     can be accessed like dicts. See FilterRowset class below.\r
-       #\r
-       #   SortBy(column, reverse=True)\r
-       #     Sorts rowset in-place on given column. for a descending sort,\r
-       #     specify reversed=True.\r
-       #\r
-       #   SortedBy(column, reverse=True)\r
-       #     Same as SortBy, except this retuens a new rowset object instead of\r
-       #     sorting in-place.\r
-       #\r
-       #   Select(columns, row=False)\r
-       #     Yields a column values tuple (value, ...) for each row in the rowset.\r
-       #     If only one column is requested, then just the column value is\r
-       #     provided instead of the values tuple.\r
-       #     When row=True, each result will be decorated with the entire row.\r
-       #\r
-\r
-       def IndexedBy(self, column):\r
-               return IndexRowset(self._cols, self._rows, column)\r
-\r
-       def GroupedBy(self, column):\r
-               return FilterRowset(self._cols, self._rows, column)\r
-\r
-       def SortBy(self, column, reverse=False):\r
-               ix = self._cols.index(column)\r
-               self.sort(key=lambda e: e[ix], reverse=reverse)\r
-\r
-       def SortedBy(self, column, reverse=False):\r
-               rs = self[:]\r
-               rs.SortBy(column, reverse)\r
-               return rs\r
-\r
-       def Select(self, *columns, **options):\r
-               if len(columns) == 1:\r
-                       i = self._cols.index(columns[0])\r
-                       if options.get("row", False):\r
-                               for line in self._rows:\r
-                                       yield (line, line[i])\r
-                       else:\r
-                               for line in self._rows:\r
-                                       yield line[i]\r
-               else:\r
-                       i = map(self._cols.index, columns)\r
-                       if options.get("row", False):\r
-                               for line in self._rows:\r
-                                       yield line, [line[x] for x in i]\r
-                       else:\r
-                               for line in self._rows:\r
-                                       yield [line[x] for x in i]\r
-\r
-\r
-       # -------------\r
-\r
-       def __init__(self, cols=None, rows=None):\r
-               self._cols = cols or []\r
-               self._rows = rows or []\r
-\r
-       def append(self, row):\r
-               if isinstance(row, list):\r
-                       self._rows.append(row)\r
-               elif isinstance(row, Row) and len(row._cols) == len(self._cols):\r
-                       self._rows.append(row._row)\r
-               else:\r
-                       raise TypeError("incompatible row type")\r
-\r
-       def __add__(self, other):\r
-               if isinstance(other, Rowset):\r
-                       if len(other._cols) == len(self._cols):\r
-                               self._rows += other._rows\r
-               raise TypeError("rowset instance expected")\r
-\r
-       def __nonzero__(self):\r
-               return not not self._rows\r
-\r
-       def __len__(self):\r
-               return len(self._rows)\r
-\r
-       def copy(self):\r
-               return self[:]\r
-\r
-       def __getitem__(self, ix):\r
-               if type(ix) is slice:\r
-                       return Rowset(self._cols, self._rows[ix])\r
-               return Row(self._cols, self._rows[ix])\r
-\r
-       def sort(self, *args, **kw):\r
-               self._rows.sort(*args, **kw)\r
-\r
-       def __str__(self):\r
-               return ("Rowset(columns=[%s], rows=%d)" % (','.join(self._cols), len(self)))\r
-\r
-       def __getstate__(self):\r
-               return (self._cols, self._rows)\r
-\r
-       def __setstate__(self, state):\r
-               self._cols, self._rows = state\r
+    # Rowsets are collections of Row objects.\r
+    #\r
+    # Rowsets support most of the list interface:\r
+    #   iteration, indexing and slicing\r
+    #\r
+    # As well as the following methods: \r
+    #\r
+    #   IndexedBy(column)\r
+    #     Returns an IndexRowset keyed on given column. Requires the column to\r
+    #     be usable as primary key.\r
+    #\r
+    #   GroupedBy(column)\r
+    #     Returns a FilterRowset keyed on given column. FilterRowset objects\r
+    #     can be accessed like dicts. See FilterRowset class below.\r
+    #\r
+    #   SortBy(column, reverse=True)\r
+    #     Sorts rowset in-place on given column. for a descending sort,\r
+    #     specify reversed=True.\r
+    #\r
+    #   SortedBy(column, reverse=True)\r
+    #     Same as SortBy, except this retuens a new rowset object instead of\r
+    #     sorting in-place.\r
+    #\r
+    #   Select(columns, row=False)\r
+    #     Yields a column values tuple (value, ...) for each row in the rowset.\r
+    #     If only one column is requested, then just the column value is\r
+    #     provided instead of the values tuple.\r
+    #     When row=True, each result will be decorated with the entire row.\r
+    #\r
+\r
+    def IndexedBy(self, column):\r
+        return IndexRowset(self._cols, self._rows, column)\r
+\r
+    def GroupedBy(self, column):\r
+        return FilterRowset(self._cols, self._rows, column)\r
+\r
+    def SortBy(self, column, reverse=False):\r
+        ix = self._cols.index(column)\r
+        self.sort(key=lambda e: e[ix], reverse=reverse)\r
+\r
+    def SortedBy(self, column, reverse=False):\r
+        rs = self[:]\r
+        rs.SortBy(column, reverse)\r
+        return rs\r
+\r
+    def Select(self, *columns, **options):\r
+        if len(columns) == 1:\r
+            i = self._cols.index(columns[0])\r
+            if options.get("row", False):\r
+                for line in self._rows:\r
+                    yield (line, line[i])\r
+            else:\r
+                for line in self._rows:\r
+                    yield line[i]\r
+        else:\r
+            i = map(self._cols.index, columns)\r
+            if options.get("row", False):\r
+                for line in self._rows:\r
+                    yield line, [line[x] for x in i]\r
+            else:\r
+                for line in self._rows:\r
+                    yield [line[x] for x in i]\r
+\r
+\r
+    # -------------\r
+\r
+    def __init__(self, cols=None, rows=None):\r
+        self._cols = cols or []\r
+        self._rows = rows or []\r
+\r
+    def append(self, row):\r
+        if isinstance(row, list):\r
+            self._rows.append(row)\r
+        elif isinstance(row, Row) and len(row._cols) == len(self._cols):\r
+            self._rows.append(row._row)\r
+        else:\r
+            raise TypeError("incompatible row type")\r
+\r
+    def __add__(self, other):\r
+        if isinstance(other, Rowset):\r
+            if len(other._cols) == len(self._cols):\r
+                self._rows += other._rows\r
+        raise TypeError("rowset instance expected")\r
+\r
+    def __nonzero__(self):\r
+        return not not self._rows\r
+\r
+    def __len__(self):\r
+        return len(self._rows)\r
+\r
+    def copy(self):\r
+        return self[:]\r
+\r
+    def __getitem__(self, ix):\r
+        if type(ix) is slice:\r
+            return Rowset(self._cols, self._rows[ix])\r
+        return Row(self._cols, self._rows[ix])\r
+\r
+    def sort(self, *args, **kw):\r
+        self._rows.sort(*args, **kw)\r
+\r
+    def __str__(self):\r
+        return ("Rowset(columns=[%s], rows=%d)" % (','.join(self._cols), len(self)))\r
+\r
+    def __getstate__(self):\r
+        return (self._cols, self._rows)\r
+\r
+    def __setstate__(self, state):\r
+        self._cols, self._rows = state\r
 \r
 \r
 \r
 class IndexRowset(Rowset):\r
-       # An IndexRowset is a Rowset that keeps an index on a column.\r
-       #\r
-       # The interface is the same as Rowset, but provides an additional method:\r
-       #\r
-       #   Get(key [, default])\r
-       #     Returns the Row mapped to provided key in the index. If there is no\r
-       #     such key in the index, KeyError is raised unless a default value was\r
-       #     specified.\r
-       #\r
-\r
-       def Get(self, key, *default):\r
-               row = self._items.get(key, None)\r
-               if row is None:\r
-                       if default:\r
-                               return default[0]\r
-                       raise KeyError, key\r
-               return Row(self._cols, row)\r
-\r
-       # -------------\r
-\r
-       def __init__(self, cols=None, rows=None, key=None):\r
-               try:\r
-                       self._ki = ki = cols.index(key)\r
-               except IndexError:\r
-                       raise ValueError("Rowset has no column %s" % key)\r
-\r
-               Rowset.__init__(self, cols, rows)\r
-               self._key = key\r
-               self._items = dict((row[ki], row) for row in self._rows)\r
-\r
-       def __getitem__(self, ix):\r
-               if type(ix) is slice:\r
-                       return IndexRowset(self._cols, self._rows[ix], self._key)\r
-               return Rowset.__getitem__(self, ix)\r
-\r
-       def append(self, row):\r
-               Rowset.append(self, row)\r
-               self._items[row[self._ki]] = row\r
-\r
-       def __getstate__(self):\r
-               return (Rowset.__getstate__(self), self._items, self._ki)\r
-\r
-       def __setstate__(self, state):\r
-               state, self._items, self._ki = state\r
-               Rowset.__setstate__(self, state)\r
+    # An IndexRowset is a Rowset that keeps an index on a column.\r
+    #\r
+    # The interface is the same as Rowset, but provides an additional method:\r
+    #\r
+    #   Get(key [, default])\r
+    #     Returns the Row mapped to provided key in the index. If there is no\r
+    #     such key in the index, KeyError is raised unless a default value was\r
+    #     specified.\r
+    #\r
+\r
+    def Get(self, key, *default):\r
+        row = self._items.get(key, None)\r
+        if row is None:\r
+            if default:\r
+                return default[0]\r
+            raise KeyError, key\r
+        return Row(self._cols, row)\r
+\r
+    # -------------\r
+\r
+    def __init__(self, cols=None, rows=None, key=None):\r
+        try:\r
+            self._ki = ki = cols.index(key)\r
+        except IndexError:\r
+            raise ValueError("Rowset has no column %s" % key)\r
+\r
+        Rowset.__init__(self, cols, rows)\r
+        self._key = key\r
+        self._items = dict((row[ki], row) for row in self._rows)\r
+\r
+    def __getitem__(self, ix):\r
+        if type(ix) is slice:\r
+            return IndexRowset(self._cols, self._rows[ix], self._key)\r
+        return Rowset.__getitem__(self, ix)\r
+\r
+    def append(self, row):\r
+        Rowset.append(self, row)\r
+        self._items[row[self._ki]] = row\r
+\r
+    def __getstate__(self):\r
+        return (Rowset.__getstate__(self), self._items, self._ki)\r
+\r
+    def __setstate__(self, state):\r
+        state, self._items, self._ki = state\r
+        Rowset.__setstate__(self, state)\r
 \r
 \r
 class FilterRowset(object):\r
-       # A FilterRowset works much like an IndexRowset, with the following\r
-       # differences:\r
-       # - FilterRowsets are accessed much like dicts\r
-       # - Each key maps to a Rowset, containing only the rows where the value\r
-       #   of the column this FilterRowset was made on matches the key.\r
-\r
-       def __init__(self, cols=None, rows=None, key=None, key2=None, dict=None):\r
-               if dict is not None:\r
-                       self._items = items = dict\r
-               elif cols is not None:\r
-                       self._items = items = {}\r
-\r
-                       idfield = cols.index(key)\r
-                       if not key2:\r
-                               for row in rows:\r
-                                       id = row[idfield]\r
-                                       if id in items:\r
-                                               items[id].append(row)\r
-                                       else:\r
-                                               items[id] = [row]\r
-                       else:\r
-                               idfield2 = cols.index(key2)\r
-                               for row in rows:\r
-                                       id = row[idfield]\r
-                                       if id in items:\r
-                                               items[id][row[idfield2]] = row\r
-                                       else:\r
-                                               items[id] = {row[idfield2]:row}\r
-\r
-               self._cols = cols\r
-               self.key = key\r
-               self.key2 = key2\r
-               self._bind()\r
-\r
-       def _bind(self):\r
-               items = self._items\r
-               self.keys = items.keys\r
-               self.iterkeys = items.iterkeys\r
-               self.__contains__ = items.__contains__\r
-               self.has_key = items.has_key\r
-               self.__len__ = items.__len__\r
-               self.__iter__ = items.__iter__\r
-\r
-       def copy(self):\r
-               return FilterRowset(self._cols[:], None, self.key, self.key2, dict=copy.deepcopy(self._items))\r
-\r
-       def get(self, key, default=_unspecified):\r
-               try:\r
-                       return self[key]\r
-               except KeyError:\r
-                       if default is _unspecified:\r
-                               raise\r
-               return default\r
-\r
-       def __getitem__(self, i):\r
-               if self.key2:\r
-                       return IndexRowset(self._cols, None, self.key2, self._items.get(i, {}))\r
-               return Rowset(self._cols, self._items[i])\r
-\r
-       def __getstate__(self):\r
-               return (self._cols, self._rows, self._items, self.key, self.key2)\r
-\r
-       def __setstate__(self, state):\r
-               self._cols, self._rows, self._items, self.key, self.key2 = state\r
-               self._bind()\r
+    # A FilterRowset works much like an IndexRowset, with the following\r
+    # differences:\r
+    # - FilterRowsets are accessed much like dicts\r
+    # - Each key maps to a Rowset, containing only the rows where the value\r
+    #   of the column this FilterRowset was made on matches the key.\r
+\r
+    def __init__(self, cols=None, rows=None, key=None, key2=None, dict=None):\r
+        if dict is not None:\r
+            self._items = items = dict\r
+        elif cols is not None:\r
+            self._items = items = {}\r
+\r
+            idfield = cols.index(key)\r
+            if not key2:\r
+                for row in rows:\r
+                    id = row[idfield]\r
+                    if id in items:\r
+                        items[id].append(row)\r
+                    else:\r
+                        items[id] = [row]\r
+            else:\r
+                idfield2 = cols.index(key2)\r
+                for row in rows:\r
+                    id = row[idfield]\r
+                    if id in items:\r
+                        items[id][row[idfield2]] = row\r
+                    else:\r
+                        items[id] = {row[idfield2]:row}\r
+\r
+        self._cols = cols\r
+        self.key = key\r
+        self.key2 = key2\r
+        self._bind()\r
+\r
+    def _bind(self):\r
+        items = self._items\r
+        self.keys = items.keys\r
+        self.iterkeys = items.iterkeys\r
+        self.__contains__ = items.__contains__\r
+        self.has_key = items.has_key\r
+        self.__len__ = items.__len__\r
+        self.__iter__ = items.__iter__\r
+\r
+    def copy(self):\r
+        return FilterRowset(self._cols[:], None, self.key, self.key2, dict=copy.deepcopy(self._items))\r
+\r
+    def get(self, key, default=_unspecified):\r
+        try:\r
+            return self[key]\r
+        except KeyError:\r
+            if default is _unspecified:\r
+                raise\r
+        return default\r
+\r
+    def __getitem__(self, i):\r
+        if self.key2:\r
+            return IndexRowset(self._cols, None, self.key2, self._items.get(i, {}))\r
+        return Rowset(self._cols, self._items[i])\r
+\r
+    def __getstate__(self):\r
+        return (self._cols, self._rows, self._items, self.key, self.key2)\r
+\r
+    def __setstate__(self, state):\r
+        self._cols, self._rows, self._items, self.key, self.key2 = state\r
+        self._bind()\r
 \r