Goodbye eveapi! You have served us well all these years!

Start stripping XML API stuff and implement ESI skill fetching.
This commit is contained in:
blitzmann
2018-02-08 23:05:01 -05:00
parent cb392e7e5f
commit 33bf5234d0
15 changed files with 93 additions and 1697 deletions

View File

@@ -27,14 +27,12 @@ from eos.effectHandlerHelpers import HandledImplantBoosterList
from eos.saveddata.implant import Implant
from eos.saveddata.user import User
from eos.saveddata.character import Character, Skill
from eos.saveddata.ssocharacter import SsoCharacter
characters_table = Table("characters", saveddata_meta,
Column("ID", Integer, primary_key=True),
Column("name", String, nullable=False),
Column("apiID", Integer),
Column("apiKey", String),
Column("defaultChar", Integer),
Column("chars", String, nullable=True),
Column("ssoCharacterID", ForeignKey("ssoCharacter.ID"), nullable=True),
Column("defaultLevel", Integer, nullable=True),
Column("alphaCloneID", Integer, nullable=True),
Column("ownerID", ForeignKey("users.ID"), nullable=True),
@@ -62,6 +60,6 @@ mapper(Character, characters_table,
single_parent=True,
primaryjoin=charImplants_table.c.charID == characters_table.c.ID,
secondaryjoin=charImplants_table.c.implantID == Implant.ID,
secondary=charImplants_table),
secondary=charImplants_table)
}
)

View File

@@ -479,7 +479,7 @@ def getSsoCharacter(lookfor, clientHash, eager=None):
filter = SsoCharacter.client == clientHash
if isinstance(lookfor, int):
filter = and_(filter, SsoCharacter.characterID == lookfor)
filter = and_(filter, SsoCharacter.ID == lookfor)
elif isinstance(lookfor, str):
filter = and_(filter, SsoCharacter.characterName == lookfor)
else:

View File

@@ -119,12 +119,9 @@ class Character(object):
return all0
def apiUpdateCharSheet(self, skills, secStatus=0):
def clearSkills(self):
del self.__skills[:]
self.__skillIdMap.clear()
for skillRow in skills:
self.addSkill(Skill(self, skillRow["typeID"], skillRow["level"]))
self.secStatus = secStatus
@property
def ro(self):

View File

@@ -119,11 +119,9 @@ class PFCrestPref(PreferenceView):
def OnModeChange(self, event):
self.settings.set('mode', event.GetInt())
self.ToggleProxySettings(self.settings.get('mode'))
Esi.restartService()
def OnServerChange(self, event):
self.settings.set('server', event.GetInt())
Esi.restartService()
def OnBtnApply(self, event):
self.settings.set('clientID', self.inputClientID.GetValue().strip())

View File

@@ -158,11 +158,11 @@ class CharacterEditor(wx.Frame):
self.sview = SkillTreeView(self.viewsNBContainer)
self.iview = ImplantEditorView(self.viewsNBContainer, self)
self.aview = APIView(self.viewsNBContainer)
# self.aview = APIView(self.viewsNBContainer)
self.viewsNBContainer.AddPage(self.sview, "Skills")
self.viewsNBContainer.AddPage(self.iview, "Implants")
self.viewsNBContainer.AddPage(self.aview, "API")
# self.viewsNBContainer.AddPage(self.aview, "API")
mainSizer.Add(self.viewsNBContainer, 1, wx.EXPAND | wx.ALL, 5)

View File

@@ -151,9 +151,7 @@ class CharacterSelection(wx.Panel):
def refreshApi(self, event):
self.btnRefresh.Enable(False)
sChar = Character.getInstance()
ID, key, charName, chars = sChar.getApiDetails(self.getActiveCharacter())
if charName:
sChar.apiFetch(self.getActiveCharacter(), charName, self.refreshAPICallback)
sChar.apiFetch(self.getActiveCharacter(), self.refreshAPICallback)
def refreshAPICallback(self, e=None):
self.btnRefresh.Enable(True)
@@ -178,7 +176,9 @@ class CharacterSelection(wx.Panel):
self.charChoice.SetSelection(self.charCache)
self.mainFrame.showCharacterEditor(event)
return
if sChar.getCharName(charID) not in ("All 0", "All 5") and sChar.apiEnabled(charID):
char = sChar.getCharacter(charID)
if sChar.getCharName(charID) not in ("All 0", "All 5") and char.ssoCharacterID is not None:
self.btnRefresh.Enable(True)
else:
self.btnRefresh.Enable(False)

