mirror of
https://github.com/nikdoof/cynomap.git
synced 2025-12-13 05:02:17 +00:00
Remove static eveapi, instead use the upstream
This commit is contained in:
877
eveapi.py
877
eveapi.py
@@ -1,877 +0,0 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# eveapi - EVE Online API access
|
||||
#
|
||||
# Copyright (c)2007 Jamie "Entity" van den Berge <entity@vapor.com>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without
|
||||
# restriction, including without limitation the rights to use,
|
||||
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the
|
||||
# Software is furnished to do so, subject to the following
|
||||
# conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
# Version: 1.1.9 - 2 September 2011
|
||||
# - added workaround for row tags with attributes that were not defined
|
||||
# in their rowset (this should fix AssetList)
|
||||
#
|
||||
# Version: 1.1.8 - 1 September 2011
|
||||
# - fix for inconsistent columns attribute in rowsets.
|
||||
#
|
||||
# Version: 1.1.7 - 1 September 2011
|
||||
# - auth() method updated to work with the new authentication scheme.
|
||||
#
|
||||
# Version: 1.1.6 - 27 May 2011
|
||||
# - Now supports composite keys for IndexRowsets.
|
||||
# - Fixed calls not working if a path was specified in the root url.
|
||||
#
|
||||
# Version: 1.1.5 - 27 Januari 2011
|
||||
# - Now supports (and defaults to) HTTPS. Non-SSL proxies will still work by
|
||||
# explicitly specifying http:// in the url.
|
||||
#
|
||||
# Version: 1.1.4 - 1 December 2010
|
||||
# - Empty explicit CDATA tags are now properly handled.
|
||||
# - _autocast now receives the name of the variable it's trying to typecast,
|
||||
# enabling custom/future casting functions to make smarter decisions.
|
||||
#
|
||||
# Version: 1.1.3 - 6 November 2010
|
||||
# - Added support for anonymous CDATA inside row tags. This makes the body of
|
||||
# mails in the rows of char/MailBodies available through the .data attribute.
|
||||
#
|
||||
# Version: 1.1.2 - 2 July 2010
|
||||
# - Fixed __str__ on row objects to work properly with unicode strings.
|
||||
#
|
||||
# Version: 1.1.1 - 10 Januari 2010
|
||||
# - Fixed bug that causes nested tags to not appear in rows of rowsets created
|
||||
# from normal Elements. This should fix the corp.MemberSecurity method,
|
||||
# which now returns all data for members. [jehed]
|
||||
#
|
||||
# Version: 1.1.0 - 15 Januari 2009
|
||||
# - Added Select() method to Rowset class. Using it avoids the creation of
|
||||
# temporary row instances, speeding up iteration considerably.
|
||||
# - Added ParseXML() function, which can be passed arbitrary API XML file or
|
||||
# string objects.
|
||||
# - Added support for proxy servers. A proxy can be specified globally or
|
||||
# per api connection instance. [suggestion by graalman]
|
||||
# - Some minor refactoring.
|
||||
# - Fixed deprecation warning when using Python 2.6.
|
||||
#
|
||||
# Version: 1.0.7 - 14 November 2008
|
||||
# - Added workaround for rowsets that are missing the (required!) columns
|
||||
# attribute. If missing, it will use the columns found in the first row.
|
||||
# Note that this is will still break when expecting columns, if the rowset
|
||||
# is empty. [Flux/Entity]
|
||||
#
|
||||
# Version: 1.0.6 - 18 July 2008
|
||||
# - Enabled expat text buffering to avoid content breaking up. [BigWhale]
|
||||
#
|
||||
# Version: 1.0.5 - 03 February 2008
|
||||
# - Added workaround to make broken XML responses (like the "row:name" bug in
|
||||
# eve/CharacterID) work as intended.
|
||||
# - Bogus datestamps before the epoch in XML responses are now set to 0 to
|
||||
# avoid breaking certain date/time functions. [Anathema Matou]
|
||||
#
|
||||
# Version: 1.0.4 - 23 December 2007
|
||||
# - Changed _autocast() to use timegm() instead of mktime(). [Invisible Hand]
|
||||
# - Fixed missing attributes of elements inside rows. [Elandra Tenari]
|
||||
#
|
||||
# Version: 1.0.3 - 13 December 2007
|
||||
# - Fixed keyless columns bugging out the parser (in CorporationSheet for ex.)
|
||||
#
|
||||
# Version: 1.0.2 - 12 December 2007
|
||||
# - Fixed parser not working with indented XML.
|
||||
#
|
||||
# Version: 1.0.1
|
||||
# - Some micro optimizations
|
||||
#
|
||||
# Version: 1.0
|
||||
# - Initial release
|
||||
#
|
||||
# Requirements:
|
||||
# Python 2.4+
|
||||
#
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
import httplib
|
||||
import urlparse
|
||||
import urllib
|
||||
import copy
|
||||
|
||||
from xml.parsers import expat
|
||||
from time import strptime
|
||||
from calendar import timegm
|
||||
|
||||
proxy = None
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class Error(StandardError):
|
||||
def __init__(self, code, message):
|
||||
self.code = code
|
||||
self.args = (message.rstrip("."),)
|
||||
|
||||
|
||||
def EVEAPIConnection(url="api.eveonline.com", cacheHandler=None, proxy=None):
|
||||
# Creates an API object through which you can call remote functions.
|
||||
#
|
||||
# The following optional arguments may be provided:
|
||||
#
|
||||
# url - root location of the EVEAPI server
|
||||
#
|
||||
# proxy - (host,port) specifying a proxy server through which to request
|
||||
# the API pages. Specifying a proxy overrides default proxy.
|
||||
#
|
||||
# cacheHandler - an object which must support the following interface:
|
||||
#
|
||||
# retrieve(host, path, params)
|
||||
#
|
||||
# Called when eveapi wants to fetch a document.
|
||||
# host is the address of the server, path is the full path to
|
||||
# the requested document, and params is a dict containing the
|
||||
# parameters passed to this api call (keyID, vCode, etc).
|
||||
# The method MUST return one of the following types:
|
||||
#
|
||||
# None - if your cache did not contain this entry
|
||||
# str/unicode - eveapi will parse this as XML
|
||||
# Element - previously stored object as provided to store()
|
||||
# file-like object - eveapi will read() XML from the stream.
|
||||
#
|
||||
# store(host, path, params, doc, obj)
|
||||
#
|
||||
# Called when eveapi wants you to cache this item.
|
||||
# You can use obj to get the info about the object (cachedUntil
|
||||
# and currentTime, etc) doc is the XML document the object
|
||||
# was generated from. It's generally best to cache the XML, not
|
||||
# the object, unless you pickle the object. Note that this method
|
||||
# will only be called if you returned None in the retrieve() for
|
||||
# this object.
|
||||
#
|
||||
|
||||
if not url.startswith("http"):
|
||||
url = "https://" + url
|
||||
p = urlparse.urlparse(url, "https")
|
||||
if p.path and p.path[-1] == "/":
|
||||
p.path = p.path[:-1]
|
||||
ctx = _RootContext(None, p.path, {}, {})
|
||||
ctx._handler = cacheHandler
|
||||
ctx._scheme = p.scheme
|
||||
ctx._host = p.netloc
|
||||
ctx._proxy = proxy or globals()["proxy"]
|
||||
return ctx
|
||||
|
||||
|
||||
def ParseXML(file_or_string):
|
||||
try:
|
||||
return _ParseXML(file_or_string, False, None)
|
||||
except TypeError:
|
||||
raise TypeError("XML data must be provided as string or file-like object")
|
||||
|
||||
|
||||
def _ParseXML(response, fromContext, storeFunc):
|
||||
# pre/post-process XML or Element data
|
||||
|
||||
if fromContext and isinstance(response, Element):
|
||||
obj = response
|
||||
elif type(response) in (str, unicode):
|
||||
obj = _Parser().Parse(response, False)
|
||||
elif hasattr(response, "read"):
|
||||
obj = _Parser().Parse(response, True)
|
||||
else:
|
||||
raise TypeError("retrieve method must return None, string, file-like object or an Element instance")
|
||||
|
||||
error = getattr(obj, "error", False)
|
||||
if error:
|
||||
raise Error(error.code, error.data)
|
||||
|
||||
result = getattr(obj, "result", False)
|
||||
if not result:
|
||||
raise RuntimeError("API object does not contain result")
|
||||
|
||||
if fromContext and storeFunc:
|
||||
# call the cache handler to store this object
|
||||
storeFunc(obj)
|
||||
|
||||
# make metadata available to caller somehow
|
||||
result._meta = obj
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# API Classes
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
_listtypes = (list, tuple, dict)
|
||||
_unspecified = []
|
||||
|
||||
class _Context(object):
|
||||
|
||||
def __init__(self, root, path, parentDict, newKeywords=None):
|
||||
self._root = root or self
|
||||
self._path = path
|
||||
if newKeywords:
|
||||
if parentDict:
|
||||
self.parameters = parentDict.copy()
|
||||
else:
|
||||
self.parameters = {}
|
||||
self.parameters.update(newKeywords)
|
||||
else:
|
||||
self.parameters = parentDict or {}
|
||||
|
||||
def context(self, *args, **kw):
|
||||
if kw or args:
|
||||
path = self._path
|
||||
if args:
|
||||
path += "/" + "/".join(args)
|
||||
return self.__class__(self._root, path, self.parameters, kw)
|
||||
else:
|
||||
return self
|
||||
|
||||
def __getattr__(self, this):
|
||||
# perform arcane attribute majick trick
|
||||
return _Context(self._root, self._path + "/" + this, self.parameters)
|
||||
|
||||
def __call__(self, **kw):
|
||||
if kw:
|
||||
# specified keywords override contextual ones
|
||||
for k, v in self.parameters.iteritems():
|
||||
if k not in kw:
|
||||
kw[k] = v
|
||||
else:
|
||||
# no keywords provided, just update with contextual ones.
|
||||
kw.update(self.parameters)
|
||||
|
||||
# now let the root context handle it further
|
||||
return self._root(self._path, **kw)
|
||||
|
||||
|
||||
class _AuthContext(_Context):
|
||||
|
||||
def character(self, characterID):
|
||||
# returns a copy of this connection object but for every call made
|
||||
# through it, it will add the folder "/char" to the url, and the
|
||||
# characterID to the parameters passed.
|
||||
return _Context(self._root, self._path + "/char", self.parameters, {"characterID":characterID})
|
||||
|
||||
def corporation(self, characterID):
|
||||
# same as character except for the folder "/corp"
|
||||
return _Context(self._root, self._path + "/corp", self.parameters, {"characterID":characterID})
|
||||
|
||||
|
||||
class _RootContext(_Context):
|
||||
|
||||
def auth(self, **kw):
|
||||
if len(kw) == 2 and (("keyID" in kw and "vCode" in kw) or ("userID" in kw and "apiKey" in kw)):
|
||||
return _AuthContext(self._root, self._path, self.parameters, kw)
|
||||
raise ValueError("Must specify keyID and vCode")
|
||||
|
||||
def setcachehandler(self, handler):
|
||||
self._root._handler = handler
|
||||
|
||||
def __call__(self, path, **kw):
|
||||
# convert list type arguments to something the API likes
|
||||
for k, v in kw.iteritems():
|
||||
if isinstance(v, _listtypes):
|
||||
kw[k] = ','.join(map(str, list(v)))
|
||||
|
||||
cache = self._root._handler
|
||||
|
||||
# now send the request
|
||||
path += ".xml.aspx"
|
||||
|
||||
if cache:
|
||||
response = cache.retrieve(self._host, path, kw)
|
||||
else:
|
||||
response = None
|
||||
|
||||
if response is None:
|
||||
if self._scheme == "https":
|
||||
connectionclass = httplib.HTTPSConnection
|
||||
else:
|
||||
connectionclass = httplib.HTTPConnection
|
||||
|
||||
if self._proxy is None:
|
||||
http = connectionclass(self._host)
|
||||
if kw:
|
||||
http.request("POST", path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"})
|
||||
else:
|
||||
http.request("GET", path)
|
||||
else:
|
||||
http = connectionclass(*self._proxy)
|
||||
if kw:
|
||||
http.request("POST", 'https://'+self._host+path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"})
|
||||
else:
|
||||
http.request("GET", 'https://'+self._host+path)
|
||||
|
||||
response = http.getresponse()
|
||||
if response.status != 200:
|
||||
if response.status == httplib.NOT_FOUND:
|
||||
raise AttributeError("'%s' not available on API server (404 Not Found)" % path)
|
||||
else:
|
||||
raise RuntimeError("'%s' request failed (%d %s)" % (path, response.status, response.reason))
|
||||
|
||||
if cache:
|
||||
store = True
|
||||
response = response.read()
|
||||
else:
|
||||
store = False
|
||||
else:
|
||||
store = False
|
||||
|
||||
retrieve_fallback = cache and getattr(cache, "retrieve_fallback", False)
|
||||
if retrieve_fallback:
|
||||
# implementor is handling fallbacks...
|
||||
try:
|
||||
return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj)))
|
||||
except Error, reason:
|
||||
response = retrieve_fallback(self._host, path, kw, reason=e)
|
||||
if response is not None:
|
||||
return response
|
||||
raise
|
||||
else:
|
||||
# implementor is not handling fallbacks...
|
||||
return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj)))
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# XML Parser
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def _autocast(key, value):
|
||||
# attempts to cast an XML string to the most probable type.
|
||||
try:
|
||||
if value.strip("-").isdigit():
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if len(value) == 19 and value[10] == ' ':
|
||||
# it could be a date string
|
||||
try:
|
||||
return max(0, int(timegm(strptime(value, "%Y-%m-%d %H:%M:%S"))))
|
||||
except OverflowError:
|
||||
pass
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# couldn't cast. return string unchanged.
|
||||
return value
|
||||
|
||||
|
||||
|
||||
class _Parser(object):
|
||||
|
||||
def Parse(self, data, isStream=False):
|
||||
self.container = self.root = None
|
||||
self._cdata = False
|
||||
p = expat.ParserCreate()
|
||||
p.StartElementHandler = self.tag_start
|
||||
p.CharacterDataHandler = self.tag_cdata
|
||||
p.StartCdataSectionHandler = self.tag_cdatasection_enter
|
||||
p.EndCdataSectionHandler = self.tag_cdatasection_exit
|
||||
p.EndElementHandler = self.tag_end
|
||||
p.ordered_attributes = True
|
||||
p.buffer_text = True
|
||||
|
||||
if isStream:
|
||||
p.ParseFile(data)
|
||||
else:
|
||||
p.Parse(data, True)
|
||||
return self.root
|
||||
|
||||
|
||||
def tag_cdatasection_enter(self):
|
||||
# encountered an explicit CDATA tag.
|
||||
self._cdata = True
|
||||
|
||||
def tag_cdatasection_exit(self):
|
||||
if self._cdata:
|
||||
# explicit CDATA without actual data. expat doesn't seem
|
||||
# to trigger an event for this case, so do it manually.
|
||||
# (_cdata is set False by this call)
|
||||
self.tag_cdata("")
|
||||
else:
|
||||
self._cdata = False
|
||||
|
||||
def tag_start(self, name, attributes):
|
||||
# <hack>
|
||||
# If there's a colon in the tag name, cut off the name from the colon
|
||||
# onward. This is a workaround to make certain bugged XML responses
|
||||
# (such as eve/CharacterID.xml.aspx) work.
|
||||
if ":" in name:
|
||||
name = name[:name.index(":")]
|
||||
# </hack>
|
||||
|
||||
if name == "rowset":
|
||||
# for rowsets, use the given name
|
||||
try:
|
||||
columns = attributes[attributes.index('columns')+1].replace(" ", "").split(",")
|
||||
except ValueError:
|
||||
# rowset did not have columns tag set (this is a bug in API)
|
||||
# columns will be extracted from first row instead.
|
||||
columns = []
|
||||
|
||||
try:
|
||||
priKey = attributes[attributes.index('key')+1]
|
||||
this = IndexRowset(cols=columns, key=priKey)
|
||||
except ValueError:
|
||||
this = Rowset(cols=columns)
|
||||
|
||||
|
||||
this._name = attributes[attributes.index('name')+1]
|
||||
this.__catch = "row" # tag to auto-add to rowset.
|
||||
else:
|
||||
this = Element()
|
||||
this._name = name
|
||||
|
||||
this.__parent = self.container
|
||||
|
||||
if self.root is None:
|
||||
# We're at the root. The first tag has to be "eveapi" or we can't
|
||||
# really assume the rest of the xml is going to be what we expect.
|
||||
if name != "eveapi":
|
||||
raise RuntimeError("Invalid API response")
|
||||
self.root = this
|
||||
|
||||
if isinstance(self.container, Rowset) and (self.container.__catch == this._name):
|
||||
# <hack>
|
||||
# - check for missing columns attribute (see above)
|
||||
# - check for extra attributes that were not defined in the rowset,
|
||||
# such as rawQuantity in the assets lists.
|
||||
# In either case the tag is assumed to be correct and the rowset's
|
||||
# columns are overwritten with the tag's version.
|
||||
if not self.container._cols or (len(attributes)/2 > len(self.container._cols)):
|
||||
self.container._cols = attributes[0::2]
|
||||
# </hack>
|
||||
|
||||
self.container.append([_autocast(attributes[i], attributes[i+1]) for i in xrange(0, len(attributes), 2)])
|
||||
this._isrow = True
|
||||
this._attributes = this._attributes2 = None
|
||||
else:
|
||||
this._isrow = False
|
||||
this._attributes = attributes
|
||||
this._attributes2 = []
|
||||
|
||||
self.container = this
|
||||
|
||||
|
||||
def tag_cdata(self, data):
|
||||
if self._cdata:
|
||||
# unset cdata flag to indicate it's been handled.
|
||||
self._cdata = False
|
||||
else:
|
||||
if data in ("\r\n", "\n") or data.strip() != data:
|
||||
return
|
||||
|
||||
this = self.container
|
||||
data = _autocast(this._name, data)
|
||||
|
||||
if this._isrow:
|
||||
# sigh. anonymous data inside rows makes Entity cry.
|
||||
# for the love of Jove, CCP, learn how to use rowsets.
|
||||
parent = this.__parent
|
||||
_row = parent._rows[-1]
|
||||
_row.append(data)
|
||||
if len(parent._cols) < len(_row):
|
||||
parent._cols.append("data")
|
||||
|
||||
elif this._attributes:
|
||||
# this tag has attributes, so we can't simply assign the cdata
|
||||
# as an attribute to the parent tag, as we'll lose the current
|
||||
# tag's attributes then. instead, we'll assign the data as
|
||||
# attribute of this tag.
|
||||
this.data = data
|
||||
else:
|
||||
# this was a simple <tag>data</tag> without attributes.
|
||||
# we won't be doing anything with this actual tag so we can just
|
||||
# bind it to its parent (done by __tag_end)
|
||||
setattr(this.__parent, this._name, data)
|
||||
|
||||
def tag_end(self, name):
|
||||
this = self.container
|
||||
if this is self.root:
|
||||
del this._attributes
|
||||
#this.__dict__.pop("_attributes", None)
|
||||
return
|
||||
|
||||
# we're done with current tag, so we can pop it off. This means that
|
||||
# self.container will now point to the container of element 'this'.
|
||||
self.container = this.__parent
|
||||
del this.__parent
|
||||
|
||||
attributes = this.__dict__.pop("_attributes")
|
||||
attributes2 = this.__dict__.pop("_attributes2")
|
||||
if attributes is None:
|
||||
# already processed this tag's closure early, in tag_start()
|
||||
return
|
||||
|
||||
if self.container._isrow:
|
||||
# Special case here. tags inside a row! Such tags have to be
|
||||
# added as attributes of the row.
|
||||
parent = self.container.__parent
|
||||
|
||||
# get the row line for this element from its parent rowset
|
||||
_row = parent._rows[-1]
|
||||
|
||||
# add this tag's value to the end of the row
|
||||
_row.append(getattr(self.container, this._name, this))
|
||||
|
||||
# fix columns if neccessary.
|
||||
if len(parent._cols) < len(_row):
|
||||
parent._cols.append(this._name)
|
||||
else:
|
||||
# see if there's already an attribute with this name (this shouldn't
|
||||
# really happen, but it doesn't hurt to handle this case!
|
||||
sibling = getattr(self.container, this._name, None)
|
||||
if sibling is None:
|
||||
self.container._attributes2.append(this._name)
|
||||
setattr(self.container, this._name, this)
|
||||
# Note: there aren't supposed to be any NON-rowset tags containing
|
||||
# multiples of some tag or attribute. Code below handles this case.
|
||||
elif isinstance(sibling, Rowset):
|
||||
# its doppelganger is a rowset, append this as a row to that.
|
||||
row = [_autocast(attributes[i], attributes[i+1]) for i in xrange(0, len(attributes), 2)]
|
||||
row.extend([getattr(this, col) for col in attributes2])
|
||||
sibling.append(row)
|
||||
elif isinstance(sibling, Element):
|
||||
# parent attribute is an element. This means we're dealing
|
||||
# with multiple of the same sub-tag. Change the attribute
|
||||
# into a Rowset, adding the sibling element and this one.
|
||||
rs = Rowset()
|
||||
rs.__catch = rs._name = this._name
|
||||
row = [_autocast(attributes[i], attributes[i+1]) for i in xrange(0, len(attributes), 2)]+[getattr(this, col) for col in attributes2]
|
||||
rs.append(row)
|
||||
row = [getattr(sibling, attributes[i]) for i in xrange(0, len(attributes), 2)]+[getattr(sibling, col) for col in attributes2]
|
||||
rs.append(row)
|
||||
rs._cols = [attributes[i] for i in xrange(0, len(attributes), 2)]+[col for col in attributes2]
|
||||
setattr(self.container, this._name, rs)
|
||||
else:
|
||||
# something else must have set this attribute already.
|
||||
# (typically the <tag>data</tag> case in tag_data())
|
||||
pass
|
||||
|
||||
# Now fix up the attributes and be done with it.
|
||||
for i in xrange(0, len(attributes), 2):
|
||||
this.__dict__[attributes[i]] = _autocast(attributes[i], attributes[i+1])
|
||||
|
||||
return
|
||||
|
||||
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# XML Data Containers
|
||||
#-----------------------------------------------------------------------------
|
||||
# The following classes are the various container types the XML data is
|
||||
# unpacked into.
|
||||
#
|
||||
# Note that objects returned by API calls are to be treated as read-only. This
|
||||
# is not enforced, but you have been warned.
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class Element(object):
|
||||
# Element is a namespace for attributes and nested tags
|
||||
def __str__(self):
|
||||
return "<Element '%s'>" % self._name
|
||||
|
||||
_fmt = u"%s:%s".__mod__
|
||||
class Row(object):
|
||||
# A Row is a single database record associated with a Rowset.
|
||||
# The fields in the record are accessed as attributes by their respective
|
||||
# column name.
|
||||
#
|
||||
# To conserve resources, Row objects are only created on-demand. This is
|
||||
# typically done by Rowsets (e.g. when iterating over the rowset).
|
||||
|
||||
def __init__(self, cols=None, row=None):
|
||||
self._cols = cols or []
|
||||
self._row = row or []
|
||||
|
||||
def __nonzero__(self):
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.__cmp__(other)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__cmp__(other) == 0
|
||||
|
||||
def __cmp__(self, other):
|
||||
if type(other) != type(self):
|
||||
raise TypeError("Incompatible comparison type")
|
||||
return cmp(self._cols, other._cols) or cmp(self._row, other._row)
|
||||
|
||||
def __getattr__(self, this):
|
||||
try:
|
||||
return self._row[self._cols.index(this)]
|
||||
except:
|
||||
raise AttributeError, this
|
||||
|
||||
def __getitem__(self, this):
|
||||
return self._row[self._cols.index(this)]
|
||||
|
||||
def __str__(self):
|
||||
return "Row(" + ','.join(map(_fmt, zip(self._cols, self._row))) + ")"
|
||||
|
||||
|
||||
class Rowset(object):
|
||||
# Rowsets are collections of Row objects.
|
||||
#
|
||||
# Rowsets support most of the list interface:
|
||||
# iteration, indexing and slicing
|
||||
#
|
||||
# As well as the following methods:
|
||||
#
|
||||
# IndexedBy(column)
|
||||
# Returns an IndexRowset keyed on given column. Requires the column to
|
||||
# be usable as primary key.
|
||||
#
|
||||
# GroupedBy(column)
|
||||
# Returns a FilterRowset keyed on given column. FilterRowset objects
|
||||
# can be accessed like dicts. See FilterRowset class below.
|
||||
#
|
||||
# SortBy(column, reverse=True)
|
||||
# Sorts rowset in-place on given column. for a descending sort,
|
||||
# specify reversed=True.
|
||||
#
|
||||
# SortedBy(column, reverse=True)
|
||||
# Same as SortBy, except this returns a new rowset object instead of
|
||||
# sorting in-place.
|
||||
#
|
||||
# Select(columns, row=False)
|
||||
# Yields a column values tuple (value, ...) for each row in the rowset.
|
||||
# If only one column is requested, then just the column value is
|
||||
# provided instead of the values tuple.
|
||||
# When row=True, each result will be decorated with the entire row.
|
||||
#
|
||||
|
||||
def IndexedBy(self, column):
|
||||
return IndexRowset(self._cols, self._rows, column)
|
||||
|
||||
def GroupedBy(self, column):
|
||||
return FilterRowset(self._cols, self._rows, column)
|
||||
|
||||
def SortBy(self, column, reverse=False):
|
||||
ix = self._cols.index(column)
|
||||
self.sort(key=lambda e: e[ix], reverse=reverse)
|
||||
|
||||
def SortedBy(self, column, reverse=False):
|
||||
rs = self[:]
|
||||
rs.SortBy(column, reverse)
|
||||
return rs
|
||||
|
||||
def Select(self, *columns, **options):
|
||||
if len(columns) == 1:
|
||||
i = self._cols.index(columns[0])
|
||||
if options.get("row", False):
|
||||
for line in self._rows:
|
||||
yield (line, line[i])
|
||||
else:
|
||||
for line in self._rows:
|
||||
yield line[i]
|
||||
else:
|
||||
i = map(self._cols.index, columns)
|
||||
if options.get("row", False):
|
||||
for line in self._rows:
|
||||
yield line, [line[x] for x in i]
|
||||
else:
|
||||
for line in self._rows:
|
||||
yield [line[x] for x in i]
|
||||
|
||||
|
||||
# -------------
|
||||
|
||||
def __init__(self, cols=None, rows=None):
|
||||
self._cols = cols or []
|
||||
self._rows = rows or []
|
||||
|
||||
def append(self, row):
|
||||
if isinstance(row, list):
|
||||
self._rows.append(row)
|
||||
elif isinstance(row, Row) and len(row._cols) == len(self._cols):
|
||||
self._rows.append(row._row)
|
||||
else:
|
||||
raise TypeError("incompatible row type")
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, Rowset):
|
||||
if len(other._cols) == len(self._cols):
|
||||
self._rows += other._rows
|
||||
raise TypeError("rowset instance expected")
|
||||
|
||||
def __nonzero__(self):
|
||||
return not not self._rows
|
||||
|
||||
def __len__(self):
|
||||
return len(self._rows)
|
||||
|
||||
def copy(self):
|
||||
return self[:]
|
||||
|
||||
def __getitem__(self, ix):
|
||||
if type(ix) is slice:
|
||||
return Rowset(self._cols, self._rows[ix])
|
||||
return Row(self._cols, self._rows[ix])
|
||||
|
||||
def sort(self, *args, **kw):
|
||||
self._rows.sort(*args, **kw)
|
||||
|
||||
def __str__(self):
|
||||
return ("Rowset(columns=[%s], rows=%d)" % (','.join(self._cols), len(self)))
|
||||
|
||||
def __getstate__(self):
|
||||
return (self._cols, self._rows)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._cols, self._rows = state
|
||||
|
||||
|
||||
|
||||
class IndexRowset(Rowset):
|
||||
# An IndexRowset is a Rowset that keeps an index on a column.
|
||||
#
|
||||
# The interface is the same as Rowset, but provides an additional method:
|
||||
#
|
||||
# Get(key [, default])
|
||||
# Returns the Row mapped to provided key in the index. If there is no
|
||||
# such key in the index, KeyError is raised unless a default value was
|
||||
# specified.
|
||||
#
|
||||
|
||||
def Get(self, key, *default):
|
||||
row = self._items.get(key, None)
|
||||
if row is None:
|
||||
if default:
|
||||
return default[0]
|
||||
raise KeyError, key
|
||||
return Row(self._cols, row)
|
||||
|
||||
# -------------
|
||||
|
||||
def __init__(self, cols=None, rows=None, key=None):
|
||||
try:
|
||||
if "," in key:
|
||||
self._ki = ki = [cols.index(k) for k in key.split(",")]
|
||||
self.composite = True
|
||||
else:
|
||||
self._ki = ki = cols.index(key)
|
||||
self.composite = False
|
||||
except IndexError:
|
||||
raise ValueError("Rowset has no column %s" % key)
|
||||
|
||||
Rowset.__init__(self, cols, rows)
|
||||
self._key = key
|
||||
|
||||
if self.composite:
|
||||
self._items = dict((tuple([row[k] for k in ki]), row) for row in self._rows)
|
||||
else:
|
||||
self._items = dict((row[ki], row) for row in self._rows)
|
||||
|
||||
def __getitem__(self, ix):
|
||||
if type(ix) is slice:
|
||||
return IndexRowset(self._cols, self._rows[ix], self._key)
|
||||
return Rowset.__getitem__(self, ix)
|
||||
|
||||
def append(self, row):
|
||||
Rowset.append(self, row)
|
||||
if self.composite:
|
||||
self._items[tuple([row[k] for k in self._ki])] = row
|
||||
else:
|
||||
self._items[row[self._ki]] = row
|
||||
|
||||
def __getstate__(self):
|
||||
return (Rowset.__getstate__(self), self._items, self._ki)
|
||||
|
||||
def __setstate__(self, state):
|
||||
state, self._items, self._ki = state
|
||||
Rowset.__setstate__(self, state)
|
||||
|
||||
|
||||
class FilterRowset(object):
|
||||
# A FilterRowset works much like an IndexRowset, with the following
|
||||
# differences:
|
||||
# - FilterRowsets are accessed much like dicts
|
||||
# - Each key maps to a Rowset, containing only the rows where the value
|
||||
# of the column this FilterRowset was made on matches the key.
|
||||
|
||||
def __init__(self, cols=None, rows=None, key=None, key2=None, dict=None):
|
||||
if dict is not None:
|
||||
self._items = items = dict
|
||||
elif cols is not None:
|
||||
self._items = items = {}
|
||||
|
||||
idfield = cols.index(key)
|
||||
if not key2:
|
||||
for row in rows:
|
||||
id = row[idfield]
|
||||
if id in items:
|
||||
items[id].append(row)
|
||||
else:
|
||||
items[id] = [row]
|
||||
else:
|
||||
idfield2 = cols.index(key2)
|
||||
for row in rows:
|
||||
id = row[idfield]
|
||||
if id in items:
|
||||
items[id][row[idfield2]] = row
|
||||
else:
|
||||
items[id] = {row[idfield2]:row}
|
||||
|
||||
self._cols = cols
|
||||
self.key = key
|
||||
self.key2 = key2
|
||||
self._bind()
|
||||
|
||||
def _bind(self):
|
||||
items = self._items
|
||||
self.keys = items.keys
|
||||
self.iterkeys = items.iterkeys
|
||||
self.__contains__ = items.__contains__
|
||||
self.has_key = items.has_key
|
||||
self.__len__ = items.__len__
|
||||
self.__iter__ = items.__iter__
|
||||
|
||||
def copy(self):
|
||||
return FilterRowset(self._cols[:], None, self.key, self.key2, dict=copy.deepcopy(self._items))
|
||||
|
||||
def get(self, key, default=_unspecified):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
if default is _unspecified:
|
||||
raise
|
||||
return default
|
||||
|
||||
def __getitem__(self, i):
|
||||
if self.key2:
|
||||
return IndexRowset(self._cols, None, self.key2, self._items.get(i, {}))
|
||||
return Rowset(self._cols, self._items[i])
|
||||
|
||||
def __getstate__(self):
|
||||
return (self._cols, self._rows, self._items, self.key, self.key2)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._cols, self._rows, self._items, self.key, self.key2 = state
|
||||
self._bind()
|
||||
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
-e git+https://github.com/ntt/eveapi.git#egg=eveapi
|
||||
Reference in New Issue
Block a user