ffed8bd39758827108f5e72360110a31ea520eaf
[mevemon] / package / src / eveapi / eveapi.py
1 #-----------------------------------------------------------------------------\r
2 # eveapi - EVE Online API access\r
3 #\r
4 # Copyright (c)2007 Jamie "Entity" van den Berge <entity@vapor.com>\r
5\r
6 # Permission is hereby granted, free of charge, to any person\r
7 # obtaining a copy of this software and associated documentation\r
8 # files (the "Software"), to deal in the Software without\r
9 # restriction, including without limitation the rights to use,\r
10 # copy, modify, merge, publish, distribute, sublicense, and/or sell\r
11 # copies of the Software, and to permit persons to whom the\r
12 # Software is furnished to do so, subject to the following\r
13 # conditions:\r
14\r
15 # The above copyright notice and this permission notice shall be\r
16 # included in all copies or substantial portions of the Software.\r
17 #\r
18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
19 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\r
20 # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\r
21 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\r
22 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r
23 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r
24 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\r
25 # OTHER DEALINGS IN THE SOFTWARE\r
26 #\r
27 #-----------------------------------------------------------------------------\r
28 # Version: 1.1.1 - 10 Januari 2010\r
29 # - Fixed bug that causes nested tags to not appear in rows of rowsets created\r
30 #   from normal Elements. This should fix the corp.MemberSecurity method,\r
31 #   which now returns all data for members. [jehed]\r
32 #\r
33 # Version: 1.1.0 - 15 Januari 2009\r
34 # - Added Select() method to Rowset class. Using it avoids the creation of\r
35 #   temporary row instances, speeding up iteration considerably.\r
36 # - Added ParseXML() function, which can be passed arbitrary API XML file or\r
37 #   string objects.\r
38 # - Added support for proxy servers. A proxy can be specified globally or\r
39 #   per api connection instance. [suggestion by graalman]\r
40 # - Some minor refactoring.\r
41 # - Fixed deprecation warning when using Python 2.6.\r
42 #\r
43 # Version: 1.0.7 - 14 November 2008\r
44 # - Added workaround for rowsets that are missing the (required!) columns\r
45 #   attribute. If missing, it will use the columns found in the first row.\r
46 #   Note that this is will still break when expecting columns, if the rowset\r
47 #   is empty. [Flux/Entity]\r
48 #\r
49 # Version: 1.0.6 - 18 July 2008\r
50 # - Enabled expat text buffering to avoid content breaking up. [BigWhale]\r
51 #\r
52 # Version: 1.0.5 - 03 February 2008\r
53 # - Added workaround to make broken XML responses (like the "row:name" bug in\r
54 #   eve/CharacterID) work as intended.\r
55 # - Bogus datestamps before the epoch in XML responses are now set to 0 to\r
56 #   avoid breaking certain date/time functions. [Anathema Matou]\r
57 #\r
58 # Version: 1.0.4 - 23 December 2007\r
59 # - Changed _autocast() to use timegm() instead of mktime(). [Invisible Hand]\r
60 # - Fixed missing attributes of elements inside rows. [Elandra Tenari]\r
61 #\r
62 # Version: 1.0.3 - 13 December 2007\r
63 # - Fixed keyless columns bugging out the parser (in CorporationSheet for ex.)\r
64 #\r
65 # Version: 1.0.2 - 12 December 2007\r
66 # - Fixed parser not working with indented XML.\r
67 #\r
68 # Version: 1.0.1\r
69 # - Some micro optimizations\r
70 #\r
71 # Version: 1.0\r
72 # - Initial release\r
73 #\r
74 # Requirements:\r
75 #   Python 2.4+\r
76 #\r
77 #-----------------------------------------------------------------------------\r
78 \r
79 import httplib\r
80 import urllib\r
81 import copy\r
82 \r
83 from xml.parsers import expat\r
84 from time import strptime\r
85 from calendar import timegm\r
86 \r
87 proxy = None\r
88 \r
89 #-----------------------------------------------------------------------------\r
90 \r
91 class Error(StandardError):\r
92     def __init__(self, code, message):\r
93         self.code = code\r
94         self.args = (message.rstrip("."),)\r
95 \r
96 \r
97 def EVEAPIConnection(url="api.eve-online.com", cacheHandler=None, proxy=None):\r
98     # Creates an API object through which you can call remote functions.\r
99     #\r
100     # The following optional arguments may be provided:\r
101     #\r
102     # url - root location of the EVEAPI server\r
103     #\r
104     # proxy - (host,port) specifying a proxy server through which to request\r
105     #         the API pages. Specifying a proxy overrides default proxy.\r
106     #\r
107     # cacheHandler - an object which must support the following interface:\r
108     #\r
109     #      retrieve(host, path, params)\r
110     #\r
111     #          Called when eveapi wants to fetch a document.\r
112     #          host is the address of the server, path is the full path to\r
113     #          the requested document, and params is a dict containing the\r
114     #          parameters passed to this api call (userID, apiKey etc).\r
115     #          The method MUST return one of the following types:\r
116     #\r
117     #           None - if your cache did not contain this entry\r
118     #           str/unicode - eveapi will parse this as XML\r
119     #           Element - previously stored object as provided to store()\r
120     #           file-like object - eveapi will read() XML from the stream.\r
121     #\r
122     #      store(host, path, params, doc, obj)\r
123     #\r
124     #          Called when eveapi wants you to cache this item.\r
125     #          You can use obj to get the info about the object (cachedUntil\r
126     #          and currentTime, etc) doc is the XML document the object\r
127     #          was generated from. It's generally best to cache the XML, not\r
128     #          the object, unless you pickle the object. Note that this method\r
129     #          will only be called if you returned None in the retrieve() for\r
130     #          this object.\r
131     #\r
132 \r
133     if url.lower().startswith("http://"):\r
134         url = url[7:]\r
135 \r
136     if "/" in url:\r
137         url, path = url.split("/", 1)\r
138     else:\r
139         path = ""\r
140 \r
141     ctx = _RootContext(None, path, {}, {})\r
142     ctx._handler = cacheHandler\r
143     ctx._host = url\r
144     ctx._proxy = proxy or globals()["proxy"]\r
145     return ctx\r
146 \r
147 \r
148 def ParseXML(file_or_string):\r
149     try:\r
150         return _ParseXML(file_or_string, False, None)\r
151     except TypeError:\r
152         raise TypeError("XML data must be provided as string or file-like object")\r
153 \r
154 \r
155 def _ParseXML(response, fromContext, storeFunc):\r
156     # pre/post-process XML or Element data\r
157 \r
158     if fromContext and isinstance(response, Element):\r
159         obj = response\r
160     elif type(response) in (str, unicode):\r
161         obj = _Parser().Parse(response, False)\r
162     elif hasattr(response, "read"):\r
163         obj = _Parser().Parse(response, True)\r
164     else:\r
165         raise TypeError("retrieve method must return None, string, file-like object or an Element instance")\r
166 \r
167     error = getattr(obj, "error", False)\r
168     if error:\r
169         raise Error(error.code, error.data)\r
170 \r
171     result = getattr(obj, "result", False)\r
172     if not result:\r
173         raise RuntimeError("API object does not contain result")\r
174 \r
175     if fromContext and storeFunc:\r
176         # call the cache handler to store this object\r
177         storeFunc(obj)\r
178 \r
179     # make metadata available to caller somehow\r
180     result._meta = obj\r
181 \r
182     return result\r
183 \r
184 \r
185     \r
186 \r
187 \r
188 #-----------------------------------------------------------------------------\r
189 # API Classes\r
190 #-----------------------------------------------------------------------------\r
191 \r
192 _listtypes = (list, tuple, dict)\r
193 _unspecified = []\r
194 \r
195 class _Context(object):\r
196 \r
197     def __init__(self, root, path, parentDict, newKeywords=None):\r
198         self._root = root or self\r
199         self._path = path\r
200         if newKeywords:\r
201             if parentDict:\r
202                 self.parameters = parentDict.copy()\r
203             else:\r
204                 self.parameters = {}\r
205             self.parameters.update(newKeywords)\r
206         else:\r
207             self.parameters = parentDict or {}\r
208 \r
209     def context(self, *args, **kw):\r
210         if kw or args:\r
211             path = self._path\r
212             if args:\r
213                 path += "/" + "/".join(args)\r
214             return self.__class__(self._root, path, self.parameters, kw)\r
215         else:\r
216             return self\r
217 \r
218     def __getattr__(self, this):\r
219         # perform arcane attribute majick trick\r
220         return _Context(self._root, self._path + "/" + this, self.parameters)\r
221 \r
222     def __call__(self, **kw):\r
223         if kw:\r
224             # specified keywords override contextual ones\r
225             for k, v in self.parameters.iteritems():\r
226                 if k not in kw:\r
227                     kw[k] = v\r
228         else:\r
229             # no keywords provided, just update with contextual ones.\r
230             kw.update(self.parameters)\r
231 \r
232         # now let the root context handle it further\r
233         return self._root(self._path, **kw)\r
234 \r
235 \r
236 class _AuthContext(_Context):\r
237 \r
238     def character(self, characterID):\r
239         # returns a copy of this connection object but for every call made\r
240         # through it, it will add the folder "/char" to the url, and the\r
241         # characterID to the parameters passed.\r
242         return _Context(self._root, self._path + "/char", self.parameters, {"characterID":characterID})\r
243 \r
244     def corporation(self, characterID):\r
245         # same as character except for the folder "/corp"\r
246         return _Context(self._root, self._path + "/corp", self.parameters, {"characterID":characterID})\r
247 \r
248 \r
249 class _RootContext(_Context):\r
250 \r
251     def auth(self, userID=None, apiKey=None):\r
252         # returns a copy of this object but for every call made through it, the\r
253         # userID and apiKey will be added to the API request.\r
254         if userID and apiKey:\r
255             return _AuthContext(self._root, self._path, self.parameters, {"userID":userID, "apiKey":apiKey})\r
256         raise ValueError("Must specify userID and apiKey")\r
257 \r
258     def setcachehandler(self, handler):\r
259         self._root._handler = handler\r
260 \r
261     def __call__(self, path, **kw):\r
262         # convert list type arguments to something the API likes\r
263         for k, v in kw.iteritems():\r
264             if isinstance(v, _listtypes):\r
265                 kw[k] = ','.join(map(str, list(v)))\r
266 \r
267         cache = self._root._handler\r
268 \r
269         # now send the request\r
270         path += ".xml.aspx"\r
271 \r
272         if cache:\r
273             response = cache.retrieve(self._host, path, kw)\r
274         else:\r
275             response = None\r
276 \r
277         if response is None:\r
278             if self._proxy is None:\r
279                 http = httplib.HTTPConnection(self._host)\r
280                 if kw:\r
281                     http.request("POST", path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"})\r
282                 else:\r
283                     http.request("GET", path)\r
284             else:\r
285                 http = httplib.HTTPConnection(*self._proxy)\r
286                 if kw:\r
287                     http.request("POST", 'http://'+self._host+path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"})\r
288                 else:\r
289                     http.request("GET", 'http://'+self._host+path)\r
290 \r
291             response = http.getresponse()\r
292             if response.status != 200:\r
293                 if response.status == httplib.NOT_FOUND:\r
294                     raise AttributeError("'%s' not available on API server (404 Not Found)" % path)\r
295                 else:\r
296                     raise RuntimeError("'%s' request failed (%d %s)" % (path, response.status, response.reason))\r
297 \r
298             if cache:\r
299                 store = True\r
300                 response = response.read()\r
301             else:\r
302                 store = False\r
303         else:\r
304             store = False\r
305 \r
306         return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj)))\r
307 \r
308 \r
309 #-----------------------------------------------------------------------------\r
310 # XML Parser\r
311 #-----------------------------------------------------------------------------\r
312 \r
313 def _autocast(s):\r
314     # attempts to cast an XML string to the most probable type.\r
315     try:\r
316         if s.strip("-").isdigit():\r
317             return int(s)\r
318     except ValueError:\r
319         pass\r
320 \r
321     try:\r
322         return float(s)\r
323     except ValueError:\r
324         pass\r
325 \r
326     if len(s) == 19 and s[10] == ' ':\r
327         # it could be a date string\r
328         try:\r
329             return max(0, int(timegm(strptime(s, "%Y-%m-%d %H:%M:%S"))))\r
330         except OverflowError:\r
331             pass\r
332         except ValueError:\r
333             pass\r
334 \r
335     # couldn't cast. return string unchanged.\r
336     return s\r
337 \r
338 \r
339 class _Parser(object):\r
340 \r
341     def Parse(self, data, isStream=False):\r
342         self.container = self.root = None\r
343         p = expat.ParserCreate()\r
344         p.StartElementHandler = self.tag_start\r
345         p.CharacterDataHandler = self.tag_cdata\r
346         p.EndElementHandler = self.tag_end\r
347         p.ordered_attributes = True\r
348         p.buffer_text = True\r
349 \r
350         if isStream:\r
351             p.ParseFile(data)\r
352         else:\r
353             p.Parse(data, True)\r
354         return self.root\r
355         \r
356 \r
357     def tag_start(self, name, attributes):\r
358         # <hack>\r
359         # If there's a colon in the tag name, cut off the name from the colon\r
360         # onward. This is a workaround to make certain bugged XML responses\r
361         # (such as eve/CharacterID.xml.aspx) work.\r
362         if ":" in name:\r
363             name = name[:name.index(":")]\r
364         # </hack>\r
365 \r
366         if name == "rowset":\r
367             # for rowsets, use the given name\r
368             try:\r
369                 columns = attributes[attributes.index('columns')+1].split(",")\r
370             except ValueError:\r
371                 # rowset did not have columns tag set (this is a bug in API)\r
372                 # columns will be extracted from first row instead.\r
373                 columns = []\r
374 \r
375             try:\r
376                 priKey = attributes[attributes.index('key')+1]\r
377                 this = IndexRowset(cols=columns, key=priKey)\r
378             except ValueError:\r
379                 this = Rowset(cols=columns)\r
380 \r
381 \r
382             this._name = attributes[attributes.index('name')+1]\r
383             this.__catch = "row" # tag to auto-add to rowset.\r
384         else:\r
385             this = Element()\r
386             this._name = name\r
387 \r
388         this.__parent = self.container\r
389 \r
390         if self.root is None:\r
391             # We're at the root. The first tag has to be "eveapi" or we can't\r
392             # really assume the rest of the xml is going to be what we expect.\r
393             if name != "eveapi":\r
394                 raise RuntimeError("Invalid API response")\r
395             self.root = this\r
396 \r
397         if isinstance(self.container, Rowset) and (self.container.__catch == this._name):\r
398             # check for missing columns attribute (see above)\r
399             if not self.container._cols:\r
400                 self.container._cols = attributes[0::2]\r
401 \r
402             self.container.append([_autocast(attributes[i]) for i in range(1, len(attributes), 2)])\r
403             this._isrow = True\r
404             this._attributes = this._attributes2 = None\r
405         else:\r
406             this._isrow = False\r
407             this._attributes = attributes\r
408             this._attributes2 = []\r
409     \r
410         self.container = this\r
411 \r
412 \r
413     def tag_cdata(self, data):\r
414         if data == "\r\n" or data.strip() != data:\r
415             return\r
416 \r
417         this = self.container\r
418         data = _autocast(data)\r
419 \r
420         if this._attributes:\r
421             # this tag has attributes, so we can't simply assign the cdata\r
422             # as an attribute to the parent tag, as we'll lose the current\r
423             # tag's attributes then. instead, we'll assign the data as\r
424             # attribute of this tag.\r
425             this.data = data\r
426         else:\r
427             # this was a simple <tag>data</tag> without attributes.\r
428             # we won't be doing anything with this actual tag so we can just\r
429             # bind it to its parent (done by __tag_end)\r
430             setattr(this.__parent, this._name, data)\r
431 \r
432 \r
433     def tag_end(self, name):\r
434         this = self.container\r
435         if this is self.root:\r
436             del this._attributes\r
437             #this.__dict__.pop("_attributes", None)\r
438             return\r
439 \r
440         # we're done with current tag, so we can pop it off. This means that\r
441         # self.container will now point to the container of element 'this'.\r
442         self.container = this.__parent\r
443         del this.__parent\r
444 \r
445         attributes = this.__dict__.pop("_attributes")\r
446         attributes2 = this.__dict__.pop("_attributes2")\r
447         if attributes is None:\r
448             # already processed this tag's closure early, in tag_start()\r
449             return\r
450 \r
451         if self.container._isrow:\r
452             # Special case here. tags inside a row! Such tags have to be\r
453             # added as attributes of the row.\r
454             parent = self.container.__parent\r
455 \r
456             # get the row line for this element from its parent rowset\r
457             _row = parent._rows[-1]\r
458 \r
459             # add this tag's value to the end of the row\r
460             _row.append(getattr(self.container, this._name, this))\r
461 \r
462             # fix columns if neccessary.\r
463             if len(parent._cols) < len(_row):\r
464                 parent._cols.append(this._name)\r
465         else:\r
466             # see if there's already an attribute with this name (this shouldn't\r
467             # really happen, but it doesn't hurt to handle this case!\r
468             sibling = getattr(self.container, this._name, None)\r
469             if sibling is None:\r
470                 self.container._attributes2.append(this._name)\r
471                 setattr(self.container, this._name, this)\r
472             # Note: there aren't supposed to be any NON-rowset tags containing\r
473             # multiples of some tag or attribute. Code below handles this case.\r
474             elif isinstance(sibling, Rowset):\r
475                 # its doppelganger is a rowset, append this as a row to that.\r
476                 row = [_autocast(attributes[i]) for i in range(1, len(attributes), 2)]\r
477                 row.extend([getattr(this, col) for col in attributes2])\r
478                 sibling.append(row)\r
479             elif isinstance(sibling, Element):\r
480                 # parent attribute is an element. This means we're dealing\r
481                 # with multiple of the same sub-tag. Change the attribute\r
482                 # into a Rowset, adding the sibling element and this one.\r
483                 rs = Rowset()\r
484                 rs.__catch = rs._name = this._name\r
485                 row = [_autocast(attributes[i]) for i in range(1, len(attributes), 2)]+[getattr(this, col) for col in attributes2]\r
486                 rs.append(row)\r
487                 row = [getattr(sibling, attributes[i]) for i in range(0, len(attributes), 2)]+[getattr(sibling, col) for col in attributes2]\r
488                 rs.append(row)\r
489                 rs._cols = [attributes[i] for i in range(0, len(attributes), 2)]+[col for col in attributes2]\r
490                 setattr(self.container, this._name, rs)\r
491             else:\r
492                 # something else must have set this attribute already.\r
493                 # (typically the <tag>data</tag> case in tag_data())\r
494                 pass\r
495 \r
496         # Now fix up the attributes and be done with it.\r
497         for i in range(1, len(attributes), 2):\r
498             this.__dict__[attributes[i-1]] = _autocast(attributes[i])\r
499 \r
500         return\r
501 \r
502 \r
503 \r
504 \r
505 #-----------------------------------------------------------------------------\r
506 # XML Data Containers\r
507 #-----------------------------------------------------------------------------\r
508 # The following classes are the various container types the XML data is\r
509 # unpacked into.\r
510 #\r
511 # Note that objects returned by API calls are to be treated as read-only. This\r
512 # is not enforced, but you have been warned.\r
513 #-----------------------------------------------------------------------------\r
514 \r
515 class Element(object):\r
516     # Element is a namespace for attributes and nested tags\r
517     def __str__(self):\r
518         return "<Element '%s'>" % self._name\r
519 \r
520 \r
521 class Row(object):\r
522     # A Row is a single database record associated with a Rowset.\r
523     # The fields in the record are accessed as attributes by their respective\r
524     # column name.\r
525     #\r
526     # To conserve resources, Row objects are only created on-demand. This is\r
527     # typically done by Rowsets (e.g. when iterating over the rowset).\r
528     \r
529     def __init__(self, cols=None, row=None):\r
530         self._cols = cols or []\r
531         self._row = row or []\r
532 \r
533     def __nonzero__(self):\r
534         return True\r
535 \r
536     def __ne__(self, other):\r
537         return self.__cmp__(other)\r
538 \r
539     def __eq__(self, other):\r
540         return self.__cmp__(other) == 0\r
541 \r
542     def __cmp__(self, other):\r
543         if type(other) != type(self):\r
544             raise TypeError("Incompatible comparison type")\r
545         return cmp(self._cols, other._cols) or cmp(self._row, other._row)\r
546 \r
547     def __getattr__(self, this):\r
548         try:\r
549             return self._row[self._cols.index(this)]\r
550         except:\r
551             raise AttributeError, this\r
552 \r
553     def __getitem__(self, this):\r
554         return self._row[self._cols.index(this)]\r
555 \r
556     def __str__(self):\r
557         return "Row(" + ','.join(map(lambda k, v: "%s:%s" % (str(k), str(v)), self._cols, self._row)) + ")"\r
558 \r
559 \r
560 class Rowset(object):\r
561     # Rowsets are collections of Row objects.\r
562     #\r
563     # Rowsets support most of the list interface:\r
564     #   iteration, indexing and slicing\r
565     #\r
566     # As well as the following methods: \r
567     #\r
568     #   IndexedBy(column)\r
569     #     Returns an IndexRowset keyed on given column. Requires the column to\r
570     #     be usable as primary key.\r
571     #\r
572     #   GroupedBy(column)\r
573     #     Returns a FilterRowset keyed on given column. FilterRowset objects\r
574     #     can be accessed like dicts. See FilterRowset class below.\r
575     #\r
576     #   SortBy(column, reverse=True)\r
577     #     Sorts rowset in-place on given column. for a descending sort,\r
578     #     specify reversed=True.\r
579     #\r
580     #   SortedBy(column, reverse=True)\r
581     #     Same as SortBy, except this retuens a new rowset object instead of\r
582     #     sorting in-place.\r
583     #\r
584     #   Select(columns, row=False)\r
585     #     Yields a column values tuple (value, ...) for each row in the rowset.\r
586     #     If only one column is requested, then just the column value is\r
587     #     provided instead of the values tuple.\r
588     #     When row=True, each result will be decorated with the entire row.\r
589     #\r
590 \r
591     def IndexedBy(self, column):\r
592         return IndexRowset(self._cols, self._rows, column)\r
593 \r
594     def GroupedBy(self, column):\r
595         return FilterRowset(self._cols, self._rows, column)\r
596 \r
597     def SortBy(self, column, reverse=False):\r
598         ix = self._cols.index(column)\r
599         self.sort(key=lambda e: e[ix], reverse=reverse)\r
600 \r
601     def SortedBy(self, column, reverse=False):\r
602         rs = self[:]\r
603         rs.SortBy(column, reverse)\r
604         return rs\r
605 \r
606     def Select(self, *columns, **options):\r
607         if len(columns) == 1:\r
608             i = self._cols.index(columns[0])\r
609             if options.get("row", False):\r
610                 for line in self._rows:\r
611                     yield (line, line[i])\r
612             else:\r
613                 for line in self._rows:\r
614                     yield line[i]\r
615         else:\r
616             i = map(self._cols.index, columns)\r
617             if options.get("row", False):\r
618                 for line in self._rows:\r
619                     yield line, [line[x] for x in i]\r
620             else:\r
621                 for line in self._rows:\r
622                     yield [line[x] for x in i]\r
623 \r
624 \r
625     # -------------\r
626 \r
627     def __init__(self, cols=None, rows=None):\r
628         self._cols = cols or []\r
629         self._rows = rows or []\r
630 \r
631     def append(self, row):\r
632         if isinstance(row, list):\r
633             self._rows.append(row)\r
634         elif isinstance(row, Row) and len(row._cols) == len(self._cols):\r
635             self._rows.append(row._row)\r
636         else:\r
637             raise TypeError("incompatible row type")\r
638 \r
639     def __add__(self, other):\r
640         if isinstance(other, Rowset):\r
641             if len(other._cols) == len(self._cols):\r
642                 self._rows += other._rows\r
643         raise TypeError("rowset instance expected")\r
644 \r
645     def __nonzero__(self):\r
646         return not not self._rows\r
647 \r
648     def __len__(self):\r
649         return len(self._rows)\r
650 \r
651     def copy(self):\r
652         return self[:]\r
653 \r
654     def __getitem__(self, ix):\r
655         if type(ix) is slice:\r
656             return Rowset(self._cols, self._rows[ix])\r
657         return Row(self._cols, self._rows[ix])\r
658 \r
659     def sort(self, *args, **kw):\r
660         self._rows.sort(*args, **kw)\r
661 \r
662     def __str__(self):\r
663         return ("Rowset(columns=[%s], rows=%d)" % (','.join(self._cols), len(self)))\r
664 \r
665     def __getstate__(self):\r
666         return (self._cols, self._rows)\r
667 \r
668     def __setstate__(self, state):\r
669         self._cols, self._rows = state\r
670 \r
671 \r
672 \r
673 class IndexRowset(Rowset):\r
674     # An IndexRowset is a Rowset that keeps an index on a column.\r
675     #\r
676     # The interface is the same as Rowset, but provides an additional method:\r
677     #\r
678     #   Get(key [, default])\r
679     #     Returns the Row mapped to provided key in the index. If there is no\r
680     #     such key in the index, KeyError is raised unless a default value was\r
681     #     specified.\r
682     #\r
683 \r
684     def Get(self, key, *default):\r
685         row = self._items.get(key, None)\r
686         if row is None:\r
687             if default:\r
688                 return default[0]\r
689             raise KeyError, key\r
690         return Row(self._cols, row)\r
691 \r
692     # -------------\r
693 \r
694     def __init__(self, cols=None, rows=None, key=None):\r
695         try:\r
696             self._ki = ki = cols.index(key)\r
697         except IndexError:\r
698             raise ValueError("Rowset has no column %s" % key)\r
699 \r
700         Rowset.__init__(self, cols, rows)\r
701         self._key = key\r
702         self._items = dict((row[ki], row) for row in self._rows)\r
703 \r
704     def __getitem__(self, ix):\r
705         if type(ix) is slice:\r
706             return IndexRowset(self._cols, self._rows[ix], self._key)\r
707         return Rowset.__getitem__(self, ix)\r
708 \r
709     def append(self, row):\r
710         Rowset.append(self, row)\r
711         self._items[row[self._ki]] = row\r
712 \r
713     def __getstate__(self):\r
714         return (Rowset.__getstate__(self), self._items, self._ki)\r
715 \r
716     def __setstate__(self, state):\r
717         state, self._items, self._ki = state\r
718         Rowset.__setstate__(self, state)\r
719 \r
720 \r
721 class FilterRowset(object):\r
722     # A FilterRowset works much like an IndexRowset, with the following\r
723     # differences:\r
724     # - FilterRowsets are accessed much like dicts\r
725     # - Each key maps to a Rowset, containing only the rows where the value\r
726     #   of the column this FilterRowset was made on matches the key.\r
727 \r
728     def __init__(self, cols=None, rows=None, key=None, key2=None, dict=None):\r
729         if dict is not None:\r
730             self._items = items = dict\r
731         elif cols is not None:\r
732             self._items = items = {}\r
733 \r
734             idfield = cols.index(key)\r
735             if not key2:\r
736                 for row in rows:\r
737                     id = row[idfield]\r
738                     if id in items:\r
739                         items[id].append(row)\r
740                     else:\r
741                         items[id] = [row]\r
742             else:\r
743                 idfield2 = cols.index(key2)\r
744                 for row in rows:\r
745                     id = row[idfield]\r
746                     if id in items:\r
747                         items[id][row[idfield2]] = row\r
748                     else:\r
749                         items[id] = {row[idfield2]:row}\r
750 \r
751         self._cols = cols\r
752         self.key = key\r
753         self.key2 = key2\r
754         self._bind()\r
755 \r
756     def _bind(self):\r
757         items = self._items\r
758         self.keys = items.keys\r
759         self.iterkeys = items.iterkeys\r
760         self.__contains__ = items.__contains__\r
761         self.has_key = items.has_key\r
762         self.__len__ = items.__len__\r
763         self.__iter__ = items.__iter__\r
764 \r
765     def copy(self):\r
766         return FilterRowset(self._cols[:], None, self.key, self.key2, dict=copy.deepcopy(self._items))\r
767 \r
768     def get(self, key, default=_unspecified):\r
769         try:\r
770             return self[key]\r
771         except KeyError:\r
772             if default is _unspecified:\r
773                 raise\r
774         return default\r
775 \r
776     def __getitem__(self, i):\r
777         if self.key2:\r
778             return IndexRowset(self._cols, None, self.key2, self._items.get(i, {}))\r
779         return Rowset(self._cols, self._items[i])\r
780 \r
781     def __getstate__(self):\r
782         return (self._cols, self._rows, self._items, self.key, self.key2)\r
783 \r
784     def __setstate__(self, state):\r
785         self._cols, self._rows, self._items, self.key, self.key2 = state\r
786         self._bind()\r
787 \r