View File

@@ -99,7 +99,7 @@ class CrestFittings(wx.Frame):
self.charChoice.Clear()
for char in chars:
self.charChoice.Append(char.characterName, char.characterID)
self.charChoice.Append(char.characterName, char.ID)
self.charChoice.SetSelection(0)
@@ -220,7 +220,7 @@ class ExportToEve(wx.Frame):
self.charChoice.Clear()
for char in chars:
self.charChoice.Append(char.characterName, char.characterID)
self.charChoice.Append(char.characterName, char.ID)
self.charChoice.SetSelection(0)
@@ -344,7 +344,7 @@ class CrestMgmt(wx.Dialog):
if item > -1:
charID = self.lcCharacters.GetItemData(item)
sCrest = Esi.getInstance()
sCrest.delCrestCharacter(charID)
sCrest.delSsoCharacter(charID)
self.popCharList()

View File

@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import sys
import copy
import itertools
@@ -34,10 +33,10 @@ import wx
import config
import eos.db
from service.eveapi import EVEAPIConnection, ParseXML
from service.esi import Esi
from eos.saveddata.implant import Implant as es_Implant
from eos.saveddata.character import Character as es_Character
from eos.saveddata.character import Character as es_Character, Skill
from eos.saveddata.module import Slot as es_Slot, Module as es_Module
from eos.saveddata.fighter import Fighter as es_Fighter
@@ -52,6 +51,9 @@ class CharacterImportThread(threading.Thread):
self.callback = callback
def run(self):
wx.CallAfter(self.callback)
# todo: Fix character import (don't need CCP SML anymore, only support evemon?)
return
paths = self.paths
sCharacter = Character.getInstance()
all5_character = es_Character("All 5", 5)
@@ -62,43 +64,34 @@ class CharacterImportThread(threading.Thread):
for path in paths:
try:
# we try to parse api XML data first
with open(path, mode='r') as charFile:
sheet = ParseXML(charFile)
char = sCharacter.new(sheet.name + " (imported)")
sCharacter.apiUpdateCharSheet(char.ID, sheet.skills)
except:
# if it's not api XML data, try this
# this is a horrible logic flow, but whatever
try:
charFile = open(path, mode='r').read()
doc = minidom.parseString(charFile)
if doc.documentElement.tagName not in ("SerializableCCPCharacter", "SerializableUriCharacter"):
pyfalog.error("Incorrect EVEMon XML sheet")
raise RuntimeError("Incorrect EVEMon XML sheet")
name = doc.getElementsByTagName("name")[0].firstChild.nodeValue
securitystatus = doc.getElementsByTagName("securityStatus")[0].firstChild.nodeValue or 0
skill_els = doc.getElementsByTagName("skill")
skills = []
for skill in skill_els:
if int(skill.getAttribute("typeID")) in all_skill_ids and (0 <= int(skill.getAttribute("level")) <= 5):
skills.append({
"typeID": int(skill.getAttribute("typeID")),
"level": int(skill.getAttribute("level")),
})
else:
pyfalog.error(
"Attempted to import unknown skill {0} (ID: {1}) (Level: {2})",
skill.getAttribute("name"),
skill.getAttribute("typeID"),
skill.getAttribute("level"),
)
char = sCharacter.new(name + " (EVEMon)")
sCharacter.apiUpdateCharSheet(char.ID, skills, securitystatus)
except Exception as e:
pyfalog.error("Exception on character import:")
pyfalog.error(e)
continue
charFile = open(path, mode='r').read()
doc = minidom.parseString(charFile)
if doc.documentElement.tagName not in ("SerializableCCPCharacter", "SerializableUriCharacter"):
pyfalog.error("Incorrect EVEMon XML sheet")
raise RuntimeError("Incorrect EVEMon XML sheet")
name = doc.getElementsByTagName("name")[0].firstChild.nodeValue
securitystatus = doc.getElementsByTagName("securityStatus")[0].firstChild.nodeValue or 0
skill_els = doc.getElementsByTagName("skill")
skills = []
for skill in skill_els:
if int(skill.getAttribute("typeID")) in all_skill_ids and (0 <= int(skill.getAttribute("level")) <= 5):
skills.append({
"typeID": int(skill.getAttribute("typeID")),
"level": int(skill.getAttribute("level")),
})
else:
pyfalog.error(
"Attempted to import unknown skill {0} (ID: {1}) (Level: {2})",
skill.getAttribute("name"),
skill.getAttribute("typeID"),
skill.getAttribute("level"),
)
char = sCharacter.new(name + " (EVEMon)")
sCharacter.apiUpdateCharSheet(char.ID, skills, securitystatus)
except Exception as e:
pyfalog.error("Exception on character import:")
pyfalog.error(e)
continue
wx.CallAfter(self.callback)
@@ -344,6 +337,8 @@ class Character(object):
@staticmethod
def getApiDetails(charID):
# todo: fix this (or get rid of?)
return ("", "", "", [])
char = eos.db.getCharacter(charID)
if char.chars is not None:
chars = json.loads(char.chars)
@@ -351,27 +346,8 @@ class Character(object):
chars = None
return char.apiID or "", char.apiKey or "", char.defaultChar or "", chars or []
def apiEnabled(self, charID):
id_, key, default, _ = self.getApiDetails(charID)
return id_ is not "" and key is not "" and default is not ""
@staticmethod
def apiCharList(charID, userID, apiKey):
char = eos.db.getCharacter(charID)
char.apiID = userID
char.apiKey = apiKey
api = EVEAPIConnection()
auth = api.auth(keyID=userID, vCode=apiKey)
apiResult = auth.account.Characters()
charList = [str(c.name) for c in apiResult.characters]
char.chars = json.dumps(charList)
return charList
def apiFetch(self, charID, charName, callback):
thread = UpdateAPIThread(charID, charName, (self.apiFetchCallback, callback))
def apiFetch(self, charID, callback):
thread = UpdateAPIThread(charID, (self.apiFetchCallback, callback))
thread.start()
def apiFetchCallback(self, guiCallback, e=None):
@@ -469,35 +445,30 @@ class Character(object):
class UpdateAPIThread(threading.Thread):
def __init__(self, charID, charName, callback):
def __init__(self, charID, callback):
threading.Thread.__init__(self)
self.name = "CheckUpdate"
self.callback = callback
self.charID = charID
self.charName = charName
def run(self):
try:
dbChar = eos.db.getCharacter(self.charID)
dbChar.defaultChar = self.charName
char = eos.db.getCharacter(self.charID)
api = EVEAPIConnection()
auth = api.auth(keyID=dbChar.apiID, vCode=dbChar.apiKey)
apiResult = auth.account.Characters()
charID = None
for char in apiResult.characters:
if char.name == self.charName:
charID = char.characterID
break
sEsi = Esi.getInstance()
resp = sEsi.getSkills(char.ssoCharacterID)
if charID is None:
return
# todo: check if alpha. if so, pop up a question if they want to apply it as alpha. Use threading events to set the answer?
char.clearSkills()
for skillRow in resp["skills"]:
char.addSkill(Skill(char, skillRow["skill_id"], skillRow["trained_skill_level"]))
sheet = auth.character(charID).CharacterSheet()
charInfo = api.eve.CharacterInfo(characterID=charID)
resp = sEsi.getSecStatus(char.ssoCharacterID)
char.secStatus = resp['security_status']
dbChar.apiUpdateCharSheet(sheet.skills, charInfo.securityStatus)
self.callback[0](self.callback[1])
except Exception:
except Exception as ex:
pyfalog.warn(ex)
self.callback[0](self.callback[1], sys.exc_info())

