diff --git a/.gitignore b/.gitignore index 96e9e15a3..13032ec18 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ *.patch #Personal -saveddata/ +/saveddata/ #PyCharm .idea/ diff --git a/config.py b/config.py index 2a5071f5e..a09bc30c9 100644 --- a/config.py +++ b/config.py @@ -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) diff --git a/eos/db/__init__.py b/eos/db/__init__.py index 8867d63c5..7aef55fd7 100644 --- a/eos/db/__init__.py +++ b/eos/db/__init__.py @@ -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:": diff --git a/eos/db/saveddata/__init__.py b/eos/db/saveddata/__init__.py index 31e71c01a..683fb499d 100644 --- a/eos/db/saveddata/__init__.py +++ b/eos/db/saveddata/__init__.py @@ -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" +] + diff --git a/eos/db/saveddata/crest.py b/eos/db/saveddata/crest.py new file mode 100644 index 000000000..0934f177e --- /dev/null +++ b/eos/db/saveddata/crest.py @@ -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 . +#=============================================================================== + +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) diff --git a/eos/db/saveddata/override.py b/eos/db/saveddata/override.py new file mode 100644 index 000000000..2d5d74a47 --- /dev/null +++ b/eos/db/saveddata/override.py @@ -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 . +#=============================================================================== + +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) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index b71f6b98a..a305994cd 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -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] diff --git a/eos/effects/armortankinggang2.py b/eos/effects/armorwarfarearmorhpreplacer.py similarity index 92% rename from eos/effects/armortankinggang2.py rename to eos/effects/armorwarfarearmorhpreplacer.py index baa284623..ffcf0efec 100644 --- a/eos/effects/armortankinggang2.py +++ b/eos/effects/armorwarfarearmorhpreplacer.py @@ -1,4 +1,4 @@ -# armorTankingGang2 +# armorWarfareArmorHpReplacer # # Used by: # Implant: Armored Warfare Mindlink diff --git a/eos/effects/boosterarmorhppenalty.py b/eos/effects/boosterarmorhppenalty.py index e7ccfb79c..0d3a3ea05 100644 --- a/eos/effects/boosterarmorhppenalty.py +++ b/eos/effects/boosterarmorhppenalty.py @@ -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")) diff --git a/eos/effects/boosterarmorrepairamountpenalty.py b/eos/effects/boosterarmorrepairamountpenalty.py index 6ecd643f6..4e3bb0738 100644 --- a/eos/effects/boosterarmorrepairamountpenalty.py +++ b/eos/effects/boosterarmorrepairamountpenalty.py @@ -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", diff --git a/eos/effects/boostermaxvelocitypenalty.py b/eos/effects/boostermaxvelocitypenalty.py index 7c9339796..6a2be21a3 100644 --- a/eos/effects/boostermaxvelocitypenalty.py +++ b/eos/effects/boostermaxvelocitypenalty.py @@ -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")) diff --git a/eos/effects/boostershieldcapacitypenalty.py b/eos/effects/boostershieldcapacitypenalty.py index a6b8f0b28..642036e3d 100644 --- a/eos/effects/boostershieldcapacitypenalty.py +++ b/eos/effects/boostershieldcapacitypenalty.py @@ -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")) diff --git a/eos/effects/boosterturretoptimalrangepenalty.py b/eos/effects/boosterturretoptimalrangepenalty.py index 2d556a4dd..9881cf501 100644 --- a/eos/effects/boosterturretoptimalrangepenalty.py +++ b/eos/effects/boosterturretoptimalrangepenalty.py @@ -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"), diff --git a/eos/effects/informationwarfaremaxtargetrangebonus.py b/eos/effects/informationwarfaremaxtargetrangebonus.py new file mode 100644 index 000000000..5e4bfed19 --- /dev/null +++ b/eos/effects/informationwarfaremaxtargetrangebonus.py @@ -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)) diff --git a/eos/effects/informationwarfaremindlinkhidden.py b/eos/effects/informationwarfaremindlinkhidden.py index f59c806ba..6d3d6c6a7 100644 --- a/eos/effects/informationwarfaremindlinkhidden.py +++ b/eos/effects/informationwarfaremindlinkhidden.py @@ -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): diff --git a/eos/effects/miningforemanmindlinkminingamountbonusreplacer.py b/eos/effects/miningforemanmindlinkminingamountbonusreplacer.py new file mode 100644 index 000000000..bdf856667 --- /dev/null +++ b/eos/effects/miningforemanmindlinkminingamountbonusreplacer.py @@ -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) diff --git a/eos/effects/miningyieldgangbonusfixed.py b/eos/effects/miningyieldgangbonusfixed.py index 9367e55e3..1b6a0a583 100644 --- a/eos/effects/miningyieldgangbonusfixed.py +++ b/eos/effects/miningyieldgangbonusfixed.py @@ -1,7 +1,6 @@ # miningYieldGangBonusFixed # # Used by: -# Implant: Mining Foreman Mindlink # Skill: Mining Foreman type = "gang" gangBoost = "miningAmount" diff --git a/eos/effects/missileskillrapidlauncherrof.py b/eos/effects/missileskillrapidlauncherrof.py index 731f97c5d..dc200dde2 100644 --- a/eos/effects/missileskillrapidlauncherrof.py +++ b/eos/effects/missileskillrapidlauncherrof.py @@ -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 diff --git a/eos/effects/overloadselfthermalhardeningbonus.py b/eos/effects/overloadselfthermalhardeningbonus.py index a3e8c91e0..5a4036980 100644 --- a/eos/effects/overloadselfthermalhardeningbonus.py +++ b/eos/effects/overloadselfthermalhardeningbonus.py @@ -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")) \ No newline at end of file diff --git a/eos/effects/reconoperationsmaxtargetrangebonuspostpercentmaxtargetrangegangships.py b/eos/effects/reconoperationsmaxtargetrangebonuspostpercentmaxtargetrangegangships.py index 00a2707a4..db12c57db 100644 --- a/eos/effects/reconoperationsmaxtargetrangebonuspostpercentmaxtargetrangegangships.py +++ b/eos/effects/reconoperationsmaxtargetrangebonuspostpercentmaxtargetrangegangships.py @@ -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" diff --git a/eos/effects/shielddefensiveoperationsshieldcapacitybonuspostpercentshieldcapacitygangships.py b/eos/effects/shielddefensiveoperationsshieldcapacitybonuspostpercentshieldcapacitygangships.py index 1c16a3066..5d38c76e3 100644 --- a/eos/effects/shielddefensiveoperationsshieldcapacitybonuspostpercentshieldcapacitygangships.py +++ b/eos/effects/shielddefensiveoperationsshieldcapacitybonuspostpercentshieldcapacitygangships.py @@ -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" diff --git a/eos/effects/shiparmoremresistancerookie.py b/eos/effects/shiparmoremresistancerookie.py index c35e701d3..9baaf9c4c 100644 --- a/eos/effects/shiparmoremresistancerookie.py +++ b/eos/effects/shiparmoremresistancerookie.py @@ -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")) diff --git a/eos/effects/shiparmorexresistancerookie.py b/eos/effects/shiparmorexresistancerookie.py index a0f2becf6..35868ef02 100644 --- a/eos/effects/shiparmorexresistancerookie.py +++ b/eos/effects/shiparmorexresistancerookie.py @@ -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")) diff --git a/eos/effects/shiparmorknresistancerookie.py b/eos/effects/shiparmorknresistancerookie.py index d3f68efa4..efc324adf 100644 --- a/eos/effects/shiparmorknresistancerookie.py +++ b/eos/effects/shiparmorknresistancerookie.py @@ -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")) diff --git a/eos/effects/shiparmorthresistancerookie.py b/eos/effects/shiparmorthresistancerookie.py index c5fe4045f..385e27b4b 100644 --- a/eos/effects/shiparmorthresistancerookie.py +++ b/eos/effects/shiparmorthresistancerookie.py @@ -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")) diff --git a/eos/effects/siegewarfareshieldcapacitybonusreplacer.py b/eos/effects/siegewarfareshieldcapacitybonusreplacer.py new file mode 100644 index 000000000..3fd0a62fa --- /dev/null +++ b/eos/effects/siegewarfareshieldcapacitybonusreplacer.py @@ -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) diff --git a/eos/effects/skirmishwarfareagilitybonus.py b/eos/effects/skirmishwarfareagilitybonus.py index 7ce7e0ad8..39ec2c90a 100644 --- a/eos/effects/skirmishwarfareagilitybonus.py +++ b/eos/effects/skirmishwarfareagilitybonus.py @@ -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" diff --git a/eos/effects/skirmishwarfareagilitybonusreplacer.py b/eos/effects/skirmishwarfareagilitybonusreplacer.py new file mode 100644 index 000000000..e89c3a217 --- /dev/null +++ b/eos/effects/skirmishwarfareagilitybonusreplacer.py @@ -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) diff --git a/eos/effects/smallenergymaxrangebonus.py b/eos/effects/smallenergymaxrangebonus.py index f67219ccd..4e3ea5f31 100644 --- a/eos/effects/smallenergymaxrangebonus.py +++ b/eos/effects/smallenergymaxrangebonus.py @@ -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"), diff --git a/eos/effects/surgicalstrikedamagemultiplierbonuspostpercentdamagemultiplierlocationshipmodulesrequiringgunnery.py b/eos/effects/surgicalstrikedamagemultiplierbonuspostpercentdamagemultiplierlocationshipmodulesrequiringgunnery.py index 1a970cad1..eb111421b 100644 --- a/eos/effects/surgicalstrikedamagemultiplierbonuspostpercentdamagemultiplierlocationshipmodulesrequiringgunnery.py +++ b/eos/effects/surgicalstrikedamagemultiplierbonuspostpercentdamagemultiplierlocationshipmodulesrequiringgunnery.py @@ -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): diff --git a/eos/effects/thermalshieldcompensationhardeningbonusgroupshieldamp.py b/eos/effects/thermalshieldcompensationhardeningbonusgroupshieldamp.py index 0f318c05e..1dec7e8d3 100644 --- a/eos/effects/thermalshieldcompensationhardeningbonusgroupshieldamp.py +++ b/eos/effects/thermalshieldcompensationhardeningbonusgroupshieldamp.py @@ -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", diff --git a/eos/effects/thermicarmorcompensationhardeningbonusgrouparmorcoating.py b/eos/effects/thermicarmorcompensationhardeningbonusgrouparmorcoating.py index 2ea9d5af5..653b75056 100644 --- a/eos/effects/thermicarmorcompensationhardeningbonusgrouparmorcoating.py +++ b/eos/effects/thermicarmorcompensationhardeningbonusgrouparmorcoating.py @@ -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", diff --git a/eos/effects/thermicarmorcompensationhardeningbonusgroupenergized.py b/eos/effects/thermicarmorcompensationhardeningbonusgroupenergized.py index c4829419a..ef2765202 100644 --- a/eos/effects/thermicarmorcompensationhardeningbonusgroupenergized.py +++ b/eos/effects/thermicarmorcompensationhardeningbonusgroupenergized.py @@ -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", diff --git a/eos/gamedata.py b/eos/gamedata.py index d5691da46..3e7c213eb 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -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 diff --git a/eos/modifiedAttributeDict.py b/eos/modifiedAttributeDict.py index de63420d1..f53384069 100644 --- a/eos/modifiedAttributeDict.py +++ b/eos/modifiedAttributeDict.py @@ -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 diff --git a/eos/saveddata/booster.py b/eos/saveddata/booster.py index c0874fe94..9d0e98ae0 100644 --- a/eos/saveddata/booster.py +++ b/eos/saveddata/booster.py @@ -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(): diff --git a/eos/saveddata/cargo.py b/eos/saveddata/cargo.py index 26509d525..676b7cebf 100644 --- a/eos/saveddata/cargo.py +++ b/eos/saveddata/cargo.py @@ -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): diff --git a/eos/saveddata/crestchar.py b/eos/saveddata/crestchar.py new file mode 100644 index 000000000..9fa78f551 --- /dev/null +++ b/eos/saveddata/crestchar.py @@ -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 . +#=============================================================================== + +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) + ''' diff --git a/eos/saveddata/drone.py b/eos/saveddata/drone.py index 1a63d57a3..2fcb6a908 100644 --- a/eos/saveddata/drone.py +++ b/eos/saveddata/drone.py @@ -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): diff --git a/eos/saveddata/implant.py b/eos/saveddata/implant.py index 64670d769..b5be77986 100644 --- a/eos/saveddata/implant.py +++ b/eos/saveddata/implant.py @@ -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 diff --git a/eos/saveddata/mode.py b/eos/saveddata/mode.py index 3a344d74d..91fbaf6eb 100644 --- a/eos/saveddata/mode.py +++ b/eos/saveddata/mode.py @@ -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): diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index e5468553a..5fbb75e1c 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -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() diff --git a/eos/saveddata/override.py b/eos/saveddata/override.py new file mode 100644 index 000000000..b33875e89 --- /dev/null +++ b/eos/saveddata/override.py @@ -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 . +#=============================================================================== + +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 diff --git a/eos/saveddata/ship.py b/eos/saveddata/ship.py index daa5e99ec..0a0e13c45 100644 --- a/eos/saveddata/ship.py +++ b/eos/saveddata/ship.py @@ -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 diff --git a/eos/types.py b/eos/types.py index 6e98749d2..c1cafd1f5 100644 --- a/eos/types.py +++ b/eos/types.py @@ -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 diff --git a/eve.db b/eve.db index 3a06c8734..5eadbbf1f 100644 Binary files a/eve.db and b/eve.db differ diff --git a/gui/PFSearchBox.py b/gui/PFSearchBox.py index 88c67617b..7fff91e9b 100644 --- a/gui/PFSearchBox.py +++ b/gui/PFSearchBox.py @@ -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 diff --git a/gui/builtinPreferenceViews/__init__.py b/gui/builtinPreferenceViews/__init__.py index 923e011ab..20d2cba38 100644 --- a/gui/builtinPreferenceViews/__init__.py +++ b/gui/builtinPreferenceViews/__init__.py @@ -1 +1 @@ -__all__ = ["pyfaGeneralPreferences","pyfaHTMLExportPreferences","pyfaUpdatePreferences","pyfaNetworkPreferences"] +__all__ = ["pyfaGeneralPreferences","pyfaHTMLExportPreferences","pyfaUpdatePreferences","pyfaNetworkPreferences","pyfaCrestPreferences"] diff --git a/gui/builtinPreferenceViews/pyfaCrestPreferences.py b/gui/builtinPreferenceViews/pyfaCrestPreferences.py new file mode 100644 index 000000000..308999953 --- /dev/null +++ b/gui/builtinPreferenceViews/pyfaCrestPreferences.py @@ -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() diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 92b3b760c..0cebb5101 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -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) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 0faf850cb..14ee68703 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -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(): diff --git a/gui/crestFittings.py b/gui/crestFittings.py new file mode 100644 index 000000000..c4a342f62 --- /dev/null +++ b/gui/crestFittings.py @@ -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 diff --git a/gui/itemStats.py b/gui/itemStats.py index 6efb9af29..ce58ac09f 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -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 diff --git a/gui/mainFrame.py b/gui/mainFrame.py index cb90ce0cc..17e39af71 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -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): diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index 7c971ae4d..cf8f85198 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -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") + diff --git a/gui/marketBrowser.py b/gui/marketBrowser.py index 53f704c37..14dea666c 100644 --- a/gui/marketBrowser.py +++ b/gui/marketBrowser.py @@ -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) diff --git a/gui/propertyEditor.py b/gui/propertyEditor.py new file mode 100644 index 000000000..406007eb3 --- /dev/null +++ b/gui/propertyEditor.py @@ -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 diff --git a/imgs/gui/eve.png b/imgs/gui/eve.png new file mode 100644 index 000000000..07bb38363 Binary files /dev/null and b/imgs/gui/eve.png differ diff --git a/scripts/itemDiff.py b/scripts/itemDiff.py index 9effcfc02..a0aaa9672 100755 --- a/scripts/itemDiff.py +++ b/scripts/itemDiff.py @@ -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): diff --git a/scripts/prep_data.py b/scripts/prep_data.py index 582a8d245..823b4d037 100644 --- a/scripts/prep_data.py +++ b/scripts/prep_data.py @@ -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") diff --git a/service/__init__.py b/service/__init__.py index 8e9e9f6f5..8f5445527 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -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 diff --git a/service/conversions/releaseParallax.py b/service/conversions/releaseParallax.py new file mode 100644 index 000000000..070962aeb --- /dev/null +++ b/service/conversions/releaseParallax.py @@ -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", +} diff --git a/service/crest.py b/service/crest.py new file mode 100644 index 000000000..d2ddb1caa --- /dev/null +++ b/service/crest.py @@ -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() diff --git a/service/fit.py b/service/fit.py index b93d3b0b2..efbf95b8b 100644 --- a/service/fit.py +++ b/service/fit.py @@ -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) diff --git a/service/market.py b/service/market.py index e0a77b8b3..9abed103c 100644 --- a/service/market.py +++ b/service/market.py @@ -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: diff --git a/service/port.py b/service/port.py index 23721945f..fc7d308b7 100644 --- a/service/port.py +++ b/service/port.py @@ -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'] = ""%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() diff --git a/service/pycrest/__init__.py b/service/pycrest/__init__.py new file mode 100644 index 000000000..97244c957 --- /dev/null +++ b/service/pycrest/__init__.py @@ -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 \ No newline at end of file diff --git a/service/pycrest/compat.py b/service/pycrest/compat.py new file mode 100644 index 000000000..06320069b --- /dev/null +++ b/service/pycrest/compat.py @@ -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 \ No newline at end of file diff --git a/service/pycrest/errors.py b/service/pycrest/errors.py new file mode 100644 index 000000000..33b9ca9ae --- /dev/null +++ b/service/pycrest/errors.py @@ -0,0 +1,2 @@ +class APIException(Exception): + pass \ No newline at end of file diff --git a/service/pycrest/eve.py b/service/pycrest/eve.py new file mode 100644 index 000000000..6e08d317e --- /dev/null +++ b/service/pycrest/eve.py @@ -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__() diff --git a/service/pycrest/weak_ciphers.py b/service/pycrest/weak_ciphers.py new file mode 100644 index 000000000..03de8321a --- /dev/null +++ b/service/pycrest/weak_ciphers.py @@ -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) diff --git a/service/server.py b/service/server.py new file mode 100644 index 000000000..a4750876f --- /dev/null +++ b/service/server.py @@ -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 = ''' + + + +Done. Please close this window. + + + +''' + +# 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 to stop server\n") + httpd.stop() + diff --git a/service/settings.py b/service/settings.py index dde48f71d..10e7389b5 100644 --- a/service/settings.py +++ b/service/settings.py @@ -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? diff --git a/setup.py b/setup.py index 24cf8b81a..b4738824a 100644 --- a/setup.py +++ b/setup.py @@ -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']