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']