Merge branch 'singularity'

This commit is contained in:
blitzmann
2015-11-02 21:28:26 -05:00
74 changed files with 2406 additions and 76 deletions

2
.gitignore vendored
View File

@@ -13,7 +13,7 @@
*.patch
#Personal
saveddata/
/saveddata/
#PyCharm
.idea/

View File

@@ -92,6 +92,9 @@ def defPaths():
__createDirs(savePath)
if isFrozen():
os.environ["REQUESTS_CA_BUNDLE"] = os.path.join(pyfaPath, "cacert.pem")
format = '%(asctime)s %(name)-24s %(levelname)-8s %(message)s'
logging.basicConfig(format=format, level=logLevel)
handler = logging.handlers.RotatingFileHandler(os.path.join(savePath, "log.txt"), maxBytes=1000000, backupCount=3)

View File

@@ -68,14 +68,8 @@ from eos.db.gamedata import *
from eos.db.saveddata import *
#Import queries
from eos.db.gamedata.queries import getItem, searchItems, getVariations, getItemsByCategory, directAttributeRequest, \
getMarketGroup, getGroup, getCategory, getAttributeInfo, getMetaData, getMetaGroup
from eos.db.saveddata.queries import getUser, getCharacter, getFit, getFitsWithShip, countFitsWithShip, searchFits, \
getCharacterList, getPrice, getDamagePatternList, getDamagePattern, \
getFitList, getFleetList, getFleet, save, remove, commit, add, \
getCharactersForUser, getMiscData, getSquadsIDsWithFitID, getWing, \
getSquad, getBoosterFits, getProjectedFits, getTargetResistsList, getTargetResists,\
clearPrices, countAllFits
from eos.db.gamedata.queries import *
from eos.db.saveddata.queries import *
#If using in memory saveddata, you'll want to reflect it so the data structure is good.
if config.saveddata_connectionstring == "sqlite:///:memory:":

View File

@@ -1,3 +1,18 @@
__all__ = ["character", "fit", "module", "user", "skill", "price",
"booster", "drone", "implant", "fleet", "damagePattern",
"miscData", "targetResists"]
__all__ = [
"character",
"fit",
"module",
"user",
"skill",
"price",
"booster",
"drone",
"implant",
"fleet",
"damagePattern",
"miscData",
"targetResists",
"override",
"crest"
]

31
eos/db/saveddata/crest.py Normal file
View File

@@ -0,0 +1,31 @@
#===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
from sqlalchemy import Table, Column, Integer, String, Boolean
from sqlalchemy.orm import mapper
from eos.db import saveddata_meta
from eos.types import CrestChar
crest_table = Table("crest", saveddata_meta,
Column("ID", Integer, primary_key = True),
Column("name", String, nullable = False, unique = True),
Column("refresh_token", String, nullable = False))
mapper(CrestChar, crest_table)

View File

@@ -0,0 +1,31 @@
#===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
from sqlalchemy import Table, Column, Integer, Float
from sqlalchemy.orm import mapper
from eos.db import saveddata_meta
from eos.types import Override
overrides_table = Table("overrides", saveddata_meta,
Column("itemID", Integer, primary_key=True, index = True),
Column("attrID", Integer, primary_key=True, index = True),
Column("value", Float, nullable = False))
mapper(Override, overrides_table)

View File

