commit b8e564714804a754a99cf2ba2b72d009200893e5 Author: Andrew Williams Date: Tue Feb 23 10:16:25 2010 +0000 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eve_api/__init__.py b/eve_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eve_api/admin.py b/eve_api/admin.py new file mode 100644 index 0000000..87822b5 --- /dev/null +++ b/eve_api/admin.py @@ -0,0 +1,32 @@ +""" +Admin interface models. Automatically detected by admin.autodiscover(). +""" +from django.contrib import admin +from eve_api.models import * + +class EVEAccountAdmin(admin.ModelAdmin): + list_display = ('id', 'user') + search_fields = ['id'] +admin.site.register(EVEAccount, EVEAccountAdmin) + +class EVEPlayerCharacterAdmin(admin.ModelAdmin): + list_display = ('id', 'name', 'corporation') + search_fields = ['id', 'name'] +admin.site.register(EVEPlayerCharacter, EVEPlayerCharacterAdmin) + +class EVEPlayerCorporationInline(admin.TabularInline): + model = EVEPlayerCorporation + fields = ('name', 'ticker') + extra = 0 + +class EVEPlayerAllianceAdmin(admin.ModelAdmin): + list_display = ('id', 'name', 'ticker', 'member_count', 'date_founded') + search_fields = ['name', 'ticker'] + date_hierarchy = 'date_founded' + inlines = [EVEPlayerCorporationInline] +admin.site.register(EVEPlayerAlliance, EVEPlayerAllianceAdmin) + +class EVEPlayerCorporationAdmin(admin.ModelAdmin): + list_display = ('id', 'name', 'ticker', 'member_count', 'alliance') + search_fields = ['name', 'ticker'] +admin.site.register(EVEPlayerCorporation, EVEPlayerCorporationAdmin) diff --git a/eve_api/api_exceptions.py b/eve_api/api_exceptions.py new file mode 100644 index 0000000..00058cf --- /dev/null +++ b/eve_api/api_exceptions.py @@ -0,0 +1,16 @@ +""" +Contains exeptions used in the eve_api app. +""" +class APIAuthException(Exception): + """ + Raised when an invalid userID and/or authKey were provided. + """ + def __str__(self): + return "An authentication was encountered while querying the EVE API." + +class APINoUserIDException(Exception): + """ + Raised when a userID is required, but missing. + """ + def __str__(self): + return "This query requires a valid userID, but yours is either missing or invalid." \ No newline at end of file diff --git a/eve_api/api_puller/__init__.py b/eve_api/api_puller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eve_api/api_puller/accounts.py b/eve_api/api_puller/accounts.py new file mode 100755 index 0000000..4fb19fa --- /dev/null +++ b/eve_api/api_puller/accounts.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +""" +This module abstracts the pulling of account data from the EVE API. +""" +from xml.dom import minidom +from datetime import datetime + +if __name__ == "__main__": + # Only mess with the environmental stuff if this is being ran directly. + from importer_path import fix_environment + fix_environment() + +from django.conf import settings +from eve_proxy.models import CachedDocument +from eve_api.api_exceptions import APIAuthException, APINoUserIDException +from eve_api.models import EVEAccount, EVEPlayerCharacter, EVEPlayerCorporation + +def import_eve_account(api_key, user_id): + """ + Imports an account from the API into the EVEAccount model. + """ + print user_id, ":", api_key + auth_params = {'userID': user_id, 'apiKey': api_key} + account_doc = CachedDocument.objects.api_query('/account/Characters.xml.aspx', + params=auth_params, + no_cache=False) + #print account_doc.body + + dom = minidom.parseString(account_doc.body) + characters_node_children = dom.getElementsByTagName('rowset')[0].childNodes + + # Create or retrieve the account last to make sure everything + # before here is good to go. + try: + account = EVEAccount.objects.get(id=user_id) + except EVEAccount.DoesNotExist: + account = EVEAccount(id=user_id) + + account.api_key = api_key + account.api_user_id = user_id + account.api_last_updated = datetime.now() + account.save() + + for node in characters_node_children: + try: + # Get this first, as it's safe. + corporation_id = node.getAttribute('corporationID') + corp, created = EVEPlayerCorporation.objects.get_or_create(id=corporation_id) + # Do this last, since the things we retrieved above are used + # on the EVEPlayerCharacter object's fields. + character_id = node.getAttribute('characterID') + pchar, created = EVEPlayerCharacter.objects.get_or_create(id=character_id) + name = node.getAttribute('name') + # Save these for last to keep the save count low. + pchar.name = name + pchar.corporation = corp + pchar.save() + account.characters.add(pchar) + except AttributeError: + # This must be a Text node, ignore it. + continue + + return account + +if __name__ == "__main__": + """ + Test import. + """ + api_key = settings.EVE_API_USER_KEY + #api_key += "1" + user_id = settings.EVE_API_USER_ID + import_eve_account(api_key, user_id) diff --git a/eve_api/api_puller/alliances.py b/eve_api/api_puller/alliances.py new file mode 100755 index 0000000..a1ed90b --- /dev/null +++ b/eve_api/api_puller/alliances.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +""" +This module pulls the master alliance XML list from the API and dumps it in the +api_puller/xml_cache directory as needed. All alliance data must be updated +in bulk, which is done reasonably quickly. +""" +from xml.dom import minidom +import os +import sys +from datetime import datetime + +if __name__ == "__main__": + # Only mess with the environmental stuff if this is being ran directly. + from importer_path import fix_environment + fix_environment() + +from django.conf import settings +from eve_api.models import EVEPlayerAlliance, EVEPlayerCorporation +from eve_proxy.models import CachedDocument + +# This stores a list of all corps whose alliance attribute has been updated. +UPDATED_CORPS = [] + +def __update_corp_from_alliance_node(alliance_node, alliance): + """ + Updates a corp's alliance membership from an alliance element. + """ + member_corp_nodelist = alliance_node.getElementsByTagName('rowset')[0].childNodes + + for node in member_corp_nodelist: + corp_row_node = None + try: + # If this fails, this is a Text node and should be ignored. + corporation_id = int(node.getAttribute('corporationID')) + except AttributeError: + # This is probably a Text node, ignore it. + continue + + corp, created = EVEPlayerCorporation.objects.get_or_create(id=corporation_id) + corp.id = corporation_id + corp.alliance = alliance + corp.alliance_join_date = datetime.strptime(alliance_node.getAttribute('startDate'), + '%Y-%m-%d %H:%M:%S') + corp.save() + # Store the corp in the updated corps list for later checks. + UPDATED_CORPS.append(corp.id) + +def __remove_invalid_corp_alliance_memberships(): + """ + Compares UPDATED_CORPS list to the full list of player corporations. If + the corporation was not updated from being found in one of the alliance + data sets, it has no alliance affiliation and needs to be set to no + alliance if it is not already a None value. + """ + all_corps = EVEPlayerCorporation.objects.all() + # This is not terribly efficient, but it will do for a background process. + for corp in all_corps: + """ + If the corp is not in the UPDATED_CORP list that was built from + alliance memberCorporations rowsets, then it does not belong to an + alliance and should be un-allianced if it currently is. + """ + if corp.id not in UPDATED_CORPS and corp.alliance != None: + corp.alliance = None + corp.save() + +def __start_full_import(): + """ + This method runs a full import of all known alliances. This may take a few + minutes and should be ran regularly if you are maintaining a full corp + list of all EVE corps as well. + """ + print "Querying /eve/AllianceList.xml.aspx/" + alliance_doc = CachedDocument.objects.api_query('/eve/AllianceList.xml.aspx') + print "Parsing..." + dom = minidom.parseString(alliance_doc.body) + result_node_children = dom.getElementsByTagName('result')[0].childNodes + + # This will hold a reference to the tags representing each alliance. + print "Updating alliance and member corporation data..." + for alliance_node in alliances_rowset_node.childNodes: + try: + # If this fails, this is a Text node and should be ignored. + alliance_id = int(alliance_node.getAttribute('allianceID')) + except AttributeError: + # This is probably a Text node, ignore it. + continue + + """ + Search for an existing EVEPlayerAlliance object with the given + alliance ID. Create one if it doesn't exist, retrieve the existing + object if it's already there. + """ + alliance, created = EVEPlayerAlliance.objects.get_or_create(id=alliance_id) + alliance.id = alliance_id + alliance.name = alliance_node.getAttribute('name') + alliance.ticker = alliance_node.getAttribute('shortName') + alliance.member_count = alliance_node.getAttribute('memberCount') + alliance.date_founded = datetime.strptime(alliance_node.getAttribute('startDate'), + '%Y-%m-%d %H:%M:%S') + alliance.save() + # Update member corp alliance attributes. + __update_corp_from_alliance_node(alliance_node, alliance) + + print "Alliances and member corps updated." + print "Removing corps alliance memberships that are no longer valid..." + __remove_invalid_corp_alliance_memberships() + +if __name__ == "__main__": + __start_full_import() diff --git a/eve_api/api_puller/corps.py b/eve_api/api_puller/corps.py new file mode 100755 index 0000000..81fb94d --- /dev/null +++ b/eve_api/api_puller/corps.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +""" +Module for updating corp information. If this is ran directly, the module will +iterate through all known alliances, looking at the corps in each alliance's +member list. This can be very time-consuming and should not be done often. + +Within your applications, you may call query_and_update_corp() to update +an individual corp object as need be. + +NOTE: To get corp data, it must be a member of an alliance. +""" + +if __name__ == "__main__": + # Only mess with the environmental stuff if this is being ran directly. + from importer_path import fix_environment + fix_environment() + +from eve_api.models import EVEPlayerAlliance, EVEPlayerCorporation + +def start_full_import(): + """ + Imports all of the corps that are in all of the known alliances. + + WARNING: THIS WILL TAKE A _LONG_ TIME AND MUST BE RAN AFTER + eve_db.api_puller.alliances.__start_full_import() OR YOU WON'T GET ALL + OF THE CORPS (or any at all). + """ + alliances = EVEPlayerAlliance.objects.all() + + # These two variables are used to track progress. + alliance_count = alliances.count() + # Use this as a progress indicator. + current_alliance_num = 1 + + for alliance in alliances: + # Keep the user informed as to the progress. + print "Alliance %d of %d..." % (current_alliance_num, alliance_count) + # A list of the alliance's member corps. + member_corps = alliance.eveplayercorporation_set.all() + # We're getting the list of corps to update from alliance memberships. + for corp in member_corps: + print "Querying", corp.id + corp.query_and_update_corp() + + # Increment progress counter. + current_alliance_num += 1 + +if __name__ == "__main__": + """ + If ran directly, this will grab all of the corps from the known alliances. + + WARNING: THIS WILL TAKE A VERY LONG TIME TO RUN! IT IS SUGGESTED YOU ONLY + GRAB CORPS AS YOU NEED THEM. + """ + start_full_import() \ No newline at end of file diff --git a/eve_api/api_puller/importer_path.py b/eve_api/api_puller/importer_path.py new file mode 100644 index 0000000..c45b811 --- /dev/null +++ b/eve_api/api_puller/importer_path.py @@ -0,0 +1,16 @@ +import os +import sys +# The path to the folder containing settings.py. +BASE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +APPS_PATH = os.path.join(BASE_PATH, 'apps') + +def fix_environment(): + """ + Callable function to set up all of the Django environmental variables and + pathing for directly executable python modules. + """ + from importer_path import BASE_PATH + # Prepare the environment + sys.path.insert(0, APPS_PATH) + sys.path.insert(0, BASE_PATH) + os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' \ No newline at end of file diff --git a/eve_api/app_defines.py b/eve_api/app_defines.py new file mode 100644 index 0000000..5da4556 --- /dev/null +++ b/eve_api/app_defines.py @@ -0,0 +1,15 @@ +""" +Standard definitions that don't change. +""" +# API status definitions for EVEAccount. +API_STATUS_PENDING = 0 +API_STATUS_OK = 1 +API_STATUS_AUTH_ERROR = 2 +API_STATUS_OTHER_ERROR = 3 +# This tuple is used to assemble the choices list for the field. +API_STATUS_CHOICES = ( + (API_STATUS_PENDING, 'Unknown'), + (API_STATUS_OK, 'OK'), + (API_STATUS_AUTH_ERROR, 'Auth Error'), + (API_STATUS_OTHER_ERROR, 'Other Error'), +) \ No newline at end of file diff --git a/eve_api/managers.py b/eve_api/managers.py new file mode 100644 index 0000000..8633a76 --- /dev/null +++ b/eve_api/managers.py @@ -0,0 +1,96 @@ +from xml.dom import minidom +from django.db import models +from eve_proxy.models import CachedDocument + +class InvalidCorpID(Exception): + """ + Thrown when an invalid corp id is given in an api query. + """ + def __init__(self, id): + self.value = "ID: %s does not match any corporation." % id + + def __str___(self): + return repr(self.value) + +def _api_get_id_from_name(name): + """ + Queries the EVE API looking for the ID of the specified corporation, + alliance, or character based on its name. This is not case sensitive. + + name: (str) Corporation name to search for. + """ + query_doc = CachedDocument.objects.api_query('/eve/CharacterID.xml.aspx', + params={'names': name}) + query_dat = query_doc.body.decode("utf-8", "replace") + dom = minidom.parseString(query_dat) + + id_node = dom.getElementsByTagName('row')[0] + object_id = id_node.getAttribute('characterID') + + if object_id == '0': + raise self.model.DoesNotExist('EVE API returned no matches for the provided corp name.') + else: + return int(object_id) + +class EVEPlayerCharacterManager(models.Manager): + def api_get_id_from_name(self, name): + """ + This uses a common call for corps, characters, and alliances. + """ + return _api_get_id_from_name(name) + +class EVEPlayerAllianceManager(models.Manager): + def api_get_id_from_name(self, name): + """ + This uses a common call for corps, characters, and alliances. + """ + return _api_get_id_from_name(name) + +class EVEPlayerCorporationManager(models.Manager): + def get_or_query_by_id(self, corp_id): + """ + Queries for a corporation. If the corp can't be founded, check the + EVE API service for information on it. If a match still can't be + found, return EVEPlayerCorporation.DoesNotExist. + + corp_id: (int) Corp's ID. + """ + try: + return self.get(id=corp_id) + except self.model.DoesNotExist: + try: + self.api_corp_sheet_xml(corp_id) + new_corp = self.create(id=corp_id) + new_corp.query_and_update_corp() + return new_corp + except InvalidCorpID: + raise + + def api_get_id_from_name(self, name): + """ + This uses a common call for corps, characters, and alliances. + """ + return _api_get_id_from_name(name) + + def api_corp_sheet_xml(self, id): + """ + Returns a corp's data sheet from the EVE API in the form of an XML + minidom doc. + """ + corp_doc = CachedDocument.objects.api_query('/corp/CorporationSheet.xml.aspx', + params={'corporationID': id}) + corp_dat = corp_doc.body.decode("utf-8", "replace") + + # Convert incoming data to UTF-8. + dom = minidom.parseString(corp_dat) + + error_node = dom.getElementsByTagName('error') + + # If there's an error, see if it's because the corp doesn't exist. + if error_node: + error_code = error_node[0].getAttribute('code') + if error_code == '523': + raise InvalidCorpID(id) + + return dom + \ No newline at end of file diff --git a/eve_api/models/__init__.py b/eve_api/models/__init__.py new file mode 100644 index 0000000..9475978 --- /dev/null +++ b/eve_api/models/__init__.py @@ -0,0 +1,6 @@ +""" +By importing all of these sub-modules, the models package is transparently +accessible by the rest of the project. This makes it act just as if it were +one monolithic models.py. +""" +from api_player import * \ No newline at end of file diff --git a/eve_api/models/api_player.py b/eve_api/models/api_player.py new file mode 100644 index 0000000..da3c4a3 --- /dev/null +++ b/eve_api/models/api_player.py @@ -0,0 +1,195 @@ +""" +This module holds data from the EVE XML API. +""" +from django.db import models +from django.contrib.auth.models import User +from eve_proxy.models import CachedDocument +from eve_api.managers import EVEPlayerCorporationManager, EVEPlayerAllianceManager, EVEPlayerCharacterManager +from eve_api.app_defines import API_STATUS_CHOICES, API_STATUS_PENDING + +class EVEAPIModel(models.Model): + """ + A simple abstract base class to set some consistent fields on the models + that are updated from the EVE API. + """ + api_last_updated = models.DateTimeField(blank=True, null=True, + verbose_name="Time last updated from API", + help_text="When this object was last updated from the EVE API.") + + class Meta: + abstract = True + +class EVEAccount(EVEAPIModel): + """ + Use this class to store EVE user account information. Note that its use is + entirely optional and up to the developer's discretion. + """ + user = models.ForeignKey(User, blank=True, null=True, + help_text="User that owns this account") + description = models.CharField(max_length=50, blank=True, + help_text="User-provided description.") + api_key = models.CharField(max_length=64, verbose_name="API Key") + api_user_id = models.IntegerField(verbose_name="API User ID") + characters = models.ManyToManyField("EVEPlayerCharacter", blank=True, + null=True) + api_status = models.IntegerField(choices=API_STATUS_CHOICES, + default=API_STATUS_PENDING, + verbose_name="API Status", + help_text="End result of the last attempt at updating this object from the API.") + + def in_corp(self, corpid): + for char in self.characters.all(): + if char.corporation_id == corpid: + return True + return False + + class Meta: + app_label = 'eve_api' + verbose_name = 'EVE Account' + verbose_name_plural = 'EVE Accounts' + ordering = ['api_user_id'] + +class EVEPlayerCharacter(EVEAPIModel): + """ + Represents an individual player character within the game. Not to be + confused with an account. + """ + name = models.CharField(max_length=255, blank=True, null=False) + corporation = models.ForeignKey('EVEPlayerCorporation', blank=True, null=True) + # TODO: Choices field + race = models.IntegerField(blank=True, null=True) + # TODO: Choices field + gender = models.IntegerField(blank=True, null=True) + balance = models.FloatField("Account Balance", blank=True, null=True) + attrib_intelligence = models.IntegerField("Intelligence", blank=True, + null=True) + attrib_memory = models.IntegerField("Memory", blank=True, null=True) + attrib_charisma = models.IntegerField("Charisma", blank=True, null=True) + attrib_perception = models.IntegerField("Perception", blank=True, null=True) + attrib_willpower = models.IntegerField("Willpower", blank=True, null=True) + + objects = EVEPlayerCharacterManager() + + def __unicode__(self): + if self.name: + return "%s (%d)" % (self.name, self.id) + else: + return "(%d)" % self.id + + def __str__(self): + return self.__unicode__() + + class Meta: + app_label = 'eve_api' + verbose_name = 'Player Character' + verbose_name_plural = 'Player Characters' + +class EVEPlayerAlliance(EVEAPIModel): + """ + Represents a player-controlled alliance. Updated from the alliance + EVE XML API puller at intervals. + """ + name = models.CharField(max_length=255, blank=True, null=False) + ticker = models.CharField(max_length=15, blank=True, null=False) + #executor_character = models.ForeignKey(EVECharacter, blank=True, null=False) + member_count = models.IntegerField(blank=True, null=True) + date_founded = models.DateField(blank=True, null=True) + + objects = EVEPlayerAllianceManager() + + class Meta: + app_label = 'eve_api' + ordering = ['date_founded'] + verbose_name = 'Player Alliance' + verbose_name_plural = 'Player Alliances' + + def __unicode__(self): + if self.name: + return "%s (%d)" % (self.name, self.id) + else: + return "(#%d)" % self.id + + def __str__(self): + return self.__unicode__() + +class EVEPlayerCorporation(EVEAPIModel): + """ + Represents a player-controlled corporation. Updated from a mixture of + the alliance and corporation API pullers. + """ + name = models.CharField(max_length=255, blank=True, null=True) + ticker = models.CharField(max_length=15, blank=True, null=True) + description = models.TextField(blank=True, null=True) + url = models.URLField(verify_exists=False, blank=True, null=True) + ceo_character = models.ForeignKey(EVEPlayerCharacter, blank=True, null=True) + #home_station = models.ForeignKey(EVEStation, blank=True, null=False) + alliance = models.ForeignKey(EVEPlayerAlliance, blank=True, null=True) + alliance_join_date = models.DateField(blank=True, null=True) + tax_rate = models.FloatField(blank=True, null=True) + member_count = models.IntegerField(blank=True, null=True) + shares = models.IntegerField(blank=True, null=True) + + # Logo generation stuff + logo_graphic_id = models.IntegerField(blank=True, null=True) + logo_shape1 = models.IntegerField(blank=True, null=True) + logo_shape2 = models.IntegerField(blank=True, null=True) + logo_shape3 = models.IntegerField(blank=True, null=True) + logo_color1 = models.IntegerField(blank=True, null=True) + logo_color2 = models.IntegerField(blank=True, null=True) + logo_color3 = models.IntegerField(blank=True, null=True) + + objects = EVEPlayerCorporationManager() + + class Meta: + app_label = 'eve_api' + verbose_name = 'Player Corporation' + verbose_name_plural = 'Player Corporations' + + def __str__(self): + if self.name: + return self.name + else: + return "Corp #%d" % self.id + + def query_and_update_corp(self): + """ + Takes an EVEPlayerCorporation object and updates it from the + EVE API service. + """ + # Pull XML from the EVE API via eve_proxy. + dom = EVEPlayerCorporation.objects.api_corp_sheet_xml(self.id) + + # Tuples of pairings of tag names and the attribute on the Corporation + # object to set the data to. + tag_mappings = ( + ('corporationName', 'name'), + ('ticker', 'ticker'), + ('url', 'url'), + ('description', 'description'), + ('memberCount', 'member_count'), + ('graphicID', 'logo_graphic_id'), + ('shape1', 'logo_shape1'), + ('shape2', 'logo_shape2'), + ('shape3', 'logo_shape3'), + ('color1', 'logo_color1'), + ('color2', 'logo_color2'), + ('color3', 'logo_color3'), + ) + + # Iterate through the tag mappings, setting the values of the tag names + # (first member of the tuple) to the attribute named in the second member + # of the tuple on the EVEPlayerCorporation object. + for tag_map in tag_mappings: + try: + setattr(self, tag_map[1], + dom.getElementsByTagName(tag_map[0])[0].firstChild.nodeValue) + except AttributeError: + # This tag has no value, skip it. + continue + except IndexError: + # Something weird has happened + print " * Index Error:", tag_map[0] + continue + + print "Updating", self.id, self.name + self.save() diff --git a/eve_proxy/__init__.py b/eve_proxy/__init__.py new file mode 100755 index 0000000..dab0451 --- /dev/null +++ b/eve_proxy/__init__.py @@ -0,0 +1,9 @@ +VERSION = (0, 4) + +# Dynamically calculate the version based on VERSION tuple +if len(VERSION)>2 and VERSION[2] is not None: + str_version = "%d.%d_%s" % VERSION[:3] +else: + str_version = "%d.%d" % VERSION[:2] + +__version__ = str_version diff --git a/eve_proxy/admin.py b/eve_proxy/admin.py new file mode 100755 index 0000000..b4e029f --- /dev/null +++ b/eve_proxy/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from eve_proxy.models import CachedDocument + +class CachedDocumentAdmin(admin.ModelAdmin): + model = CachedDocument + list_display = ('url_path', 'time_retrieved', 'cached_until') + verbose_name = 'Cached Document' + verbose_name_plural = 'Cached Documents' +admin.site.register(CachedDocument, CachedDocumentAdmin) diff --git a/eve_proxy/models.py b/eve_proxy/models.py new file mode 100755 index 0000000..2b58089 --- /dev/null +++ b/eve_proxy/models.py @@ -0,0 +1,126 @@ +import httplib +import urllib +import xml +from datetime import datetime, timedelta +from xml.dom import minidom +from django.db import models +from eve_api.api_exceptions import APIAuthException, APINoUserIDException + +# You generally never want to change this unless you have a very good reason. +API_URL = 'api.eve-online.com' + +class CachedDocumentManager(models.Manager): + """ + This manager handles querying or retrieving CachedDocuments. + """ + def cache_from_eve_api(self, cached_doc, url_path, params, no_cache=False): + """ + Connect to the EVE API server, send the request, and cache it to + a CachedDocument. This is typically not something you want to call + directly. Use api_query(). + """ + headers = {"Content-type": "application/x-www-form-urlencoded"} + # This is the connection to the EVE API server. + conn = httplib.HTTPConnection(API_URL) + # Combine everything into an HTTP request. + conn.request("POST", url_path, params, headers) + # Retrieve the response from the server. + response = conn.getresponse() + # Save the response (an XML document) to the CachedDocument. + cached_doc.body = response.read() + + try: + # Parse the response via minidom + dom = minidom.parseString(cached_doc.body) + except xml.parsers.expat.ExpatError: + print "XML Parser Error:" + print cached_doc.body + return + + # Set the CachedDocument's time_retrieved and cached_until times based + # on the values in the XML response. This will be used in future + # requests to see if the CachedDocument can be retrieved directly or + # if it needs to be re-cached. + cached_doc.time_retrieved = datetime.utcnow() + cached_doc.cached_until = dom.getElementsByTagName('cachedUntil')[0].childNodes[0].nodeValue + + # Finish up and return the resulting document just in case. + if no_cache == False: + cached_doc.save() + + return dom + + def api_query(self, url_path, params=None, no_cache=False): + """ + Transparently handles querying EVE API or retrieving the document from + the cache. + + Arguments: + url_path: (string) Path to the EVE API page to query. For example: + /eve/ErrorList.xml.aspx + params: (dictionary/string) A dictionary of extra parameters to include. + May also be a string representation of + the query: userID=1&characterID=xxxxxxxx + """ + if type({}) == type(params): + # If 'params' is a dictionary, convert it to a URL string. + params = urllib.urlencode(params) + elif params == None or params.strip() == '': + # For whatever reason, EVE API freaks out if there are no parameters. + # Add a bogus parameter if none are specified. I'm sure there's a + # better fix for this. + params = 'odd_parm=1' + + # Combine the URL path and the parameters to create the full query. + query_name = '%s?%s' % (url_path, params) + + if no_cache: + # If no_cache is enabled, don't even attempt a lookup. + cached_doc = CachedDocument() + created = False + else: + # Retrieve or create a new CachedDocument based on the full URL + # and parameters. + cached_doc, created = self.get_or_create(url_path=query_name) + + # EVE uses UTC. + current_eve_time = datetime.utcnow() + + # Figure out if we need hit EVE API and re-cache, or just pull from + # the local cache (based on cached_until). + if no_cache or created or \ + cached_doc.cached_until == None or \ + current_eve_time > cached_doc.cached_until: + # Cache from EVE API + dom = self.cache_from_eve_api(cached_doc, url_path, params, + no_cache=no_cache) + else: + # Parse the document here since it was retrieved from the + # database cache instead of queried for. + dom = minidom.parseString(cached_doc.body) + + # Check for the presence errors. Only check the bare minimum, + # generic stuff that applies to most or all queries. User-level code + # should check for the more specific errors. + error_node = dom.getElementsByTagName('error') + if error_node: + error_code = error_node[0].getAttribute('code') + # User specified an invalid userid and/or auth key. + if error_code == '203': + raise APIAuthException() + elif error_code == '106': + raise APINoUserIDException() + + return cached_doc + +class CachedDocument(models.Model): + """ + This is a cached XML document from the EVE API. + """ + url_path = models.CharField(max_length=255) + body = models.TextField() + time_retrieved = models.DateTimeField(blank=True, null=True) + cached_until = models.DateTimeField(blank=True, null=True) + + # The custom manager handles the querying. + objects = CachedDocumentManager() \ No newline at end of file diff --git a/eve_proxy/urls.py b/eve_proxy/urls.py new file mode 100755 index 0000000..6c2e37f --- /dev/null +++ b/eve_proxy/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('eve_proxy.views', + # This view can be used just like EVE API's http://api.eve-online.com. + # Any parameters or URL paths are sent through the cache system and + # forwarded to the EVE API site as needed. + url(r'^', 'retrieve_xml', name='eve_proxy-retrieve_xml'), +) diff --git a/eve_proxy/views.py b/eve_proxy/views.py new file mode 100755 index 0000000..b14aba9 --- /dev/null +++ b/eve_proxy/views.py @@ -0,0 +1,22 @@ +from django.http import HttpResponse +from eve_proxy.models import CachedDocument + +def retrieve_xml(request): + """ + A view that forwards EVE API requests through the cache system, either + retrieving a cached document or querying and caching as needed. + """ + # This is the URL path (minus the parameters). + url_path = request.META['PATH_INFO'] + # The parameters attached to the end of the URL path. + params = request.META['QUERY_STRING'] + + if url_path == '/' or url_path == '': + # If they don't provide any kind of query, shoot a quick error message. + return HttpResponse('No API query specified.') + + # The query system will retrieve a cached_doc that was either previously + # or newly cached depending on cache intervals. + cached_doc = CachedDocument.objects.api_query(url_path, params) + # Return the document's body as XML. + return HttpResponse(cached_doc.body, mimetype='text/xml') diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..bcdd55e --- /dev/null +++ b/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/python +from django.core.management import execute_manager +try: + import settings # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..b1286e7 --- /dev/null +++ b/settings.py @@ -0,0 +1,85 @@ +# Django settings for login project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. +DATABASE_NAME = '/home/dreddit/www/login.dredd.it/login.db3' # Or path to database file if using sqlite3. +DATABASE_USER = '' # Not used with sqlite3. +DATABASE_PASSWORD = '' # Not used with sqlite3. +DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. +DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'UTC' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '8i2+dd-b2tg9g%mq$&i$-8beh4i5^2mm=e-nh^$p47^w=z1igr' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', +) + +ROOT_URLCONF = 'login.urls' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'registration', + 'eve_proxy', + 'eve_api', + 'sso', +) + +AUTH_PROFILE_MODULE = 'sso.UserProfile' diff --git a/sso/__init__.py b/sso/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sso/models.py b/sso/models.py new file mode 100644 index 0000000..2c3ccdf --- /dev/null +++ b/sso/models.py @@ -0,0 +1,18 @@ +from django.db import models +from django.contrib.auth.models import User +from eve_api.models.api_player import EVEAccount + +class UserProfile(User): + eveaccount = models.ForeignKey(EVEAccount) + +class Site(models.Model): + url = models.CharField(max_length=200) + active = models.BooleanField() + api = models.CharField(max_length=200) + +class SiteAccount(models.Model): + user = models.ForeignKey(UserProfile) + site = models.ForeignKey(Site) + username = models.CharField(max_length=200) + active = models.BooleanField() + diff --git a/sso/tests.py b/sso/tests.py new file mode 100644 index 0000000..2247054 --- /dev/null +++ b/sso/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/sso/views.py b/sso/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/sso/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/urls.py b/urls.py new file mode 100644 index 0000000..a4780af --- /dev/null +++ b/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls.defaults import * + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('', + (r'^login/', include('django.contrib.auth.views.login')), + + (r'^admin/', include(admin.site.urls)), +)