added code to extract skill data from MySQL version of CCP database dump, as well...
[mevemon] / 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