@@ -19,7 +19,8 @@
from eos.db.util import processEager, processWhere
from eos.db import saveddata_session, sd_lock
from eos.types import User, Character, Fit, Price, DamagePattern, Fleet, MiscData, Wing, Squad, TargetResists
from eos.types import User, Character, Fit, Price, DamagePattern, Fleet, MiscData, Wing, Squad, TargetResists, Override, CrestChar
from eos.db.saveddata.fleet import squadmembers_table
from eos.db.saveddata.fit import projectedFits_table
from sqlalchemy.sql import and_
@@ -182,7 +183,7 @@ def getFit(lookfor, eager=None):
else:
eager = processEager(eager)
with sd_lock:
fit = saveddata_session.query(Fit).options(*eager).filter(Fit.ID == fitID).first()
fit = saveddata_session.query(Fit).options(*eager).filter(Fit.ID == lookfor).first()
else:
raise TypeError("Need integer as argument")
@@ -416,6 +417,45 @@ def getProjectedFits(fitID):
else:
raise TypeError("Need integer as argument")
def getCrestCharacters(eager=None):
eager = processEager(eager)
with sd_lock:
characters = saveddata_session.query(CrestChar).options(*eager).all()
return characters
@cachedQuery(CrestChar, 1, "lookfor")
def getCrestCharacter(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
with sd_lock:
character = saveddata_session.query(CrestChar).get(lookfor)
else:
eager = processEager(eager)
with sd_lock:
character = saveddata_session.query(CrestChar).options(*eager).filter(CrestChar.ID == lookfor).first()
elif isinstance(lookfor, basestring):
eager = processEager(eager)
with sd_lock:
character = saveddata_session.query(CrestChar).options(*eager).filter(CrestChar.name == lookfor).first()
else:
raise TypeError("Need integer or string as argument")
return character
def getOverrides(itemID, eager=None):
if isinstance(itemID, int):
return saveddata_session.query(Override).filter(Override.itemID == itemID).all()
else:
raise TypeError("Need integer as argument")
def clearOverrides():
with sd_lock:
deleted_rows = saveddata_session.query(Override).delete()
commit()
return deleted_rows
def getAllOverrides(eager=None):
return saveddata_session.query(Override).all()
def removeInvalid(fits):
invalids = [f for f in fits if f.isInvalid]

View File

@@ -1,4 +1,4 @@
# armorTankingGang2
# armorWarfareArmorHpReplacer
#
# Used by:
# Implant: Armored Warfare Mindlink

View File

@@ -1,7 +1,7 @@
# boosterArmorHpPenalty
#
# Used by:
# Implants from group: Booster (12 of 39)
# Implants from group: Booster (12 of 37)
type = "boosterSideEffect"
def handler(fit, booster, context):
fit.ship.boostItemAttr("armorHP", booster.getModifiedItemAttr("boosterArmorHPPenalty"))

View File

@@ -1,7 +1,7 @@
# boosterArmorRepairAmountPenalty
#
# Used by:
# Implants from group: Booster (9 of 39)
# Implants from group: Booster (9 of 37)
type = "boosterSideEffect"
def handler(fit, booster, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Armor Repair Unit",

View File

@@ -1,7 +1,7 @@
# boosterMaxVelocityPenalty
#
# Used by:
# Implants from group: Booster (12 of 39)
# Implants from group: Booster (12 of 37)
type = "boosterSideEffect"
def handler(fit, booster, context):
fit.ship.boostItemAttr("maxVelocity", booster.getModifiedItemAttr("boosterMaxVelocityPenalty"))

View File

@@ -1,7 +1,7 @@
# boosterShieldCapacityPenalty
#
# Used by:
# Implants from group: Booster (12 of 39)
# Implants from group: Booster (12 of 37)
type = "boosterSideEffect"
def handler(fit, booster, context):
fit.ship.boostItemAttr("shieldCapacity", booster.getModifiedItemAttr("boosterShieldCapacityPenalty"))

View File

@@ -1,7 +1,7 @@
# boosterTurretOptimalRangePenalty
#
# Used by:
# Implants from group: Booster (9 of 39)
# Implants from group: Booster (9 of 37)
type = "boosterSideEffect"
def handler(fit, booster, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"),

View File

@@ -0,0 +1,11 @@
# informationWarfareMaxTargetRangeBonus
#
# Used by:
# Implant: Caldari Navy Warfare Mindlink
# Implant: Imperial Navy Warfare Mindlink
# Implant: Information Warfare Mindlink
type = "gang"
gangBoost = "maxTargetRange"
gangBonus = "maxTargetRangeBonus"
def handler(fit, container, context):
fit.ship.boostItemAttr(gangBoost, container.getModifiedItemAttr(gangBonus))

View File

@@ -2,7 +2,6 @@
#
# Used by:
# Implant: Caldari Navy Warfare Mindlink
# Implant: Imperial Navy Warfare Mindlink
# Implant: Information Warfare Mindlink
type = "passive"
def handler(fit, implant, context):

View File

@@ -0,0 +1,10 @@
# miningForemanMindLinkMiningAmountBonusReplacer
#
# Used by:
# Implant: Mining Foreman Mindlink
type = "gang"
gangBoost = "miningAmount"
gangBonus = "miningAmountBonus"
def handler(fit, container, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining"),
gangBoost, container.getModifiedItemAttr(gangBonus) * level)

View File

@@ -1,7 +1,6 @@
# miningYieldGangBonusFixed
#
# Used by:
# Implant: Mining Foreman Mindlink
# Skill: Mining Foreman
type = "gang"
gangBoost = "miningAmount"

View File

@@ -1,7 +1,7 @@
# missileSkillRapidLauncherRoF
#
# Used by:
# Implants named like: Cerebral Accelerator (5 of 5)
# Implants named like: Cerebral Accelerator (3 of 3)
# Implants named like: Zainou 'Deadeye' Rapid Launch RL (6 of 6)
# Implant: Whelan Machorin's Ballistic Smartlink
# Skill: Missile Launcher Operation

View File

@@ -1,9 +1,9 @@
# overloadSelfThermalHardeningBonus
#
# Used by:
# Variations of module: Armor Thermic Hardener I (39 of 39)
# Variations of module: Thermic Dissipation Field I (19 of 19)
# Module: Civilian Thermic Dissipation Field
# Variations of module: Armor Thermal Hardener I (39 of 39)
# Variations of module: Thermal Dissipation Field I (19 of 19)
# Module: Civilian Thermal Dissipation Field
type = "overheat"
def handler(fit, module, context):
module.boostItemAttr("thermalDamageResistanceBonus", module.getModifiedItemAttr("overloadHardeningBonus"))

View File

@@ -1,9 +1,6 @@
# reconOperationsMaxTargetRangeBonusPostPercentMaxTargetRangeGangShips
#
# Used by:
# Implant: Caldari Navy Warfare Mindlink
# Implant: Imperial Navy Warfare Mindlink
# Implant: Information Warfare Mindlink
# Skill: Information Warfare
type = "gang"
gangBoost = "maxTargetRange"

View File

@@ -1,9 +1,6 @@
# shieldDefensiveOperationsShieldCapacityBonusPostPercentShieldCapacityGangShips
#
# Used by:
# Implant: Caldari Navy Warfare Mindlink
# Implant: Republic Fleet Warfare Mindlink
# Implant: Siege Warfare Mindlink
# Skill: Siege Warfare
type = "gang"
gangBoost = "shieldCapacity"

View File

@@ -2,8 +2,10 @@
#
# Used by:
# Ship: Devoter
# Ship: Gold Magnate
# Ship: Impairor
# Ship: Phobos
# Ship: Silver Magnate
type = "passive"
def handler(fit, ship, context):
fit.ship.boostItemAttr("armorEmDamageResonance", ship.getModifiedItemAttr("rookieArmorResistanceBonus"))

View File

@@ -2,8 +2,10 @@
#
# Used by:
# Ship: Devoter
# Ship: Gold Magnate
# Ship: Impairor
# Ship: Phobos
# Ship: Silver Magnate
type = "passive"
def handler(fit, ship, context):
fit.ship.boostItemAttr("armorExplosiveDamageResonance", ship.getModifiedItemAttr("rookieArmorResistanceBonus"))

View File

@@ -2,8 +2,10 @@
#
# Used by:
# Ship: Devoter
# Ship: Gold Magnate
# Ship: Impairor
# Ship: Phobos
# Ship: Silver Magnate
type = "passive"
def handler(fit, ship, context):
fit.ship.boostItemAttr("armorKineticDamageResonance", ship.getModifiedItemAttr("rookieArmorResistanceBonus"))

View File

@@ -2,8 +2,10 @@
#
# Used by:
# Ship: Devoter
# Ship: Gold Magnate
# Ship: Impairor
# Ship: Phobos
# Ship: Silver Magnate
type = "passive"
def handler(fit, ship, context):
fit.ship.boostItemAttr("armorThermalDamageResonance", ship.getModifiedItemAttr("rookieArmorResistanceBonus"))

View File

@@ -0,0 +1,11 @@
# siegeWarfareShieldCapacityBonusReplacer
#
# Used by:
# Implant: Caldari Navy Warfare Mindlink
# Implant: Republic Fleet Warfare Mindlink
# Implant: Siege Warfare Mindlink
type = "gang"
gangBoost = "shieldCapacity"
gangBonus = "shieldCapacityBonus"
def handler(fit, container, context):
fit.ship.boostItemAttr(gangBoost, container.getModifiedItemAttr(gangBonus) * level)

View File

@@ -1,9 +1,6 @@
# skirmishWarfareAgilityBonus
#
# Used by:
# Implant: Federation Navy Warfare Mindlink
# Implant: Republic Fleet Warfare Mindlink
# Implant: Skirmish Warfare Mindlink
# Skill: Skirmish Warfare
type = "gang"
gangBoost = "agility"

View File

@@ -0,0 +1,11 @@
# skirmishWarfareAgilityBonusReplacer
#
# Used by:
# Implant: Federation Navy Warfare Mindlink
# Implant: Republic Fleet Warfare Mindlink
# Implant: Skirmish Warfare Mindlink
type = "gang"
gangBoost = "agility"
gangBonus = "agilityBonus"
def handler(fit, container, context):
fit.ship.boostItemAttr(gangBoost, container.getModifiedItemAttr(gangBonus) * level)

View File

@@ -2,6 +2,8 @@
#
# Used by:
# Ship: Coercer
# Ship: Gold Magnate
# Ship: Silver Magnate
type = "passive"
def handler(fit, ship, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Energy Turret"),

View File

@@ -1,7 +1,7 @@
# surgicalStrikeDamageMultiplierBonusPostPercentDamageMultiplierLocationShipModulesRequiringGunnery
#
# Used by:
# Implants named like: Cerebral Accelerator (5 of 5)
# Implants named like: Cerebral Accelerator (3 of 3)
# Implants named like: Eifyr and Co. 'Gunslinger' Surgical Strike SS (6 of 6)
type = "passive"
def handler(fit, implant, context):

View File

@@ -1,7 +1,7 @@
# thermalShieldCompensationHardeningBonusGroupShieldAmp
#
# Used by:
# Skill: Thermic Shield Compensation
# Skill: Thermal Shield Compensation
type = "passive"
def handler(fit, skill, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Shield Amplifier",

View File

@@ -1,7 +1,7 @@
# thermicArmorCompensationHardeningBonusGroupArmorCoating
#
# Used by:
# Skill: Thermic Armor Compensation
# Skill: Thermal Armor Compensation
type = "passive"
def handler(fit, skill, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Armor Coating",

View File

@@ -1,7 +1,7 @@
# thermicArmorCompensationHardeningBonusGroupEnergized
#
# Used by:
# Skill: Thermic Armor Compensation
# Skill: Thermal Armor Compensation
type = "passive"
def handler(fit, skill, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Armor Plating Energized",

View File

@@ -24,6 +24,7 @@ from sqlalchemy.orm import reconstructor
from eqBase import EqBase
import traceback
import eos.db
try:
from collections import OrderedDict
@@ -168,7 +169,6 @@ class Item(EqBase):
info = getattr(cls, "MOVE_ATTR_INFO", None)
if info is None:
cls.MOVE_ATTR_INFO = info = []
import eos.db
for id in cls.MOVE_ATTRS:
info.append(eos.db.getAttributeInfo(id))
@@ -191,6 +191,7 @@ class Item(EqBase):
self.__moved = False
self.__offensive = None
self.__assistive = None
self.__overrides = None
@property
def attributes(self):
@@ -210,6 +211,32 @@ class Item(EqBase):
return False
@property
def overrides(self):
if self.__overrides is None:
self.__overrides = {}
overrides = eos.db.getOverrides(self.ID)
for x in overrides:
if x.attr.name in self.__attributes:
self.__overrides[x.attr.name] = x
return self.__overrides
def setOverride(self, attr, value):
from eos.saveddata.override import Override
if attr.name in self.__overrides:
override = self.__overrides.get(attr.name)
override.value = value
else:
override = Override(self, attr, value)
self.__overrides[attr.name] = override
eos.db.save(override)
def deleteOverride(self, attr):
override = self.__overrides.pop(attr.name, None)
eos.db.saveddata_session.delete(override)
eos.db.commit()
@property
def requiredSkills(self):
if self.__requiredSkills is None:
@@ -345,6 +372,12 @@ class Item(EqBase):
return False
def __repr__(self):
return "Item(ID={}, name={}) at {}".format(
self.ID, self.name, hex(id(self))
)
class MetaData(EqBase):
pass

View File

@@ -38,6 +38,9 @@ class ChargeAttrShortcut(object):
return None
class ModifiedAttributeDict(collections.MutableMapping):
OVERRIDES = False
class CalculationPlaceholder():
pass
@@ -51,6 +54,8 @@ class ModifiedAttributeDict(collections.MutableMapping):
self.__modified = {}
# Affected by entities
self.__affectedBy = {}
# Overrides
self.__overrides = {}
# Dictionaries for various value modification types
self.__forced = {}
self.__preAssigns = {}
@@ -79,6 +84,14 @@ class ModifiedAttributeDict(collections.MutableMapping):
self.__original = val
self.__modified.clear()
@property
def overrides(self):
return self.__overrides
@overrides.setter
def overrides(self, val):
self.__overrides = val
def __getitem__(self, key):
# Check if we have final calculated value
if key in self.__modified:
@@ -99,6 +112,8 @@ class ModifiedAttributeDict(collections.MutableMapping):
del self.__intermediary[key]
def getOriginal(self, key):
if self.OVERRIDES and key in self.__overrides:
return self.__overrides.get(key).value
val = self.__original.get(key)
if val is None:
return None

View File

@@ -58,6 +58,7 @@ class Booster(HandledItem, ItemAttrShortcut):
self.__sideEffects = []
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes
self.__itemModifiedAttributes.overrides = self.__item.overrides
self.__slot = self.__calculateSlot(self.__item)
for effect in self.__item.effects.itervalues():

View File

@@ -34,6 +34,7 @@ class Cargo(HandledItem, ItemAttrShortcut):
self.amount = 0
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = item.attributes
self.__itemModifiedAttributes.overrides = item.overrides
@reconstructor
def init(self):
@@ -48,6 +49,7 @@ class Cargo(HandledItem, ItemAttrShortcut):
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes
self.__itemModifiedAttributes.overrides = self.__item.overrides
@property
def itemModifiedAttributes(self):

View File

@@ -0,0 +1,46 @@
#===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
import urllib
from cStringIO import StringIO
from sqlalchemy.orm import reconstructor
#from tomorrow import threads
class CrestChar(object):
def __init__(self, id, name, refresh_token=None):
self.ID = id
self.name = name
self.refresh_token = refresh_token
@reconstructor
def init(self):
pass
'''
@threads(1)
def fetchImage(self):
url = 'https://image.eveonline.com/character/%d_128.jpg'%self.ID
fp = urllib.urlopen(url)
data = fp.read()
fp.close()
self.img = StringIO(data)
'''

View File

@@ -67,6 +67,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__miningyield = None
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes
self.__itemModifiedAttributes.overrides = self.__item.overrides
self.__chargeModifiedAttributes = ModifiedAttributeDict()
chargeID = self.getModifiedItemAttr("entityMissileTypeID")
@@ -74,6 +75,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
charge = eos.db.getItem(int(chargeID))
self.__charge = charge
self.__chargeModifiedAttributes.original = charge.attributes
self.__chargeModifiedAttributes.overrides = charge.overrides
@property
def itemModifiedAttributes(self):

View File

@@ -56,6 +56,7 @@ class Implant(HandledItem, ItemAttrShortcut):
""" Build object. Assumes proper and valid item already set """
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes
self.__itemModifiedAttributes.overrides = self.__item.overrides
self.__slot = self.__calculateSlot(self.__item)
@property

View File

@@ -30,6 +30,7 @@ class Mode(ItemAttrShortcut, HandledItem):
self.__item = item
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.item.attributes
self.__itemModifiedAttributes.overrides = self.item.overrides
@property
def item(self):

View File

@@ -113,10 +113,13 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if self.__item:
self.__itemModifiedAttributes.original = self.__item.attributes
self.__itemModifiedAttributes.overrides = self.__item.overrides
self.__hardpoint = self.__calculateHardpoint(self.__item)
self.__slot = self.__calculateSlot(self.__item)
if self.__charge:
self.__chargeModifiedAttributes.original = self.__charge.attributes
self.__chargeModifiedAttributes.overrides = self.__charge.overrides
@classmethod
def buildEmpty(cls, slot):
@@ -283,9 +286,11 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if charge is not None:
self.chargeID = charge.ID
self.__chargeModifiedAttributes.original = charge.attributes
self.__chargeModifiedAttributes.overrides = charge.overrides
else:
self.chargeID = None
self.__chargeModifiedAttributes.original = None
self.__chargeModifiedAttributes.overrides = {}
self.__itemModifiedAttributes.clear()

59
eos/saveddata/override.py Normal file
View File

@@ -0,0 +1,59 @@
#===============================================================================
# Copyright (C) 2015 Ryan Holmes
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
from eos.eqBase import EqBase
from sqlalchemy.orm import validates, reconstructor
import eos.db
import logging
logger = logging.getLogger(__name__)
class Override(EqBase):
def __init__(self, item, attr, value):
self.itemID = item.ID
self.__item = item
self.attrID = attr.ID
self.__attr = attr
self.value = value
@reconstructor
def init(self):
self.__attr = None
self.__item = None
if self.attrID:
self.__attr = eos.db.getAttributeInfo(self.attrID)
if self.__attr is None:
logger.error("Attribute (id: %d) does not exist", self.attrID)
return
if self.itemID:
self.__item = eos.db.getItem(self.itemID)
if self.__item is None:
logger.error("Item (id: %d) does not exist", self.itemID)
return
@property
def attr(self):
return self.__attr
@property
def item(self):
return self.__item

View File

@@ -51,6 +51,7 @@ class Ship(ItemAttrShortcut, HandledItem):
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = dict(self.item.attributes)
self.__itemModifiedAttributes.original.update(self.EXTRA_ATTRIBUTES)
self.__itemModifiedAttributes.overrides = self.item.overrides
self.commandBonus = 0

View File

@@ -21,6 +21,7 @@ from eos.gamedata import Attribute, Category, Effect, Group, Icon, Item, MarketG
MetaGroup, AttributeInfo, Unit, EffectInfo, MetaType, MetaData, Traits
from eos.saveddata.price import Price
from eos.saveddata.user import User
from eos.saveddata.crestchar import CrestChar
from eos.saveddata.damagePattern import DamagePattern
from eos.saveddata.targetResists import TargetResists
from eos.saveddata.character import Character, Skill
@@ -35,4 +36,5 @@ from eos.saveddata.fit import Fit
from eos.saveddata.mode import Mode
from eos.saveddata.fleet import Fleet, Wing, Squad
from eos.saveddata.miscData import MiscData
from eos.saveddata.override import Override
import eos.db

BIN
eve.db

Binary file not shown.

View File

@@ -11,7 +11,7 @@ TextTyped, EVT_TEXT = wx.lib.newevent.NewEvent()
class PFSearchBox(wx.Window):
def __init__(self, parent, id = wx.ID_ANY, value = "", pos = wx.DefaultPosition, size = wx.Size(-1,24), style = 0):
wx.Window.__init__(self, parent, id, pos, size, style = 0)
wx.Window.__init__(self, parent, id, pos, size, style = style)
self.isSearchButtonVisible = False
self.isCancelButtonVisible = False

View File

@@ -1 +1 @@
__all__ = ["pyfaGeneralPreferences","pyfaHTMLExportPreferences","pyfaUpdatePreferences","pyfaNetworkPreferences"]
__all__ = ["pyfaGeneralPreferences","pyfaHTMLExportPreferences","pyfaUpdatePreferences","pyfaNetworkPreferences","pyfaCrestPreferences"]

View File

@@ -0,0 +1,120 @@
import wx
from gui.preferenceView import PreferenceView
from gui.bitmapLoader import BitmapLoader
import gui.mainFrame
import service
from service.crest import CrestModes
class PFCrestPref ( PreferenceView):
title = "CREST"
def populatePanel( self, panel ):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = service.settings.CRESTSettings.getInstance()
self.dirtySettings = False
dlgWidth = panel.GetParent().GetParent().ClientSize.width
mainSizer = wx.BoxSizer( wx.VERTICAL )
self.stTitle = wx.StaticText( panel, wx.ID_ANY, self.title, wx.DefaultPosition, wx.DefaultSize, 0 )
self.stTitle.Wrap( -1 )
self.stTitle.SetFont( wx.Font( 12, 70, 90, 90, False, wx.EmptyString ) )
mainSizer.Add( self.stTitle, 0, wx.ALL, 5 )
self.m_staticline1 = wx.StaticLine( panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
mainSizer.Add( self.m_staticline1, 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5 )
self.stInfo = wx.StaticText( panel, wx.ID_ANY, u"Please see the pyfa wiki on GitHub for information regarding these options.", wx.DefaultPosition, wx.DefaultSize, 0 )
self.stInfo.Wrap(dlgWidth - 50)
mainSizer.Add( self.stInfo, 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5 )
rbSizer = wx.BoxSizer(wx.HORIZONTAL)
self.rbMode = wx.RadioBox(panel, -1, "Mode", wx.DefaultPosition, wx.DefaultSize, ['Implicit', 'User-supplied details'], 1, wx.RA_SPECIFY_COLS)
self.rbServer = wx.RadioBox(panel, -1, "Server", wx.DefaultPosition, wx.DefaultSize, ['Tranquility', 'Singularity'], 1, wx.RA_SPECIFY_COLS)
self.rbMode.SetSelection(self.settings.get('mode'))
self.rbServer.SetSelection(self.settings.get('server'))
rbSizer.Add(self.rbMode, 1, wx.TOP | wx.RIGHT, 5 )
rbSizer.Add(self.rbServer, 1, wx.ALL, 5 )
self.rbMode.Bind(wx.EVT_RADIOBOX, self.OnModeChange)
self.rbServer.Bind(wx.EVT_RADIOBOX, self.OnServerChange)
mainSizer.Add(rbSizer, 1, wx.ALL|wx.EXPAND, 0)
detailsTitle = wx.StaticText( panel, wx.ID_ANY, "CREST client details", wx.DefaultPosition, wx.DefaultSize, 0 )
detailsTitle.Wrap( -1 )
detailsTitle.SetFont( wx.Font( 12, 70, 90, 90, False, wx.EmptyString ) )
mainSizer.Add( detailsTitle, 0, wx.ALL, 5 )
mainSizer.Add( wx.StaticLine( panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ), 0, wx.EXPAND, 5 )
fgAddrSizer = wx.FlexGridSizer( 2, 2, 0, 0 )
fgAddrSizer.AddGrowableCol( 1 )
fgAddrSizer.SetFlexibleDirection( wx.BOTH )
fgAddrSizer.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED )
self.stSetID = wx.StaticText( panel, wx.ID_ANY, u"Client ID:", wx.DefaultPosition, wx.DefaultSize, 0 )
self.stSetID.Wrap( -1 )
fgAddrSizer.Add( self.stSetID, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
self.inputClientID = wx.TextCtrl( panel, wx.ID_ANY, self.settings.get('clientID'), wx.DefaultPosition, wx.DefaultSize, 0 )
fgAddrSizer.Add( self.inputClientID, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5 )
self.stSetSecret = wx.StaticText( panel, wx.ID_ANY, u"Client Secret:", wx.DefaultPosition, wx.DefaultSize, 0 )
self.stSetSecret.Wrap( -1 )
fgAddrSizer.Add( self.stSetSecret, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
self.inputClientSecret = wx.TextCtrl( panel, wx.ID_ANY, self.settings.get('clientSecret'), wx.DefaultPosition, wx.DefaultSize, 0 )
fgAddrSizer.Add( self.inputClientSecret, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5 )
self.btnApply = wx.Button( panel, wx.ID_ANY, u"Save Client Settings", wx.DefaultPosition, wx.DefaultSize, 0 )
self.btnApply.Bind(wx.EVT_BUTTON, self.OnBtnApply)
mainSizer.Add( fgAddrSizer, 0, wx.EXPAND, 5)
mainSizer.Add( self.btnApply, 0, wx.ALIGN_RIGHT, 5)
self.ToggleProxySettings(self.settings.get('mode'))
panel.SetSizer( mainSizer )
panel.Layout()
def OnModeChange(self, event):
self.settings.set('mode', event.GetInt())
self.ToggleProxySettings(self.settings.get('mode'))
service.Crest.restartService()
def OnServerChange(self, event):
self.settings.set('server', event.GetInt())
service.Crest.restartService()
def OnBtnApply(self, event):
self.settings.set('clientID', self.inputClientID.GetValue())
self.settings.set('clientSecret', self.inputClientSecret.GetValue())
sCrest = service.Crest.getInstance()
sCrest.delAllCharacters()
def ToggleProxySettings(self, mode):
if mode:
self.stSetID.Enable()
self.inputClientID.Enable()
self.stSetSecret.Enable()
self.inputClientSecret.Enable()
else:
self.stSetID.Disable()
self.inputClientID.Disable()
self.stSetSecret.Disable()
self.inputClientSecret.Disable()
def getImage(self):
return BitmapLoader.getBitmap("eve", "gui")
PFCrestPref.register()

View File

@@ -35,9 +35,6 @@ class CharacterEditor(wx.Frame):
size=wx.Size(641, 600), style=wx.DEFAULT_FRAME_STYLE|wx.FRAME_FLOAT_ON_PARENT|wx.TAB_TRAVERSAL)
i = wx.IconFromBitmap(BitmapLoader.getBitmap("character_small", "gui"))
self.mainFrame = parent
self.SetIcon(i)
self.disableWin= wx.WindowDisabler(self)

View File

@@ -25,16 +25,18 @@ class CopySelectDialog(wx.Dialog):
copyFormatEftImps = 1
copyFormatXml = 2
copyFormatDna = 3
copyFormatCrest = 4
def __init__(self, parent):
wx.Dialog.__init__(self, parent, id = wx.ID_ANY, title = u"Select a format", size = (-1,-1), style = wx.DEFAULT_DIALOG_STYLE)
mainSizer = wx.BoxSizer(wx.VERTICAL)
copyFormats = [u"EFT", u"EFT (Implants)", u"XML", u"DNA"]
copyFormats = [u"EFT", u"EFT (Implants)", u"XML", u"DNA", u"CREST"]
copyFormatTooltips = {CopySelectDialog.copyFormatEft: u"EFT text format",
CopySelectDialog.copyFormatEftImps: u"EFT text format",
CopySelectDialog.copyFormatXml: u"EVE native XML format",
CopySelectDialog.copyFormatDna: u"A one-line text format"}
CopySelectDialog.copyFormatDna: u"A one-line text format",
CopySelectDialog.copyFormatCrest: u"A JSON format used for EVE CREST"}
selector = wx.RadioBox(self, wx.ID_ANY, label = u"Copy to the clipboard using:", choices = copyFormats, style = wx.RA_SPECIFY_ROWS)
selector.Bind(wx.EVT_RADIOBOX, self.Selected)
for format, tooltip in copyFormatTooltips.iteritems():

363
gui/crestFittings.py Normal file
View File

@@ -0,0 +1,363 @@
import time
import webbrowser
import json
import wx
from wx.lib.pubsub import setupkwargs
from wx.lib.pubsub import pub
import service
from service.crest import CrestModes
import gui.display as d
from eos.types import Cargo
from eos.db import getItem
class CrestFittings(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, id=wx.ID_ANY, title="Browse EVE Fittings", pos=wx.DefaultPosition, size=wx.Size( 550,450 ), style=wx.DEFAULT_FRAME_STYLE | wx.TAB_TRAVERSAL)
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
self.mainFrame = parent
mainSizer = wx.BoxSizer(wx.VERTICAL)
sCrest = service.Crest.getInstance()
characterSelectSizer = wx.BoxSizer( wx.HORIZONTAL )
if sCrest.settings.get('mode') == CrestModes.IMPLICIT:
self.stLogged = wx.StaticText(self, wx.ID_ANY, "Currently logged in as %s"%sCrest.implicitCharacter.name, wx.DefaultPosition, wx.DefaultSize)
self.stLogged.Wrap( -1 )
characterSelectSizer.Add( self.stLogged, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
else:
self.charChoice = wx.Choice(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, [])
characterSelectSizer.Add( self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
self.updateCharList()
self.fetchBtn = wx.Button( self, wx.ID_ANY, u"Fetch Fits", wx.DefaultPosition, wx.DefaultSize, 5 )
characterSelectSizer.Add( self.fetchBtn, 0, wx.ALL, 5 )
mainSizer.Add( characterSelectSizer, 0, wx.EXPAND, 5 )
self.sl = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
mainSizer.Add( self.sl, 0, wx.EXPAND |wx.ALL, 5 )
contentSizer = wx.BoxSizer( wx.HORIZONTAL )
browserSizer = wx.BoxSizer( wx.VERTICAL )
self.fitTree = FittingsTreeView(self)
browserSizer.Add( self.fitTree, 1, wx.ALL|wx.EXPAND, 5 )
contentSizer.Add( browserSizer, 1, wx.EXPAND, 0 )
fitSizer = wx.BoxSizer( wx.VERTICAL )
self.fitView = FitView(self)
fitSizer.Add( self.fitView, 1, wx.ALL|wx.EXPAND, 5 )
btnSizer = wx.BoxSizer( wx.HORIZONTAL )
self.importBtn = wx.Button( self, wx.ID_ANY, u"Import to pyfa", wx.DefaultPosition, wx.DefaultSize, 5 )
self.deleteBtn = wx.Button( self, wx.ID_ANY, u"Delete from EVE", wx.DefaultPosition, wx.DefaultSize, 5 )
btnSizer.Add( self.importBtn, 1, wx.ALL, 5 )
btnSizer.Add( self.deleteBtn, 1, wx.ALL, 5 )
fitSizer.Add( btnSizer, 0, wx.EXPAND )
contentSizer.Add(fitSizer, 1, wx.EXPAND, 0)
mainSizer.Add(contentSizer, 1, wx.EXPAND, 5)
self.fetchBtn.Bind(wx.EVT_BUTTON, self.fetchFittings)
self.importBtn.Bind(wx.EVT_BUTTON, self.importFitting)
self.deleteBtn.Bind(wx.EVT_BUTTON, self.deleteFitting)
pub.subscribe(self.ssoLogout, 'logout_success')
self.statusbar = wx.StatusBar(self)
self.statusbar.SetFieldsCount()
self.SetStatusBar(self.statusbar)
self.cacheTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.updateCacheStatus, self.cacheTimer)
self.SetSizer(mainSizer)
self.Layout()
self.Centre(wx.BOTH)
def updateCharList(self):
sCrest = service.Crest.getInstance()
chars = sCrest.getCrestCharacters()
if len(chars) == 0:
self.Close()
for char in chars:
self.charChoice.Append(char.name, char.ID)
self.charChoice.SetSelection(0)
def updateCacheStatus(self, event):
t = time.gmtime(self.cacheTime-time.time())
if t < 0:
self.cacheTimer.Stop()
self.statusbar.Hide()
else:
sTime = time.strftime("%H:%M:%S", t)
self.statusbar.SetStatusText("Cached for %s"%sTime, 0)
def ssoLogout(self, message):
self.Close()
def getActiveCharacter(self):
sCrest = service.Crest.getInstance()
if sCrest.settings.get('mode') == CrestModes.IMPLICIT:
return sCrest.implicitCharacter.ID
selection = self.charChoice.GetCurrentSelection()
return self.charChoice.GetClientData(selection) if selection is not None else None
def fetchFittings(self, event):
sCrest = service.Crest.getInstance()
waitDialog = wx.BusyInfo("Fetching fits, please wait...", parent=self)
fittings = sCrest.getFittings(self.getActiveCharacter())
self.cacheTime = fittings.get('cached_until')
self.updateCacheStatus(None)
self.cacheTimer.Start(1000)
self.fitTree.populateSkillTree(fittings)
del waitDialog
def importFitting(self, event):
selection = self.fitView.fitSelection
if not selection:
return
data = self.fitTree.fittingsTreeCtrl.GetPyData(selection)
sFit = service.Fit.getInstance()
fits = sFit.importFitFromBuffer(data)
self.mainFrame._openAfterImport(fits)
def deleteFitting(self, event):
sCrest = service.Crest.getInstance()
selection = self.fitView.fitSelection
if not selection:
return
data = json.loads(self.fitTree.fittingsTreeCtrl.GetPyData(selection))
dlg = wx.MessageDialog(self,
"Do you really want to delete %s (%s) from EVE?"%(data['name'], data['ship']['name']),
"Confirm Delete", wx.YES | wx.NO | wx.ICON_QUESTION)
if dlg.ShowModal() == wx.ID_YES:
sCrest.delFitting(self.getActiveCharacter(), data['fittingID'])
class ExportToEve(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, id=wx.ID_ANY, title="Export fit to EVE", pos=wx.DefaultPosition, size=(wx.Size(350,100)), style=wx.DEFAULT_FRAME_STYLE | wx.TAB_TRAVERSAL)
self.mainFrame = parent
self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE))
sCrest = service.Crest.getInstance()
mainSizer = wx.BoxSizer(wx.VERTICAL)
hSizer = wx.BoxSizer(wx.HORIZONTAL)
if sCrest.settings.get('mode') == CrestModes.IMPLICIT:
self.stLogged = wx.StaticText(self, wx.ID_ANY, "Currently logged in as %s"%sCrest.implicitCharacter.name, wx.DefaultPosition, wx.DefaultSize)
self.stLogged.Wrap( -1 )
hSizer.Add( self.stLogged, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
else:
self.charChoice = wx.Choice(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, [])
hSizer.Add( self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
self.updateCharList()
self.charChoice.SetSelection(0)
self.exportBtn = wx.Button( self, wx.ID_ANY, u"Export Fit", wx.DefaultPosition, wx.DefaultSize, 5 )
hSizer.Add( self.exportBtn, 0, wx.ALL, 5 )
mainSizer.Add( hSizer, 0, wx.EXPAND, 5 )
self.exportBtn.Bind(wx.EVT_BUTTON, self.exportFitting)
self.statusbar = wx.StatusBar(self)
self.statusbar.SetFieldsCount(2)
self.statusbar.SetStatusWidths([100, -1])
pub.subscribe(self.ssoLogout, 'logout_success')
self.SetSizer(hSizer)
self.SetStatusBar(self.statusbar)
self.Layout()
self.Centre(wx.BOTH)
def updateCharList(self):
sCrest = service.Crest.getInstance()
chars = sCrest.getCrestCharacters()
if len(chars) == 0:
self.Close()
for char in chars:
self.charChoice.Append(char.name, char.ID)
self.charChoice.SetSelection(0)
def ssoLogout(self, message):
self.Close()
def getActiveCharacter(self):
sCrest = service.Crest.getInstance()
if sCrest.settings.get('mode') == CrestModes.IMPLICIT:
return sCrest.implicitCharacter.ID
selection = self.charChoice.GetCurrentSelection()
return self.charChoice.GetClientData(selection) if selection is not None else None
def exportFitting(self, event):
self.statusbar.SetStatusText("", 0)
self.statusbar.SetStatusText("Sending request and awaiting response", 1)
sCrest = service.Crest.getInstance()
sFit = service.Fit.getInstance()
data = sFit.exportCrest(self.mainFrame.getActiveFit())
res = sCrest.postFitting(self.getActiveCharacter(), data)
self.statusbar.SetStatusText("%d: %s"%(res.status_code, res.reason), 0)
try:
text = json.loads(res.text)
self.statusbar.SetStatusText(text['message'], 1)
except ValueError:
self.statusbar.SetStatusText("", 1)
class CrestMgmt(wx.Dialog):
def __init__( self, parent ):
wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = "CREST Character Management", pos = wx.DefaultPosition, size = wx.Size( 550,250 ), style = wx.DEFAULT_DIALOG_STYLE )
mainSizer = wx.BoxSizer( wx.HORIZONTAL )
self.lcCharacters = wx.ListCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LC_REPORT)
self.lcCharacters.InsertColumn(0, heading='Character')
self.lcCharacters.InsertColumn(1, heading='Refresh Token')
self.popCharList()
mainSizer.Add( self.lcCharacters, 1, wx.ALL|wx.EXPAND, 5 )
btnSizer = wx.BoxSizer( wx.VERTICAL )
self.addBtn = wx.Button( self, wx.ID_ANY, u"Add Character", wx.DefaultPosition, wx.DefaultSize, 0 )
btnSizer.Add( self.addBtn, 0, wx.ALL | wx.EXPAND, 5 )
self.deleteBtn = wx.Button( self, wx.ID_ANY, u"Revoke Character", wx.DefaultPosition, wx.DefaultSize, 0 )
btnSizer.Add( self.deleteBtn, 0, wx.ALL | wx.EXPAND, 5 )
mainSizer.Add( btnSizer, 0, wx.EXPAND, 5 )
self.addBtn.Bind(wx.EVT_BUTTON, self.addChar)
self.deleteBtn.Bind(wx.EVT_BUTTON, self.delChar)
pub.subscribe(self.ssoLogin, 'login_success')
self.SetSizer( mainSizer )
self.Layout()
self.Centre( wx.BOTH )
def ssoLogin(self, type):
self.popCharList()
def popCharList(self):
sCrest = service.Crest.getInstance()
chars = sCrest.getCrestCharacters()
self.lcCharacters.DeleteAllItems()
for index, char in enumerate(chars):
self.lcCharacters.InsertStringItem(index, char.name)
self.lcCharacters.SetStringItem(index, 1, char.refresh_token)
self.lcCharacters.SetItemData(index, char.ID)
self.lcCharacters.SetColumnWidth(0, wx.LIST_AUTOSIZE)
self.lcCharacters.SetColumnWidth(1, wx.LIST_AUTOSIZE)
def addChar(self, event):
sCrest = service.Crest.getInstance()
uri = sCrest.startServer()
webbrowser.open(uri)
def delChar(self, event):
item = self.lcCharacters.GetFirstSelected()
charID = self.lcCharacters.GetItemData(item)
sCrest = service.Crest.getInstance()
sCrest.delCrestCharacter(charID)
self.popCharList()
class FittingsTreeView(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, id=wx.ID_ANY)
self.parent = parent
pmainSizer = wx.BoxSizer(wx.VERTICAL)
tree = self.fittingsTreeCtrl = wx.TreeCtrl(self, wx.ID_ANY, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT)
pmainSizer.Add(tree, 1, wx.EXPAND | wx.ALL, 0)
self.root = tree.AddRoot("Fits")
self.populateSkillTree(None)
self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.displayFit)
self.SetSizer(pmainSizer)
self.Layout()
def populateSkillTree(self, data):
if data is None:
return
root = self.root
tree = self.fittingsTreeCtrl
tree.DeleteChildren(root)
dict = {}
fits = data['items']
for fit in fits:
if fit['ship']['name'] not in dict:
dict[fit['ship']['name']] = []
dict[fit['ship']['name']].append(fit)
for name, fits in dict.iteritems():
shipID = tree.AppendItem(root, name)
for fit in fits:
fitId = tree.AppendItem(shipID, fit['name'])
tree.SetPyData(fitId, json.dumps(fit))
tree.SortChildren(root)
def displayFit(self, event):
selection = self.fittingsTreeCtrl.GetSelection()
fit = json.loads(self.fittingsTreeCtrl.GetPyData(selection))
list = []
for item in fit['items']:
try:
cargo = Cargo(getItem(item['type']['id']))
cargo.amount = item['quantity']
list.append(cargo)
except:
pass
self.parent.fitView.fitSelection = selection
self.parent.fitView.update(list)
class FitView(d.Display):
DEFAULT_COLS = ["Base Icon",
"Base Name"]
def __init__(self, parent):
d.Display.__init__(self, parent, style=wx.LC_SINGLE_SEL)
self.fitSelection = None

View File

@@ -80,7 +80,7 @@ class ItemStatsDialog(wx.Dialog):
itemImg = BitmapLoader.getBitmap(iconFile, "icons")
if itemImg is not None:
self.SetIcon(wx.IconFromBitmap(itemImg))
self.SetTitle("%s: %s" % ("%s Stats" % itmContext if itmContext is not None else "Stats", item.name))
self.SetTitle("%s: %s%s" % ("%s Stats" % itmContext if itmContext is not None else "Stats", item.name, " (%d)"%item.ID if config.debug else ""))
self.SetMinSize((300, 200))
if "wxGTK" in wx.PlatformInfo: # GTK has huge tab widgets, give it a bit more room

View File

@@ -30,6 +30,7 @@ from wx.lib.wordwrap import wordwrap
import service
import config
import threading
import webbrowser
import gui.aboutData
import gui.chromeTabs
@@ -44,6 +45,7 @@ from gui.multiSwitch import MultiSwitch
from gui.statsPane import StatsPane
from gui.shipBrowser import ShipBrowser, FitSelected, ImportSelected, Stage3Selected
from gui.characterEditor import CharacterEditor, SaveCharacterAs
from gui.crestFittings import CrestFittings, ExportToEve, CrestMgmt
from gui.characterSelection import CharacterSelection
from gui.patternEditor import DmgPatternEditorDlg
from gui.resistsEditor import ResistsEditorDlg
@@ -53,10 +55,18 @@ from gui.copySelectDialog import CopySelectDialog
from gui.utils.clipboard import toClipboard, fromClipboard
from gui.fleetBrowser import FleetBrowser
from gui.updateDialog import UpdateDialog
from gui.propertyEditor import AttributeEditor
from gui.builtinViews import *
# import this to access override setting
from eos.modifiedAttributeDict import ModifiedAttributeDict
from time import gmtime, strftime
from service.crest import CrestModes
from wx.lib.pubsub import setupkwargs
from wx.lib.pubsub import pub
#dummy panel(no paint no erasebk)
class PFPanel(wx.Panel):
@@ -101,8 +111,8 @@ class MainFrame(wx.Frame):
return cls.__instance if cls.__instance is not None else MainFrame()
def __init__(self):
title="pyfa %s%s - Python Fitting Assistant"%(config.version, "" if config.tag.lower() != 'git' else " (git)")
wx.Frame.__init__(self, None, wx.ID_ANY, title)
self.title="pyfa %s%s - Python Fitting Assistant"%(config.version, "" if config.tag.lower() != 'git' else " (git)")
wx.Frame.__init__(self, None, wx.ID_ANY, self.title)
MainFrame.__instance = self
@@ -193,6 +203,12 @@ class MainFrame(wx.Frame):
self.sUpdate = service.Update.getInstance()
self.sUpdate.CheckUpdate(self.ShowUpdateBox)
pub.subscribe(self.onSSOLogin, 'login_success')
pub.subscribe(self.onSSOLogout, 'logout_success')
self.titleTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.updateTitle, self.titleTimer)
def ShowUpdateBox(self, release):
dlg = UpdateDialog(self, release)
dlg.ShowModal()
@@ -329,6 +345,10 @@ class MainFrame(wx.Frame):
dlg=CharacterEditor(self)
dlg.Show()
def showAttrEditor(self, event):
dlg=AttributeEditor(self)
dlg.Show()
def showTargetResistsEditor(self, event):
dlg=ResistsEditorDlg(self)
dlg.ShowModal()
@@ -370,10 +390,10 @@ class MainFrame(wx.Frame):
dlg.ShowModal()
def goWiki(self, event):
wx.LaunchDefaultBrowser('https://github.com/DarkFenX/Pyfa/wiki')
webbrowser.open('https://github.com/DarkFenX/Pyfa/wiki')
def goForums(self, event):
wx.LaunchDefaultBrowser('https://forums.eveonline.com/default.aspx?g=posts&t=247609')
webbrowser.open('https://forums.eveonline.com/default.aspx?g=posts&t=247609')
def registerMenu(self):
menuBar = self.GetMenuBar()
@@ -417,6 +437,18 @@ class MainFrame(wx.Frame):
# Save current character
self.Bind(wx.EVT_MENU, self.revertChar, id = menuBar.revertCharId)
# Browse fittings
self.Bind(wx.EVT_MENU, self.eveFittings, id = menuBar.eveFittingsId)
# Export to EVE
self.Bind(wx.EVT_MENU, self.exportToEve, id = menuBar.exportToEveId)
# Login to EVE
self.Bind(wx.EVT_MENU, self.ssoLogin, id = menuBar.ssoLoginId)
# Open attribute editor
self.Bind(wx.EVT_MENU, self.showAttrEditor, id = menuBar.attrEditorId)
# Toggle Overrides
self.Bind(wx.EVT_MENU, self.toggleOverrides, id = menuBar.toggleOverridesId)
#Clipboard exports
self.Bind(wx.EVT_MENU, self.exportToClipboard, id=wx.ID_COPY)
@@ -480,6 +512,49 @@ class MainFrame(wx.Frame):
atable = wx.AcceleratorTable(actb)
self.SetAcceleratorTable(atable)
def eveFittings(self, event):
dlg=CrestFittings(self)
dlg.Show()
def updateTitle(self, event):
sCrest = service.Crest.getInstance()
char = sCrest.implicitCharacter
if char:
t = time.gmtime(char.eve.expires-time.time())
sTime = time.strftime("%H:%M:%S", t if t >= 0 else 0)
newTitle = "%s | %s - %s"%(self.title, char.name, sTime)
self.SetTitle(newTitle)
def onSSOLogin(self, type):
if type == 0:
self.titleTimer.Start(1000)
def onSSOLogout(self, message):
self.titleTimer.Stop()
self.SetTitle(self.title)
def ssoLogin(self, event):
sCrest = service.Crest.getInstance()
if sCrest.settings.get('mode') == CrestModes.IMPLICIT:
if sCrest.implicitCharacter is not None:
sCrest.logout()
else:
uri = sCrest.startServer()
webbrowser.open(uri)
else:
dlg=CrestMgmt(self)
dlg.Show()
def exportToEve(self, event):
dlg=ExportToEve(self)
dlg.Show()
def toggleOverrides(self, event):
ModifiedAttributeDict.OVERRIDES = not ModifiedAttributeDict.OVERRIDES
wx.PostEvent(self, GE.FitChanged(fitID=self.getActiveFit()))
menu = self.GetMenuBar()
menu.SetLabel(menu.toggleOverridesId, "Turn Overrides Off" if ModifiedAttributeDict.OVERRIDES else "Turn Overrides On")
def saveChar(self, event):
sChr = service.Character.getInstance()
charID = self.charSelection.getActiveCharacter()
@@ -542,6 +617,10 @@ class MainFrame(wx.Frame):
sFit = service.Fit.getInstance()
toClipboard(sFit.exportDna(self.getActiveFit()))
def clipboardCrest(self):
sFit = service.Fit.getInstance()
toClipboard(sFit.exportCrest(self.getActiveFit()))
def clipboardXml(self):
sFit = service.Fit.getInstance()
toClipboard(sFit.exportXml(None, self.getActiveFit()))
@@ -559,14 +638,15 @@ class MainFrame(wx.Frame):
CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft,
CopySelectDialog.copyFormatEftImps: self.clipboardEftImps,
CopySelectDialog.copyFormatXml: self.clipboardXml,
CopySelectDialog.copyFormatDna: self.clipboardDna}
CopySelectDialog.copyFormatDna: self.clipboardDna,
CopySelectDialog.copyFormatCrest: self.clipboardCrest}
dlg = CopySelectDialog(self)
dlg.ShowModal()
selected = dlg.GetSelected()
try:
CopySelectDict[selected]()
except:
pass
CopySelectDict[selected]()
dlg.Destroy()
def exportSkillsNeeded(self, event):

View File

@@ -24,6 +24,10 @@ import gui.mainFrame
import gui.graphFrame
import gui.globalEvents as GE
import service
from service.crest import CrestModes
from wx.lib.pubsub import setupkwargs
from wx.lib.pubsub import pub
class MainMenuBar(wx.MenuBar):
def __init__(self):
@@ -40,9 +44,16 @@ class MainMenuBar(wx.MenuBar):
self.saveCharId = wx.NewId()
self.saveCharAsId = wx.NewId()
self.revertCharId = wx.NewId()
self.eveFittingsId = wx.NewId()
self.exportToEveId = wx.NewId()
self.ssoLoginId = wx.NewId()
self.attrEditorId = wx.NewId()
self.toggleOverridesId = wx.NewId()
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sCrest = service.Crest.getInstance()
wx.MenuBar.__init__(self)
# File menu
@@ -78,6 +89,9 @@ class MainMenuBar(wx.MenuBar):
editMenu.Append(self.saveCharId, "Save Character")
editMenu.Append(self.saveCharAsId, "Save Character As...")
editMenu.Append(self.revertCharId, "Revert Character")
editMenu.AppendSeparator()
editMenu.Append(self.toggleOverridesId, "Turn Overrides On")
# Character menu
windowMenu = wx.Menu()
self.Append(windowMenu, "&Window")
@@ -102,6 +116,24 @@ class MainMenuBar(wx.MenuBar):
preferencesItem.SetBitmap(BitmapLoader.getBitmap("preferences_small", "gui"))
windowMenu.AppendItem(preferencesItem)
# CREST Menu
crestMenu = wx.Menu()
self.Append(crestMenu, "&CREST")
if self.sCrest.settings.get('mode') != CrestModes.IMPLICIT:
crestMenu.Append(self.ssoLoginId, "Manage Characters")
else:
crestMenu.Append(self.ssoLoginId, "Login to EVE")
crestMenu.Append(self.eveFittingsId, "Browse EVE Fittings")
crestMenu.Append(self.exportToEveId, "Export To EVE")
if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT or len(self.sCrest.getCrestCharacters()) == 0:
self.Enable(self.eveFittingsId, False)
self.Enable(self.exportToEveId, False)
attrItem = wx.MenuItem(windowMenu, self.attrEditorId, "Attribute Overrides\tCTRL+A")
attrItem.SetBitmap(BitmapLoader.getBitmap("fit_rename_small", "gui"))
windowMenu.AppendItem(attrItem)
# Help menu
helpMenu = wx.Menu()
self.Append(helpMenu, "&Help")
@@ -114,6 +146,9 @@ class MainMenuBar(wx.MenuBar):
helpMenu.Append( self.mainFrame.widgetInspectMenuID, "Open Widgets Inspect tool", "Open Widgets Inspect tool")
self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged)
pub.subscribe(self.ssoLogin, 'login_success')
pub.subscribe(self.ssoLogout, 'logout_success')
pub.subscribe(self.updateCrest, 'crest_changed')
def fitChanged(self, event):
enable = event.fitID is not None
@@ -131,3 +166,25 @@ class MainMenuBar(wx.MenuBar):
self.Enable(self.revertCharId, char.isDirty)
event.Skip()
def ssoLogin(self, type):
if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT:
self.SetLabel(self.ssoLoginId, "Logout Character")
self.Enable(self.eveFittingsId, True)
self.Enable(self.exportToEveId, True)
def ssoLogout(self, message):
if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT:
self.SetLabel(self.ssoLoginId, "Login to EVE")
self.Enable(self.eveFittingsId, False)
self.Enable(self.exportToEveId, False)
def updateCrest(self, message):
bool = self.sCrest.settings.get('mode') == CrestModes.IMPLICIT or len(self.sCrest.getCrestCharacters()) == 0
self.Enable(self.eveFittingsId, not bool)
self.Enable(self.exportToEveId, not bool)
if self.sCrest.settings.get('mode') == CrestModes.IMPLICIT:
self.SetLabel(self.ssoLoginId, "Login to EVE")
else:
self.SetLabel(self.ssoLoginId, "Manage Characters")

View File

@@ -103,8 +103,8 @@ class MarketBrowser(wx.Panel):
self.marketView.jump(item)
class SearchBox(SBox.PFSearchBox):
def __init__(self, parent):
SBox.PFSearchBox.__init__(self, parent)
def __init__(self, parent, **kwargs):
SBox.PFSearchBox.__init__(self, parent, **kwargs)
cancelBitmap = BitmapLoader.getBitmap("fit_delete_small","gui")
searchBitmap = BitmapLoader.getBitmap("fsearch_small","gui")
self.SetSearchBitmap(searchBitmap)

264
gui/propertyEditor.py Normal file
View File

@@ -0,0 +1,264 @@
import wx
import wx.propgrid as wxpg
import gui.PFSearchBox as SBox
from gui.marketBrowser import SearchBox
import gui.display as d
import gui.globalEvents as GE
from gui.bitmapLoader import BitmapLoader
import service
import csv
import eos.db
import logging
logger = logging.getLogger(__name__)
class AttributeEditor( wx.Frame ):
def __init__( self, parent ):
wx.Frame.__init__(self, parent, wx.ID_ANY, title="Attribute Editor", pos=wx.DefaultPosition,
size=wx.Size(650, 600), style=wx.DEFAULT_FRAME_STYLE|wx.FRAME_FLOAT_ON_PARENT|wx.TAB_TRAVERSAL)
i = wx.IconFromBitmap(BitmapLoader.getBitmap("fit_rename_small", "gui"))
self.SetIcon(i)
self.mainFrame = parent
menubar = wx.MenuBar()
fileMenu = wx.Menu()
fileImport = fileMenu.Append(wx.ID_ANY, 'Import', 'Import overrides')
fileExport = fileMenu.Append(wx.ID_ANY, 'Export', 'Import overrides')
fileClear = fileMenu.Append(wx.ID_ANY, 'Clear All', 'Clear all overrides')
menubar.Append(fileMenu, '&File')
self.SetMenuBar(menubar)
self.Bind(wx.EVT_MENU, self.OnImport, fileImport)
self.Bind(wx.EVT_MENU, self.OnExport, fileExport)
self.Bind(wx.EVT_MENU, self.OnClear, fileClear)
i = wx.IconFromBitmap(BitmapLoader.getBitmap("fit_rename_small", "gui"))
self.SetIcon(i)
self.mainFrame = parent
self.panel = panel = wx.Panel(self, wx.ID_ANY)
mainSizer = wx.BoxSizer(wx.HORIZONTAL)
leftSizer = wx.BoxSizer(wx.VERTICAL)
leftPanel = wx.Panel(panel, wx.ID_ANY, style=wx.DOUBLE_BORDER if 'wxMSW' in wx.PlatformInfo else wx.SIMPLE_BORDER)
self.searchBox = SearchBox(leftPanel)
self.itemView = ItemView(leftPanel)
leftSizer.Add(self.searchBox, 0, wx.EXPAND)
leftSizer.Add(self.itemView, 1, wx.EXPAND)
leftPanel.SetSizer(leftSizer)
mainSizer.Add(leftPanel, 1, wx.ALL | wx.EXPAND, 5)
rightSizer = wx.BoxSizer(wx.VERTICAL)
self.btnRemoveOverrides = wx.Button( panel, wx.ID_ANY, u"Remove Overides for Item", wx.DefaultPosition, wx.DefaultSize, 0 )
self.pg = AttributeGrid(panel)
rightSizer.Add(self.pg, 1, wx.ALL|wx.EXPAND, 5)
rightSizer.Add(self.btnRemoveOverrides, 0, wx.ALL | wx.EXPAND, 5 )
self.btnRemoveOverrides.Bind(wx.EVT_BUTTON, self.pg.removeOverrides)
self.btnRemoveOverrides.Enable(False)
mainSizer.Add(rightSizer, 1, wx.EXPAND)
panel.SetSizer(mainSizer)
mainSizer.SetSizeHints(panel)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(panel, 1, wx.EXPAND)
self.SetSizer(sizer)
self.SetAutoLayout(True)
self.Bind(wx.EVT_CLOSE, self.OnClose)
def OnClose(self, event):
fitID = self.mainFrame.getActiveFit()
if fitID is not None:
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID))
self.Destroy()
def OnImport(self, event):
dlg = wx.FileDialog(self, "Import pyfa override file",
wildcard = "pyfa override file (*.csv)|*.csv",
style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
if (dlg.ShowModal() == wx.ID_OK):
path = dlg.GetPath()
with open(path, 'rb') as csvfile:
spamreader = csv.reader(csvfile)
for row in spamreader:
itemID, attrID, value = row
item = eos.db.getItem(int(itemID))
attr = eos.db.getAttributeInfo(int(attrID))
item.setOverride(attr, float(value))
self.itemView.updateItems(True)
def OnExport(self, event):
sMkt = service.Market.getInstance()
items = sMkt.getItemsWithOverrides()
defaultFile = "pyfa_overrides.csv"
dlg = wx.FileDialog(self, "Save Overrides As...",
wildcard = "pyfa overrides (*.csv)|*.csv",
style = wx.FD_SAVE,
defaultFile=defaultFile)
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
with open(path, 'wb') as csvfile:
writer = csv.writer(csvfile)
for item in items:
for key, override in item.overrides.iteritems():
writer.writerow([item.ID, override.attrID, override.value])
def OnClear(self, event):
dlg = wx.MessageDialog(self,
"Are you sure you want to delete all overrides?",
"Confirm Delete", wx.YES | wx.NO | wx.ICON_EXCLAMATION)
if dlg.ShowModal() == wx.ID_YES:
sMkt = service.Market.getInstance()
items = sMkt.getItemsWithOverrides()
# We can't just delete overrides, as loaded items will still have
# them assigned. Deleting them from the database won't propagate
# them due to the eve/user database disconnect. We must loop through
# all items that have overrides and remove them
for item in items:
for _, x in item.overrides.items():
item.deleteOverride(x.attr)
self.itemView.updateItems(True)
self.pg.Clear()
# This is literally a stripped down version of the market.
class ItemView(d.Display):
DEFAULT_COLS = ["Base Icon",
"Base Name",
"attr:power,,,True",
"attr:cpu,,,True"]
def __init__(self, parent):
d.Display.__init__(self, parent)
sMkt = service.Market.getInstance()
self.things = sMkt.getItemsWithOverrides()
self.items = self.things
self.searchBox = parent.Parent.Parent.searchBox
# Bind search actions
self.searchBox.Bind(SBox.EVT_TEXT_ENTER, self.scheduleSearch)
self.searchBox.Bind(SBox.EVT_SEARCH_BTN, self.scheduleSearch)
self.searchBox.Bind(SBox.EVT_CANCEL_BTN, self.clearSearch)
self.searchBox.Bind(SBox.EVT_TEXT, self.scheduleSearch)
self.update(self.items)
def clearSearch(self, event=None):
if event:
self.searchBox.Clear()
self.items = self.things
self.update(self.items)
def updateItems(self, updateDisplay=False):
sMkt = service.Market.getInstance()
self.things = sMkt.getItemsWithOverrides()
self.items = self.things
if updateDisplay:
self.update(self.things)
def scheduleSearch(self, event=None):
sMkt = service.Market.getInstance()
search = self.searchBox.GetLineText(0)
# Make sure we do not count wildcard as search symbol
realsearch = search.replace("*", "")
# Show nothing if query is too short
if len(realsearch) < 3:
self.clearSearch()
return
sMkt.searchItems(search, self.populateSearch, False)
def populateSearch(self, items):
self.items = list(items)
self.update(items)
class AttributeGrid(wxpg.PropertyGrid):
def __init__(self, parent):
wxpg.PropertyGrid.__init__(self, parent, style=wxpg.PG_HIDE_MARGIN|wxpg.PG_HIDE_CATEGORIES|wxpg.PG_BOLD_MODIFIED|wxpg.PG_TOOLTIPS)
self.SetExtraStyle(wxpg.PG_EX_HELP_AS_TOOLTIPS)
self.item = None
self.itemView = parent.Parent.itemView
self.btn = parent.Parent.btnRemoveOverrides
self.Bind( wxpg.EVT_PG_CHANGED, self.OnPropGridChange )
self.Bind( wxpg.EVT_PG_SELECTED, self.OnPropGridSelect )
self.Bind( wxpg.EVT_PG_RIGHT_CLICK, self.OnPropGridRightClick )
self.itemView.Bind(wx.EVT_LIST_ITEM_SELECTED, self.itemActivated)
self.itemView.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemActivated)
def itemActivated(self, event):
self.Clear()
self.btn.Enable(True)
sel = event.EventObject.GetFirstSelected()
self.item = item = self.itemView.items[sel]
for key in sorted(item.attributes.keys()):
override = item.overrides.get(key, None)
default = item.attributes[key].value
if override and override.value != default:
prop = wxpg.FloatProperty(key, value=override.value)
prop.SetModifiedStatus(True)
else:
prop = wxpg.FloatProperty(key, value=default)
prop.SetClientData(item.attributes[key]) # set this so that we may access it later
prop.SetHelpString("%s\n%s"%(item.attributes[key].displayName or key, "Default Value: %0.3f"%default))
self.Append(prop)
def removeOverrides(self, event):
if self.item is None:
return
for _, x in self.item.overrides.items():
self.item.deleteOverride(x.attr)
self.itemView.updateItems(True)
self.ClearModifiedStatus()
self.itemView.Select(self.itemView.GetFirstSelected(), on=False)
self.Clear()
def Clear(self):
self.item = None
self.btn.Enable(False)
wxpg.PropertyGrid.Clear(self)
def OnPropGridChange(self, event):
p = event.GetProperty()
attr = p.GetClientData()
if p.GetValue() == attr.value:
self.item.deleteOverride(attr)
p.SetModifiedStatus(False)
else:
self.item.setOverride(attr, p.GetValue())
self.itemView.updateItems()
logger.debug('%s changed to "%s"' % (p.GetName(), p.GetValueAsString()))
def OnPropGridSelect(self, event):
pass
def OnPropGridRightClick(self, event):
pass

