├── .gitignore ├── LICENSE ├── README ├── __init__.py ├── apitest.py ├── eveapi.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | dist 4 | MANIFEST 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | eveapi - EVE Online API access 2 | 3 | Copyright (c)2007-2013 Jamie "Entity" van den Berge 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE 25 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntt/eveapi/f0f9b3a8be7485b442ed5b53997475ae94488e66/README -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntt/eveapi/f0f9b3a8be7485b442ed5b53997475ae94488e66/__init__.py -------------------------------------------------------------------------------- /apitest.py: -------------------------------------------------------------------------------- 1 | # ============================================================================= 2 | # eveapi module demonstration script - Jamie van den Berge 3 | # ============================================================================= 4 | # 5 | # This file is in the Public Domain - Do with it as you please. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 8 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 9 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 10 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 12 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 13 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 14 | # OTHER DEALINGS IN THE SOFTWARE 15 | # 16 | # ---------------------------------------------------------------------------- 17 | # Dev docs: 18 | # https://eveonline-third-party-documentation.readthedocs.org/en/latest/reference/guidelines/ 19 | 20 | # Python 2/3 compatibility http://python-future.org/ 21 | from __future__ import print_function 22 | from __future__ import absolute_import 23 | from future import standard_library 24 | standard_library.install_aliases() 25 | from builtins import str 26 | from builtins import object 27 | 28 | import time 29 | import tempfile 30 | import pickle 31 | import zlib 32 | import os 33 | from os.path import join, exists 34 | 35 | import eveapi 36 | 37 | # Put your userID and apiKey (full access) here before running this script. 38 | YOUR_KEYID = 123456 39 | YOUR_VCODE = "nyanyanyanyanyanyanyanyanyanyanyanyanyanyanyanyanyanya:3" 40 | 41 | # Provide a good User-Agent header 42 | eveapi.set_user_agent("eveapi.py/1.3") 43 | 44 | api = eveapi.EVEAPIConnection() 45 | 46 | 47 | # ---------------------------------------------------------------------------- 48 | print() 49 | print("EXAMPLE 1: GETTING THE ALLIANCE LIST") 50 | print(" (and showing alliances with 1000 or more members)") 51 | print() 52 | 53 | # Let's get the list of alliances. 54 | # The API function we need to get the list is: 55 | # 56 | # /eve/AllianceList.xml.aspx 57 | # 58 | # There is a 1:1 correspondence between folders/files and attributes on api 59 | # objects, so to call this particular function, we simply do this: 60 | result1 = api.eve.AllianceList() 61 | 62 | # This result contains a rowset object called "alliances". Rowsets are like 63 | # database tables and you can do various useful things with them. For now 64 | # we'll just iterate over it and display all alliances with more than 1000 65 | # members: 66 | for alliance in result1.alliances: 67 | if alliance.memberCount >= 1000: 68 | print("{} <{}> has {} members".format( 69 | alliance.name, alliance.shortName, alliance.memberCount)) 70 | 71 | 72 | # ----------------------------------------------------------------------------- 73 | print() 74 | print("EXAMPLE 2: GETTING WALLET BALANCE OF ALL YOUR CHARACTERS") 75 | print() 76 | 77 | # To get any info on character/corporation related stuff, we need to acquire 78 | # an authentication context. All API requests that require authentication need 79 | # to be called through this object. While it is possible to call such API 80 | # functions directly through the api object, you would have to specify the 81 | # userID and apiKey on every call. If you are iterating over many accounts, 82 | # that may actually be the better option. However, for these examples we only 83 | # use one account, so this is more convenient. 84 | auth = api.auth(keyID=YOUR_KEYID, vCode=YOUR_VCODE) 85 | 86 | # Now let's say you want to the wallet balance of all your characters. 87 | # The API function we need to get the characters on your account is: 88 | # 89 | # /account/Characters.xml.aspx 90 | # 91 | # As in example 1, this simply means adding folder names as attributes 92 | # and calling the function named after the base page name: 93 | result2 = auth.account.Characters() 94 | 95 | # Some tracking for later examples. 96 | rich = 0 97 | rich_charID = 0 98 | 99 | # Now the best way to iterate over the characters on your account and show 100 | # the isk balance is probably this way: 101 | for character in result2.characters: 102 | wallet = auth.char.AccountBalance(characterID=character.characterID) 103 | isk = wallet.accounts[0].balance 104 | print("{} has {:,.2f} ISK.".format(character.name, isk)) 105 | 106 | if isk > rich: 107 | rich = isk 108 | rich_charID = character.characterID 109 | 110 | 111 | # ----------------------------------------------------------------------------- 112 | print() 113 | print("EXAMPLE 3: WHEN STUFF GOES WRONG") 114 | print() 115 | 116 | # Obviously you cannot assume an API call to succeed. There's a myriad of 117 | # things that can go wrong: 118 | # 119 | # - Connection error 120 | # - Server error 121 | # - Invalid parameters passed 122 | # - Hamsters died 123 | # 124 | # Therefor it is important to handle errors properly. eveapi will raise 125 | # an AttributeError if the requested function does not exist on the server 126 | # (ie. when it returns a 404), a RuntimeError on any other webserver error 127 | # (such as 500 Internal Server error). 128 | # On top of this, you can get any of the httplib (which eveapi uses) and 129 | # socket (which httplib uses) exceptions so you might want to catch those 130 | # as well. 131 | # 132 | 133 | try: 134 | # Try calling account/Characters without authentication context 135 | api.account.Characters() 136 | except eveapi.Error as e: 137 | print("Oops! eveapi returned the following error:") 138 | print("code:", e.code) 139 | print("message:", e.message) 140 | except Exception as e: 141 | print("Something went horribly wrong:", str(e)) 142 | raise 143 | 144 | 145 | # ----------------------------------------------------------------------------- 146 | print() 147 | print("EXAMPLE 4: GETTING CHARACTER SHEET INFORMATION") 148 | print() 149 | 150 | # We grab ourselves a character context object. 151 | # Note that this is a convenience function that takes care of passing the 152 | # characterID=x parameter to every API call much like auth() does (in fact 153 | # it's exactly like that, apart from the fact it also automatically adds the 154 | # "/char" folder). Again, it is possible to use the API functions directly 155 | # from the api or auth context, but then you have to provide the missing 156 | # keywords on every call (characterID in this case). 157 | # 158 | # The victim we'll use is the last character on the account we used in 159 | # example 1. 160 | me = auth.character(result2.characters[-1].characterID) 161 | 162 | # Now that we have a character context, we can display skills trained on 163 | # a character. First we have to get the skill tree. A real application 164 | # would cache this data; all objects returned by the api interface can be 165 | # pickled. 166 | skilltree = api.eve.SkillTree() 167 | 168 | # Now we have to fetch the charactersheet. 169 | # Note that the call below is identical to: 170 | # 171 | # acc.char.CharacterSheet(characterID=your_character_id) 172 | # 173 | # But, as explained above, the context ("me") we created automatically takes 174 | # care of adding the characterID parameter and /char folder attribute. 175 | sheet = me.CharacterSheet() 176 | 177 | # This list should look familiar. They're the skillpoints at each level for 178 | # a rank 1 skill. We could use the formula, but this is much simpler :) 179 | sp = [0, 250, 1414, 8000, 45255, 256000] 180 | 181 | total_sp = 0 182 | total_skills = 0 183 | 184 | # Now the fun bit starts. We walk the skill tree, and for every group in the 185 | # tree... 186 | for g in skilltree.skillGroups: 187 | 188 | skills_trained_in_this_group = False 189 | 190 | # ... iterate over the skills in this group... 191 | for skill in g.skills: 192 | 193 | # see if we trained this skill by checking the character sheet object 194 | trained = sheet.skills.Get(skill.typeID, False) 195 | if trained: 196 | # yep, we trained this skill. 197 | 198 | # print the group name if we haven't done so already 199 | if not skills_trained_in_this_group: 200 | print(g.groupName) 201 | skills_trained_in_this_group = True 202 | 203 | # and display some info about the skill! 204 | print("- {} Rank({}) - SP: {}/{} - Level: {}".format( 205 | skill.typeName, skill.rank, trained.skillpoints, 206 | (skill.rank * sp[trained.level]), trained.level) 207 | ) 208 | total_skills += 1 209 | total_sp += trained.skillpoints 210 | 211 | 212 | # And to top it off, display totals. 213 | print("You currently have {} skills and {:,d} skill points".format( 214 | total_skills, total_sp) 215 | ) 216 | 217 | 218 | # ----------------------------------------------------------------------------- 219 | print() 220 | print("EXAMPLE 5: USING ROWSETS") 221 | print() 222 | 223 | # For this one we will use the result1 that contains the alliance list from 224 | # the first example. 225 | rowset = result1.alliances 226 | 227 | # Now, what if we want to sort the alliances by ticker name. We could unpack 228 | # all alliances into a list and then use python's sort(key=...) on that list, 229 | # but that's not efficient. The rowset objects support sorting on columns 230 | # directly: 231 | rowset.SortBy("shortName") 232 | 233 | # Note the use of Select() here. The Select method speeds up iterating over 234 | # large rowsets considerably as no temporary row instances are created. 235 | for ticker in rowset.Select("shortName"): 236 | print(ticker, end=' ') 237 | print() 238 | 239 | # The sort above modified the result inplace. There is another method, called 240 | # SortedBy, which returns a new rowset. 241 | print(rowset.SortedBy("allianceID", reverse=True, dtype=int)) 242 | print() 243 | 244 | # Another useful method of rowsets is IndexBy, which enables you to do direct 245 | # key lookups on columns. We already used this feature in example 3. Indeed 246 | # most rowsets returned are IndexRowsets already if the data has a primary 247 | # key attribute defined in the tag in the XML data. 248 | # 249 | # IndexRowsets are efficient, they reference the data from the rowset they 250 | # were created from, and create an index mapping on top of it. 251 | # 252 | # Anyway, to create an index: 253 | alliances_by_ticker = rowset.IndexedBy("shortName") 254 | 255 | # Now use the Get() method to get a row directly. 256 | # Assumes ISD alliance exists. If it doesn't, we probably have bigger 257 | # problems than the unhandled exception here -_- 258 | try: 259 | print(alliances_by_ticker.Get("ISD")) 260 | except: 261 | print("Blimey! CCP let the ISD alliance expire -AGAIN-. How inconvenient!") 262 | 263 | # You may specify a default to return in case the row wasn't found: 264 | print(alliances_by_ticker.Get("123456", 42)) 265 | 266 | # If no default was specified and you try to look up a key that does not 267 | # exist, an appropriate exception will be raised: 268 | try: 269 | print(alliances_by_ticker.Get("123456")) 270 | except KeyError: 271 | print("This concludes example 5") 272 | 273 | 274 | # ----------------------------------------------------------------------------- 275 | print() 276 | print("EXAMPLE 6: CACHING DATA") 277 | print() 278 | 279 | # For some calls you will want caching. To facilitate this, a customized 280 | # cache handler can be attached. Below is an example of a simple cache 281 | # handler. 282 | 283 | 284 | class MyCacheHandler(object): 285 | # Note: this is an example handler to demonstrate how to use them. 286 | # a -real- handler should probably be thread-safe and handle errors 287 | # properly (and perhaps use a better hashing scheme). 288 | 289 | def __init__(self, debug=False): 290 | self.debug = debug 291 | self.count = 0 292 | self.cache = {} 293 | self.tempdir = join(tempfile.gettempdir(), "eveapi") 294 | if not exists(self.tempdir): 295 | os.makedirs(self.tempdir) 296 | 297 | def log(self, what): 298 | if self.debug: 299 | print("[%d] %s" % (self.count, what)) 300 | 301 | def retrieve(self, host, path, params): 302 | # eveapi asks if we have this request cached 303 | key = hash((host, path, frozenset(list(params.items())))) 304 | 305 | self.count += 1 # for logging 306 | 307 | # see if we have the requested page cached... 308 | cached = self.cache.get(key, None) 309 | if cached: 310 | cacheFile = None 311 | # print "'%s': retrieving from memory" % path 312 | else: 313 | # it wasn't cached in memory, but it might be on disk. 314 | cacheFile = join(self.tempdir, str(key) + ".cache") 315 | if exists(cacheFile): 316 | self.log("%s: retrieving from disk" % path) 317 | with open(cacheFile, "rb") as opencache: 318 | cached = self.cache[key] = pickle.loads(zlib.decompress( 319 | opencache.read()) 320 | ) 321 | 322 | if cached: 323 | # check if the cached doc is fresh enough 324 | if time.time() < cached[0]: 325 | self.log("%s: returning cached document" % path) 326 | return cached[1] # return the cached XML doc 327 | 328 | # it's stale. purge it. 329 | self.log("%s: cache expired, purging!" % path) 330 | del self.cache[key] 331 | if cacheFile: 332 | os.remove(cacheFile) 333 | 334 | self.log("%s: not cached, fetching from server..." % path) 335 | # we didn't get a cache hit so return None to indicate that the data 336 | # should be requested from the server. 337 | return None 338 | 339 | def store(self, host, path, params, doc, obj): 340 | # eveapi is asking us to cache an item 341 | key = hash((host, path, frozenset(list(params.items())))) 342 | 343 | cachedFor = obj.cachedUntil - obj.currentTime 344 | if cachedFor: 345 | self.log("%s: cached (%d seconds)" % (path, cachedFor)) 346 | 347 | cachedUntil = time.time() + cachedFor 348 | 349 | # store in memory 350 | cached = self.cache[key] = (cachedUntil, doc) 351 | 352 | # store in cache folder 353 | cacheFile = join(self.tempdir, str(key) + ".cache") 354 | f = open(cacheFile, "wb") 355 | f.write(zlib.compress(pickle.dumps(cached, -1))) 356 | f.close() 357 | 358 | 359 | # Now try out the handler! Even though were initializing a new api object 360 | # here, a handler can be attached or removed from an existing one at any 361 | # time with its setcachehandler() method. 362 | cachedApi = eveapi.EVEAPIConnection(cacheHandler=MyCacheHandler(debug=True)) 363 | 364 | # First time around this will fetch the document from the server. That is, 365 | # if this demo is run for the first time, otherwise it will attempt to load 366 | # the cache written to disk on the previous run. 367 | result = cachedApi.eve.SkillTree() 368 | 369 | # But the second time it should be returning the cached version 370 | result = cachedApi.eve.SkillTree() 371 | 372 | 373 | # ----------------------------------------------------------------------------- 374 | print() 375 | print("EXAMPLE 7: TRANSACTION DATA") 376 | print("(and doing more nifty stuff with rowsets)") 377 | print() 378 | 379 | # okay since we have a caching api object now it is fairly safe to do this 380 | # example repeatedly without server locking you out for an hour every time! 381 | 382 | # Let's use the first character on the account (using the richest character 383 | # found in example 2). Note how we are chaining the various contexts here to 384 | # arrive directly at a character context. If you're not using any intermediate 385 | # contexts in the chain anyway, this is okay. 386 | me = cachedApi.auth(keyID=YOUR_KEYID, vCode=YOUR_VCODE).character(rich_charID) 387 | 388 | # Now fetch the journal. Since this character context was created through 389 | # the cachedApi object, it will still use the cachehandler from example 5. 390 | journal = me.WalletJournal() 391 | 392 | # Let's see how much we paid SCC in transaction tax in the first page 393 | # of data! 394 | 395 | # Righto, now we -could- sift through the rows and extract what we want, 396 | # but we can do it in a much more clever way using the GroupedBy method 397 | # of the rowset in the result. This creates a mapping that maps keys 398 | # to Rowsets of all rows with that key value in specified column. 399 | # These data structures are also quite efficient as the only extra data 400 | # created is the index and grouping. 401 | entriesByRefType = journal.transactions.GroupedBy("refTypeID") 402 | 403 | # Also note that we're using a hardcoded refTypeID of 54 here. You're 404 | # supposed to use .eve.RefTypes() though (however they are not likely 405 | # to be changed anyway so we can get away with it) 406 | # Note the use of Select() to speed things up here. 407 | amount = 0.0 408 | date = 0 409 | try: 410 | for taxAmount, date in entriesByRefType[54].Select("amount", "date"): 411 | amount += -taxAmount 412 | except KeyError: # no taxes paid 413 | pass 414 | 415 | print("You paid a {:,.2f} ISK transaction tax since {}".format( 416 | amount, 417 | time.asctime(time.gmtime(date)), 418 | )) 419 | 420 | 421 | # You might also want to see how much a certain item yielded you recently. 422 | typeName = "Expanded Cargohold II" # change this to something you sold. 423 | amount = 0.0 424 | date = 0 425 | 426 | wallet = me.WalletTransactions() 427 | try: 428 | soldTx = wallet.transactions.GroupedBy("transactionType")["sell"] 429 | for row in soldTx.GroupedBy("typeName")[typeName]: 430 | amount += (row.quantity * row.price) 431 | date = row.transactionDateTime 432 | except KeyError: # has not sold any 433 | pass 434 | 435 | print("{} sales yielded {:,.2f} ISK since {}".format( 436 | typeName, 437 | amount, 438 | time.asctime(time.gmtime(date)), 439 | )) 440 | 441 | # I'll leave walking the transaction pages as an excercise to the reader ;) 442 | # Please also see the eveapi module itself for more documentation. 443 | 444 | # That's all folks! 445 | -------------------------------------------------------------------------------- /eveapi.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # eveapi - EVE Online API access 3 | # 4 | # Copyright (c)2007-2014 Jamie "Entity" van den Berge 5 | # 6 | # Permission is hereby granted, free of charge, to any person 7 | # obtaining a copy of this software and associated documentation 8 | # files (the "Software"), to deal in the Software without 9 | # restriction, including without limitation the rights to use, 10 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the 12 | # Software is furnished to do so, subject to the following 13 | # conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | # OTHER DEALINGS IN THE SOFTWARE 26 | # 27 | #----------------------------------------------------------------------------- 28 | # 29 | # Version: 1.3.2 - 29 August 2015 30 | # - Added Python 3 support 31 | # 32 | # Version: 1.3.1 - 02 November 2014 33 | # - Fix problem with strings ending in spaces (this is not supposed to happen, 34 | # but apparently tiancity thinks it is ok to bypass constraints) 35 | # 36 | # Version: 1.3.0 - 27 May 2014 37 | # - Added set_user_agent() module-level function to set the User-Agent header 38 | # to be used for any requests by the library. If this function is not used, 39 | # a warning will be thrown for every API request. 40 | # 41 | # Version: 1.2.9 - 14 September 2013 42 | # - Updated error handling: Raise an AuthenticationError in case 43 | # the API returns HTTP Status Code 403 - Forbidden 44 | # 45 | # Version: 1.2.8 - 9 August 2013 46 | # - the XML value cast function (_autocast) can now be changed globally to a 47 | # custom one using the set_cast_func(func) module-level function. 48 | # 49 | # Version: 1.2.7 - 3 September 2012 50 | # - Added get() method to Row object. 51 | # 52 | # Version: 1.2.6 - 29 August 2012 53 | # - Added finer error handling + added setup.py to allow distributing eveapi 54 | # through pypi. 55 | # 56 | # Version: 1.2.5 - 1 August 2012 57 | # - Row objects now have __hasattr__ and __contains__ methods 58 | # 59 | # Version: 1.2.4 - 12 April 2012 60 | # - API version of XML response now available as _meta.version 61 | # 62 | # Version: 1.2.3 - 10 April 2012 63 | # - fix for tags of the form 64 | # 65 | # Version: 1.2.2 - 27 February 2012 66 | # - fix for the workaround in 1.2.1. 67 | # 68 | # Version: 1.2.1 - 23 February 2012 69 | # - added workaround for row tags missing attributes that were defined 70 | # in their rowset (this should fix ContractItems) 71 | # 72 | # Version: 1.2.0 - 18 February 2012 73 | # - fix handling of empty XML tags. 74 | # - improved proxy support a bit. 75 | # 76 | # Version: 1.1.9 - 2 September 2011 77 | # - added workaround for row tags with attributes that were not defined 78 | # in their rowset (this should fix AssetList) 79 | # 80 | # Version: 1.1.8 - 1 September 2011 81 | # - fix for inconsistent columns attribute in rowsets. 82 | # 83 | # Version: 1.1.7 - 1 September 2011 84 | # - auth() method updated to work with the new authentication scheme. 85 | # 86 | # Version: 1.1.6 - 27 May 2011 87 | # - Now supports composite keys for IndexRowsets. 88 | # - Fixed calls not working if a path was specified in the root url. 89 | # 90 | # Version: 1.1.5 - 27 Januari 2011 91 | # - Now supports (and defaults to) HTTPS. Non-SSL proxies will still work by 92 | # explicitly specifying http:// in the url. 93 | # 94 | # Version: 1.1.4 - 1 December 2010 95 | # - Empty explicit CDATA tags are now properly handled. 96 | # - _autocast now receives the name of the variable it's trying to typecast, 97 | # enabling custom/future casting functions to make smarter decisions. 98 | # 99 | # Version: 1.1.3 - 6 November 2010 100 | # - Added support for anonymous CDATA inside row tags. This makes the body of 101 | # mails in the rows of char/MailBodies available through the .data attribute. 102 | # 103 | # Version: 1.1.2 - 2 July 2010 104 | # - Fixed __str__ on row objects to work properly with unicode strings. 105 | # 106 | # Version: 1.1.1 - 10 Januari 2010 107 | # - Fixed bug that causes nested tags to not appear in rows of rowsets created 108 | # from normal Elements. This should fix the corp.MemberSecurity method, 109 | # which now returns all data for members. [jehed] 110 | # 111 | # Version: 1.1.0 - 15 Januari 2009 112 | # - Added Select() method to Rowset class. Using it avoids the creation of 113 | # temporary row instances, speeding up iteration considerably. 114 | # - Added ParseXML() function, which can be passed arbitrary API XML file or 115 | # string objects. 116 | # - Added support for proxy servers. A proxy can be specified globally or 117 | # per api connection instance. [suggestion by graalman] 118 | # - Some minor refactoring. 119 | # - Fixed deprecation warning when using Python 2.6. 120 | # 121 | # Version: 1.0.7 - 14 November 2008 122 | # - Added workaround for rowsets that are missing the (required!) columns 123 | # attribute. If missing, it will use the columns found in the first row. 124 | # Note that this is will still break when expecting columns, if the rowset 125 | # is empty. [Flux/Entity] 126 | # 127 | # Version: 1.0.6 - 18 July 2008 128 | # - Enabled expat text buffering to avoid content breaking up. [BigWhale] 129 | # 130 | # Version: 1.0.5 - 03 February 2008 131 | # - Added workaround to make broken XML responses (like the "row:name" bug in 132 | # eve/CharacterID) work as intended. 133 | # - Bogus datestamps before the epoch in XML responses are now set to 0 to 134 | # avoid breaking certain date/time functions. [Anathema Matou] 135 | # 136 | # Version: 1.0.4 - 23 December 2007 137 | # - Changed _autocast() to use timegm() instead of mktime(). [Invisible Hand] 138 | # - Fixed missing attributes of elements inside rows. [Elandra Tenari] 139 | # 140 | # Version: 1.0.3 - 13 December 2007 141 | # - Fixed keyless columns bugging out the parser (in CorporationSheet for ex.) 142 | # 143 | # Version: 1.0.2 - 12 December 2007 144 | # - Fixed parser not working with indented XML. 145 | # 146 | # Version: 1.0.1 147 | # - Some micro optimizations 148 | # 149 | # Version: 1.0 150 | # - Initial release 151 | # 152 | # Requirements: 153 | # Python 2.6+ or Python 3.3+ 154 | # 155 | #----------------------------------------------------------------------------- 156 | from __future__ import division 157 | from past.builtins import cmp 158 | from future import standard_library 159 | standard_library.install_aliases() 160 | from past.builtins import basestring 161 | from builtins import map 162 | from builtins import zip 163 | from builtins import range 164 | from builtins import object 165 | 166 | import http.client 167 | from urllib.parse import urlparse, urlencode 168 | # from urllib.request import urlopen, Request 169 | # from urllib.error import HTTPError 170 | 171 | import copy 172 | import warnings 173 | 174 | from xml.parsers import expat 175 | from time import strptime 176 | from calendar import timegm 177 | 178 | __version__ = "1.3.2" 179 | _default_useragent = "eveapi.py/{}".format(__version__) 180 | _useragent = None # use set_user_agent() to set this. 181 | 182 | proxy = None 183 | proxySSL = False 184 | 185 | #----------------------------------------------------------------------------- 186 | 187 | def set_cast_func(func): 188 | """Sets an alternative value casting function for the XML parser. 189 | The function must have 2 arguments; key and value. It should return a 190 | value or object of the type appropriate for the given attribute name/key. 191 | func may be None and will cause the default _autocast function to be used. 192 | """ 193 | global _castfunc 194 | _castfunc = _autocast if func is None else func 195 | 196 | def set_user_agent(user_agent_string): 197 | """Sets a User-Agent for any requests sent by the library.""" 198 | global _useragent 199 | _useragent = user_agent_string 200 | 201 | 202 | class Error(Exception): 203 | def __init__(self, code, message): 204 | self.code = code 205 | self.message = message 206 | def __str__(self): 207 | return u'%s [code=%s]' % (self.message, self.code) 208 | 209 | class RequestError(Error): 210 | pass 211 | 212 | class AuthenticationError(Error): 213 | pass 214 | 215 | class ServerError(Error): 216 | pass 217 | 218 | 219 | def EVEAPIConnection(url="api.eveonline.com", cacheHandler=None, proxy=None, proxySSL=False): 220 | # Creates an API object through which you can call remote functions. 221 | # 222 | # The following optional arguments may be provided: 223 | # 224 | # url - root location of the EVEAPI server 225 | # 226 | # proxy - (host,port) specifying a proxy server through which to request 227 | # the API pages. Specifying a proxy overrides default proxy. 228 | # 229 | # proxySSL - True if the proxy requires SSL, False otherwise. 230 | # 231 | # cacheHandler - an object which must support the following interface: 232 | # 233 | # retrieve(host, path, params) 234 | # 235 | # Called when eveapi wants to fetch a document. 236 | # host is the address of the server, path is the full path to 237 | # the requested document, and params is a dict containing the 238 | # parameters passed to this api call (keyID, vCode, etc). 239 | # The method MUST return one of the following types: 240 | # 241 | # None - if your cache did not contain this entry 242 | # str/unicode - eveapi will parse this as XML 243 | # Element - previously stored object as provided to store() 244 | # file-like object - eveapi will read() XML from the stream. 245 | # 246 | # store(host, path, params, doc, obj) 247 | # 248 | # Called when eveapi wants you to cache this item. 249 | # You can use obj to get the info about the object (cachedUntil 250 | # and currentTime, etc) doc is the XML document the object 251 | # was generated from. It's generally best to cache the XML, not 252 | # the object, unless you pickle the object. Note that this method 253 | # will only be called if you returned None in the retrieve() for 254 | # this object. 255 | # 256 | 257 | if not url.startswith("http"): 258 | url = "https://" + url 259 | p = urlparse(url, "https") 260 | if p.path and p.path[-1] == "/": 261 | p.path = p.path[:-1] 262 | ctx = _RootContext(None, p.path, {}, {}) 263 | ctx._handler = cacheHandler 264 | ctx._scheme = p.scheme 265 | ctx._host = p.netloc 266 | ctx._proxy = proxy or globals()["proxy"] 267 | ctx._proxySSL = proxySSL or globals()["proxySSL"] 268 | return ctx 269 | 270 | 271 | def ParseXML(file_or_string): 272 | try: 273 | return _ParseXML(file_or_string, False, None) 274 | except TypeError: 275 | raise TypeError("XML data must be provided as string or file-like object") 276 | 277 | 278 | def _ParseXML(response, fromContext, storeFunc): 279 | # pre/post-process XML or Element data 280 | if fromContext and isinstance(response, Element): 281 | obj = response 282 | elif isinstance(response, basestring): 283 | obj = _Parser().Parse(response, False) 284 | elif hasattr(response, "read"): 285 | obj = _Parser().Parse(response, True) 286 | else: 287 | raise TypeError("retrieve method must return None, string, file-like object or an Element instance") 288 | 289 | error = getattr(obj, "error", False) 290 | if error: 291 | if error.code >= 500: 292 | raise ServerError(error.code, error.data) 293 | elif error.code >= 200: 294 | raise AuthenticationError(error.code, error.data) 295 | elif error.code >= 100: 296 | raise RequestError(error.code, error.data) 297 | else: 298 | raise Error(error.code, error.data) 299 | 300 | result = getattr(obj, "result", False) 301 | if not result: 302 | raise RuntimeError("API object does not contain result") 303 | 304 | if fromContext and storeFunc: 305 | # call the cache handler to store this object 306 | storeFunc(obj) 307 | 308 | # make metadata available to caller somehow 309 | result._meta = obj 310 | 311 | return result 312 | 313 | 314 | 315 | 316 | 317 | #----------------------------------------------------------------------------- 318 | # API Classes 319 | #----------------------------------------------------------------------------- 320 | 321 | _listtypes = (list, tuple, dict) 322 | _unspecified = [] 323 | 324 | class _Context(object): 325 | 326 | def __init__(self, root, path, parentDict, newKeywords=None): 327 | self._root = root or self 328 | self._path = path 329 | if newKeywords: 330 | if parentDict: 331 | self.parameters = parentDict.copy() 332 | else: 333 | self.parameters = {} 334 | self.parameters.update(newKeywords) 335 | else: 336 | self.parameters = parentDict or {} 337 | 338 | def context(self, *args, **kw): 339 | if kw or args: 340 | path = self._path 341 | if args: 342 | path += "/" + "/".join(args) 343 | return self.__class__(self._root, path, self.parameters, kw) 344 | else: 345 | return self 346 | 347 | def __getattr__(self, this): 348 | # perform arcane attribute majick trick 349 | return _Context(self._root, self._path + "/" + this, self.parameters) 350 | 351 | def __call__(self, **kw): 352 | if kw: 353 | # specified keywords override contextual ones 354 | for k, v in self.parameters.items(): 355 | if k not in kw: 356 | kw[k] = v 357 | else: 358 | # no keywords provided, just update with contextual ones. 359 | kw.update(self.parameters) 360 | 361 | # now let the root context handle it further 362 | return self._root(self._path, **kw) 363 | 364 | 365 | class _AuthContext(_Context): 366 | 367 | def character(self, characterID): 368 | # returns a copy of this connection object but for every call made 369 | # through it, it will add the folder "/char" to the url, and the 370 | # characterID to the parameters passed. 371 | return _Context(self._root, self._path + "/char", self.parameters, {"characterID":characterID}) 372 | 373 | def corporation(self, characterID): 374 | # same as character except for the folder "/corp" 375 | return _Context(self._root, self._path + "/corp", self.parameters, {"characterID":characterID}) 376 | 377 | 378 | class _RootContext(_Context): 379 | 380 | def auth(self, **kw): 381 | if len(kw) == 2 and (("keyID" in kw and "vCode" in kw) or ("userID" in kw and "apiKey" in kw)): 382 | return _AuthContext(self._root, self._path, self.parameters, kw) 383 | raise ValueError("Must specify keyID and vCode") 384 | 385 | def setcachehandler(self, handler): 386 | self._root._handler = handler 387 | 388 | def __bool__(self): 389 | return True 390 | 391 | def __call__(self, path, **kw): 392 | # convert list type arguments to something the API likes 393 | for k, v in kw.items(): 394 | if isinstance(v, _listtypes): 395 | kw[k] = ','.join(map(str, list(v))) 396 | 397 | cache = self._root._handler 398 | 399 | # now send the request 400 | path += ".xml.aspx" 401 | 402 | if cache: 403 | response = cache.retrieve(self._host, path, kw) 404 | else: 405 | response = None 406 | 407 | if response is None: 408 | if not _useragent: 409 | warnings.warn("No User-Agent set! Please use the set_user_agent() module-level function before accessing the EVE API.", stacklevel=3) 410 | 411 | if self._proxy is None: 412 | req = path 413 | if self._scheme == "https": 414 | conn = http.client.HTTPSConnection(self._host) 415 | else: 416 | conn = http.client.HTTPConnection(self._host) 417 | else: 418 | req = self._scheme+'://'+self._host+path 419 | if self._proxySSL: 420 | conn = http.client.HTTPSConnection(*self._proxy) 421 | else: 422 | conn = http.client.HTTPConnection(*self._proxy) 423 | 424 | if kw: 425 | conn.request("POST", req, urlencode(kw), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": _useragent or _default_useragent}) 426 | else: 427 | conn.request("GET", req, "", {"User-Agent": _useragent or _default_useragent}) 428 | 429 | response = conn.getresponse() 430 | if response.status != 200: 431 | if response.status == http.client.NOT_FOUND: 432 | raise AttributeError("'%s' not available on API server (404 Not Found)" % path) 433 | elif response.status == http.client.FORBIDDEN: 434 | raise AuthenticationError(response.status, 'HTTP 403 - Forbidden') 435 | else: 436 | raise ServerError(response.status, "'%s' request failed (%s)" % (path, response.reason)) 437 | 438 | if cache: 439 | store = True 440 | response = response.read() 441 | else: 442 | store = False 443 | else: 444 | store = False 445 | 446 | retrieve_fallback = cache and getattr(cache, "retrieve_fallback", False) 447 | if retrieve_fallback: 448 | # implementor is handling fallbacks... 449 | try: 450 | return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj))) 451 | except Error as e: 452 | response = retrieve_fallback(self._host, path, kw, reason=e) 453 | if response is not None: 454 | return response 455 | raise 456 | else: 457 | # implementor is not handling fallbacks... 458 | return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj))) 459 | 460 | #----------------------------------------------------------------------------- 461 | # XML Parser 462 | #----------------------------------------------------------------------------- 463 | 464 | def _autocast(key, value): 465 | # attempts to cast an XML string to the most probable type. 466 | try: 467 | if value.strip("-").isdigit(): 468 | return int(value) 469 | except ValueError: 470 | pass 471 | 472 | try: 473 | return float(value) 474 | except ValueError: 475 | pass 476 | 477 | if len(value) == 19 and value[10] == ' ': 478 | # it could be a date string 479 | try: 480 | return max(0, int(timegm(strptime(value, "%Y-%m-%d %H:%M:%S")))) 481 | except OverflowError: 482 | pass 483 | except ValueError: 484 | pass 485 | 486 | # couldn't cast. return string unchanged. 487 | return value 488 | 489 | _castfunc = _autocast 490 | 491 | 492 | class _Parser(object): 493 | 494 | def Parse(self, data, isStream=False): 495 | self.container = self.root = None 496 | self._cdata = False 497 | p = expat.ParserCreate() 498 | p.StartElementHandler = self.tag_start 499 | p.CharacterDataHandler = self.tag_cdata 500 | p.StartCdataSectionHandler = self.tag_cdatasection_enter 501 | p.EndCdataSectionHandler = self.tag_cdatasection_exit 502 | p.EndElementHandler = self.tag_end 503 | p.ordered_attributes = True 504 | p.buffer_text = True 505 | 506 | if isStream: 507 | p.ParseFile(data) 508 | else: 509 | p.Parse(data, True) 510 | return self.root 511 | 512 | 513 | def tag_cdatasection_enter(self): 514 | # encountered an explicit CDATA tag. 515 | self._cdata = True 516 | 517 | def tag_cdatasection_exit(self): 518 | if self._cdata: 519 | # explicit CDATA without actual data. expat doesn't seem 520 | # to trigger an event for this case, so do it manually. 521 | # (_cdata is set False by this call) 522 | self.tag_cdata("") 523 | else: 524 | self._cdata = False 525 | 526 | def tag_start(self, name, attributes): 527 | # 528 | # If there's a colon in the tag name, cut off the name from the colon 529 | # onward. This is a workaround to make certain bugged XML responses 530 | # (such as eve/CharacterID.xml.aspx) work. 531 | if ":" in name: 532 | name = name[:name.index(":")] 533 | # 534 | 535 | if name == "rowset": 536 | # for rowsets, use the given name 537 | try: 538 | columns = attributes[attributes.index('columns')+1].replace(" ", "").split(",") 539 | except ValueError: 540 | # rowset did not have columns tag set (this is a bug in API) 541 | # columns will be extracted from first row instead. 542 | columns = [] 543 | 544 | try: 545 | priKey = attributes[attributes.index('key')+1] 546 | this = IndexRowset(cols=columns, key=priKey) 547 | except ValueError: 548 | this = Rowset(cols=columns) 549 | 550 | 551 | this._name = attributes[attributes.index('name')+1] 552 | this.__catch = "row" # tag to auto-add to rowset. 553 | else: 554 | this = Element() 555 | this._name = name 556 | 557 | this.__parent = self.container 558 | 559 | if self.root is None: 560 | # We're at the root. The first tag has to be "eveapi" or we can't 561 | # really assume the rest of the xml is going to be what we expect. 562 | if name != "eveapi": 563 | raise RuntimeError("Invalid API response") 564 | try: 565 | this.version = attributes[attributes.index("version")+1] 566 | except KeyError: 567 | raise RuntimeError("Invalid API response") 568 | self.root = this 569 | 570 | if isinstance(self.container, Rowset) and (self.container.__catch == this._name): 571 | # 572 | # - check for missing columns attribute (see above). 573 | # - check for missing row attributes. 574 | # - check for extra attributes that were not defined in the rowset, 575 | # such as rawQuantity in the assets lists. 576 | # In either case the tag is assumed to be correct and the rowset's 577 | # columns are overwritten with the tag's version, if required. 578 | numAttr = len(attributes) / 2 579 | numCols = len(self.container._cols) 580 | if numAttr < numCols and (attributes[-2] == self.container._cols[-1]): 581 | # the row data is missing attributes that were defined in the rowset. 582 | # missing attributes' values will be set to None. 583 | fixed = [] 584 | row_idx = 0; hdr_idx = 0; numAttr*=2 585 | for col in self.container._cols: 586 | if col == attributes[row_idx]: 587 | fixed.append(_castfunc(col, attributes[row_idx+1])) 588 | row_idx += 2 589 | else: 590 | fixed.append(None) 591 | hdr_idx += 1 592 | self.container.append(fixed) 593 | else: 594 | if not self.container._cols or (numAttr > numCols): 595 | # the row data contains more attributes than were defined. 596 | self.container._cols = attributes[0::2] 597 | self.container.append([_castfunc(attributes[i], attributes[i+1]) for i in range(0, len(attributes), 2)]) 598 | # 599 | 600 | this._isrow = True 601 | this._attributes = this._attributes2 = None 602 | else: 603 | this._isrow = False 604 | this._attributes = attributes 605 | this._attributes2 = [] 606 | 607 | self.container = self._last = this 608 | self.has_cdata = False 609 | 610 | def tag_cdata(self, data): 611 | self.has_cdata = True 612 | if self._cdata: 613 | # unset cdata flag to indicate it's been handled. 614 | self._cdata = False 615 | else: 616 | if data in ("\r\n", "\n") or data.lstrip() != data: 617 | return 618 | 619 | this = self.container 620 | data = _castfunc(this._name, data) 621 | 622 | if this._isrow: 623 | # sigh. anonymous data inside rows makes Entity cry. 624 | # for the love of Jove, CCP, learn how to use rowsets. 625 | parent = this.__parent 626 | _row = parent._rows[-1] 627 | _row.append(data) 628 | if len(parent._cols) < len(_row): 629 | parent._cols.append("data") 630 | 631 | elif this._attributes: 632 | # this tag has attributes, so we can't simply assign the cdata 633 | # as an attribute to the parent tag, as we'll lose the current 634 | # tag's attributes then. instead, we'll assign the data as 635 | # attribute of this tag. 636 | this.data = data 637 | else: 638 | # this was a simple data without attributes. 639 | # we won't be doing anything with this actual tag so we can just 640 | # bind it to its parent (done by __tag_end) 641 | setattr(this.__parent, this._name, data) 642 | 643 | def tag_end(self, name): 644 | this = self.container 645 | 646 | if this is self.root: 647 | del this._attributes 648 | #this.__dict__.pop("_attributes", None) 649 | return 650 | 651 | # we're done with current tag, so we can pop it off. This means that 652 | # self.container will now point to the container of element 'this'. 653 | self.container = this.__parent 654 | del this.__parent 655 | 656 | attributes = this.__dict__.pop("_attributes") 657 | attributes2 = this.__dict__.pop("_attributes2") 658 | if attributes is None: 659 | # already processed this tag's closure early, in tag_start() 660 | return 661 | 662 | if self.container._isrow: 663 | # Special case here. tags inside a row! Such tags have to be 664 | # added as attributes of the row. 665 | parent = self.container.__parent 666 | 667 | # get the row line for this element from its parent rowset 668 | _row = parent._rows[-1] 669 | 670 | # add this tag's value to the end of the row 671 | _row.append(getattr(self.container, this._name, this)) 672 | 673 | # fix columns if neccessary. 674 | if len(parent._cols) < len(_row): 675 | parent._cols.append(this._name) 676 | else: 677 | # see if there's already an attribute with this name (this shouldn't 678 | # really happen, but it doesn't hurt to handle this case! 679 | sibling = getattr(self.container, this._name, None) 680 | if sibling is None: 681 | if (not self.has_cdata) and (self._last is this) and (name != "rowset"): 682 | if attributes: 683 | # tag of the form 684 | e = Element() 685 | e._name = this._name 686 | setattr(self.container, this._name, e) 687 | for i in range(0, len(attributes), 2): 688 | setattr(e, attributes[i], attributes[i+1]) 689 | else: 690 | # tag of the form: , treat as empty string. 691 | setattr(self.container, this._name, "") 692 | else: 693 | self.container._attributes2.append(this._name) 694 | setattr(self.container, this._name, this) 695 | 696 | # Note: there aren't supposed to be any NON-rowset tags containing 697 | # multiples of some tag or attribute. Code below handles this case. 698 | elif isinstance(sibling, Rowset): 699 | # its doppelganger is a rowset, append this as a row to that. 700 | row = [_castfunc(attributes[i], attributes[i+1]) for i in range(0, len(attributes), 2)] 701 | row.extend([getattr(this, col) for col in attributes2]) 702 | sibling.append(row) 703 | elif isinstance(sibling, Element): 704 | # parent attribute is an element. This means we're dealing 705 | # with multiple of the same sub-tag. Change the attribute 706 | # into a Rowset, adding the sibling element and this one. 707 | rs = Rowset() 708 | rs.__catch = rs._name = this._name 709 | row = [_castfunc(attributes[i], attributes[i+1]) for i in range(0, len(attributes), 2)]+[getattr(this, col) for col in attributes2] 710 | rs.append(row) 711 | row = [getattr(sibling, attributes[i]) for i in range(0, len(attributes), 2)]+[getattr(sibling, col) for col in attributes2] 712 | rs.append(row) 713 | rs._cols = [attributes[i] for i in range(0, len(attributes), 2)]+[col for col in attributes2] 714 | setattr(self.container, this._name, rs) 715 | else: 716 | # something else must have set this attribute already. 717 | # (typically the data case in tag_data()) 718 | pass 719 | 720 | # Now fix up the attributes and be done with it. 721 | for i in range(0, len(attributes), 2): 722 | this.__dict__[attributes[i]] = _castfunc(attributes[i], attributes[i+1]) 723 | 724 | return 725 | 726 | 727 | 728 | 729 | #----------------------------------------------------------------------------- 730 | # XML Data Containers 731 | #----------------------------------------------------------------------------- 732 | # The following classes are the various container types the XML data is 733 | # unpacked into. 734 | # 735 | # Note that objects returned by API calls are to be treated as read-only. This 736 | # is not enforced, but you have been warned. 737 | #----------------------------------------------------------------------------- 738 | 739 | class Element(object): 740 | # Element is a namespace for attributes and nested tags 741 | def __str__(self): 742 | return "" % self._name 743 | 744 | _fmt = u"%s:%s".__mod__ 745 | class Row(object): 746 | # A Row is a single database record associated with a Rowset. 747 | # The fields in the record are accessed as attributes by their respective 748 | # column name. 749 | # 750 | # To conserve resources, Row objects are only created on-demand. This is 751 | # typically done by Rowsets (e.g. when iterating over the rowset). 752 | 753 | def __init__(self, cols=None, row=None): 754 | self._cols = cols or [] 755 | self._row = row or [] 756 | 757 | def __bool__(self): 758 | return True 759 | 760 | def __ne__(self, other): 761 | return self.__cmp__(other) 762 | 763 | def __eq__(self, other): 764 | return self.__cmp__(other) == 0 765 | 766 | def __cmp__(self, other): 767 | if type(other) != type(self): 768 | raise TypeError("Incompatible comparison type") 769 | return cmp(self._cols, other._cols) or cmp(self._row, other._row) 770 | 771 | def __hasattr__(self, this): 772 | if this in self._cols: 773 | return self._cols.index(this) < len(self._row) 774 | return False 775 | 776 | __contains__ = __hasattr__ 777 | 778 | def get(self, this, default=None): 779 | if (this in self._cols) and (self._cols.index(this) < len(self._row)): 780 | return self._row[self._cols.index(this)] 781 | return default 782 | 783 | def __getattr__(self, this): 784 | try: 785 | return self._row[self._cols.index(this)] 786 | except: 787 | raise AttributeError(this) 788 | 789 | def __getitem__(self, this): 790 | return self._row[self._cols.index(this)] 791 | 792 | def __str__(self): 793 | return "Row(" + ','.join(map(_fmt, list(zip(self._cols, self._row)))) + ")" 794 | 795 | 796 | class Rowset(object): 797 | # Rowsets are collections of Row objects. 798 | # 799 | # Rowsets support most of the list interface: 800 | # iteration, indexing and slicing 801 | # 802 | # As well as the following methods: 803 | # 804 | # IndexedBy(column) 805 | # Returns an IndexRowset keyed on given column. Requires the column to 806 | # be usable as primary key. 807 | # 808 | # GroupedBy(column) 809 | # Returns a FilterRowset keyed on given column. FilterRowset objects 810 | # can be accessed like dicts. See FilterRowset class below. 811 | # 812 | # SortBy(column, reverse=True) 813 | # Sorts rowset in-place on given column. for a descending sort, 814 | # specify reversed=True. 815 | # 816 | # SortedBy(column, reverse=True) 817 | # Same as SortBy, except this returns a new rowset object instead of 818 | # sorting in-place. 819 | # 820 | # Select(columns, row=False) 821 | # Yields a column values tuple (value, ...) for each row in the rowset. 822 | # If only one column is requested, then just the column value is 823 | # provided instead of the values tuple. 824 | # When row=True, each result will be decorated with the entire row. 825 | # 826 | 827 | def IndexedBy(self, column): 828 | return IndexRowset(self._cols, self._rows, column) 829 | 830 | def GroupedBy(self, column): 831 | return FilterRowset(self._cols, self._rows, column) 832 | 833 | def SortBy(self, column, reverse=False, dtype=str): 834 | ix = self._cols.index(column) 835 | self.sort(key=lambda e: dtype(e[ix]), reverse=reverse) 836 | 837 | def SortedBy(self, column, reverse=False, dtype=str): 838 | rs = self[:] 839 | rs.SortBy(column, reverse, dtype) 840 | return rs 841 | 842 | def Select(self, *columns, **options): 843 | if len(columns) == 1: 844 | i = self._cols.index(columns[0]) 845 | if options.get("row", False): 846 | for line in self._rows: 847 | yield (line, line[i]) 848 | else: 849 | for line in self._rows: 850 | yield line[i] 851 | else: 852 | i = list(map(self._cols.index, columns)) 853 | if options.get("row", False): 854 | for line in self._rows: 855 | yield line, [line[x] for x in i] 856 | else: 857 | for line in self._rows: 858 | yield [line[x] for x in i] 859 | 860 | 861 | # ------------- 862 | 863 | def __init__(self, cols=None, rows=None): 864 | self._cols = cols or [] 865 | self._rows = rows or [] 866 | 867 | def append(self, row): 868 | if isinstance(row, list): 869 | self._rows.append(row) 870 | elif isinstance(row, Row) and len(row._cols) == len(self._cols): 871 | self._rows.append(row._row) 872 | else: 873 | raise TypeError("incompatible row type") 874 | 875 | def __add__(self, other): 876 | if isinstance(other, Rowset): 877 | if len(other._cols) == len(self._cols): 878 | self._rows += other._rows 879 | raise TypeError("rowset instance expected") 880 | 881 | def __bool__(self): 882 | return True if self._rows else False 883 | 884 | def __len__(self): 885 | return len(self._rows) 886 | 887 | def copy(self): 888 | return self[:] 889 | 890 | def __getitem__(self, ix): 891 | if type(ix) is slice: 892 | return Rowset(self._cols, self._rows[ix]) 893 | return Row(self._cols, self._rows[ix]) 894 | 895 | def sort(self, *args, **kw): 896 | self._rows.sort(*args, **kw) 897 | 898 | def __str__(self): 899 | return ("Rowset(columns=[%s], rows=%d)" % (','.join(self._cols), len(self))) 900 | 901 | def __getstate__(self): 902 | return (self._cols, self._rows) 903 | 904 | def __setstate__(self, state): 905 | self._cols, self._rows = state 906 | 907 | 908 | 909 | class IndexRowset(Rowset): 910 | # An IndexRowset is a Rowset that keeps an index on a column. 911 | # 912 | # The interface is the same as Rowset, but provides an additional method: 913 | # 914 | # Get(key [, default]) 915 | # Returns the Row mapped to provided key in the index. If there is no 916 | # such key in the index, KeyError is raised unless a default value was 917 | # specified. 918 | # 919 | 920 | def Get(self, key, *default): 921 | row = self._items.get(key, None) 922 | if row is None: 923 | if default: 924 | return default[0] 925 | raise KeyError(key) 926 | return Row(self._cols, row) 927 | 928 | # ------------- 929 | 930 | def __init__(self, cols=None, rows=None, key=None): 931 | try: 932 | if "," in key: 933 | self._ki = ki = [cols.index(k) for k in key.split(",")] 934 | self.composite = True 935 | else: 936 | self._ki = ki = cols.index(key) 937 | self.composite = False 938 | except IndexError: 939 | raise ValueError("Rowset has no column %s" % key) 940 | 941 | Rowset.__init__(self, cols, rows) 942 | self._key = key 943 | 944 | if self.composite: 945 | self._items = dict((tuple([row[k] for k in ki]), row) for row in self._rows) 946 | else: 947 | self._items = dict((row[ki], row) for row in self._rows) 948 | 949 | def __getitem__(self, ix): 950 | if type(ix) is slice: 951 | return IndexRowset(self._cols, self._rows[ix], self._key) 952 | return Rowset.__getitem__(self, ix) 953 | 954 | def append(self, row): 955 | Rowset.append(self, row) 956 | if self.composite: 957 | self._items[tuple([row[k] for k in self._ki])] = row 958 | else: 959 | self._items[row[self._ki]] = row 960 | 961 | def __getstate__(self): 962 | return (Rowset.__getstate__(self), self._items, self._ki) 963 | 964 | def __setstate__(self, state): 965 | state, self._items, self._ki = state 966 | Rowset.__setstate__(self, state) 967 | 968 | 969 | class FilterRowset(object): 970 | # A FilterRowset works much like an IndexRowset, with the following 971 | # differences: 972 | # - FilterRowsets are accessed much like dicts 973 | # - Each key maps to a Rowset, containing only the rows where the value 974 | # of the column this FilterRowset was made on matches the key. 975 | 976 | def __init__(self, cols=None, rows=None, key=None, key2=None, dict=None): 977 | if dict is not None: 978 | self._items = items = dict 979 | elif cols is not None: 980 | self._items = items = {} 981 | 982 | idfield = cols.index(key) 983 | if not key2: 984 | for row in rows: 985 | id = row[idfield] 986 | if id in items: 987 | items[id].append(row) 988 | else: 989 | items[id] = [row] 990 | else: 991 | idfield2 = cols.index(key2) 992 | for row in rows: 993 | id = row[idfield] 994 | if id in items: 995 | items[id][row[idfield2]] = row 996 | else: 997 | items[id] = {row[idfield2]:row} 998 | 999 | self._cols = cols 1000 | self.key = key 1001 | self.key2 = key2 1002 | self._bind() 1003 | 1004 | def _bind(self): 1005 | items = self._items 1006 | self.keys = items.keys 1007 | self.__contains__ = items.__contains__ 1008 | self.__len__ = items.__len__ 1009 | self.__iter__ = items.__iter__ 1010 | 1011 | def copy(self): 1012 | return FilterRowset(self._cols[:], None, self.key, self.key2, dict=copy.deepcopy(self._items)) 1013 | 1014 | def get(self, key, default=_unspecified): 1015 | try: 1016 | return self[key] 1017 | except KeyError: 1018 | if default is _unspecified: 1019 | raise 1020 | return default 1021 | 1022 | def __getitem__(self, i): 1023 | if self.key2: 1024 | return IndexRowset(self._cols, None, self.key2, self._items.get(i, {})) 1025 | return Rowset(self._cols, self._items[i]) 1026 | 1027 | def __getstate__(self): 1028 | return (self._cols, self._rows, self._items, self.key, self.key2) 1029 | 1030 | def __setstate__(self, state): 1031 | self._cols, self._rows, self._items, self.key, self.key2 = state 1032 | self._bind() 1033 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import re 5 | import sys 6 | 7 | if sys.version_info < (2, 6): 8 | sys.stderr.write('ERROR: requires Python versions >= 2.6 or 3.3+\n') 9 | sys.exit(1) 10 | 11 | from setuptools import setup 12 | 13 | 14 | def find_version(filename): 15 | """Uses re to pull out the assigned value to __version__ in filename.""" 16 | 17 | with io.open(filename, "r", encoding="utf-8") as version_file: 18 | version_match = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', 19 | version_file.read(), re.M) 20 | if version_match: 21 | return version_match.group(1) 22 | return "0.0.0" 23 | 24 | 25 | setup( 26 | # GENERAL INFO 27 | name='eveapi', 28 | version=find_version("eveapi.py"), 29 | description='Python library for accessing the EVE Online API.', 30 | author='Jamie van den Berge', 31 | author_email='jamie@hlekkir.com', 32 | url='https://github.com/ntt/eveapi', 33 | keywords=('eve-online', 'api'), 34 | platforms='any', 35 | install_requires=[ 36 | 'future>=0.15', 37 | ], 38 | classifiers=[ 39 | 'Development Status :: 5 - Production/Stable', 40 | 'License :: OSI Approved :: MIT License', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python', 43 | 'Programming Language :: Python :: 2.6', 44 | 'Programming Language :: Python :: 2.7', 45 | 'Programming Language :: Python :: 3', 46 | 'Programming Language :: Python :: 3.3', 47 | 'Programming Language :: Python :: 3.4', 48 | 'Programming Language :: Python :: 3.5', 49 | 'Topic :: Games/Entertainment', 50 | ], 51 | # CONTENTS 52 | zip_safe=True, 53 | py_modules=['eveapi'], 54 | ) 55 | --------------------------------------------------------------------------------