View File

@@ -2,12 +2,12 @@
import wx
from logbook import Logger
import threading
import copy
import uuid
import time
import config
import base64
import json
import os
import eos.db
from eos.enum import Enum
@@ -15,18 +15,13 @@ from eos.saveddata.ssocharacter import SsoCharacter
import gui.globalEvents as GE
from service.settings import CRESTSettings
from service.server import StoppableHTTPServer, AuthHandler
from service.pycrest.eve import EVE
from .esi_security_proxy import EsiSecurityProxy
from esipy import EsiClient, EsiApp
from esipy.cache import FileCache
import os
import logging
pyfalog = Logger(__name__)
server = "https://blitzmann.pythonanywhere.com"
cache_path = os.path.join(config.savePath, config.ESI_CACHE)
if not os.path.exists(cache_path):
@@ -34,32 +29,13 @@ if not os.path.exists(cache_path):
file_cache = FileCache(cache_path)
esiRdy = threading.Event()
class Servers(Enum):
TQ = 0
SISI = 1
class CrestModes(Enum):
IMPLICIT = 0
USER = 1
from utils.timer import Timer
class Esi(object):
clientIDs = {
Servers.TQ : 'f9be379951c046339dc13a00e6be7704',
Servers.SISI: 'af87365240d644f7950af563b8418bad'
}
# @todo: move this to settings
clientCallback = 'http://localhost:6461'
clientTest = True
esiapp = None
esi_v1 = None
esi_v4 = None
@@ -68,12 +44,9 @@ class Esi(object):
@classmethod
def initEsiApp(cls):
with Timer("Main EsiApp") as t:
cls.esiapp = EsiApp(cache=file_cache)
with Timer('ESI v1') as t:
cls.esi_v1 = cls.esiapp.get_v1_swagger
with Timer('ESI v4') as t:
cls.esi_v4 = cls.esiapp.get_v4_swagger
cls.esiapp = EsiApp(cache=file_cache)
cls.esi_v1 = cls.esiapp.get_v1_swagger
cls.esi_v4 = cls.esiapp.get_v4_swagger
# esiRdy.set()
@@ -92,51 +65,16 @@ class Esi(object):
return cls._instance
@classmethod
def restartService(cls):
# This is here to reseed pycrest values when changing preferences
# We first stop the server n case one is running, as creating a new
# instance doesn't do this.
if cls._instance.httpd:
cls._instance.stopServer()
cls._instance = Esi()
cls._instance.mainFrame.updateCrestMenus(type=cls._instance.settings.get('mode'))
return cls._instance
def __init__(self):
"""
A note on login/logout events: the character login events happen
whenever a characters is logged into via the SSO, regardless of mod.
However, the mode should be send as an argument. Similarily,
the Logout even happens whenever the character is deleted for either
mode. The mode is sent as an argument, as well as the umber of
characters still in the cache (if USER mode)
"""
Esi.initEsiApp()
# prefetch = EsiInitThread()
# prefetch.daemon = True
# prefetch.start()
self.settings = CRESTSettings.getInstance()
self.scopes = ['characterFittingsRead', 'characterFittingsWrite']
# these will be set when needed
self.httpd = None
self.state = None
self.ssoTimer = None
self.eve_options = {
'client_id': self.settings.get('clientID') if self.settings.get('mode') == CrestModes.USER else self.clientIDs.get(self.settings.get('server')),
'api_key': self.settings.get('clientSecret') if self.settings.get('mode') == CrestModes.USER else None,
'redirect_uri': self.clientCallback,
'testing': self.isTestServer
}
# Base EVE connection that is copied to all characters
self.eve = EVE(**self.eve_options)
self.implicitCharacter = None
# The database cache does not seem to be working for some reason. Use
@@ -151,73 +89,61 @@ class Esi(object):
def isTestServer(self):
return self.settings.get('server') == Servers.SISI
def delCrestCharacter(self, charID):
char = eos.db.getSsoCharacter(charID)
del self.charCache[char.ID]
def delSsoCharacter(self, id):
char = eos.db.getSsoCharacter(id)
eos.db.remove(char)
wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=len(self.charCache)))
def delAllCharacters(self):
chars = eos.db.getSsoCharacters()
for char in chars:
eos.db.remove(char)
self.charCache = {}
wx.PostEvent(self.mainFrame, GE.SsoLogout(type=CrestModes.USER, numChars=0))
def getSsoCharacters(self):
chars = eos.db.getSsoCharacters(config.getClientSecret())
return chars
def getSsoCharacter(self, charID):
def getSsoCharacter(self, id):
"""
Get character, and modify to include the eve connection
"""
char = eos.db.getSsoCharacter(charID, config.getClientSecret())
if char.esi_client is None:
char = eos.db.getSsoCharacter(id, config.getClientSecret())
if char is not None and char.esi_client is None:
char.esi_client = Esi.genEsiClient()
char.esi_client.security.update_token(char.get_sso_data())
return char
def getFittings(self, charID):
char = self.getSsoCharacter(charID)
print(repr(char))
op = Esi.esi_v1.op['get_characters_character_id_fittings'](
character_id=charID
)
def getSkills(self, id):
char = self.getSsoCharacter(id)
op = Esi.esi_v4.op['get_characters_character_id_skills'](character_id=char.characterID)
resp = char.esi_client.request(op)
return resp.data
def postFitting(self, charID, json_str):
# @todo: new fitting ID can be recovered from resp.data,
char = self.getSsoCharacter(charID)
def getSecStatus(self, id):
char = self.getSsoCharacter(id)
op = Esi.esi_v4.op['get_characters_character_id'](character_id=char.characterID)
resp = char.esi_client.request(op)
return resp.data
def getFittings(self, id):
char = self.getSsoCharacter(id)
op = Esi.esi_v1.op['get_characters_character_id_fittings'](character_id=char.characterID)
resp = char.esi_client.request(op)
return resp.data
def postFitting(self, id, json_str):
# @todo: new fitting ID can be recovered from resp.data,
char = self.getSsoCharacter(id)
op = Esi.esi_v1.op['post_characters_character_id_fittings'](
character_id=char.characterID,
fitting=json.loads(json_str)
)
resp = char.esi_client.request(op)
return resp.data
def delFitting(self, charID, fittingID):
char = self.getSsoCharacter(charID)
print(repr(char))
def delFitting(self, id, fittingID):
char = self.getSsoCharacter(id)
op = Esi.esi_v1.op['delete_characters_character_id_fittings_fitting_id'](
character_id=charID,
character_id=char.characterID,
fitting_id=fittingID
)
resp = char.esi_client.request(op)
return resp.data
def logout(self):
"""Logout of implicit character"""
pyfalog.debug("Character logout")
self.implicitCharacter = None
wx.PostEvent(self.mainFrame, GE.SsoLogout(type=self.settings.get('mode')))
def stopServer(self):
pyfalog.debug("Stopping Server")
self.httpd.stop()
@@ -278,4 +204,3 @@ class Esi(object):
eos.db.save(currentCharacter)
wx.PostEvent(self.mainFrame, GE.SsoLogin(type=CrestModes.USER)) # todo: remove user / implicit authentication

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
version = "0.0.1"