BIN
imgs/gui/eve.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -103,7 +103,7 @@ def main(old, new, groups=True, effects=True, attributes=True, renames=True):
if implementedtag:
print("\n[{0}] \"{1}\"\n[{2}] \"{3}\"".format(geteffst(couple[0]), couple[0], geteffst(couple[1]), couple[1]))
else:
print("\n\"{0}\"\n\"{1}\"".format(couple[0], couple[1]))
print(" \"{0}\": \"{1}\",".format(couple[0], couple[1]))
groupcats = {}
def getgroupcat(grp):

View File

@@ -62,27 +62,25 @@ if not args.nojson:
pickle_miner = ResourcePickleMiner(rvr)
trans = Translator(pickle_miner)
bulkdata_miner = BulkdataMiner(rvr, trans)
staticcache_miner = StaticdataCacheMiner(eve_path, trans)
staticcache_miner = ResourceStaticCacheMiner(rvr, trans)
miners = (
MetadataMiner(eve_path),
bulkdata_miner,
TraitMiner(staticcache_miner, bulkdata_miner, trans),
SqliteMiner(eve_path, trans),
staticcache_miner,
#CachedCallsMiner(rvr, trans),
TraitMiner(staticcache_miner, bulkdata_miner, trans),
SqliteMiner(rvr.paths.root, trans),
CachedCallsMiner(rvr, trans),
pickle_miner
)
writers = (
JsonWriter(dump_path, indent=2),
)
list = "dgmexpressions,dgmattribs,dgmeffects,dgmtypeattribs,dgmtypeeffects,"\
"dgmunits,icons,invcategories,invgroups,invmetagroups,invmetatypes,"\
"invtypes,mapbulk_marketGroups,phbmetadata,phbtraits,fsdTypeOverrides"\
"evegroups,evetypes"
"invtypes,mapbulk_marketGroups,phbmetadata,phbtraits,fsdTypeOverrides,"\
"evegroups,evetypes,evecategories"
FlowManager(miners, writers).run(list, "multi")

