#-----------------------------------------------------------------------------\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
#-----------------------------------------------------------------------------\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
\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
#-----------------------------------------------------------------------------\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
#-----------------------------------------------------------------------------\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