View File

@@ -1,24 +0,0 @@
import sys
PY3 = sys.version_info[0] == 3
if PY3: # pragma: no cover
string_types = str,
text_type = str
binary_type = bytes
else: # pragma: no cover
string_types = str,
text_type = str
binary_type = str
def text_(s, encoding='latin-1', errors='strict'): # pragma: no cover
if isinstance(s, binary_type):
return s.decode(encoding, errors)
return s
def bytes_(s, encoding='latin-1', errors='strict'): # pragma: no cover
if isinstance(s, text_type):
return s.encode(encoding, errors)
return s

View File

@@ -1,2 +0,0 @@
class APIException(Exception):
pass

View File

@@ -1,318 +0,0 @@
import base64
from logbook import Logger
import os
import re
import time
import zlib
import requests
from requests.adapters import HTTPAdapter
import config
from service.pycrest.compat import bytes_, text_
from service.pycrest.errors import APIException
from urllib.parse import urlparse, urlunparse, parse_qsl
try:
import pickle
except ImportError: # pragma: no cover
# noinspection PyPep8Naming
import pickle as pickle
pyfalog = Logger(__name__)
cache_re = re.compile(r'max-age=([0-9]+)')
class APICache(object):
def put(self, key, value):
raise NotImplementedError
def get(self, key):
raise NotImplementedError
def invalidate(self, key):
raise NotImplementedError
class FileCache(APICache):
def __init__(self, path):
self._cache = {}
self.path = path
if not os.path.isdir(self.path):
os.mkdir(self.path, 0o700)
def _getpath(self, key):
return os.path.join(self.path, str(hash(key)) + '.cache')
def put(self, key, value):
with open(self._getpath(key), 'wb') as f:
f.write(zlib.compress(pickle.dumps(value, -1)))
self._cache[key] = value
def get(self, key):
if key in self._cache:
return self._cache[key]
try:
with open(self._getpath(key), 'rb') as f:
return pickle.loads(zlib.decompress(f.read()))
except IOError as ex:
pyfalog.debug("IO error opening zip file. (May not exist yet)")
if ex.errno == 2: # file does not exist (yet)
return None
else:
raise
def invalidate(self, key):
self._cache.pop(key, None)
try:
os.unlink(self._getpath(key))
except OSError as ex:
pyfalog.debug("Caught exception in invalidate")
pyfalog.debug(ex)
if ex.errno == 2: # does not exist
pass
else:
raise
class DictCache(APICache):
def __init__(self):
self._dict = {}
def get(self, key):
return self._dict.get(key, None)
def put(self, key, value):
self._dict[key] = value
def invalidate(self, key):
self._dict.pop(key, None)
class APIConnection(object):
def __init__(self, additional_headers=None, user_agent=None, cache_dir=None, cache=None):
# Set up a Requests Session
session = requests.Session()
if additional_headers is None:
additional_headers = {}
if user_agent is None:
user_agent = "pyfa/{0} ({1})".format(config.version, config.tag)
session.headers.update({
"User-Agent": user_agent,
"Accept": "application/json",
})
session.headers.update(additional_headers)
session.mount('https://public-crest.eveonline.com', HTTPAdapter())
self._session = session
if cache:
if isinstance(cache, APICache):
self.cache = cache # Inherit from parents
elif isinstance(cache, type):
self.cache = cache() # Instantiate a new cache
elif cache_dir:
self.cache_dir = cache_dir
self.cache = FileCache(self.cache_dir)
else:
self.cache = DictCache()
def get(self, resource, params=None):
pyfalog.debug('Getting resource {0}', resource)
if params is None:
params = {}
# remove params from resource URI (needed for paginated stuff)
parsed_uri = urlparse(resource)
qs = parsed_uri.query
resource = urlunparse(parsed_uri._replace(query=''))
prms = {}
for tup in parse_qsl(qs):
prms[tup[0]] = tup[1]
# params supplied to self.get() override parsed params
for key in params:
prms[key] = params[key]
# check cache
key = (resource, frozenset(list(self._session.headers.items())), frozenset(list(prms.items())))
cached = self.cache.get(key)
if cached and cached['cached_until'] > time.time():
pyfalog.debug('Cache hit for resource {0} (params={1})', resource, prms)
return cached
elif cached:
pyfalog.debug('Cache stale for resource {0} (params={1})', resource, prms)
self.cache.invalidate(key)
else:
pyfalog.debug('Cache miss for resource {0} (params={1})', resource, prms)
pyfalog.debug('Getting resource {0} (params={1})', resource, prms)
res = self._session.get(resource, params=prms)
if res.status_code != 200:
raise APIException("Got unexpected status code from server: {0}" % res.status_code)
ret = res.json()
# cache result
expires = self._get_expires(res)
if expires > 0:
ret.update({'cached_until': time.time() + expires})
self.cache.put(key, ret)
return ret
@staticmethod
def _get_expires(response):
if 'Cache-Control' not in response.headers:
return 0
if any([s in response.headers['Cache-Control'] for s in ['no-cache', 'no-store']]):
return 0
match = cache_re.search(response.headers['Cache-Control'])
if match:
return int(match.group(1))
return 0
class EVE(APIConnection):
def __init__(self, **kwargs):
self.api_key = kwargs.pop('api_key', None)
self.client_id = kwargs.pop('client_id', None)
self.redirect_uri = kwargs.pop('redirect_uri', None)
if kwargs.pop('testing', False):
self._public_endpoint = "http://public-crest-sisi.testeveonline.com/"
self._authed_endpoint = "https://api-sisi.testeveonline.com/"
self._image_server = "https://image.testeveonline.com/"
self._oauth_endpoint = "https://sisilogin.testeveonline.com/oauth"
else:
self._public_endpoint = "https://public-crest.eveonline.com/"
self._authed_endpoint = "https://crest-tq.eveonline.com/"
self._image_server = "https://image.eveonline.com/"
self._oauth_endpoint = "https://login.eveonline.com/oauth"
self._endpoint = self._public_endpoint
self._cache = {}
self._data = None
self.token = None
self.refresh_token = None
self.expires = None
APIConnection.__init__(self, **kwargs)
def __call__(self):
if not self._data:
self._data = APIObject(self.get(self._endpoint), self)
return self._data
def __getattr__(self, item):
return self._data.__getattr__(item)
def auth_uri(self, scopes=None, state=None):
s = [] if not scopes else scopes
grant_type = "token" if self.api_key is None else "code"
return "%s/authorize?response_type=%s&redirect_uri=%s&client_id=%s%s%s" % (
self._oauth_endpoint,
grant_type,
self.redirect_uri,
self.client_id,
"&scope=%s" % '+'.join(s) if scopes else '',
"&state=%s" % state if state else ''
)
def _authorize(self, params):
auth = text_(base64.b64encode(bytes_("%s:%s" % (self.client_id, self.api_key))))
headers = {"Authorization": "Basic %s" % auth}
res = self._session.post("%s/token" % self._oauth_endpoint, params=params, headers=headers)
if res.status_code != 200:
raise APIException("Got unexpected status code from API: %i" % res.status_code)
return res.json()
def set_auth_values(self, res):
self.__class__ = AuthedConnection
self.token = res['access_token']
self.refresh_token = res['refresh_token']
self.expires = int(time.time()) + res['expires_in']
self._endpoint = self._authed_endpoint
self._session.headers.update({"Authorization": "Bearer %s" % self.token})
def authorize(self, code):
res = self._authorize(params={"grant_type": "authorization_code", "code": code})
self.set_auth_values(res)
def refr_authorize(self, refresh_token):
res = self._authorize(params={"grant_type": "refresh_token", "refresh_token": refresh_token})
self.set_auth_values(res)
def temptoken_authorize(self, access_token=None, expires_in=0, refresh_token=None):
self.set_auth_values({'access_token': access_token,
'refresh_token': refresh_token,
'expires_in': expires_in})
class AuthedConnection(EVE):
def __call__(self):
if not self._data:
self._data = APIObject(self.get(self._endpoint), self)
return self._data
def whoami(self):
# if 'whoami' not in self._cache:
# print "Setting this whoami cache"
# self._cache['whoami'] = self.get("%s/verify" % self._oauth_endpoint)
return self.get("%s/verify" % self._oauth_endpoint)
def get(self, resource, params=None):
if self.refresh_token and int(time.time()) >= self.expires:
self.refr_authorize(self.refresh_token)
return super(self.__class__, self).get(resource, params)
def post(self, resource, data, params=None):
if self.refresh_token and int(time.time()) >= self.expires:
self.refr_authorize(self.refresh_token)
return self._session.post(resource, data=data, params=params)
def delete(self, resource, params=None):
if self.refresh_token and int(time.time()) >= self.expires:
self.refr_authorize(self.refresh_token)
return self._session.delete(resource, params=params)
class APIObject(object):
def __init__(self, parent, connection):
self._dict = {}
self.connection = connection
for k, v in list(parent.items()):
if type(v) is dict:
self._dict[k] = APIObject(v, connection)
elif type(v) is list:
self._dict[k] = self._wrap_list(v)
else:
self._dict[k] = v
def _wrap_list(self, list_):
new = []
for item in list_:
if type(item) is dict:
new.append(APIObject(item, self.connection))
elif type(item) is list:
new.append(self._wrap_list(item))
else:
new.append(item)
return new
def __getattr__(self, item):
if item in self._dict:
return self._dict[item]
raise AttributeError(item)
def __call__(self, **kwargs):
# Caching is now handled by APIConnection
if 'href' in self._dict:
return APIObject(self.connection.get(self._dict['href'], params=kwargs), self.connection)
else:
return self
def __str__(self): # pragma: no cover
return self._dict.__str__()
def __repr__(self): # pragma: no cover
return self._dict.__repr__()