View File

@@ -10,3 +10,6 @@ from service.update import Update
from service.price import Price
from service.network import Network
from service.eveapi import EVEAPIConnection, ParseXML
from service.crest import Crest
from service.server import StoppableHTTPServer, AuthHandler
from service.pycrest import EVE

View File

@@ -0,0 +1,174 @@
"""
Conversion pack for Parallax renames
"""
CONVERSIONS = {
# Renamed items
"Basic Thermic Plating": "Basic Thermal Plating",
"Thermic Plating I": "Thermal Plating I",
"Thermic Plating II": "Thermal Plating II",
"Basic Thermic Dissipation Amplifier": "Basic Thermal Dissipation Amplifier",
"Thermic Dissipation Field I": "Thermal Dissipation Field I",
"Thermic Dissipation Field II": "Thermal Dissipation Field II",
"Thermic Dissipation Amplifier I": "Thermal Dissipation Amplifier I",
"Thermic Dissipation Amplifier II": "Thermal Dissipation Amplifier II",
"Supplemental Thermic Dissipation Amplifier": "Supplemental Thermal Dissipation Amplifier",
"Upgraded Thermic Dissipation Amplifier I": "Upgraded Thermal Dissipation Amplifier I",
"Limited Thermic Dissipation Field I": "Limited Thermal Dissipation Field I",
"Basic Energized Thermic Membrane": "Basic Energized Thermal Membrane",
"Energized Thermic Membrane I": "Energized Thermal Membrane I",
"Energized Thermic Membrane II": "Energized Thermal Membrane II",
"Armor Thermic Hardener I": "Armor Thermal Hardener I",
"Thermic Shield Compensation": "Thermal Shield Compensation",
"Armor Thermic Hardener II": "Armor Thermal Hardener II",
"Dread Guristas Thermic Dissipation Field": "Dread Guristas Thermal Dissipation Field",
"True Sansha Armor Thermic Hardener": "True Sansha Armor Thermal Hardener",
"Dark Blood Armor Thermic Hardener": "Dark Blood Armor Thermal Hardener",
"Domination Armor Thermic Hardener": "Domination Armor Thermal Hardener",
"Domination Thermic Dissipation Field": "Domination Thermal Dissipation Field",
"Domination Thermic Plating": "Domination Thermal Plating",
"True Sansha Thermic Plating": "True Sansha Thermal Plating",
"Dark Blood Thermic Plating": "Dark Blood Thermal Plating",
"Domination Thermic Dissipation Amplifier": "Domination Thermal Dissipation Amplifier",
"Dread Guristas Thermic Dissipation Amplifier": "Dread Guristas Thermal Dissipation Amplifier",
"Shadow Serpentis Thermic Plating": "Shadow Serpentis Thermal Plating",
"Shadow Serpentis Armor Thermic Hardener": "Shadow Serpentis Armor Thermal Hardener",
"Dark Blood Energized Thermic Membrane": "Dark Blood Energized Thermal Membrane",
"True Sansha Energized Thermic Membrane": "True Sansha Energized Thermal Membrane",
"Shadow Serpentis Energized Thermic Membrane": "Shadow Serpentis Energized Thermal Membrane",
"Mizuro's Modified Thermic Plating": "Mizuro's Modified Thermal Plating",
"Gotan's Modified Thermic Plating": "Gotan's Modified Thermal Plating",
"Hakim's Modified Thermic Dissipation Amplifier": "Hakim's Modified Thermal Dissipation Amplifier",
"Tobias' Modified Thermic Dissipation Amplifier": "Tobias' Modified Thermal Dissipation Amplifier",
"Kaikka's Modified Thermic Dissipation Amplifier": "Kaikka's Modified Thermal Dissipation Amplifier",
"Thon's Modified Thermic Dissipation Amplifier": "Thon's Modified Thermal Dissipation Amplifier",
"Vepas' Modified Thermic Dissipation Amplifier": "Vepas' Modified Thermal Dissipation Amplifier",
"Estamel's Modified Thermic Dissipation Amplifier": "Estamel's Modified Thermal Dissipation Amplifier",
"Kaikka's Modified Thermic Dissipation Field": "Kaikka's Modified Thermal Dissipation Field",
"Thon's Modified Thermic Dissipation Field": "Thon's Modified Thermal Dissipation Field",
"Vepas's Modified Thermic Dissipation Field": "Vepas's Modified Thermal Dissipation Field",
"Estamel's Modified Thermic Dissipation Field": "Estamel's Modified Thermal Dissipation Field",
"Brokara's Modified Thermic Plating": "Brokara's Modified Thermal Plating",
"Tairei's Modified Thermic Plating": "Tairei's Modified Thermal Plating",
"Selynne's Modified Thermic Plating": "Selynne's Modified Thermal Plating",
"Raysere's Modified Thermic Plating": "Raysere's Modified Thermal Plating",
"Vizan's Modified Thermic Plating": "Vizan's Modified Thermal Plating",
"Ahremen's Modified Thermic Plating": "Ahremen's Modified Thermal Plating",
"Chelm's Modified Thermic Plating": "Chelm's Modified Thermal Plating",
"Draclira's Modified Thermic Plating": "Draclira's Modified Thermal Plating",
"Brokara's Modified Energized Thermic Membrane": "Brokara's Modified Energized Thermal Membrane",
"Tairei's Modified Energized Thermic Membrane": "Tairei's Modified Energized Thermal Membrane",
"Selynne's Modified Energized Thermic Membrane": "Selynne's Modified Energized Thermal Membrane",
"Raysere's Modified Energized Thermic Membrane": "Raysere's Modified Energized Thermal Membrane",
"Vizan's Modified Energized Thermic Membrane": "Vizan's Modified Energized Thermal Membrane",
"Ahremen's Modified Energized Thermic Membrane": "Ahremen's Modified Energized Thermal Membrane",
"Chelm's Modified Energized Thermic Membrane": "Chelm's Modified Energized Thermal Membrane",
"Draclira's Modified Energized Thermic Membrane": "Draclira's Modified Energized Thermal Membrane",
"Brokara's Modified Armor Thermic Hardener": "Brokara's Modified Armor Thermal Hardener",
"Tairei's Modified Armor Thermic Hardener": "Tairei's Modified Armor Thermal Hardener",
"Selynne's Modified Armor Thermic Hardener": "Selynne's Modified Armor Thermal Hardener",
"Raysere's Modified Armor Thermic Hardener": "Raysere's Modified Armor Thermal Hardener",
"Vizan's Modified Armor Thermic Hardener": "Vizan's Modified Armor Thermal Hardener",
"Ahremen's Modified Armor Thermic Hardener": "Ahremen's Modified Armor Thermal Hardener",
"Chelm's Modified Armor Thermic Hardener": "Chelm's Modified Armor Thermal Hardener",
"Draclira's Modified Armor Thermic Hardener": "Draclira's Modified Armor Thermal Hardener",
"Brynn's Modified Thermic Plating": "Brynn's Modified Thermal Plating",
"Tuvan's Modified Thermic Plating": "Tuvan's Modified Thermal Plating",
"Setele's Modified Thermic Plating": "Setele's Modified Thermal Plating",
"Cormack's Modified Thermic Plating": "Cormack's Modified Thermal Plating",
"Brynn's Modified Energized Thermic Membrane": "Brynn's Modified Energized Thermal Membrane",
"Tuvan's Modified Energized Thermic Membrane": "Tuvan's Modified Energized Thermal Membrane",
"Setele's Modified Energized Thermic Membrane": "Setele's Modified Energized Thermal Membrane",
"Cormack's Modified Energized Thermic Membrane": "Cormack's Modified Energized Thermal Membrane",
"Brynn's Modified Armor Thermic Hardener": "Brynn's Modified Armor Thermal Hardener",
"Tuvan's Modified Armor Thermic Hardener": "Tuvan's Modified Armor Thermal Hardener",
"Setele's Modified Armor Thermic Hardener": "Setele's Modified Armor Thermal Hardener",
"Cormack's Modified Armor Thermic Hardener": "Cormack's Modified Armor Thermal Hardener",
"Shaqil's Modified Energized Thermic Membrane": "Shaqil's Modified Energized Thermal Membrane",
"Imperial Navy Thermic Plating": "Imperial Navy Thermal Plating",
"Republic Fleet Thermic Plating": "Republic Fleet Thermal Plating",
"Imperial Navy Armor Thermic Hardener": "Imperial Navy Armor Thermal Hardener",
"Republic Fleet Armor Thermic Hardener": "Republic Fleet Armor Thermal Hardener",
"Imperial Navy Energized Thermic Membrane": "Imperial Navy Energized Thermal Membrane",
"Federation Navy Energized Thermic Membrane": "Federation Navy Energized Thermal Membrane",
"Caldari Navy Thermic Dissipation Amplifier": "Caldari Navy Thermal Dissipation Amplifier",
"Republic Fleet Thermic Dissipation Amplifier": "Republic Fleet Thermal Dissipation Amplifier",
"Upgraded Thermic Plating I": "Upgraded Thermal Plating I",
"Limited Thermic Plating I": "Limited Thermal Plating I",
"Experimental Thermic Plating I": "Experimental Thermal Plating I",
"Prototype Thermic Plating I": "Prototype Thermal Plating I",
"Upgraded Armor Thermic Hardener I": "Upgraded Armor Thermal Hardener I",
"Limited Armor Thermic Hardener I": "Limited Armor Thermal Hardener I",
"Experimental Armor Thermic Hardener I": "Experimental Armor Thermal Hardener I",
"Prototype Armor Thermic Hardener I": "Prototype Armor Thermal Hardener I",
"Upgraded Energized Thermic Membrane I": "Upgraded Energized Thermal Membrane I",
"Limited Energized Thermic Membrane I": "Limited Energized Thermal Membrane I",
"Experimental Energized Thermic Membrane I": "Experimental Energized Thermal Membrane I",
"Prototype Energized Thermic Membrane I": "Prototype Energized Thermal Membrane I",
"Caldari Navy Thermic Dissipation Field": "Caldari Navy Thermal Dissipation Field",
"Ammatar Navy Armor Thermic Hardener": "Ammatar Navy Armor Thermal Hardener",
"Ammatar Navy Energized Thermic Membrane": "Ammatar Navy Energized Thermal Membrane",
"Federation Navy Thermic Plating": "Federation Navy Thermal Plating",
"Federation Navy Armor Thermic Hardener": "Federation Navy Armor Thermal Hardener",
"Corpii C-Type Thermic Plating": "Corpii C-Type Thermal Plating",
"Centii C-Type Thermic Plating": "Centii C-Type Thermal Plating",
"Corpii B-Type Thermic Plating": "Corpii B-Type Thermal Plating",
"Centii B-Type Thermic Plating": "Centii B-Type Thermal Plating",
"Corpii A-Type Thermic Plating": "Corpii A-Type Thermal Plating",
"Centii A-Type Thermic Plating": "Centii A-Type Thermal Plating",
"Coreli C-Type Thermic Plating": "Coreli C-Type Thermal Plating",
"Coreli B-Type Thermic Plating": "Coreli B-Type Thermal Plating",
"Coreli A-Type Thermic Plating": "Coreli A-Type Thermal Plating",
"Corelum C-Type Energized Thermic Membrane": "Corelum C-Type Energized Thermal Membrane",
"Corelum B-Type Energized Thermic Membrane": "Corelum B-Type Energized Thermal Membrane",
"Corelum A-Type Energized Thermic Membrane": "Corelum A-Type Energized Thermal Membrane",
"Corpum C-Type Energized Thermic Membrane": "Corpum C-Type Energized Thermal Membrane",
"Centum C-Type Energized Thermic Membrane": "Centum C-Type Energized Thermal Membrane",
"Corpum B-Type Energized Thermic Membrane": "Corpum B-Type Energized Thermal Membrane",
"Centum B-Type Energized Thermic Membrane": "Centum B-Type Energized Thermal Membrane",
"Corpum A-Type Energized Thermic Membrane": "Corpum A-Type Energized Thermal Membrane",
"Centum A-Type Energized Thermic Membrane": "Centum A-Type Energized Thermal Membrane",
"Corpus C-Type Armor Thermic Hardener": "Corpus C-Type Armor Thermal Hardener",
"Centus C-Type Armor Thermic Hardener": "Centus C-Type Armor Thermal Hardener",
"Corpus B-Type Armor Thermic Hardener": "Corpus B-Type Armor Thermal Hardener",
"Centus B-Type Armor Thermic Hardener": "Centus B-Type Armor Thermal Hardener",
"Corpus A-Type Armor Thermic Hardener": "Corpus A-Type Armor Thermal Hardener",
"Centus A-Type Armor Thermic Hardener": "Centus A-Type Armor Thermal Hardener",
"Corpus X-Type Armor Thermic Hardener": "Corpus X-Type Armor Thermal Hardener",
"Centus X-Type Armor Thermic Hardener": "Centus X-Type Armor Thermal Hardener",
"Core C-Type Armor Thermic Hardener": "Core C-Type Armor Thermal Hardener",
"Core B-Type Armor Thermic Hardener": "Core B-Type Armor Thermal Hardener",
"Core A-Type Armor Thermic Hardener": "Core A-Type Armor Thermal Hardener",
"Core X-Type Armor Thermic Hardener": "Core X-Type Armor Thermal Hardener",
"Pithum C-Type Thermic Dissipation Amplifier": "Pithum C-Type Thermal Dissipation Amplifier",
"Pithum B-Type Thermic Dissipation Amplifier": "Pithum B-Type Thermal Dissipation Amplifier",
"Pithum A-Type Thermic Dissipation Amplifier": "Pithum A-Type Thermal Dissipation Amplifier",
"Gistum C-Type Thermic Dissipation Amplifier": "Gistum C-Type Thermal Dissipation Amplifier",
"Gistum B-Type Thermic Dissipation Amplifier": "Gistum B-Type Thermal Dissipation Amplifier",
"Gistum A-Type Thermic Dissipation Amplifier": "Gistum A-Type Thermal Dissipation Amplifier",
"Gist C-Type Thermic Dissipation Field": "Gist C-Type Thermal Dissipation Field",
"Pith C-Type Thermic Dissipation Field": "Pith C-Type Thermal Dissipation Field",
"Gist B-Type Thermic Dissipation Field": "Gist B-Type Thermal Dissipation Field",
"Pith B-Type Thermic Dissipation Field": "Pith B-Type Thermal Dissipation Field",
"Gist A-Type Thermic Dissipation Field": "Gist A-Type Thermal Dissipation Field",
"Pith A-Type Thermic Dissipation Field": "Pith A-Type Thermal Dissipation Field",
"Gist X-Type Thermic Dissipation Field": "Gist X-Type Thermal Dissipation Field",
"Pith X-Type Thermic Dissipation Field": "Pith X-Type Thermal Dissipation Field",
"'High Noon' Thermic Dissipation Amplifier": "'High Noon' Thermal Dissipation Amplifier",
"'Desert Heat' Thermic Dissipation Field": "'Desert Heat' Thermal Dissipation Field",
"Thermic Armor Compensation": "Thermal Armor Compensation",
"'Moonshine' Energized Thermic Membrane I": "'Moonshine' Energized Thermal Membrane I",
"Large Anti-Thermic Pump I": "Large Anti-Thermal Pump I",
"Large Anti-Thermic Pump II": "Large Anti-Thermal Pump II",
"Khanid Navy Armor Thermic Hardener": "Khanid Navy Armor Thermal Hardener",
"Khanid Navy Energized Thermic Membrane": "Khanid Navy Energized Thermal Membrane",
"Khanid Navy Thermic Plating": "Khanid Navy Thermal Plating",
"Civilian Thermic Dissipation Field": "Civilian Thermal Dissipation Field",
"Small Anti-Thermic Pump I": "Small Anti-Thermal Pump I",
"Medium Anti-Thermic Pump I": "Medium Anti-Thermal Pump I",
"Capital Anti-Thermic Pump I": "Capital Anti-Thermal Pump I",
"Small Anti-Thermic Pump II": "Small Anti-Thermal Pump II",
"Medium Anti-Thermic Pump II": "Medium Anti-Thermal Pump II",
"Capital Anti-Thermic Pump II": "Capital Anti-Thermal Pump II",
"Ammatar Navy Thermic Plating": "Ammatar Navy Thermal Plating",
}

