From 7ed28965c8fb953fb095fcc5a86107175b86c163 Mon Sep 17 00:00:00 2001 From: Ryan Campbell Date: Tue, 20 Apr 2010 10:45:14 -0600 Subject: [PATCH] moved the code into src/ and added LICENSE file --- apicache.py | 80 ----- eveapi/apitest.py | 416 ------------------------- eveapi/eveapi.py | 787 ------------------------------------------------ fetchimg.py | 32 -- imgs/error.jpg | Bin 598 -> 0 bytes mevemon.py | 146 --------- src/LICENSE | 674 +++++++++++++++++++++++++++++++++++++++++ src/apicache.py | 80 +++++ src/eveapi/apitest.py | 416 +++++++++++++++++++++++++ src/eveapi/eveapi.py | 787 ++++++++++++++++++++++++++++++++++++++++++++++++ src/fetchimg.py | 32 ++ src/imgs/error.jpg | Bin 0 -> 598 bytes src/mevemon.py | 146 +++++++++ src/ui/diablo/ui.py | 255 ++++++++++++++++ src/ui/fremantle/ui.py | 270 +++++++++++++++++ ui/diablo/ui.py | 255 ---------------- ui/fremantle/ui.py | 270 ----------------- 17 files changed, 2660 insertions(+), 1986 deletions(-) delete mode 100644 apicache.py delete mode 100644 eveapi/__init__.py delete mode 100644 eveapi/apitest.py delete mode 100644 eveapi/eveapi.py delete mode 100644 fetchimg.py delete mode 100644 imgs/error.jpg delete mode 100644 mevemon.py create mode 100644 src/LICENSE create mode 100644 src/apicache.py create mode 100644 src/eveapi/__init__.py create mode 100644 src/eveapi/apitest.py create mode 100644 src/eveapi/eveapi.py create mode 100644 src/fetchimg.py create mode 100644 src/imgs/error.jpg create mode 100644 src/mevemon.py create mode 100644 src/ui/__init__.py create mode 100644 src/ui/diablo/__init__.py create mode 100644 src/ui/diablo/ui.py create mode 100644 src/ui/fremantle/__init__.py create mode 100644 src/ui/fremantle/ui.py delete mode 100644 ui/__init__.py delete mode 100644 ui/diablo/__init__.py delete mode 100644 ui/diablo/ui.py delete mode 100644 ui/fremantle/__init__.py delete mode 100644 ui/fremantle/ui.py diff --git a/apicache.py b/apicache.py deleted file mode 100644 index a8f4e18..0000000 --- a/apicache.py +++ /dev/null @@ -1,80 +0,0 @@ -import time -import tempfile -import cPickle -import zlib -import os -from os.path import join, exists - -class cache_handler( object ): - # adapted from http://home.wanadoo.nl/ntt/eve/library/files/api/apitest.py (does this satisfy the terms of the license?), will need work, but we need basic cache functionality... I feel guilty for abusing the server. FIXME --danny - - def __init__( self, debug = False ): - self.debug = debug - self.count = 0 - self.cache = {} - self.tempdir = join( tempfile.gettempdir(), "eveapi" ) - if not exists( self.tempdir ): - os.makedirs( self.tempdir ) - - # remove this later --danny - def log( self, what ): - if self.debug: - print "[%d] %s" % ( self.count, what ) - - def retrieve( self, host, path, params ): - # eveapi asks if we have this request cached - key = hash( ( host, path, frozenset( params.items() ) ) ) - - # for logging - self.count += 1 - - # see if we have the requested page cached... - cached = self.cache.get( key, None ) - if cached: - cacheFile = None - else: - # not in memory, maybe on disk --danny - cacheFile = join( self.tempdir, str( key ) + ".cache" ) - if exists( cacheFile ): - self.log( "%s: retreiving from disk." % path ) - f = open( cacheFile, "rb" ) - cached = self.cache[key] = cPickle.loads( zlib.decompress( f.read() ) ) - f.close() - - if cached: - # check if the cached doc is fresh enough - if time.time() < cached[0]: - self.log( "%s: returning cached document." % path ) - # return the cached XML doc - return cached[1] - - # if it's stale, purge it --danny - self.log( "%s: cache expired, purging!" % path ) - del self.cache[key] - if cacheFile: - os.remove( cacheFile ) - - self.log( "%s: not cached, fetching from server..." % path ) - # We didn't get a cache hit so return None to indicate that the data should be requested from server - return None - - def store( self, host, path, params, doc, obj ): - # eveapi is asking us to cache an item - key = hash( ( host, path, frozenset( params.items() ) ) ) - - cachedFor = obj.cachedUntil - obj.currentTime - if cachedFor: - self.log( "%s: cached (%d seconds)." % ( path, cachedFor ) ) - - cachedUntil = time.time() + cachedFor - - # store in memory - cached = self.cache[key] = ( cachedUntil, doc ) - - # store in cache folder - cacheFile = join( self.tempdir, str( key ) + ".cache" ) - f = open( cacheFile, "wb" ) - f.write( zlib.compress( cPickle.dumps( cached, -1 ) ) ) - f.close - - diff --git a/eveapi/__init__.py b/eveapi/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/eveapi/apitest.py b/eveapi/apitest.py deleted file mode 100644 index b37b9b3..0000000 --- a/eveapi/apitest.py +++ /dev/null @@ -1,416 +0,0 @@ -#============================================================================= -# eveapi module demonstration script - Jamie van den Berge -#============================================================================= -# -# This file is in the Public Domain - Do with it as you please. -# -# 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 -# -#---------------------------------------------------------------------------- -# Put your userID and apiKey (full access) here before running this script. -YOUR_USERID = 123456 -YOUR_APIKEY = "hPx6rxdYfVNeGcuOgPKRL-ohhithere-aUg6OfxCtMH4FUn5GUzf8YqIQDdc5gF7" - -import time -import tempfile -import cPickle -import zlib -import os -from os.path import join, exists -from httplib import HTTPException - -import eveapi - -api = eveapi.EVEAPIConnection() - -#---------------------------------------------------------------------------- -print -print "EXAMPLE 1: GETTING THE ALLIANCE LIST" -print " (and showing alliances with 1000 or more members)" -print - -# Let's get the list of alliances. -# The API function we need to get the list is: -# -# /eve/AllianceList.xml.aspx -# -# There is a 1:1 correspondence between folders/files and attributes on api -# objects, so to call this particular function, we simply do this: -result1 = api.eve.AllianceList() - -# This result contains a rowset object called "alliances". Rowsets are like -# database tables and you can do various useful things with them. For now -# we'll just iterate over it and display all alliances with more than 1000 -# members: -for alliance in result1.alliances: - if alliance.memberCount >= 1000: - print "%s <%s> has %d members" %\ - (alliance.name, alliance.shortName, alliance.memberCount) - - -#----------------------------------------------------------------------------- -print -print "EXAMPLE 2: GETTING WALLET BALANCE OF ALL YOUR CHARACTERS" -print - -# To get any info on character/corporation related stuff, we need to acquire -# an authentication context. All API requests that require authentication need -# to be called through this object. While it is possible to call such API -# functions directly through the api object, you would have to specify the -# userID and apiKey on every call. If you are iterating over many accounts, -# that may actually be the better option. However, for these examples we only -# use one account, so this is more convenient. -auth = api.auth(userID=YOUR_USERID, apiKey=YOUR_APIKEY) - -# Now let's say you want to the wallet balance of all your characters. -# The API function we need to get the characters on your account is: -# -# /account/Characters.xml.aspx -# -# As in example 1, this simply means adding folder names as attributes -# and calling the function named after the base page name: -result2 = auth.account.Characters() - -# Some tracking for later examples. -rich = 0 -rich_charID = 0 - -# Now the best way to iterate over the characters on your account and show -# the isk balance is probably this way: -for character in result2.characters: - wallet = auth.char.AccountBalance(characterID=character.characterID) - isk = wallet.accounts[0].balance - print character.name, "has", isk, "ISK." - - if isk > rich: - rich = isk - rich_charID = character.characterID - - - -#----------------------------------------------------------------------------- -print -print "EXAMPLE 3: WHEN STUFF GOES WRONG" -print - -# Obviously you cannot assume an API call to succeed. There's a myriad of -# things that can go wrong: -# -# - Connection error -# - Server error -# - Invalid parameters passed -# - Hamsters died -# -# Therefor it is important to handle errors properly. eveapi will raise -# an AttributeError if the requested function does not exist on the server -# (ie. when it returns a 404), a RuntimeError on any other webserver error -# (such as 500 Internal Server error). -# On top of this, you can get any of the httplib (which eveapi uses) and -# socket (which httplib uses) exceptions so you might want to catch those -# as well. -# - -try: - # Try calling account/Characters without authentication context - api.account.Characters() -except eveapi.Error, e: - print "Oops! eveapi returned the following error:" - print "code:", e.code - print "message:", e.message -except Exception, e: - print "Something went horribly wrong:", str(e) - raise - - -#----------------------------------------------------------------------------- -print -print "EXAMPLE 4: GETTING CHARACTER SHEET INFORMATION" -print - -# We grab ourselves a character context object. -# Note that this is a convenience function that takes care of passing the -# characterID=x parameter to every API call much like auth() does (in fact -# it's exactly like that, apart from the fact it also automatically adds the -# "/char" folder). Again, it is possible to use the API functions directly -# from the api or auth context, but then you have to provide the missing -# keywords on every call (characterID in this case). -# -# The victim we'll use is the last character on the account we used in -# example 1. -me = auth.character(result2.characters[-1].characterID) - -# Now that we have a character context, we can display skills trained on -# a character. First we have to get the skill tree. A real application -# would cache this data; all objects returned by the api interface can be -# pickled. -skilltree = api.eve.SkillTree() - -# Now we have to fetch the charactersheet. -# Note that the call below is identical to: -# -# acc.char.CharacterSheet(characterID=your_character_id) -# -# But, as explained above, the context ("me") we created automatically takes -# care of adding the characterID parameter and /char folder attribute. -sheet = me.CharacterSheet() - -# This list should look familiar. They're the skillpoints at each level for -# a rank 1 skill. We could use the formula, but this is much simpler :) -sp = [0, 250, 1414, 8000, 45255, 256000] - -total_sp = 0 -total_skills = 0 - -# Now the fun bit starts. We walk the skill tree, and for every group in the -# tree... -for g in skilltree.skillGroups: - - skills_trained_in_this_group = False - - # ... iterate over the skills in this group... - for skill in g.skills: - - # see if we trained this skill by checking the character sheet object - trained = sheet.skills.Get(skill.typeID, False) - if trained: - # yep, we trained this skill. - - # print the group name if we haven't done so already - if not skills_trained_in_this_group: - print g.groupName - skills_trained_in_this_group = True - - # and display some info about the skill! - print "- %s Rank(%d) - SP: %d/%d - Level: %d" %\ - (skill.typeName, skill.rank, trained.skillpoints, (skill.rank * sp[trained.level]), trained.level) - total_skills += 1 - total_sp += trained.skillpoints - - -# And to top it off, display totals. -print "You currently have %d skills and %d skill points" % (total_skills, total_sp) - - - -#----------------------------------------------------------------------------- -print -print "EXAMPLE 5: USING ROWSETS" -print - -# For this one we will use the result1 that contains the alliance list from -# the first example. -rowset = result1.alliances - -# Now, what if we want to sort the alliances by ticker name. We could unpack -# all alliances into a list and then use python's sort(key=...) on that list, -# but that's not efficient. The rowset objects support sorting on columns -# directly: -rowset.SortBy("shortName") - -# Note the use of Select() here. The Select method speeds up iterating over -# large rowsets considerably as no temporary row instances are created. -for ticker in rowset.Select("shortName"): - print ticker, -print - -# The sort above modified the result inplace. There is another method, called -# SortedBy, which returns a new rowset. - -print - -# Another useful method of rowsets is IndexBy, which enables you to do direct -# key lookups on columns. We already used this feature in example 3. Indeed -# most rowsets returned are IndexRowsets already if the data has a primary -# key attribute defined in the tag in the XML data. -# -# IndexRowsets are efficient, they reference the data from the rowset they -# were created from, and create an index mapping on top of it. -# -# Anyway, to create an index: -alliances_by_ticker = rowset.IndexedBy("shortName") - -# Now use the Get() method to get a row directly. -# Assumes ISD alliance exists. If it doesn't, we probably have bigger -# problems than the unhandled exception here -_- -try: - print alliances_by_ticker.Get("ISD") -except : - print "Blimey! CCP let the ISD alliance expire -AGAIN-. How inconvenient!" - -# You may specify a default to return in case the row wasn't found: -print alliances_by_ticker.Get("123456", 42) - -# If no default was specified and you try to look up a key that does not -# exist, an appropriate exception will be raised: -try: - print alliances_by_ticker.Get("123456") -except KeyError: - print "This concludes example 5" - - - -#----------------------------------------------------------------------------- -print -print "EXAMPLE 6: CACHING DATA" -print - -# For some calls you will want caching. To facilitate this, a customized -# cache handler can be attached. Below is an example of a simple cache -# handler. - -class MyCacheHandler(object): - # Note: this is an example handler to demonstrate how to use them. - # a -real- handler should probably be thread-safe and handle errors - # properly (and perhaps use a better hashing scheme). - - def __init__(self, debug=False): - self.debug = debug - self.count = 0 - self.cache = {} - self.tempdir = join(tempfile.gettempdir(), "eveapi") - if not exists(self.tempdir): - os.makedirs(self.tempdir) - - def log(self, what): - if self.debug: - print "[%d] %s" % (self.count, what) - - def retrieve(self, host, path, params): - # eveapi asks if we have this request cached - key = hash((host, path, frozenset(params.items()))) - - self.count += 1 # for logging - - # see if we have the requested page cached... - cached = self.cache.get(key, None) - if cached: - cacheFile = None - #print "'%s': retrieving from memory" % path - else: - # it wasn't cached in memory, but it might be on disk. - cacheFile = join(self.tempdir, str(key) + ".cache") - if exists(cacheFile): - self.log("%s: retrieving from disk" % path) - f = open(cacheFile, "rb") - cached = self.cache[key] = cPickle.loads(zlib.decompress(f.read())) - f.close() - - if cached: - # check if the cached doc is fresh enough - if time.time() < cached[0]: - self.log("%s: returning cached document" % path) - return cached[1] # return the cached XML doc - - # it's stale. purge it. - self.log("%s: cache expired, purging!" % path) - del self.cache[key] - if cacheFile: - os.remove(cacheFile) - - self.log("%s: not cached, fetching from server..." % path) - # we didn't get a cache hit so return None to indicate that the data - # should be requested from the server. - return None - - def store(self, host, path, params, doc, obj): - # eveapi is asking us to cache an item - key = hash((host, path, frozenset(params.items()))) - - cachedFor = obj.cachedUntil - obj.currentTime - if cachedFor: - self.log("%s: cached (%d seconds)" % (path, cachedFor)) - - cachedUntil = time.time() + cachedFor - - # store in memory - cached = self.cache[key] = (cachedUntil, doc) - - # store in cache folder - cacheFile = join(self.tempdir, str(key) + ".cache") - f = open(cacheFile, "wb") - f.write(zlib.compress(cPickle.dumps(cached, -1))) - f.close() - - -# Now try out the handler! Even though were initializing a new api object -# here, a handler can be attached or removed from an existing one at any -# time with its setcachehandler() method. -cachedApi = eveapi.EVEAPIConnection(cacheHandler=MyCacheHandler(debug=True)) - -# First time around this will fetch the document from the server. That is, -# if this demo is run for the first time, otherwise it will attempt to load -# the cache written to disk on the previous run. -result = cachedApi.eve.SkillTree() - -# But the second time it should be returning the cached version -result = cachedApi.eve.SkillTree() - - - -#----------------------------------------------------------------------------- -print -print "EXAMPLE 7: TRANSACTION DATA" -print "(and doing more nifty stuff with rowsets)" -print - -# okay since we have a caching api object now it is fairly safe to do this -# example repeatedly without server locking you out for an hour every time! - -# Let's use the first character on the account (using the richest character -# found in example 2). Note how we are chaining the various contexts here to -# arrive directly at a character context. If you're not using any intermediate -# contexts in the chain anyway, this is okay. -me = cachedApi.auth(YOUR_USERID, YOUR_APIKEY).character(rich_charID) - -# Now fetch the journal. Since this character context was created through -# the cachedApi object, it will still use the cachehandler from example 5. -journal = me.WalletJournal() - -# Let's see how much we paid SCC in transaction tax in the first page -# of data! - -# Righto, now we -could- sift through the rows and extract what we want, -# but we can do it in a much more clever way using the GroupedBy method -# of the rowset in the result. This creates a mapping that maps keys -# to Rowsets of all rows with that key value in specified column. -# These data structures are also quite efficient as the only extra data -# created is the index and grouping. -entriesByRefType = journal.entries.GroupedBy("refTypeID") - -# Also note that we're using a hardcoded refTypeID of 54 here. You're -# supposed to use .eve.RefTypes() though (however they are not likely -# to be changed anyway so we can get away with it) -# Note the use of Select() to speed things up here. -amount = 0.0 -date = 0 -for taxAmount, date in entriesByRefType[54].Select("amount", "date"): - amount += -taxAmount - -print "You paid a %.2f ISK transaction tax since %s" %\ - (amount, time.asctime(time.gmtime(date))) - - -# You might also want to see how much a certain item yielded you recently. -typeName = "Expanded Cargohold II" -amount = 0.0 - -wallet = me.WalletTransactions() -soldTx = wallet.transactions.GroupedBy("transactionType")["sell"] -for row in soldTx.GroupedBy("typeName")[typeName]: - amount += (row.quantity * row.price) - -print "%s sales yielded %.2f ISK since %s" %\ - (typeName, amount, time.asctime(time.gmtime(row.transactionDateTime))) - -# I'll leave walking the transaction pages as an excercise to the reader ;) -# Please also see the eveapi module itself for more documentation. - -# That's all folks! - diff --git a/eveapi/eveapi.py b/eveapi/eveapi.py deleted file mode 100644 index 4633c14..0000000 --- a/eveapi/eveapi.py +++ /dev/null @@ -1,787 +0,0 @@ -#----------------------------------------------------------------------------- -# eveapi - EVE Online API access -# -# Copyright (c)2007 Jamie "Entity" van den Berge -# -# 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.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 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.eve-online.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 (userID, apiKey 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 url.lower().startswith("http://"): - url = url[7:] - - if "/" in url: - url, path = url.split("/", 1) - else: - path = "" - - ctx = _RootContext(None, path, {}, {}) - ctx._handler = cacheHandler - ctx._host = url - 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, userID=None, apiKey=None): - # returns a copy of this object but for every call made through it, the - # userID and apiKey will be added to the API request. - if userID and apiKey: - return _AuthContext(self._root, self._path, self.parameters, {"userID":userID, "apiKey":apiKey}) - raise ValueError("Must specify userID and apiKey") - - 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._proxy is None: - http = httplib.HTTPConnection(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 = httplib.HTTPConnection(*self._proxy) - if kw: - http.request("POST", 'http://'+self._host+path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"}) - else: - http.request("GET", 'http://'+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 - - return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj))) - - -#----------------------------------------------------------------------------- -# XML Parser -#----------------------------------------------------------------------------- - -def _autocast(s): - # attempts to cast an XML string to the most probable type. - try: - if s.strip("-").isdigit(): - return int(s) - except ValueError: - pass - - try: - return float(s) - except ValueError: - pass - - if len(s) == 19 and s[10] == ' ': - # it could be a date string - try: - return max(0, int(timegm(strptime(s, "%Y-%m-%d %H:%M:%S")))) - except OverflowError: - pass - except ValueError: - pass - - # couldn't cast. return string unchanged. - return s - - -class _Parser(object): - - def Parse(self, data, isStream=False): - self.container = self.root = None - p = expat.ParserCreate() - p.StartElementHandler = self.tag_start - p.CharacterDataHandler = self.tag_cdata - 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_start(self, name, attributes): - # - # 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(":")] - # - - if name == "rowset": - # for rowsets, use the given name - try: - columns = attributes[attributes.index('columns')+1].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): - # check for missing columns attribute (see above) - if not self.container._cols: - self.container._cols = attributes[0::2] - - self.container.append([_autocast(attributes[i]) for i in range(1, 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 data == "\r\n" or data.strip() != data: - return - - this = self.container - data = _autocast(data) - - if 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 data 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]) for i in range(1, 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]) for i in range(1, len(attributes), 2)]+[getattr(this, col) for col in attributes2] - rs.append(row) - row = [getattr(sibling, attributes[i]) for i in range(0, len(attributes), 2)]+[getattr(sibling, col) for col in attributes2] - rs.append(row) - rs._cols = [attributes[i] for i in range(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 data case in tag_data()) - pass - - # Now fix up the attributes and be done with it. - for i in range(1, len(attributes), 2): - this.__dict__[attributes[i-1]] = _autocast(attributes[i]) - - 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 "" % self._name - - -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(lambda k, v: "%s:%s" % (str(k), str(v)), 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 retuens 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: - self._ki = ki = cols.index(key) - except IndexError: - raise ValueError("Rowset has no column %s" % key) - - Rowset.__init__(self, cols, rows) - self._key = key - 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) - 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() - diff --git a/fetchimg.py b/fetchimg.py deleted file mode 100644 index d6d1939..0000000 --- a/fetchimg.py +++ /dev/null @@ -1,32 +0,0 @@ -import urllib -import os.path - -def portrait_filename( char_id, img_size ): - - err_img = 'imgs/error.jpg' - - # we can only accept 64 or 256... I know an exclamation point is not an error message, but I'll come back to this. FIXME --danny - if not ( img_size == 64 or img_size == 256 ): - return err_img - - # if asked for the large version, save it under a diff name --danny - if img_size == 64: - filename = "imgs/%s.jpg" % char_id - elif img_size == 256: - filename = "imgs/%s_lg.jpg" % char_id - - if os.path.isfile( filename ): - return filename - - # specify size and cid --danny - img_url = "http://img.eve.is/serv.asp?s=%s&c=%s" % ( str( img_size ), char_id ) - - # fetch it, and hit the road. --danny - try: - urllib.urlretrieve( img_url, filename, report_handler ) - except ContentTooShortError: - filename = err_img - return filename - -def report_handler( *a ): - ( blocks_transferred, block_size, total_size ) = a diff --git a/imgs/error.jpg b/imgs/error.jpg deleted file mode 100644 index e8a7b9ae7aee38131073b6482683939c3c090cf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 598 zcmex=S7or{Bmi=PXKg?PERc!fj+1cU?x zLC5bm;@P_ z1sVSzVUS>8WMF1wKn9FV%D+US~CbAe!6fSH$7&Pg_|62?^K>a`oK?Zw<#|m((Knz}k zq_3N<%YEH%{PAvdwBS7HjLcf^qt&J5^96;C`z=~N6&#b<>@oL9R=M4arJE(Aue>-m z_u0lhZfa`P?^Llb$?bq*8|5mPiJO6v`-MGHO)$#l6xA@j)K0Eg_ z(k{tvk)l-H4ga-sUfn)&q{hYlWwW~iP$%coUHZ|-9~;`oUNoHX`^{F}%Ng?@8|HVO zD!PB=WcH6&Gv|u-xLMrH_5OM*_x7r^8;_0W)QNf<0CkET&AP0boAotJeQ$lnmNbib zQJGQN88iD<9eI|0rp{;E+D}ne*88R(^*W|J^U2jMQI`#6MQ5G~@9*97W>sVZ&;U4K H{(lnya|x#> diff --git a/mevemon.py b/mevemon.py deleted file mode 100644 index bf8448c..0000000 --- a/mevemon.py +++ /dev/null @@ -1,146 +0,0 @@ -# -# mEveMon - A character monitor for EVE Online -# Copyright (c) 2010 Ryan and Danny Campbell, and the mEveMon Team -# -# mEveMon is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# mEveMon is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - - -import hildon -import gtk -from eveapi import eveapi -import fetchimg -import apicache - -# we will store our preferences in gconf -import gnome.gconf - -#ugly hack to check maemo version. any better way? -if hasattr(hildon, "StackableWindow"): - from ui.fremantle import ui -else: - from ui.diablo import ui - -class mEveMon(): - def __init__(self): - self.program = hildon.Program() - self.program.__init__() - self.gconf = gnome.gconf.client_get_default() - self.set_auth() - self.ui = ui.mEveMonUI(self) - - def run(self): - gtk.main() - - def quit(self, *args): - gtk.main_quit() - - def get_api_key(self): - return self.gconf.get_string("/apps/maemo/mevemon/eve_api_key") or '' - - def get_uid(self): - return self.gconf.get_string("/apps/maemo/mevemon/eve_uid") or '' - - def set_api_key(self, key): - self.gconf.set_string("/apps/maemo/mevemon/eve_api_key", key) - - def set_uid(self, uid): - self.gconf.set_string("/apps/maemo/mevemon/eve_uid", uid) - - - def set_auth(self): - """ - set self.auth to None if there was a problem. somehow later on we'll - have to pass the error to the UI, but for now I just want the program - to not be broken. --danny - """ - uid = self.get_uid() - api_key = self.get_api_key() - self.cached_api = eveapi.EVEAPIConnection( cacheHandler = \ - apicache.cache_handler( debug = False ) ) - try: - self.auth = self.cached_api.auth( userID = uid, apiKey = api_key ) - except eveapi.Error, e: - # we need to deal with this, so raise --danny - raise - except ValueError, e: - self.auth = None - #raise - - def get_auth(self): - return self.auth - - def get_char_sheet(self, charID): - if not self.auth: return None - try: - sheet = self.auth.character(charID).CharacterSheet() - except eveapi.Error, e: - # we should really have a logger that logs this error somewhere - return None - - return sheet - - def char_id2name(self, charID): - # the api can take a comma-seperated list of ids, but we'll just take - # a single id for now - try: - chars = self.cached_api.eve.CharacterName(ids=charID).characters - name = chars[0].characterName - except eveapi.Error, e: - return None - - return name - - def char_name2id(self, name): - # the api can take a comma-seperated list of names, but we'll just take - # a single name for now - try: - chars = self.cached_api.eve.CharacterID(names=name).characters - char_id = chars[0].characterID - except eveapi.Error, e: - return None - - return char_id - - - def get_characters( self ): - """ - returns a list containing a single character with an error message for a - name, if there's a problem. FIXME --danny - """ - ui_char_list = [] - placeholder_chars = [("Please check your API settings.", "imgs/error.jpg")] - if not self.auth: return placeholder_chars - try: - api_char_list = self.auth.account.Characters() - # append each char we get to the list we'll return to the - # UI --danny - for character in api_char_list.characters: - ui_char_list.append( ( character.name, fetchimg.portrait_filename( character.characterID, 64 ) ) ) - except eveapi.Error, e: - # again, we need to handle this... --danny - raise - - return ui_char_list - - def get_portrait(self, char_name, size): - """ - returns the relative path of the retrieved portrait - """ - charID = self.char_name2id(char_name) - return fetchimg.portrait_filename(charID, size) - -if __name__ == "__main__": - app = mEveMon() - app.run() diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/src/apicache.py b/src/apicache.py new file mode 100644 index 0000000..a8f4e18 --- /dev/null +++ b/src/apicache.py @@ -0,0 +1,80 @@ +import time +import tempfile +import cPickle +import zlib +import os +from os.path import join, exists + +class cache_handler( object ): + # adapted from http://home.wanadoo.nl/ntt/eve/library/files/api/apitest.py (does this satisfy the terms of the license?), will need work, but we need basic cache functionality... I feel guilty for abusing the server. FIXME --danny + + def __init__( self, debug = False ): + self.debug = debug + self.count = 0 + self.cache = {} + self.tempdir = join( tempfile.gettempdir(), "eveapi" ) + if not exists( self.tempdir ): + os.makedirs( self.tempdir ) + + # remove this later --danny + def log( self, what ): + if self.debug: + print "[%d] %s" % ( self.count, what ) + + def retrieve( self, host, path, params ): + # eveapi asks if we have this request cached + key = hash( ( host, path, frozenset( params.items() ) ) ) + + # for logging + self.count += 1 + + # see if we have the requested page cached... + cached = self.cache.get( key, None ) + if cached: + cacheFile = None + else: + # not in memory, maybe on disk --danny + cacheFile = join( self.tempdir, str( key ) + ".cache" ) + if exists( cacheFile ): + self.log( "%s: retreiving from disk." % path ) + f = open( cacheFile, "rb" ) + cached = self.cache[key] = cPickle.loads( zlib.decompress( f.read() ) ) + f.close() + + if cached: + # check if the cached doc is fresh enough + if time.time() < cached[0]: + self.log( "%s: returning cached document." % path ) + # return the cached XML doc + return cached[1] + + # if it's stale, purge it --danny + self.log( "%s: cache expired, purging!" % path ) + del self.cache[key] + if cacheFile: + os.remove( cacheFile ) + + self.log( "%s: not cached, fetching from server..." % path ) + # We didn't get a cache hit so return None to indicate that the data should be requested from server + return None + + def store( self, host, path, params, doc, obj ): + # eveapi is asking us to cache an item + key = hash( ( host, path, frozenset( params.items() ) ) ) + + cachedFor = obj.cachedUntil - obj.currentTime + if cachedFor: + self.log( "%s: cached (%d seconds)." % ( path, cachedFor ) ) + + cachedUntil = time.time() + cachedFor + + # store in memory + cached = self.cache[key] = ( cachedUntil, doc ) + + # store in cache folder + cacheFile = join( self.tempdir, str( key ) + ".cache" ) + f = open( cacheFile, "wb" ) + f.write( zlib.compress( cPickle.dumps( cached, -1 ) ) ) + f.close + + diff --git a/src/eveapi/__init__.py b/src/eveapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/eveapi/apitest.py b/src/eveapi/apitest.py new file mode 100644 index 0000000..b37b9b3 --- /dev/null +++ b/src/eveapi/apitest.py @@ -0,0 +1,416 @@ +#============================================================================= +# eveapi module demonstration script - Jamie van den Berge +#============================================================================= +# +# This file is in the Public Domain - Do with it as you please. +# +# 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 +# +#---------------------------------------------------------------------------- +# Put your userID and apiKey (full access) here before running this script. +YOUR_USERID = 123456 +YOUR_APIKEY = "hPx6rxdYfVNeGcuOgPKRL-ohhithere-aUg6OfxCtMH4FUn5GUzf8YqIQDdc5gF7" + +import time +import tempfile +import cPickle +import zlib +import os +from os.path import join, exists +from httplib import HTTPException + +import eveapi + +api = eveapi.EVEAPIConnection() + +#---------------------------------------------------------------------------- +print +print "EXAMPLE 1: GETTING THE ALLIANCE LIST" +print " (and showing alliances with 1000 or more members)" +print + +# Let's get the list of alliances. +# The API function we need to get the list is: +# +# /eve/AllianceList.xml.aspx +# +# There is a 1:1 correspondence between folders/files and attributes on api +# objects, so to call this particular function, we simply do this: +result1 = api.eve.AllianceList() + +# This result contains a rowset object called "alliances". Rowsets are like +# database tables and you can do various useful things with them. For now +# we'll just iterate over it and display all alliances with more than 1000 +# members: +for alliance in result1.alliances: + if alliance.memberCount >= 1000: + print "%s <%s> has %d members" %\ + (alliance.name, alliance.shortName, alliance.memberCount) + + +#----------------------------------------------------------------------------- +print +print "EXAMPLE 2: GETTING WALLET BALANCE OF ALL YOUR CHARACTERS" +print + +# To get any info on character/corporation related stuff, we need to acquire +# an authentication context. All API requests that require authentication need +# to be called through this object. While it is possible to call such API +# functions directly through the api object, you would have to specify the +# userID and apiKey on every call. If you are iterating over many accounts, +# that may actually be the better option. However, for these examples we only +# use one account, so this is more convenient. +auth = api.auth(userID=YOUR_USERID, apiKey=YOUR_APIKEY) + +# Now let's say you want to the wallet balance of all your characters. +# The API function we need to get the characters on your account is: +# +# /account/Characters.xml.aspx +# +# As in example 1, this simply means adding folder names as attributes +# and calling the function named after the base page name: +result2 = auth.account.Characters() + +# Some tracking for later examples. +rich = 0 +rich_charID = 0 + +# Now the best way to iterate over the characters on your account and show +# the isk balance is probably this way: +for character in result2.characters: + wallet = auth.char.AccountBalance(characterID=character.characterID) + isk = wallet.accounts[0].balance + print character.name, "has", isk, "ISK." + + if isk > rich: + rich = isk + rich_charID = character.characterID + + + +#----------------------------------------------------------------------------- +print +print "EXAMPLE 3: WHEN STUFF GOES WRONG" +print + +# Obviously you cannot assume an API call to succeed. There's a myriad of +# things that can go wrong: +# +# - Connection error +# - Server error +# - Invalid parameters passed +# - Hamsters died +# +# Therefor it is important to handle errors properly. eveapi will raise +# an AttributeError if the requested function does not exist on the server +# (ie. when it returns a 404), a RuntimeError on any other webserver error +# (such as 500 Internal Server error). +# On top of this, you can get any of the httplib (which eveapi uses) and +# socket (which httplib uses) exceptions so you might want to catch those +# as well. +# + +try: + # Try calling account/Characters without authentication context + api.account.Characters() +except eveapi.Error, e: + print "Oops! eveapi returned the following error:" + print "code:", e.code + print "message:", e.message +except Exception, e: + print "Something went horribly wrong:", str(e) + raise + + +#----------------------------------------------------------------------------- +print +print "EXAMPLE 4: GETTING CHARACTER SHEET INFORMATION" +print + +# We grab ourselves a character context object. +# Note that this is a convenience function that takes care of passing the +# characterID=x parameter to every API call much like auth() does (in fact +# it's exactly like that, apart from the fact it also automatically adds the +# "/char" folder). Again, it is possible to use the API functions directly +# from the api or auth context, but then you have to provide the missing +# keywords on every call (characterID in this case). +# +# The victim we'll use is the last character on the account we used in +# example 1. +me = auth.character(result2.characters[-1].characterID) + +# Now that we have a character context, we can display skills trained on +# a character. First we have to get the skill tree. A real application +# would cache this data; all objects returned by the api interface can be +# pickled. +skilltree = api.eve.SkillTree() + +# Now we have to fetch the charactersheet. +# Note that the call below is identical to: +# +# acc.char.CharacterSheet(characterID=your_character_id) +# +# But, as explained above, the context ("me") we created automatically takes +# care of adding the characterID parameter and /char folder attribute. +sheet = me.CharacterSheet() + +# This list should look familiar. They're the skillpoints at each level for +# a rank 1 skill. We could use the formula, but this is much simpler :) +sp = [0, 250, 1414, 8000, 45255, 256000] + +total_sp = 0 +total_skills = 0 + +# Now the fun bit starts. We walk the skill tree, and for every group in the +# tree... +for g in skilltree.skillGroups: + + skills_trained_in_this_group = False + + # ... iterate over the skills in this group... + for skill in g.skills: + + # see if we trained this skill by checking the character sheet object + trained = sheet.skills.Get(skill.typeID, False) + if trained: + # yep, we trained this skill. + + # print the group name if we haven't done so already + if not skills_trained_in_this_group: + print g.groupName + skills_trained_in_this_group = True + + # and display some info about the skill! + print "- %s Rank(%d) - SP: %d/%d - Level: %d" %\ + (skill.typeName, skill.rank, trained.skillpoints, (skill.rank * sp[trained.level]), trained.level) + total_skills += 1 + total_sp += trained.skillpoints + + +# And to top it off, display totals. +print "You currently have %d skills and %d skill points" % (total_skills, total_sp) + + + +#----------------------------------------------------------------------------- +print +print "EXAMPLE 5: USING ROWSETS" +print + +# For this one we will use the result1 that contains the alliance list from +# the first example. +rowset = result1.alliances + +# Now, what if we want to sort the alliances by ticker name. We could unpack +# all alliances into a list and then use python's sort(key=...) on that list, +# but that's not efficient. The rowset objects support sorting on columns +# directly: +rowset.SortBy("shortName") + +# Note the use of Select() here. The Select method speeds up iterating over +# large rowsets considerably as no temporary row instances are created. +for ticker in rowset.Select("shortName"): + print ticker, +print + +# The sort above modified the result inplace. There is another method, called +# SortedBy, which returns a new rowset. + +print + +# Another useful method of rowsets is IndexBy, which enables you to do direct +# key lookups on columns. We already used this feature in example 3. Indeed +# most rowsets returned are IndexRowsets already if the data has a primary +# key attribute defined in the tag in the XML data. +# +# IndexRowsets are efficient, they reference the data from the rowset they +# were created from, and create an index mapping on top of it. +# +# Anyway, to create an index: +alliances_by_ticker = rowset.IndexedBy("shortName") + +# Now use the Get() method to get a row directly. +# Assumes ISD alliance exists. If it doesn't, we probably have bigger +# problems than the unhandled exception here -_- +try: + print alliances_by_ticker.Get("ISD") +except : + print "Blimey! CCP let the ISD alliance expire -AGAIN-. How inconvenient!" + +# You may specify a default to return in case the row wasn't found: +print alliances_by_ticker.Get("123456", 42) + +# If no default was specified and you try to look up a key that does not +# exist, an appropriate exception will be raised: +try: + print alliances_by_ticker.Get("123456") +except KeyError: + print "This concludes example 5" + + + +#----------------------------------------------------------------------------- +print +print "EXAMPLE 6: CACHING DATA" +print + +# For some calls you will want caching. To facilitate this, a customized +# cache handler can be attached. Below is an example of a simple cache +# handler. + +class MyCacheHandler(object): + # Note: this is an example handler to demonstrate how to use them. + # a -real- handler should probably be thread-safe and handle errors + # properly (and perhaps use a better hashing scheme). + + def __init__(self, debug=False): + self.debug = debug + self.count = 0 + self.cache = {} + self.tempdir = join(tempfile.gettempdir(), "eveapi") + if not exists(self.tempdir): + os.makedirs(self.tempdir) + + def log(self, what): + if self.debug: + print "[%d] %s" % (self.count, what) + + def retrieve(self, host, path, params): + # eveapi asks if we have this request cached + key = hash((host, path, frozenset(params.items()))) + + self.count += 1 # for logging + + # see if we have the requested page cached... + cached = self.cache.get(key, None) + if cached: + cacheFile = None + #print "'%s': retrieving from memory" % path + else: + # it wasn't cached in memory, but it might be on disk. + cacheFile = join(self.tempdir, str(key) + ".cache") + if exists(cacheFile): + self.log("%s: retrieving from disk" % path) + f = open(cacheFile, "rb") + cached = self.cache[key] = cPickle.loads(zlib.decompress(f.read())) + f.close() + + if cached: + # check if the cached doc is fresh enough + if time.time() < cached[0]: + self.log("%s: returning cached document" % path) + return cached[1] # return the cached XML doc + + # it's stale. purge it. + self.log("%s: cache expired, purging!" % path) + del self.cache[key] + if cacheFile: + os.remove(cacheFile) + + self.log("%s: not cached, fetching from server..." % path) + # we didn't get a cache hit so return None to indicate that the data + # should be requested from the server. + return None + + def store(self, host, path, params, doc, obj): + # eveapi is asking us to cache an item + key = hash((host, path, frozenset(params.items()))) + + cachedFor = obj.cachedUntil - obj.currentTime + if cachedFor: + self.log("%s: cached (%d seconds)" % (path, cachedFor)) + + cachedUntil = time.time() + cachedFor + + # store in memory + cached = self.cache[key] = (cachedUntil, doc) + + # store in cache folder + cacheFile = join(self.tempdir, str(key) + ".cache") + f = open(cacheFile, "wb") + f.write(zlib.compress(cPickle.dumps(cached, -1))) + f.close() + + +# Now try out the handler! Even though were initializing a new api object +# here, a handler can be attached or removed from an existing one at any +# time with its setcachehandler() method. +cachedApi = eveapi.EVEAPIConnection(cacheHandler=MyCacheHandler(debug=True)) + +# First time around this will fetch the document from the server. That is, +# if this demo is run for the first time, otherwise it will attempt to load +# the cache written to disk on the previous run. +result = cachedApi.eve.SkillTree() + +# But the second time it should be returning the cached version +result = cachedApi.eve.SkillTree() + + + +#----------------------------------------------------------------------------- +print +print "EXAMPLE 7: TRANSACTION DATA" +print "(and doing more nifty stuff with rowsets)" +print + +# okay since we have a caching api object now it is fairly safe to do this +# example repeatedly without server locking you out for an hour every time! + +# Let's use the first character on the account (using the richest character +# found in example 2). Note how we are chaining the various contexts here to +# arrive directly at a character context. If you're not using any intermediate +# contexts in the chain anyway, this is okay. +me = cachedApi.auth(YOUR_USERID, YOUR_APIKEY).character(rich_charID) + +# Now fetch the journal. Since this character context was created through +# the cachedApi object, it will still use the cachehandler from example 5. +journal = me.WalletJournal() + +# Let's see how much we paid SCC in transaction tax in the first page +# of data! + +# Righto, now we -could- sift through the rows and extract what we want, +# but we can do it in a much more clever way using the GroupedBy method +# of the rowset in the result. This creates a mapping that maps keys +# to Rowsets of all rows with that key value in specified column. +# These data structures are also quite efficient as the only extra data +# created is the index and grouping. +entriesByRefType = journal.entries.GroupedBy("refTypeID") + +# Also note that we're using a hardcoded refTypeID of 54 here. You're +# supposed to use .eve.RefTypes() though (however they are not likely +# to be changed anyway so we can get away with it) +# Note the use of Select() to speed things up here. +amount = 0.0 +date = 0 +for taxAmount, date in entriesByRefType[54].Select("amount", "date"): + amount += -taxAmount + +print "You paid a %.2f ISK transaction tax since %s" %\ + (amount, time.asctime(time.gmtime(date))) + + +# You might also want to see how much a certain item yielded you recently. +typeName = "Expanded Cargohold II" +amount = 0.0 + +wallet = me.WalletTransactions() +soldTx = wallet.transactions.GroupedBy("transactionType")["sell"] +for row in soldTx.GroupedBy("typeName")[typeName]: + amount += (row.quantity * row.price) + +print "%s sales yielded %.2f ISK since %s" %\ + (typeName, amount, time.asctime(time.gmtime(row.transactionDateTime))) + +# I'll leave walking the transaction pages as an excercise to the reader ;) +# Please also see the eveapi module itself for more documentation. + +# That's all folks! + diff --git a/src/eveapi/eveapi.py b/src/eveapi/eveapi.py new file mode 100644 index 0000000..4633c14 --- /dev/null +++ b/src/eveapi/eveapi.py @@ -0,0 +1,787 @@ +#----------------------------------------------------------------------------- +# eveapi - EVE Online API access +# +# Copyright (c)2007 Jamie "Entity" van den Berge +# +# 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.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 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.eve-online.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 (userID, apiKey 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 url.lower().startswith("http://"): + url = url[7:] + + if "/" in url: + url, path = url.split("/", 1) + else: + path = "" + + ctx = _RootContext(None, path, {}, {}) + ctx._handler = cacheHandler + ctx._host = url + 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, userID=None, apiKey=None): + # returns a copy of this object but for every call made through it, the + # userID and apiKey will be added to the API request. + if userID and apiKey: + return _AuthContext(self._root, self._path, self.parameters, {"userID":userID, "apiKey":apiKey}) + raise ValueError("Must specify userID and apiKey") + + 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._proxy is None: + http = httplib.HTTPConnection(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 = httplib.HTTPConnection(*self._proxy) + if kw: + http.request("POST", 'http://'+self._host+path, urllib.urlencode(kw), {"Content-type": "application/x-www-form-urlencoded"}) + else: + http.request("GET", 'http://'+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 + + return _ParseXML(response, True, store and (lambda obj: cache.store(self._host, path, kw, response, obj))) + + +#----------------------------------------------------------------------------- +# XML Parser +#----------------------------------------------------------------------------- + +def _autocast(s): + # attempts to cast an XML string to the most probable type. + try: + if s.strip("-").isdigit(): + return int(s) + except ValueError: + pass + + try: + return float(s) + except ValueError: + pass + + if len(s) == 19 and s[10] == ' ': + # it could be a date string + try: + return max(0, int(timegm(strptime(s, "%Y-%m-%d %H:%M:%S")))) + except OverflowError: + pass + except ValueError: + pass + + # couldn't cast. return string unchanged. + return s + + +class _Parser(object): + + def Parse(self, data, isStream=False): + self.container = self.root = None + p = expat.ParserCreate() + p.StartElementHandler = self.tag_start + p.CharacterDataHandler = self.tag_cdata + 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_start(self, name, attributes): + # + # 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(":")] + # + + if name == "rowset": + # for rowsets, use the given name + try: + columns = attributes[attributes.index('columns')+1].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): + # check for missing columns attribute (see above) + if not self.container._cols: + self.container._cols = attributes[0::2] + + self.container.append([_autocast(attributes[i]) for i in range(1, 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 data == "\r\n" or data.strip() != data: + return + + this = self.container + data = _autocast(data) + + if 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 data 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]) for i in range(1, 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]) for i in range(1, len(attributes), 2)]+[getattr(this, col) for col in attributes2] + rs.append(row) + row = [getattr(sibling, attributes[i]) for i in range(0, len(attributes), 2)]+[getattr(sibling, col) for col in attributes2] + rs.append(row) + rs._cols = [attributes[i] for i in range(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 data case in tag_data()) + pass + + # Now fix up the attributes and be done with it. + for i in range(1, len(attributes), 2): + this.__dict__[attributes[i-1]] = _autocast(attributes[i]) + + 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 "" % self._name + + +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(lambda k, v: "%s:%s" % (str(k), str(v)), 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 retuens 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: + self._ki = ki = cols.index(key) + except IndexError: + raise ValueError("Rowset has no column %s" % key) + + Rowset.__init__(self, cols, rows) + self._key = key + 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) + 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() + diff --git a/src/fetchimg.py b/src/fetchimg.py new file mode 100644 index 0000000..d6d1939 --- /dev/null +++ b/src/fetchimg.py @@ -0,0 +1,32 @@ +import urllib +import os.path + +def portrait_filename( char_id, img_size ): + + err_img = 'imgs/error.jpg' + + # we can only accept 64 or 256... I know an exclamation point is not an error message, but I'll come back to this. FIXME --danny + if not ( img_size == 64 or img_size == 256 ): + return err_img + + # if asked for the large version, save it under a diff name --danny + if img_size == 64: + filename = "imgs/%s.jpg" % char_id + elif img_size == 256: + filename = "imgs/%s_lg.jpg" % char_id + + if os.path.isfile( filename ): + return filename + + # specify size and cid --danny + img_url = "http://img.eve.is/serv.asp?s=%s&c=%s" % ( str( img_size ), char_id ) + + # fetch it, and hit the road. --danny + try: + urllib.urlretrieve( img_url, filename, report_handler ) + except ContentTooShortError: + filename = err_img + return filename + +def report_handler( *a ): + ( blocks_transferred, block_size, total_size ) = a diff --git a/src/imgs/error.jpg b/src/imgs/error.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e8a7b9ae7aee38131073b6482683939c3c090cf4 GIT binary patch literal 598 zcmex=S7or{Bmi=PXKg?PERc!fj+1cU?x zLC5bm;@P_ z1sVSzVUS>8WMF1wKn9FV%D+US~CbAe!6fSH$7&Pg_|62?^K>a`oK?Zw<#|m((Knz}k zq_3N<%YEH%{PAvdwBS7HjLcf^qt&J5^96;C`z=~N6&#b<>@oL9R=M4arJE(Aue>-m z_u0lhZfa`P?^Llb$?bq*8|5mPiJO6v`-MGHO)$#l6xA@j)K0Eg_ z(k{tvk)l-H4ga-sUfn)&q{hYlWwW~iP$%coUHZ|-9~;`oUNoHX`^{F}%Ng?@8|HVO zD!PB=WcH6&Gv|u-xLMrH_5OM*_x7r^8;_0W)QNf<0CkET&AP0boAotJeQ$lnmNbib zQJGQN88iD<9eI|0rp{;E+D}ne*88R(^*W|J^U2jMQI`#6MQ5G~@9*97W>sVZ&;U4K H{(lnya|x#> literal 0 HcmV?d00001 diff --git a/src/mevemon.py b/src/mevemon.py new file mode 100644 index 0000000..bf8448c --- /dev/null +++ b/src/mevemon.py @@ -0,0 +1,146 @@ +# +# mEveMon - A character monitor for EVE Online +# Copyright (c) 2010 Ryan and Danny Campbell, and the mEveMon Team +# +# mEveMon is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# mEveMon is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + + +import hildon +import gtk +from eveapi import eveapi +import fetchimg +import apicache + +# we will store our preferences in gconf +import gnome.gconf + +#ugly hack to check maemo version. any better way? +if hasattr(hildon, "StackableWindow"): + from ui.fremantle import ui +else: + from ui.diablo import ui + +class mEveMon(): + def __init__(self): + self.program = hildon.Program() + self.program.__init__() + self.gconf = gnome.gconf.client_get_default() + self.set_auth() + self.ui = ui.mEveMonUI(self) + + def run(self): + gtk.main() + + def quit(self, *args): + gtk.main_quit() + + def get_api_key(self): + return self.gconf.get_string("/apps/maemo/mevemon/eve_api_key") or '' + + def get_uid(self): + return self.gconf.get_string("/apps/maemo/mevemon/eve_uid") or '' + + def set_api_key(self, key): + self.gconf.set_string("/apps/maemo/mevemon/eve_api_key", key) + + def set_uid(self, uid): + self.gconf.set_string("/apps/maemo/mevemon/eve_uid", uid) + + + def set_auth(self): + """ + set self.auth to None if there was a problem. somehow later on we'll + have to pass the error to the UI, but for now I just want the program + to not be broken. --danny + """ + uid = self.get_uid() + api_key = self.get_api_key() + self.cached_api = eveapi.EVEAPIConnection( cacheHandler = \ + apicache.cache_handler( debug = False ) ) + try: + self.auth = self.cached_api.auth( userID = uid, apiKey = api_key ) + except eveapi.Error, e: + # we need to deal with this, so raise --danny + raise + except ValueError, e: + self.auth = None + #raise + + def get_auth(self): + return self.auth + + def get_char_sheet(self, charID): + if not self.auth: return None + try: + sheet = self.auth.character(charID).CharacterSheet() + except eveapi.Error, e: + # we should really have a logger that logs this error somewhere + return None + + return sheet + + def char_id2name(self, charID): + # the api can take a comma-seperated list of ids, but we'll just take + # a single id for now + try: + chars = self.cached_api.eve.CharacterName(ids=charID).characters + name = chars[0].characterName + except eveapi.Error, e: + return None + + return name + + def char_name2id(self, name): + # the api can take a comma-seperated list of names, but we'll just take + # a single name for now + try: + chars = self.cached_api.eve.CharacterID(names=name).characters + char_id = chars[0].characterID + except eveapi.Error, e: + return None + + return char_id + + + def get_characters( self ): + """ + returns a list containing a single character with an error message for a + name, if there's a problem. FIXME --danny + """ + ui_char_list = [] + placeholder_chars = [("Please check your API settings.", "imgs/error.jpg")] + if not self.auth: return placeholder_chars + try: + api_char_list = self.auth.account.Characters() + # append each char we get to the list we'll return to the + # UI --danny + for character in api_char_list.characters: + ui_char_list.append( ( character.name, fetchimg.portrait_filename( character.characterID, 64 ) ) ) + except eveapi.Error, e: + # again, we need to handle this... --danny + raise + + return ui_char_list + + def get_portrait(self, char_name, size): + """ + returns the relative path of the retrieved portrait + """ + charID = self.char_name2id(char_name) + return fetchimg.portrait_filename(charID, size) + +if __name__ == "__main__": + app = mEveMon() + app.run() diff --git a/src/ui/__init__.py b/src/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/diablo/__init__.py b/src/ui/diablo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/diablo/ui.py b/src/ui/diablo/ui.py new file mode 100644 index 0000000..c37ddbb --- /dev/null +++ b/src/ui/diablo/ui.py @@ -0,0 +1,255 @@ +# +# mEveMon - A character monitor for EVE Online +# Copyright (c) 2010 Ryan and Danny Campbell, and the mEveMon Team +# +# mEveMon is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# mEveMon is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# Based on Ry's Fremantle Python code. --danny + +import sys + +import gtk +import hildon +import gobject + +class mEveMonUI(): + + about_name = 'mEveMon' + about_text = ('Mobile character monitor for EVE Online') + about_authors = ['Ryan Campbell','Danny Campbell'] + about_website = 'http://example.site.org' + app_version = '0.1' + + menu_items = ("Settings", "About", "Refresh") + + def __init__(self, controller): + self.controller = controller + + gtk.set_application_name("mEveMon") + + # create the main window + win = hildon.Window() + win.connect("destroy", self.controller.quit) + win.show_all() + progress_bar = hildon.hildon_banner_show_progress(win, None, "Loading overview...") + progress_bar.set_fraction( 0.4 ) + + # Create menu + menu = self.create_menu(win) + # Attach menu to the window + win.set_menu(menu) + + # will probably need to refer to http://maemo.org/community/maemo-developers/gtktreeview_issue/ for sample code again when we make these clickable --danny + self.char_model = self.create_char_model() + treeview = gtk.TreeView( model = self.char_model ) + treeview.connect( 'row-activated', self.build_window ) + treeview.set_model(self.char_model) + self.add_columns_to_treeview(treeview) + + win.add(treeview) + win.show_all() + + progress_bar.set_fraction( 1 ) + progress_bar.destroy() + + def build_window(self, treeview, path, view_column): + win = hildon.Window() + win.show_all() + + progress_bar = hildon.hildon_banner_show_progress(win, None, "Loading character sheet...") + progress_bar.set_fraction( 0.4 ) + + # Create menu + # NOTE: we probably want a window-specific menu for this page, but the + # main appmenu works for now + menu = self.create_menu(win) + # Attach menu to the window + win.set_menu(menu) + + #pannable_area = hildon.PannableArea() + + model = treeview.get_model() + miter = model.get_iter(path) + + # column 0 is the portrait, column 1 is name + + char_name = model.get_value(miter, 1) + char_id = self.controller.char_name2id(char_name) + + win.set_title(char_name) + + skillLabel = gtk.Label("Skills") + + # TODO: replace these with api calls + corp_name = "" + skill_points = 0 + + name = gtk.Label("Name: %s" % char_name) + name.set_alignment(0, 0.5) + + corp = gtk.Label("Corp: %s" % corp_name) + corp.set_alignment(0, 0.5) + + balance = gtk.Label("Balance: %s ISK" % + self.controller.get_account_balance(char_id)) + balance.set_alignment(0, 0.5) + + sp = gtk.Label("Skill points: %s" % skill_points) + sp.set_alignment(0, 0.5) + + portrait = gtk.Image() + portrait.set_from_file(self.controller.get_portrait(char_name, 256)) + portrait.show() + + hbox = gtk.HBox(False, 0) + + info_vbox = gtk.VBox(False, 0) + info_vbox.pack_start(name, False, False, 1) + info_vbox.pack_start(corp, False, False, 1) + info_vbox.pack_start(balance, False, False, 1) + info_vbox.pack_start(sp, False, False, 1) + + hbox.pack_start(portrait, False, False, 10) + hbox.pack_start(info_vbox, False, False, 5) + #hbox.pack_start(stats_vbox, False, False, 5) + + vbox = gtk.VBox(False, 0) + #pannable_area.add(vbox) + + vbox.pack_start(hbox, False, False, 0) + vbox.pack_start(skillLabel, False, False, 5) + + win.add(vbox) + win.show_all() + + progress_bar.set_fraction( 1 ) + progress_bar.destroy() + + def create_char_model(self): + lstore = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING) + #get icon and name and put in a liststore + self.fill_char_model(lstore) + return lstore + + def fill_char_model(self, lstore): + char_list = self.controller.get_characters() + for name, icon in char_list: + liter = lstore.append() + lstore.set(liter, 1, name, 0, self.set_pix(icon)) + + def update_model(self, lstore): + lstore.clear() + self.fill_char_model(lstore) + + def set_pix(self, filename): + pixbuf = gtk.gdk.pixbuf_new_from_file(filename) + return pixbuf + + def add_columns_to_treeview(self, treeview): + #Column 0 for the treeview + renderer = gtk.CellRendererPixbuf() + column = gtk.TreeViewColumn() + column.pack_start(renderer, True) + column.add_attribute(renderer, "pixbuf", 0) + treeview.append_column(column) + + #Column 1 for the treeview + renderer = gtk.CellRendererText() + column = gtk.TreeViewColumn('title', renderer, text=1) + column.set_property("expand", True) + treeview.append_column(column) + + + def settings_clicked(self, button, window): + + dialog = gtk.Dialog() + + #get the vbox to pack all the settings into + vbox = dialog.vbox + + dialog.set_transient_for(window) + dialog.set_title("Settings") + + uidLabel = gtk.Label("User ID:") + uidLabel.set_justify(gtk.JUSTIFY_LEFT) + vbox.add(uidLabel) + + uidEntry = gtk.Entry() + uidEntry.set_text(self.controller.get_uid()) + uidEntry.set_property('is_focus', False) + + vbox.add(uidEntry) + + apiLabel = gtk.Label("API key:") + apiLabel.set_justify(gtk.JUSTIFY_LEFT) + vbox.add(apiLabel) + + apiEntry = gtk.Entry() + apiEntry.set_text(self.controller.get_api_key()) + apiEntry.set_property('is_focus', False) + + vbox.add(apiEntry) + + ok_button = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) + help_button = dialog.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP) + + + dialog.show_all() + result = dialog.run() + + if result == gtk.RESPONSE_OK: + self.controller.set_api_key(apiEntry.get_text()) + self.controller.set_uid(uidEntry.get_text()) + self.update_model(self.char_model) + + dialog.destroy() + + return result + + def about_clicked(self, button): + + dialog = gtk.AboutDialog() + dialog.set_website(self.about_website) + dialog.set_website_label(self.about_website) + dialog.set_name(self.about_name) + dialog.set_authors(self.about_authors) + dialog.set_comments(self.about_text) + dialog.set_version(self.app_version) + dialog.run() + dialog.destroy() + + def refresh_clicked(self, button, window): + self.update_model(self.char_model) + + def create_menu(self, window): + menu = gtk.Menu() + for command in self.menu_items: + button = gtk.MenuItem( command ) + if command == "About": + button.connect("activate", self.about_clicked) + elif command == "Settings": + button.connect("activate", self.settings_clicked, window) + elif command == "Refresh": + button.connect("activate", self.refresh_clicked, window) + else: + assert False, command + # Add entry to the view menu + menu.append(button) + menu.show_all() + return menu + +if __name__ == "__main__": + main() + diff --git a/src/ui/fremantle/__init__.py b/src/ui/fremantle/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/fremantle/ui.py b/src/ui/fremantle/ui.py new file mode 100644 index 0000000..6900389 --- /dev/null +++ b/src/ui/fremantle/ui.py @@ -0,0 +1,270 @@ +# +# mEveMon - A character monitor for EVE Online +# Copyright (c) 2010 Ryan and Danny Campbell, and the mEveMon Team +# +# mEveMon is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# mEveMon is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import sys + +import gtk +import hildon +import gobject + +class mEveMonUI(): + + about_name = 'mEveMon' + about_text = ('Mobile character monitor for EVE Online') + about_authors = ['Ryan Campbell', 'Danny Campbell'] + about_website = 'http://example.site.org' + app_version = '0.1' + + menu_items = ("Settings", "About", "Refresh") + + def __init__(self, controller): + self.controller = controller + gtk.set_application_name("mEveMon") + + #create the main window + win = hildon.StackableWindow() + win.connect("destroy", self.controller.quit) + win.show_all() + hildon.hildon_gtk_window_set_progress_indicator(win, 1) + + # Create menu + menu = self.create_menu(win) + # Attach menu to the window + win.set_app_menu(menu) + + pannable_area = hildon.PannableArea() + + # gtk.HILDON_UI_MODE_NORMAL -> not selection in the treeview + # gtk.HILDON_UI_MODE_EDIT -> selection in the treeview + treeview = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL) + treeview.connect('row-activated', self.build_window) + + self.char_model = self.create_char_model() + treeview.set_model(self.char_model) + + self.add_columns_to_treeview(treeview) + + pannable_area.add(treeview) + + win.add(pannable_area); + + win.show_all() + + hildon.hildon_gtk_window_set_progress_indicator(win, 0) + + def build_window(self, treeview, path, view_column): + win = hildon.StackableWindow() + win.show_all() + hildon.hildon_gtk_window_set_progress_indicator(win, 1) + + # Create menu + # NOTE: we probably want a window-specific menu for this page, but the + # main appmenu works for now + menu = self.create_menu(win) + # Attach menu to the window + win.set_app_menu(menu) + + pannable_area = hildon.PannableArea() + + model = treeview.get_model() + miter = model.get_iter(path) + + # column 0 is the portrait, column 1 is name + + char_name = model.get_value(miter, 1) + char_id = self.controller.char_name2id(char_name) + sheet = self.controller.get_char_sheet(char_id) + + win.set_title(char_name) + + skillLabel = gtk.Label("Skills") + + name = gtk.Label("%s" % char_name) + name.set_alignment(0, 0.5) + + race = gtk.Label("%s %s %s" % (sheet.gender, sheet.race, + sheet.bloodLine)) + race.set_alignment(0, 0.5) + + corp = gtk.Label("Corp: %s" % sheet.corporationName) + corp.set_alignment(0, 0.5) + + balance = gtk.Label("Balance: %s ISK" % sheet.balance) + balance.set_alignment(0, 0.5) + + portrait = gtk.Image() + portrait.set_from_file(self.controller.get_portrait(char_name, 256)) + portrait.show() + + hbox = gtk.HBox(False, 0) + + info_vbox = gtk.VBox(False, 0) + info_vbox.pack_start(name, False, False, 1) + info_vbox.pack_start(race, False, False, 1) + info_vbox.pack_start(corp, False, False, 1) + info_vbox.pack_start(balance, False, False, 1) + + hbox.pack_start(portrait, False, False, 10) + hbox.pack_start(info_vbox, False, False, 5) + #hbox.pack_start(stats_vbox, False, False, 5) + + vbox = gtk.VBox(False, 0) + pannable_area.add_with_viewport(vbox) + + vbox.pack_start(hbox, False, False, 0) + vbox.pack_start(skillLabel, False, False, 5) + + win.add(pannable_area) + win.show_all() + + hildon.hildon_gtk_window_set_progress_indicator(win, 0) + + def create_char_model(self): + lstore = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING) + + #get icon and name and put in a liststore + self.fill_char_model(lstore) + + return lstore + + + def fill_char_model(self, lstore): + char_list = self.controller.get_characters() + + for name, icon in char_list: + liter = lstore.append() + lstore.set(liter, 1, name, 0, self.set_pix(icon)) + + + def update_model(self, lstore): + lstore.clear() + self.fill_char_model(lstore) + + + def set_pix(self, filename): + pixbuf = gtk.gdk.pixbuf_new_from_file(filename) + return pixbuf + + + def add_columns_to_treeview(self, treeview): + #Column 0 for the treeview + renderer = gtk.CellRendererPixbuf() + column = gtk.TreeViewColumn() + column.pack_start(renderer, True) + column.add_attribute(renderer, "pixbuf", 0) + treeview.append_column(column) + + #Column 1 for the treeview + renderer = gtk.CellRendererText() + column = gtk.TreeViewColumn('title', renderer, text=1) + column.set_property("expand", True) + treeview.append_column(column) + + + def settings_clicked(self, button, window): + dialog = gtk.Dialog() + + #get the vbox to pack all the settings into + vbox = dialog.vbox + + dialog.set_transient_for(window) + dialog.set_title("Settings") + + uidLabel = gtk.Label("User ID:") + uidLabel.set_justify(gtk.JUSTIFY_LEFT) + vbox.add(uidLabel) + + uidEntry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT) + uidEntry.set_placeholder("User ID") + uidEntry.set_text(self.controller.get_uid()) + uidEntry.set_property('is_focus', False) + + vbox.add(uidEntry) + + apiLabel = gtk.Label("API key:") + apiLabel.set_justify(gtk.JUSTIFY_LEFT) + vbox.add(apiLabel) + + apiEntry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT) + apiEntry.set_placeholder("API Key") + apiEntry.set_text(self.controller.get_api_key()) + apiEntry.set_property('is_focus', False) + + vbox.add(apiEntry) + + ok_button = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) + help_button = dialog.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP) + + dialog.show_all() + result = dialog.run() + if result == gtk.RESPONSE_OK: + self.controller.set_api_key(apiEntry.get_text()) + self.controller.set_uid(uidEntry.get_text()) + self.controller.set_auth() + self.update_model(self.char_model) + + + dialog.destroy() + + return result + + + def about_clicked(self, button): + dialog = gtk.AboutDialog() + dialog.set_website(self.about_website) + dialog.set_website_label(self.about_website) + dialog.set_name(self.about_name) + dialog.set_authors(self.about_authors) + dialog.set_comments(self.about_text) + dialog.set_version(self.app_version) + dialog.run() + dialog.destroy() + + + def refresh_clicked(self, button, window): + self.update_model(self.char_model) + + + def create_menu(self, window): + menu = hildon.AppMenu() + + for command in self.menu_items: + # Create menu entries + button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO) + button.set_label(command) + + if command == "About": + button.connect("clicked", self.about_clicked) + elif command == "Settings": + button.connect("clicked", self.settings_clicked, window) + elif command == "Refresh": + button.connect("clicked", self.refresh_clicked, window) + else: + assert False, command + + # Add entry to the view menu + menu.append(button) + + menu.show_all() + + return menu + + +if __name__ == "__main__": + main() + diff --git a/ui/__init__.py b/ui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ui/diablo/__init__.py b/ui/diablo/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ui/diablo/ui.py b/ui/diablo/ui.py deleted file mode 100644 index c37ddbb..0000000 --- a/ui/diablo/ui.py +++ /dev/null @@ -1,255 +0,0 @@ -# -# mEveMon - A character monitor for EVE Online -# Copyright (c) 2010 Ryan and Danny Campbell, and the mEveMon Team -# -# mEveMon is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# mEveMon is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -# Based on Ry's Fremantle Python code. --danny - -import sys - -import gtk -import hildon -import gobject - -class mEveMonUI(): - - about_name = 'mEveMon' - about_text = ('Mobile character monitor for EVE Online') - about_authors = ['Ryan Campbell','Danny Campbell'] - about_website = 'http://example.site.org' - app_version = '0.1' - - menu_items = ("Settings", "About", "Refresh") - - def __init__(self, controller): - self.controller = controller - - gtk.set_application_name("mEveMon") - - # create the main window - win = hildon.Window() - win.connect("destroy", self.controller.quit) - win.show_all() - progress_bar = hildon.hildon_banner_show_progress(win, None, "Loading overview...") - progress_bar.set_fraction( 0.4 ) - - # Create menu - menu = self.create_menu(win) - # Attach menu to the window - win.set_menu(menu) - - # will probably need to refer to http://maemo.org/community/maemo-developers/gtktreeview_issue/ for sample code again when we make these clickable --danny - self.char_model = self.create_char_model() - treeview = gtk.TreeView( model = self.char_model ) - treeview.connect( 'row-activated', self.build_window ) - treeview.set_model(self.char_model) - self.add_columns_to_treeview(treeview) - - win.add(treeview) - win.show_all() - - progress_bar.set_fraction( 1 ) - progress_bar.destroy() - - def build_window(self, treeview, path, view_column): - win = hildon.Window() - win.show_all() - - progress_bar = hildon.hildon_banner_show_progress(win, None, "Loading character sheet...") - progress_bar.set_fraction( 0.4 ) - - # Create menu - # NOTE: we probably want a window-specific menu for this page, but the - # main appmenu works for now - menu = self.create_menu(win) - # Attach menu to the window - win.set_menu(menu) - - #pannable_area = hildon.PannableArea() - - model = treeview.get_model() - miter = model.get_iter(path) - - # column 0 is the portrait, column 1 is name - - char_name = model.get_value(miter, 1) - char_id = self.controller.char_name2id(char_name) - - win.set_title(char_name) - - skillLabel = gtk.Label("Skills") - - # TODO: replace these with api calls - corp_name = "" - skill_points = 0 - - name = gtk.Label("Name: %s" % char_name) - name.set_alignment(0, 0.5) - - corp = gtk.Label("Corp: %s" % corp_name) - corp.set_alignment(0, 0.5) - - balance = gtk.Label("Balance: %s ISK" % - self.controller.get_account_balance(char_id)) - balance.set_alignment(0, 0.5) - - sp = gtk.Label("Skill points: %s" % skill_points) - sp.set_alignment(0, 0.5) - - portrait = gtk.Image() - portrait.set_from_file(self.controller.get_portrait(char_name, 256)) - portrait.show() - - hbox = gtk.HBox(False, 0) - - info_vbox = gtk.VBox(False, 0) - info_vbox.pack_start(name, False, False, 1) - info_vbox.pack_start(corp, False, False, 1) - info_vbox.pack_start(balance, False, False, 1) - info_vbox.pack_start(sp, False, False, 1) - - hbox.pack_start(portrait, False, False, 10) - hbox.pack_start(info_vbox, False, False, 5) - #hbox.pack_start(stats_vbox, False, False, 5) - - vbox = gtk.VBox(False, 0) - #pannable_area.add(vbox) - - vbox.pack_start(hbox, False, False, 0) - vbox.pack_start(skillLabel, False, False, 5) - - win.add(vbox) - win.show_all() - - progress_bar.set_fraction( 1 ) - progress_bar.destroy() - - def create_char_model(self): - lstore = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING) - #get icon and name and put in a liststore - self.fill_char_model(lstore) - return lstore - - def fill_char_model(self, lstore): - char_list = self.controller.get_characters() - for name, icon in char_list: - liter = lstore.append() - lstore.set(liter, 1, name, 0, self.set_pix(icon)) - - def update_model(self, lstore): - lstore.clear() - self.fill_char_model(lstore) - - def set_pix(self, filename): - pixbuf = gtk.gdk.pixbuf_new_from_file(filename) - return pixbuf - - def add_columns_to_treeview(self, treeview): - #Column 0 for the treeview - renderer = gtk.CellRendererPixbuf() - column = gtk.TreeViewColumn() - column.pack_start(renderer, True) - column.add_attribute(renderer, "pixbuf", 0) - treeview.append_column(column) - - #Column 1 for the treeview - renderer = gtk.CellRendererText() - column = gtk.TreeViewColumn('title', renderer, text=1) - column.set_property("expand", True) - treeview.append_column(column) - - - def settings_clicked(self, button, window): - - dialog = gtk.Dialog() - - #get the vbox to pack all the settings into - vbox = dialog.vbox - - dialog.set_transient_for(window) - dialog.set_title("Settings") - - uidLabel = gtk.Label("User ID:") - uidLabel.set_justify(gtk.JUSTIFY_LEFT) - vbox.add(uidLabel) - - uidEntry = gtk.Entry() - uidEntry.set_text(self.controller.get_uid()) - uidEntry.set_property('is_focus', False) - - vbox.add(uidEntry) - - apiLabel = gtk.Label("API key:") - apiLabel.set_justify(gtk.JUSTIFY_LEFT) - vbox.add(apiLabel) - - apiEntry = gtk.Entry() - apiEntry.set_text(self.controller.get_api_key()) - apiEntry.set_property('is_focus', False) - - vbox.add(apiEntry) - - ok_button = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) - help_button = dialog.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP) - - - dialog.show_all() - result = dialog.run() - - if result == gtk.RESPONSE_OK: - self.controller.set_api_key(apiEntry.get_text()) - self.controller.set_uid(uidEntry.get_text()) - self.update_model(self.char_model) - - dialog.destroy() - - return result - - def about_clicked(self, button): - - dialog = gtk.AboutDialog() - dialog.set_website(self.about_website) - dialog.set_website_label(self.about_website) - dialog.set_name(self.about_name) - dialog.set_authors(self.about_authors) - dialog.set_comments(self.about_text) - dialog.set_version(self.app_version) - dialog.run() - dialog.destroy() - - def refresh_clicked(self, button, window): - self.update_model(self.char_model) - - def create_menu(self, window): - menu = gtk.Menu() - for command in self.menu_items: - button = gtk.MenuItem( command ) - if command == "About": - button.connect("activate", self.about_clicked) - elif command == "Settings": - button.connect("activate", self.settings_clicked, window) - elif command == "Refresh": - button.connect("activate", self.refresh_clicked, window) - else: - assert False, command - # Add entry to the view menu - menu.append(button) - menu.show_all() - return menu - -if __name__ == "__main__": - main() - diff --git a/ui/fremantle/__init__.py b/ui/fremantle/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ui/fremantle/ui.py b/ui/fremantle/ui.py deleted file mode 100644 index 6900389..0000000 --- a/ui/fremantle/ui.py +++ /dev/null @@ -1,270 +0,0 @@ -# -# mEveMon - A character monitor for EVE Online -# Copyright (c) 2010 Ryan and Danny Campbell, and the mEveMon Team -# -# mEveMon is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# mEveMon is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import sys - -import gtk -import hildon -import gobject - -class mEveMonUI(): - - about_name = 'mEveMon' - about_text = ('Mobile character monitor for EVE Online') - about_authors = ['Ryan Campbell', 'Danny Campbell'] - about_website = 'http://example.site.org' - app_version = '0.1' - - menu_items = ("Settings", "About", "Refresh") - - def __init__(self, controller): - self.controller = controller - gtk.set_application_name("mEveMon") - - #create the main window - win = hildon.StackableWindow() - win.connect("destroy", self.controller.quit) - win.show_all() - hildon.hildon_gtk_window_set_progress_indicator(win, 1) - - # Create menu - menu = self.create_menu(win) - # Attach menu to the window - win.set_app_menu(menu) - - pannable_area = hildon.PannableArea() - - # gtk.HILDON_UI_MODE_NORMAL -> not selection in the treeview - # gtk.HILDON_UI_MODE_EDIT -> selection in the treeview - treeview = hildon.GtkTreeView(gtk.HILDON_UI_MODE_NORMAL) - treeview.connect('row-activated', self.build_window) - - self.char_model = self.create_char_model() - treeview.set_model(self.char_model) - - self.add_columns_to_treeview(treeview) - - pannable_area.add(treeview) - - win.add(pannable_area); - - win.show_all() - - hildon.hildon_gtk_window_set_progress_indicator(win, 0) - - def build_window(self, treeview, path, view_column): - win = hildon.StackableWindow() - win.show_all() - hildon.hildon_gtk_window_set_progress_indicator(win, 1) - - # Create menu - # NOTE: we probably want a window-specific menu for this page, but the - # main appmenu works for now - menu = self.create_menu(win) - # Attach menu to the window - win.set_app_menu(menu) - - pannable_area = hildon.PannableArea() - - model = treeview.get_model() - miter = model.get_iter(path) - - # column 0 is the portrait, column 1 is name - - char_name = model.get_value(miter, 1) - char_id = self.controller.char_name2id(char_name) - sheet = self.controller.get_char_sheet(char_id) - - win.set_title(char_name) - - skillLabel = gtk.Label("Skills") - - name = gtk.Label("%s" % char_name) - name.set_alignment(0, 0.5) - - race = gtk.Label("%s %s %s" % (sheet.gender, sheet.race, - sheet.bloodLine)) - race.set_alignment(0, 0.5) - - corp = gtk.Label("Corp: %s" % sheet.corporationName) - corp.set_alignment(0, 0.5) - - balance = gtk.Label("Balance: %s ISK" % sheet.balance) - balance.set_alignment(0, 0.5) - - portrait = gtk.Image() - portrait.set_from_file(self.controller.get_portrait(char_name, 256)) - portrait.show() - - hbox = gtk.HBox(False, 0) - - info_vbox = gtk.VBox(False, 0) - info_vbox.pack_start(name, False, False, 1) - info_vbox.pack_start(race, False, False, 1) - info_vbox.pack_start(corp, False, False, 1) - info_vbox.pack_start(balance, False, False, 1) - - hbox.pack_start(portrait, False, False, 10) - hbox.pack_start(info_vbox, False, False, 5) - #hbox.pack_start(stats_vbox, False, False, 5) - - vbox = gtk.VBox(False, 0) - pannable_area.add_with_viewport(vbox) - - vbox.pack_start(hbox, False, False, 0) - vbox.pack_start(skillLabel, False, False, 5) - - win.add(pannable_area) - win.show_all() - - hildon.hildon_gtk_window_set_progress_indicator(win, 0) - - def create_char_model(self): - lstore = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING) - - #get icon and name and put in a liststore - self.fill_char_model(lstore) - - return lstore - - - def fill_char_model(self, lstore): - char_list = self.controller.get_characters() - - for name, icon in char_list: - liter = lstore.append() - lstore.set(liter, 1, name, 0, self.set_pix(icon)) - - - def update_model(self, lstore): - lstore.clear() - self.fill_char_model(lstore) - - - def set_pix(self, filename): - pixbuf = gtk.gdk.pixbuf_new_from_file(filename) - return pixbuf - - - def add_columns_to_treeview(self, treeview): - #Column 0 for the treeview - renderer = gtk.CellRendererPixbuf() - column = gtk.TreeViewColumn() - column.pack_start(renderer, True) - column.add_attribute(renderer, "pixbuf", 0) - treeview.append_column(column) - - #Column 1 for the treeview - renderer = gtk.CellRendererText() - column = gtk.TreeViewColumn('title', renderer, text=1) - column.set_property("expand", True) - treeview.append_column(column) - - - def settings_clicked(self, button, window): - dialog = gtk.Dialog() - - #get the vbox to pack all the settings into - vbox = dialog.vbox - - dialog.set_transient_for(window) - dialog.set_title("Settings") - - uidLabel = gtk.Label("User ID:") - uidLabel.set_justify(gtk.JUSTIFY_LEFT) - vbox.add(uidLabel) - - uidEntry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT) - uidEntry.set_placeholder("User ID") - uidEntry.set_text(self.controller.get_uid()) - uidEntry.set_property('is_focus', False) - - vbox.add(uidEntry) - - apiLabel = gtk.Label("API key:") - apiLabel.set_justify(gtk.JUSTIFY_LEFT) - vbox.add(apiLabel) - - apiEntry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT) - apiEntry.set_placeholder("API Key") - apiEntry.set_text(self.controller.get_api_key()) - apiEntry.set_property('is_focus', False) - - vbox.add(apiEntry) - - ok_button = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) - help_button = dialog.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP) - - dialog.show_all() - result = dialog.run() - if result == gtk.RESPONSE_OK: - self.controller.set_api_key(apiEntry.get_text()) - self.controller.set_uid(uidEntry.get_text()) - self.controller.set_auth() - self.update_model(self.char_model) - - - dialog.destroy() - - return result - - - def about_clicked(self, button): - dialog = gtk.AboutDialog() - dialog.set_website(self.about_website) - dialog.set_website_label(self.about_website) - dialog.set_name(self.about_name) - dialog.set_authors(self.about_authors) - dialog.set_comments(self.about_text) - dialog.set_version(self.app_version) - dialog.run() - dialog.destroy() - - - def refresh_clicked(self, button, window): - self.update_model(self.char_model) - - - def create_menu(self, window): - menu = hildon.AppMenu() - - for command in self.menu_items: - # Create menu entries - button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO) - button.set_label(command) - - if command == "About": - button.connect("clicked", self.about_clicked) - elif command == "Settings": - button.connect("clicked", self.settings_clicked, window) - elif command == "Refresh": - button.connect("clicked", self.refresh_clicked, window) - else: - assert False, command - - # Add entry to the view menu - menu.append(button) - - menu.show_all() - - return menu - - -if __name__ == "__main__": - main() - -- 1.7.9.5