View File

@@ -1,132 +0,0 @@
import datetime
import ssl
import warnings
from requests.adapters import HTTPAdapter
try:
from requests.packages import urllib3
from requests.packages.urllib3.util import ssl_
from requests.packages.urllib3.exceptions import (
SystemTimeWarning,
SecurityWarning,
)
from requests.packages.urllib3.packages.ssl_match_hostname import \
match_hostname
except:
import urllib3
from urllib3.util import ssl_
from urllib3.exceptions import SystemTimeWarning, SecurityWarning
from urllib3.packages.ssl_match_hostname import match_hostname
class WeakCiphersHTTPSConnection(urllib3.connection.VerifiedHTTPSConnection): # pragma: no cover
# Python versions >=2.7.9 and >=3.4.1 do not (by default) allow ciphers
# with MD5. Unfortunately, the CREST public server _only_ supports
# TLS_RSA_WITH_RC4_128_MD5 (as of 5 Jan 2015). The cipher list below is
# nearly identical except for allowing that cipher as a last resort (and
# excluding export versions of ciphers).
DEFAULT_CIPHERS = (
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:'
'ECDH+HIGH:DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:'
'RSA+3DES:ECDH+RC4:DH+RC4:RSA+RC4:!aNULL:!eNULL:!EXP:-MD5:RSA+RC4+MD5'
)
def __init__(self, host, port, ciphers=None, **kwargs):
self.ciphers = ciphers if ciphers is not None else self.DEFAULT_CIPHERS
super(WeakCiphersHTTPSConnection, self).__init__(host, port, **kwargs)
def connect(self):
# Yup, copied in VerifiedHTTPSConnection.connect just to change the
# default cipher list.
# Add certificate verification
conn = self._new_conn()
resolved_cert_reqs = ssl_.resolve_cert_reqs(self.cert_reqs)
resolved_ssl_version = ssl_.resolve_ssl_version(self.ssl_version)
hostname = self.host
if getattr(self, '_tunnel_host', None):
# _tunnel_host was added in Python 2.6.3
# (See: http://hg.python.org/cpython/rev/0f57b30a152f)
self.sock = conn
# Calls self._set_hostport(), so self.host is
# self._tunnel_host below.
self._tunnel()
# Mark this connection as not reusable
self.auto_open = 0
# Override the host with the one we're requesting data from.
hostname = self._tunnel_host
is_time_off = datetime.date.today() < urllib3.connection.RECENT_DATE
if is_time_off:
warnings.warn((
'System time is way off (before {0}). This will probably '
'lead to SSL verification errors').format(
urllib3.connection.RECENT_DATE),
SystemTimeWarning
)
# Wrap socket using verification with the root certs in
# trusted_root_certs
self.sock = ssl_.ssl_wrap_socket(
conn,
self.key_file,
self.cert_file,
cert_reqs=resolved_cert_reqs,
ca_certs=self.ca_certs,
server_hostname=hostname,
ssl_version=resolved_ssl_version,
ciphers=self.ciphers,
)
if self.assert_fingerprint:
ssl_.assert_fingerprint(self.sock.getpeercert(binary_form=True),
self.assert_fingerprint)
elif resolved_cert_reqs != ssl.CERT_NONE \
and self.assert_hostname is not False:
cert = self.sock.getpeercert()
if not cert.get('subjectAltName', ()):
warnings.warn((
'Certificate has no `subjectAltName`, falling back to check for a `commonName` for now. '
'This feature is being removed by major browsers and deprecated by RFC 2818. '
'(See https://github.com/shazow/urllib3/issues/497 for details.)'),
SecurityWarning
)
match_hostname(cert, self.assert_hostname or hostname)
self.is_verified = (resolved_cert_reqs == ssl.CERT_REQUIRED or self.assert_fingerprint is not None)
class WeakCiphersHTTPSConnectionPool(urllib3.connectionpool.HTTPSConnectionPool):
ConnectionCls = WeakCiphersHTTPSConnection
class WeakCiphersPoolManager(urllib3.poolmanager.PoolManager):
def _new_pool(self, scheme, host, port):
if scheme == 'https':
return WeakCiphersHTTPSConnectionPool(host, port, **self.connection_pool_kw)
return super(WeakCiphersPoolManager, self)._new_pool(scheme, host, port)
class WeakCiphersAdapter(HTTPAdapter):
""""Transport adapter" that allows us to use TLS_RSA_WITH_RC4_128_MD5."""
def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
# Rewrite of the requests.adapters.HTTPAdapter.init_poolmanager method
# to use WeakCiphersPoolManager instead of urllib3's PoolManager
self._pool_connections = connections
self._pool_maxsize = maxsize
self._pool_block = block
self.poolmanager = WeakCiphersPoolManager(
num_pools=connections,
maxsize=maxsize,
block=block,
strict=True,
**pool_kwargs
)