203
service/crest.py Normal file
View File

@@ -0,0 +1,203 @@
import thread
import config
import logging
import threading
import copy
import uuid
import wx
import time
from wx.lib.pubsub import pub
import eos.db
from eos.enum import Enum
from eos.types import CrestChar
import service
logger = logging.getLogger(__name__)
class Servers(Enum):
TQ = 0
SISI = 1
class CrestModes(Enum):
IMPLICIT = 0
USER = 1
class Crest():
clientIDs = {
Servers.TQ: 'f9be379951c046339dc13a00e6be7704',
Servers.SISI: 'af87365240d644f7950af563b8418bad'
}
# @todo: move this to settings
clientCallback = 'http://localhost:6461'
clientTest = True
_instance = None
@classmethod
def getInstance(cls):
if cls._instance == None:
cls._instance = Crest()
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 = Crest()
wx.CallAfter(pub.sendMessage, 'crest_changed', message=None)
return cls._instance
def __init__(self):
self.settings = service.settings.CRESTSettings.getInstance()
self.scopes = ['characterFittingsRead', 'characterFittingsWrite']
# these will be set when needed
self.httpd = None
self.state = None
self.ssoTimer = None
# Base EVE connection that is copied to all characters
self.eve = service.pycrest.EVE(
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
)
self.implicitCharacter = None
# The database cache does not seem to be working for some reason. Use
# this as a temporary measure
self.charCache = {}
pub.subscribe(self.handleLogin, 'sso_login')
@property
def isTestServer(self):
return self.settings.get('server') == Servers.SISI
def delCrestCharacter(self, charID):
char = eos.db.getCrestCharacter(charID)
eos.db.remove(char)
wx.CallAfter(pub.sendMessage, 'crest_delete', message=None)
def delAllCharacters(self):
chars = eos.db.getCrestCharacters()
for char in chars:
eos.db.remove(char)
self.charCache = {}
wx.CallAfter(pub.sendMessage, 'crest_delete', message=None)
def getCrestCharacters(self):
chars = eos.db.getCrestCharacters()
return chars
def getCrestCharacter(self, charID):
'''
Get character, and modify to include the eve connection
'''
if self.settings.get('mode') == CrestModes.IMPLICIT:
if self.implicitCharacter.ID != charID:
raise ValueError("CharacterID does not match currently logged in character.")
return self.implicitCharacter
if charID in self.charCache:
return self.charCache.get(charID)
char = eos.db.getCrestCharacter(charID)
if char and not hasattr(char, "eve"):
char.eve = copy.deepcopy(self.eve)
char.eve.temptoken_authorize(refresh_token=char.refresh_token)
self.charCache[charID] = char
return char
def getFittings(self, charID):
char = self.getCrestCharacter(charID)
return char.eve.get('https://api-sisi.testeveonline.com/characters/%d/fittings/'%char.ID)
def postFitting(self, charID, json):
#@todo: new fitting ID can be recovered from Location header, ie: Location -> https://api-sisi.testeveonline.com/characters/1611853631/fittings/37486494/
char = self.getCrestCharacter(charID)
return char.eve.post('https://api-sisi.testeveonline.com/characters/%d/fittings/'%char.ID, data=json)
def delFitting(self, charID, fittingID):
char = self.getCrestCharacter(charID)
return char.eve.delete('https://api-sisi.testeveonline.com/characters/%d/fittings/%d/'%(char.ID, fittingID))
def logout(self):
logging.debug("Character logout")
self.implicitCharacter = None
wx.CallAfter(pub.sendMessage, 'logout_success', message=None)
def stopServer(self):
logging.debug("Stopping Server")
self.httpd.stop()
self.httpd = None
def startServer(self):
logging.debug("Starting server")
if self.httpd:
self.stopServer()
time.sleep(1) # we need this to ensure that the previous get_request finishes, and then the socket will close
self.httpd = service.StoppableHTTPServer(('', 6461), service.AuthHandler)
thread.start_new_thread(self.httpd.serve, ())
self.state = str(uuid.uuid4())
return self.eve.auth_uri(scopes=self.scopes, state=self.state)
def handleLogin(self, message):
if not message:
return
if message['state'][0] != self.state:
logger.warn("OAUTH state mismatch")
return
logger.debug("Handling CREST login with: %s"%message)
if 'access_token' in message: # implicit
eve = copy.deepcopy(self.eve)
eve.temptoken_authorize(
access_token=message['access_token'][0],
expires_in=int(message['expires_in'][0])
)
self.ssoTimer = threading.Timer(int(message['expires_in'][0]), self.logout)
self.ssoTimer.start()
eve()
info = eve.whoami()
logger.debug("Got character info: %s" % info)
self.implicitCharacter = CrestChar(info['CharacterID'], info['CharacterName'])
self.implicitCharacter.eve = eve
#self.implicitCharacter.fetchImage()
wx.CallAfter(pub.sendMessage, 'login_success', type=CrestModes.IMPLICIT)
elif 'code' in message:
eve = copy.deepcopy(self.eve)
eve.authorize(message['code'][0])
eve()
info = eve.whoami()
logger.debug("Got character info: %s" % info)
# check if we have character already. If so, simply replace refresh_token
char = self.getCrestCharacter(int(info['CharacterID']))
if char:
char.refresh_token = eve.refresh_token
else:
char = CrestChar(info['CharacterID'], info['CharacterName'], eve.refresh_token)
char.eve = eve
self.charCache[int(info['CharacterID'])] = char
eos.db.save(char)
wx.CallAfter(pub.sendMessage, 'login_success', type=CrestModes.USER)
self.stopServer()

View File

@@ -820,6 +820,10 @@ class Fit(object):
fit = eos.db.getFit(fitID)
return Port.exportDna(fit)
def exportCrest(self, fitID, callback=None):
fit = eos.db.getFit(fitID)
return Port.exportCrest(fit, callback)
def exportXml(self, callback=None, *fitIDs):
fits = map(lambda fitID: eos.db.getFit(fitID), fitIDs)
return Port.exportXml(callback, *fits)

View File

@@ -116,12 +116,15 @@ class SearchWorkerThread(threading.Thread):
while self.searchRequest is None:
cv.wait()
request, callback = self.searchRequest
request, callback, filterOn = self.searchRequest
self.searchRequest = None
cv.release()
sMkt = Market.getInstance()
# Rely on category data provided by eos as we don't hardcode them much in service
filter = eos.types.Category.name.in_(sMkt.SEARCH_CATEGORIES)
if filterOn:
# Rely on category data provided by eos as we don't hardcode them much in service
filter = eos.types.Category.name.in_(sMkt.SEARCH_CATEGORIES)
else:
filter=None
results = eos.db.searchItems(request, where=filter,
join=(eos.types.Item.group, eos.types.Group.category),
eager=("icon", "group.category", "metaGroup", "metaGroup.parent"))
@@ -133,9 +136,9 @@ class SearchWorkerThread(threading.Thread):
items.add(item)
wx.CallAfter(callback, items)
def scheduleSearch(self, text, callback):
def scheduleSearch(self, text, callback, filterOn=True):
self.cv.acquire()
self.searchRequest = (text, callback)
self.searchRequest = (text, callback, filterOn)
self.cv.notify()
self.cv.release()
@@ -665,9 +668,16 @@ class Market():
ships.add(item)
return ships
def searchItems(self, name, callback):
def searchItems(self, name, callback, filterOn=True):
"""Find items according to given text pattern"""
self.searchWorkerThread.scheduleSearch(name, callback)
self.searchWorkerThread.scheduleSearch(name, callback, filterOn)
def getItemsWithOverrides(self):
overrides = eos.db.getAllOverrides()
items = set()
for x in overrides:
items.add(x.item)
return list(items)
def directAttrRequest(self, items, attribs):
try:

View File

@@ -25,6 +25,9 @@ from eos.types import State, Slot, Module, Cargo, Fit, Ship, Drone, Implant, Boo
import service
import wx
import logging
import config
import collections
import json
logger = logging.getLogger("pyfa.service.port")
@@ -34,9 +37,63 @@ except ImportError:
from utils.compat import OrderedDict
EFT_SLOT_ORDER = [Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM]
INV_FLAGS = {
Slot.LOW: 11,
Slot.MED: 19,
Slot.HIGH: 27,
Slot.RIG: 92,
Slot.SUBSYSTEM: 125}
class Port(object):
"""Service which houses all import/export format functions"""
@classmethod
def exportCrest(cls, ofit, callback=None):
# A few notes:
# max fit name length is 50 characters
# Most keys are created simply because they are required, but bogus data is okay
nested_dict = lambda: collections.defaultdict(nested_dict)
fit = nested_dict()
sCrest = service.Crest.getInstance()
eve = sCrest.eve
# max length is 50 characters
name = ofit.name[:47] + '...' if len(ofit.name) > 50 else ofit.name
fit['name'] = name
fit['ship']['href'] = "%stypes/%d/"%(eve._authed_endpoint, ofit.ship.item.ID)
fit['ship']['id'] = ofit.ship.item.ID
fit['ship']['name'] = ''
fit['description'] = "<pyfa:%d />"%ofit.ID
fit['items'] = []
slotNum = {}
for module in ofit.modules:
if module.isEmpty:
continue
item = nested_dict()
slot = module.slot
if slot == Slot.SUBSYSTEM:
# Order of subsystem matters based on this attr. See GH issue #130
slot = int(module.getModifiedItemAttr("subSystemSlot"))
item['flag'] = slot
else:
if not slot in slotNum:
slotNum[slot] = INV_FLAGS[slot]
item['flag'] = slotNum[slot]
slotNum[slot] += 1
item['quantity'] = 1
item['type']['href'] = "%stypes/%d/"%(eve._authed_endpoint, module.item.ID)
item['type']['id'] = module.item.ID
item['type']['name'] = ''
fit['items'].append(item)
return json.dumps(fit)
@classmethod
def importAuto(cls, string, path=None, activeFit=None, callback=None, encoding=None):
@@ -48,6 +105,10 @@ class Port(object):
if re.match("<", firstLine):
return "XML", cls.importXml(string, callback, encoding)
# If JSON-style start, parse os CREST/JSON
if firstLine[0] == '{':
return "JSON", (cls.importCrest(string),)
# If we've got source file name which is used to describe ship name
# and first line contains something like [setup name], detect as eft config file
if re.match("\[.*\]", firstLine) and path is not None:
@@ -63,6 +124,47 @@ class Port(object):
# Use DNA format for all other cases
return "DNA", (cls.importDna(string),)
@staticmethod
def importCrest(str):
fit = json.loads(str)
sMkt = service.Market.getInstance()
f = Fit()
f.name = fit['name']
try:
f.ship = Ship(sMkt.getItem(fit['ship']['id']))
except:
return None
items = fit['items']
items.sort(key=lambda k: k['flag'])
for module in items:
try:
item = sMkt.getItem(module['type']['id'], eager="group.category")
if item.category.name == "Drone":
d = Drone(item)
d.amount = module['quantity']
f.drones.append(d)
elif item.category.name == "Charge":
c = Cargo(item)
c.amount = module['quantity']
f.cargo.append(c)
else:
try:
m = Module(item)
# When item can't be added to any slot (unknown item or just charge), ignore it
except ValueError:
continue
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
f.modules.append(m)
except:
continue
return f
@staticmethod
def importDna(string):
sMkt = service.Market.getInstance()

View File

@@ -0,0 +1,13 @@
import logging
class NullHandler(logging.Handler):
def emit(self, record):
pass
logger = logging.getLogger('pycrest')
logger.addHandler(NullHandler())
version = "0.0.1"
from .eve import EVE

24
service/pycrest/compat.py Normal file
View File

@@ -0,0 +1,24 @@
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 = basestring,
text_type = unicode
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

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

322
service/pycrest/eve.py Normal file
View File

@@ -0,0 +1,322 @@
import os
import base64
import time
import zlib
import requests
from . import version
from compat import bytes_, text_
from errors import APIException
from weak_ciphers import WeakCiphersAdapter
try:
from urllib.parse import urlparse, urlunparse, parse_qsl
except ImportError: # pragma: no cover
from urlparse import urlparse, urlunparse, parse_qsl
try:
import pickle
except ImportError: # pragma: no cover
import cPickle as pickle
try:
from urllib.parse import quote
except ImportError: # pragma: no cover
from urllib import quote
import logging
import re
logger = logging.getLogger("pycrest.eve")
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:
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:
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 = "PyCrest/{0}".format(version)
session.headers.update({
"User-Agent": user_agent,
"Accept": "application/json",
})
session.headers.update(additional_headers)
session.mount('https://public-crest.eveonline.com',
WeakCiphersAdapter())
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):
logger.debug('Getting resource %s', 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(self._session.headers.items()), frozenset(prms.items()))
cached = self.cache.get(key)
if cached and cached['cached_until'] > time.time():
logger.debug('Cache hit for resource %s (params=%s)', resource, prms)
return cached
elif cached:
logger.debug('Cache stale for resource %s (params=%s)', resource, prms)
self.cache.invalidate(key)
else:
logger.debug('Cache miss for resource %s (params=%s', resource, prms)
logger.debug('Getting resource %s (params=%s)', resource, prms)
res = self._session.get(resource, params=prms)
if res.status_code != 200:
raise APIException("Got unexpected status code from server: %i" % 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
def _get_expires(self, 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 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

@@ -0,0 +1,140 @@
import datetime
import ssl
import sys
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)

101
service/server.py Normal file
View File

@@ -0,0 +1,101 @@
import BaseHTTPServer
import urlparse
import socket
import thread
import wx
from wx.lib.pubsub import setupkwargs
from wx.lib.pubsub import pub
import logging
logger = logging.getLogger(__name__)
HTML = '''
<!DOCTYPE html>
<html>
<body>
Done. Please close this window.
<script type="text/javascript">
function extractFromHash(name, hash) {
var match = hash.match(new RegExp(name + "=([^&]+)"));
return !!match && match[1];
}
var hash = window.location.hash;
var token = extractFromHash("access_token", hash);
if (token){
var redirect = window.location.origin.concat('/?', window.location.hash.substr(1));
window.location = redirect;
}
else {
console.log("do nothing");
}
</script>
</body>
</html>
'''
# https://github.com/fuzzysteve/CREST-Market-Downloader/
class AuthHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/favicon.ico":
return
parsed_path = urlparse.urlparse(self.path)
parts = urlparse.parse_qs(parsed_path.query)
self.send_response(200)
self.end_headers()
self.wfile.write(HTML)
wx.CallAfter(pub.sendMessage, 'sso_login', message=parts)
def log_message(self, format, *args):
return
# http://code.activestate.com/recipes/425210-simple-stoppable-server-using-socket-timeout/
class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
def server_bind(self):
BaseHTTPServer.HTTPServer.server_bind(self)
# Allow listening for 60 seconds
sec = 60
self.socket.settimeout(0.5)
self.max_tries = sec / self.socket.gettimeout()
self.tries = 0
self.run = True
def get_request(self):
while self.run:
try:
sock, addr = self.socket.accept()
sock.settimeout(None)
return (sock, addr)
except socket.timeout:
pass
def stop(self):
self.run = False
self.server_close()
def handle_timeout(self):
logger.debug("Number of tries: %d"%self.tries)
self.tries += 1
if self.tries == self.max_tries:
logger.debug("Server timed out waiting for connection")
self.stop()
def serve(self):
while self.run:
try:
self.handle_request()
except TypeError:
pass
if __name__ == "__main__":
httpd = StoppableHTTPServer(('', 6461), AuthHandler)
thread.start_new_thread(httpd.serve, ())
raw_input("Press <RETURN> to stop server\n")
httpd.stop()

View File

@@ -263,4 +263,30 @@ class UpdateSettings():
def set(self, type, value):
self.serviceUpdateSettings[type] = value
class CRESTSettings():
_instance = None
@classmethod
def getInstance(cls):
if cls._instance is None:
cls._instance = CRESTSettings()
return cls._instance
def __init__(self):
# mode
# 0 - Implicit authentication
# 1 - User-supplied client details
serviceCRESTDefaultSettings = {"mode": 0, "server": 0, "clientID": "", "clientSecret": ""}
self.serviceCRESTSettings = SettingsProvider.getInstance().getSettings("pyfaServiceCRESTSettings", serviceCRESTDefaultSettings)
def get(self, type):
return self.serviceCRESTSettings[type]
def set(self, type, value):
self.serviceCRESTSettings[type] = value
# @todo: migrate fit settings (from fit service) here?

View File

@@ -4,11 +4,12 @@ Distribution builder for pyfa.
Windows executable: python setup.py build
Windows executable + installer: python setup.py bdist_msi
"""
import requests.certs
# The modules that contain the bulk of teh source
packages = ['eos', 'gui', 'service', 'utils']
# Extra files that will be copied into the root directory
include_files = ['eve.db', 'LICENSE', 'README.md']
include_files = ['eve.db', 'LICENSE', 'README.md', (requests.certs.where(),'cacert.pem')]
# this is read by dist.py to package the icons
icon_dirs = ['gui', 'icons', 'renders']