Merge branch 'pyfa-org:master' into master
This commit is contained in:
@@ -69,7 +69,7 @@ class Ammo:
|
||||
falloff = (mod.item.getAttribute('falloff') or 0) * \
|
||||
(charge.getAttribute('fallofMultiplier') or 1)
|
||||
for type_ in DmgTypes.names():
|
||||
d = charge.getAttribute('%sDamage' % type_)
|
||||
d = charge.getAttribute('%sDamage' % type_, default=0)
|
||||
if d > 0:
|
||||
damage += d
|
||||
# Take optimal and falloff as range factor
|
||||
|
||||
@@ -44,7 +44,7 @@ class EsiEndpoints(Enum):
|
||||
"""
|
||||
CHAR = "/v5/characters/{character_id}/"
|
||||
CHAR_SKILLS = "/v4/characters/{character_id}/skills/"
|
||||
CHAR_FITTINGS = "/v1/characters/{character_id}/fittings/"
|
||||
CHAR_FITTINGS = "/v2/characters/{character_id}/fittings/"
|
||||
CHAR_DEL_FIT = "/v1/characters/{character_id}/fittings/{fitting_id}/"
|
||||
DYNAMIC_ITEM = "/v1/dogma/dynamic/items/{type_id}/{item_id}/"
|
||||
|
||||
|
||||
19
service/conversions/releaseEquinox.py
Normal file
19
service/conversions/releaseEquinox.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Conversion pack for Equinox release
|
||||
"""
|
||||
|
||||
CONVERSIONS = {
|
||||
# Renamed items
|
||||
"Imperial Navy Mjolnir Auto-Targeting Cruise Missile I": "Legion Mjolnir Auto-Targeting Cruise Missile",
|
||||
"Caldari Navy Scourge Auto-Targeting Cruise Missile I": "Legion Scourge Auto-Targeting Cruise Missile",
|
||||
"Federation Navy Inferno Auto-Targeting Cruise Missile I": "Legion Inferno Auto-Targeting Cruise Missile",
|
||||
"Republic Fleet Nova Auto-Targeting Cruise Missile I": "Legion Nova Auto-Targeting Cruise Missile",
|
||||
"Imperial Navy Mjolnir Auto-Targeting Heavy Missile I": "Legion Mjolnir Auto-Targeting Heavy Missile",
|
||||
"Caldari Navy Scourge Auto-Targeting Heavy Missile I": "Legion Scourge Auto-Targeting Heavy Missile",
|
||||
"Federation Navy Inferno Auto-Targeting Heavy Missile I": "Legion Inferno Auto-Targeting Heavy Missile",
|
||||
"Republic Fleet Nova Auto-Targeting Heavy Missile I": "Legion Nova Auto-Targeting Heavy Missile",
|
||||
"Imperial Navy Mjolnir Auto-Targeting Light Missile I": "Legion Mjolnir Auto-Targeting Light Missile",
|
||||
"Caldari Navy Scourge Auto-Targeting Light Missile I": "Legion Scourge Auto-Targeting Light Missile",
|
||||
"Federation Navy Inferno Auto-Targeting Light Missile I": "Legion Inferno Auto-Targeting Light Missile",
|
||||
"Republic Fleet Nova Auto-Targeting Light Missile I": "Legion Nova Auto-Targeting Light Missile",
|
||||
}
|
||||
10
service/conversions/releaseSep2023.py
Normal file
10
service/conversions/releaseSep2023.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Actually renamed somewhere during summer, but updated only in september
|
||||
"""
|
||||
|
||||
CONVERSIONS = {
|
||||
# Renamed items
|
||||
"‘Atgeir’ Explosive Disruptive Lance": "'Atgeir' Explosive Disruptive Lance",
|
||||
"‘Steel Yari’ Kinetic Disruptive Lance": "'Steel Yari' Kinetic Disruptive Lance",
|
||||
"‘Sarissa’ Thermal Disruptive Lance": "'Sarissa' Thermal Disruptive Lance",
|
||||
}
|
||||
24
service/conversions/releaseSep2024.py
Normal file
24
service/conversions/releaseSep2024.py
Normal file
@@ -0,0 +1,24 @@
|
||||
CONVERSIONS = {
|
||||
# Renamed items
|
||||
"Large Rudimentary Concussion Bomb I": "'Concussion' Compact Large Graviton Smartbomb",
|
||||
"Small Rudimentary Concussion Bomb I": "'Concussion' Compact Small Graviton Smartbomb",
|
||||
"Large 'Vehemence' Shockwave Charge": "'Vehemence' Compact Large EMP Smartbomb",
|
||||
"Small 'Vehemence' Shockwave Charge": "'Vehemence' Compact Small EMP Smartbomb",
|
||||
"Medium Rudimentary Concussion Bomb I": "'Concussion' Compact Medium Graviton Smartbomb",
|
||||
"Medium 'Vehemence' Shockwave Charge": "'Vehemence' Compact Medium EMP Smartbomb",
|
||||
"Small 'Notos' Explosive Charge I": "'Notos' Compact Small Proton Smartbomb",
|
||||
"Medium 'Notos' Explosive Charge I": "'Notos' Compact Medium Proton Smartbomb",
|
||||
"Large 'Notos' Explosive Charge I": "'Notos' Compact Large Proton Smartbomb",
|
||||
"Small YF-12a Smartbomb": "'YF-12a' Compact Small Plasma Smartbomb",
|
||||
"Medium YF-12a Smartbomb": "'YF-12a' Compact Medium Plasma Smartbomb",
|
||||
"Large YF-12a Smartbomb": "'YF-12a' Compact Large Plasma Smartbomb",
|
||||
"Small Degenerative Concussion Bomb I": "'Degenerative' Small Proton Smartbomb",
|
||||
"Small Degenerative Concussion Bomb I Blueprint": "'Degenerative' Small Proton Smartbomb Blueprint",
|
||||
"Medium Degenerative Concussion Bomb I": "'Dwindling' Medium Proton Smartbomb",
|
||||
"Medium Degenerative Concussion Bomb I Blueprint": "'Dwindling' Medium Proton Smartbomb Blueprint",
|
||||
"Large Degenerative Concussion Bomb I": "'Regressive' Large Proton Smartbomb",
|
||||
"Large Degenerative Concussion Bomb I Blueprint": "'Regressive' Large Proton Smartbomb Blueprint",
|
||||
"'Pike' Small EMP Smartbomb I": "'Pike' Small EMP Smartbomb",
|
||||
"'Lance' Medium EMP Smartbomb I": "'Lance' Medium EMP Smartbomb",
|
||||
"'Warhammer' Large EMP Smartbomb I": "'Warhammer' Large EMP Smartbomb",
|
||||
}
|
||||
@@ -6,14 +6,14 @@ import time
|
||||
import base64
|
||||
import json
|
||||
import config
|
||||
import webbrowser
|
||||
import re
|
||||
|
||||
import eos.db
|
||||
from service.const import EsiLoginMethod, EsiSsoMode
|
||||
from eos.saveddata.ssocharacter import SsoCharacter
|
||||
from service.esiAccess import APIException, GenericSsoError
|
||||
import gui.globalEvents as GE
|
||||
from gui.ssoLogin import SsoLogin, SsoLoginServer
|
||||
from gui.ssoLogin import SsoLogin
|
||||
from service.server import StoppableHTTPServer, AuthHandler
|
||||
from service.settings import EsiSettings
|
||||
from service.esiAccess import EsiAccess
|
||||
@@ -22,6 +22,7 @@ import gui.mainFrame
|
||||
from requests import Session
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
_t = wx.GetTranslation
|
||||
|
||||
|
||||
class Esi(EsiAccess):
|
||||
@@ -69,8 +70,8 @@ class Esi(EsiAccess):
|
||||
chars = eos.db.getSsoCharacters(config.getClientSecret())
|
||||
return chars
|
||||
|
||||
def getSsoCharacter(self, id):
|
||||
char = eos.db.getSsoCharacter(id, config.getClientSecret())
|
||||
def getSsoCharacter(self, id, server=None):
|
||||
char = eos.db.getSsoCharacter(id, config.getClientSecret(), server)
|
||||
eos.db.commit()
|
||||
return char
|
||||
|
||||
@@ -101,15 +102,36 @@ class Esi(EsiAccess):
|
||||
self.fittings_deleted.add(fittingID)
|
||||
|
||||
def login(self):
|
||||
# always start the local server if user is using client details. Otherwise, start only if they choose to do so.
|
||||
if self.settings.get('loginMode') == EsiLoginMethod.SERVER:
|
||||
with gui.ssoLogin.SsoLoginServer(0) as dlg:
|
||||
dlg.ShowModal()
|
||||
else:
|
||||
with gui.ssoLogin.SsoLogin() as dlg:
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
message = json.loads(base64.b64decode(dlg.ssoInfoCtrl.Value.strip()))
|
||||
self.handleLogin(message)
|
||||
start_server = self.settings.get('loginMode') == EsiLoginMethod.SERVER and self.server_base.supports_auto_login
|
||||
with gui.ssoLogin.SsoLogin(self.server_base, start_server) as dlg:
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
from gui.esiFittings import ESIExceptionHandler
|
||||
|
||||
try:
|
||||
if self.server_name == "Serenity":
|
||||
s = re.search(r'(?<=code=)[a-zA-Z0-9\-_]*', dlg.ssoInfoCtrl.Value.strip())
|
||||
if s:
|
||||
# skip state verification and go directly through the auth code processing
|
||||
self.handleLogin(s.group(0))
|
||||
else:
|
||||
pass
|
||||
# todo: throw error
|
||||
else:
|
||||
self.handleServerRequest(json.loads(base64.b64decode(dlg.ssoInfoCtrl.Value.strip())))
|
||||
except GenericSsoError as ex:
|
||||
pyfalog.error(ex)
|
||||
with wx.MessageDialog(
|
||||
self.mainFrame,
|
||||
str(ex),
|
||||
_t("SSO Error"),
|
||||
wx.OK | wx.ICON_ERROR
|
||||
) as dlg:
|
||||
dlg.ShowModal()
|
||||
except APIException as ex:
|
||||
pyfalog.error(ex)
|
||||
ESIExceptionHandler(ex)
|
||||
pass
|
||||
|
||||
|
||||
def stopServer(self):
|
||||
pyfalog.debug("Stopping Server")
|
||||
@@ -127,24 +149,26 @@ class Esi(EsiAccess):
|
||||
|
||||
self.httpd = StoppableHTTPServer(('localhost', port), AuthHandler)
|
||||
port = self.httpd.socket.getsockname()[1]
|
||||
self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerLogin,))
|
||||
self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerRequest,))
|
||||
self.serverThread.name = "SsoCallbackServer"
|
||||
self.serverThread.daemon = True
|
||||
self.serverThread.start()
|
||||
|
||||
return 'http://localhost:{}'.format(port)
|
||||
|
||||
def handleLogin(self, message):
|
||||
auth_response, data = self.auth(message['code'])
|
||||
def handleLogin(self, code):
|
||||
auth_response, data = self.auth(code)
|
||||
|
||||
currentCharacter = self.getSsoCharacter(data['name'])
|
||||
currentCharacter = self.getSsoCharacter(data['name'], self.server_base.name)
|
||||
|
||||
sub_split = data["sub"].split(":")
|
||||
if (len(sub_split) != 3):
|
||||
|
||||
if len(sub_split) != 3:
|
||||
raise GenericSsoError("JWT sub does not contain the expected data. Contents: %s" % data["sub"])
|
||||
|
||||
cid = sub_split[-1]
|
||||
if currentCharacter is None:
|
||||
currentCharacter = SsoCharacter(cid, data['name'], config.getClientSecret())
|
||||
currentCharacter = SsoCharacter(cid, data['name'], config.getClientSecret(), self.server_base.name)
|
||||
|
||||
Esi.update_token(currentCharacter, auth_response)
|
||||
|
||||
@@ -153,7 +177,7 @@ class Esi(EsiAccess):
|
||||
|
||||
# get (endpoint, char, data?)
|
||||
|
||||
def handleServerLogin(self, message):
|
||||
def handleServerRequest(self, message):
|
||||
if not message:
|
||||
raise GenericSsoError("Could not parse out querystring parameters.")
|
||||
|
||||
@@ -169,4 +193,4 @@ class Esi(EsiAccess):
|
||||
|
||||
pyfalog.debug("Handling SSO login with: {0}", message)
|
||||
|
||||
self.handleLogin(message)
|
||||
self.handleLogin(message['code'])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# noinspection PyPackageRequirements
|
||||
from collections import namedtuple
|
||||
|
||||
import requests
|
||||
from logbook import Logger
|
||||
import uuid
|
||||
import time
|
||||
@@ -30,13 +31,6 @@ scopes = [
|
||||
'esi-fittings.write_fittings.v1'
|
||||
]
|
||||
|
||||
ApiBase = namedtuple('ApiBase', ['sso', 'esi'])
|
||||
supported_servers = {
|
||||
"Tranquility": ApiBase("login.eveonline.com", "esi.evetech.net"),
|
||||
"Singularity": ApiBase("sisilogin.testeveonline.com", "esi.evetech.net"),
|
||||
"Serenity": ApiBase("login.evepc.163.com", "esi.evepc.163.com")
|
||||
}
|
||||
|
||||
class GenericSsoError(Exception):
|
||||
""" Exception used for generic SSO errors that aren't directly related to an API call
|
||||
"""
|
||||
@@ -63,10 +57,11 @@ class APIException(Exception):
|
||||
|
||||
|
||||
class EsiAccess:
|
||||
server_meta = {}
|
||||
def __init__(self):
|
||||
self.settings = EsiSettings.getInstance()
|
||||
self.server_base: ApiBase = supported_servers[self.settings.get("server")]
|
||||
|
||||
self.default_server_name = self.settings.get('server')
|
||||
self.default_server_base = config.supported_servers[self.default_server_name]
|
||||
# session request stuff
|
||||
self._session = Session()
|
||||
self._basicHeaders = {
|
||||
@@ -78,23 +73,38 @@ class EsiAccess:
|
||||
self._session.headers.update(self._basicHeaders)
|
||||
self._session.proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat()
|
||||
|
||||
self.mem_cached_session = {}
|
||||
|
||||
# Set up cached session. This is only used for SSO meta data for now, but can be expanded to actually handle
|
||||
# various ESI caching (using ETag, for example) in the future
|
||||
cached_session = CachedSession(
|
||||
self.cached_session = CachedSession(
|
||||
os.path.join(config.savePath, config.ESI_CACHE),
|
||||
backend="sqlite",
|
||||
cache_control=True, # Use Cache-Control headers for expiration, if available
|
||||
expire_after=timedelta(days=1), # Otherwise expire responses after one day
|
||||
stale_if_error=True, # In case of request errors, use stale cache data if possible
|
||||
)
|
||||
cached_session.headers.update(self._basicHeaders)
|
||||
cached_session.proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat()
|
||||
self.cached_session.headers.update(self._basicHeaders)
|
||||
self.cached_session.proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat()
|
||||
self.init(self.default_server_base)
|
||||
|
||||
def init(self, server_base):
|
||||
self.server_base: config.ApiServer = server_base
|
||||
self.server_name = self.server_base.name
|
||||
try:
|
||||
meta_call = self.cached_session.get("https://%s/.well-known/oauth-authorization-server" % self.server_base.sso)
|
||||
except:
|
||||
# The http data of expire_after in evepc.163.com is -1
|
||||
meta_call = requests.get("https://%s/.well-known/oauth-authorization-server" % self.server_base.sso)
|
||||
|
||||
meta_call = cached_session.get("https://%s/.well-known/oauth-authorization-server" % self.server_base.sso)
|
||||
meta_call.raise_for_status()
|
||||
self.server_meta = meta_call.json()
|
||||
|
||||
jwks_call = cached_session.get(self.server_meta["jwks_uri"])
|
||||
try:
|
||||
jwks_call = self.cached_session.get(self.server_meta["jwks_uri"])
|
||||
except:
|
||||
jwks_call = requests.get(self.server_meta["jwks_uri"])
|
||||
|
||||
jwks_call.raise_for_status()
|
||||
self.jwks = jwks_call.json()
|
||||
|
||||
@@ -116,7 +126,7 @@ class EsiAccess:
|
||||
|
||||
@property
|
||||
def client_id(self):
|
||||
return self.settings.get('clientID') or config.API_CLIENT_ID
|
||||
return self.settings.get('clientID') or self.server_base.client_id
|
||||
|
||||
@staticmethod
|
||||
def update_token(char, tokenResponse):
|
||||
@@ -142,16 +152,25 @@ class EsiAccess:
|
||||
'state': self.state
|
||||
}
|
||||
|
||||
args = {
|
||||
'response_type': 'code',
|
||||
'redirect_uri': config.SSO_CALLBACK,
|
||||
'client_id': self.client_id,
|
||||
'scope': ' '.join(scopes),
|
||||
'code_challenge': code_challenge,
|
||||
'code_challenge_method': 'S256',
|
||||
'state': base64.b64encode(bytes(json.dumps(state_arg), 'utf-8'))
|
||||
}
|
||||
|
||||
if(self.server_name=="Serenity"):
|
||||
args = {
|
||||
'response_type': 'code',
|
||||
'redirect_uri': self.server_base.callback,
|
||||
'client_id': self.client_id,
|
||||
'scope': ' '.join(scopes),
|
||||
'state': 'hilltech',
|
||||
'device_id': 'eims'
|
||||
}
|
||||
else:
|
||||
args = {
|
||||
'response_type': 'code',
|
||||
'redirect_uri': self.server_base.callback,
|
||||
'client_id': self.client_id,
|
||||
'scope': ' '.join(scopes),
|
||||
'code_challenge': code_challenge,
|
||||
'code_challenge_method': 'S256',
|
||||
'state': base64.b64encode(bytes(json.dumps(state_arg), 'utf-8'))
|
||||
}
|
||||
return '%s?%s' % (
|
||||
self.oauth_authorize,
|
||||
urlencode(args)
|
||||
@@ -252,6 +271,11 @@ class EsiAccess:
|
||||
"https://login.eveonline.com: {}".format(str(e)))
|
||||
|
||||
def _before_request(self, ssoChar):
|
||||
if ssoChar:
|
||||
self.init(config.supported_servers[ssoChar.server])
|
||||
else:
|
||||
self.init(self.default_server_base)
|
||||
|
||||
self._session.headers.clear()
|
||||
self._session.headers.update(self._basicHeaders)
|
||||
if ssoChar is None:
|
||||
@@ -280,17 +304,17 @@ class EsiAccess:
|
||||
def get(self, ssoChar, endpoint, **kwargs):
|
||||
self._before_request(ssoChar)
|
||||
endpoint = endpoint.format(**kwargs)
|
||||
return self._after_request(self._session.get("{}{}".format(self.esi_url, endpoint)))
|
||||
return self._after_request(self._session.get("{}{}?datasource={}".format(self.esi_url, endpoint, self.server_name.lower())))
|
||||
|
||||
def post(self, ssoChar, endpoint, json, **kwargs):
|
||||
self._before_request(ssoChar)
|
||||
endpoint = endpoint.format(**kwargs)
|
||||
return self._after_request(self._session.post("{}{}".format(self.esi_url, endpoint), data=json))
|
||||
return self._after_request(self._session.post("{}{}?datasource={}".format(self.esi_url, endpoint, self.server_name.lower()), data=json))
|
||||
|
||||
def delete(self, ssoChar, endpoint, **kwargs):
|
||||
self._before_request(ssoChar)
|
||||
endpoint = endpoint.format(**kwargs)
|
||||
return self._after_request(self._session.delete("{}{}".format(self.esi_url, endpoint)))
|
||||
return self._after_request(self._session.delete("{}{}?datasource={}".format(self.esi_url, endpoint, self.server_name.lower())))
|
||||
|
||||
# todo: move these off to another class which extends this one. This class should only handle the low level
|
||||
# authentication and
|
||||
|
||||
@@ -88,7 +88,7 @@ class Fit:
|
||||
"enableGaugeAnimation": True,
|
||||
"openFitInNew": False,
|
||||
"priceSystem": "Jita",
|
||||
"priceSource": "evemarketer",
|
||||
"priceSource": "fuzzwork market",
|
||||
"showShipBrowserTooltip": True,
|
||||
"marketSearchDelay": 250,
|
||||
"ammoChangeAll": False,
|
||||
|
||||
@@ -263,6 +263,9 @@ bcs:
|
||||
bcu:
|
||||
- 'bcu'
|
||||
- 'ballistic control system'
|
||||
vts:
|
||||
- 'vts'
|
||||
- 'vorton tuning system'
|
||||
tc:
|
||||
- 'tc'
|
||||
- '(?<!remote )tracking computer'
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
import pkg_resources
|
||||
from importlib.resources import files
|
||||
|
||||
DEFAULT_DATA = pkg_resources.resource_string(__name__, 'defaults.yaml').decode()
|
||||
DEFAULT_HEADER = pkg_resources.resource_string(__name__, 'header.yaml').decode()
|
||||
PACKAGE_NAME = __name__.rsplit(".", maxsplit=1)[0]
|
||||
|
||||
DEFAULT_DATA = files(PACKAGE_NAME).joinpath('defaults.yaml').open('r', encoding='utf8').read()
|
||||
DEFAULT_HEADER = files(PACKAGE_NAME).joinpath('header.yaml').open('r', encoding='utf8').read()
|
||||
|
||||
@@ -184,7 +184,7 @@ class SearchWorkerThread(threading.Thread):
|
||||
def _prepareRequestNormal(self, request):
|
||||
# Escape regexp-specific symbols, and un-escape whitespaces
|
||||
request = re.escape(request)
|
||||
request = re.sub(r'\\(?P<ws>\s+)', '\g<ws>', request)
|
||||
request = re.sub(r'\\(?P<ws>\s+)', r'\g<ws>', request)
|
||||
# Imitate wildcard search
|
||||
request = re.sub(r'\\\*', r'\\w*', request)
|
||||
request = re.sub(r'\\\?', r'\\w?', request)
|
||||
@@ -322,6 +322,11 @@ class Market:
|
||||
"Geri" : self.les_grp, # AT18 prize
|
||||
"Bestla" : self.les_grp, # AT18 prize
|
||||
"Metamorphosis" : self.les_grp, # Seems to be anniversary gift
|
||||
"Shapash" : self.les_grp, # AT19 prize
|
||||
"Cybele" : self.les_grp, # AT19 prize
|
||||
"Sidewinder" : self.les_grp, # AT20 prize
|
||||
"Cobra" : self.les_grp, # AT20 prize
|
||||
"Python" : self.les_grp, # AT20 prize
|
||||
}
|
||||
|
||||
self.ITEMS_FORCEGROUP_R = self.__makeRevDict(self.ITEMS_FORCEGROUP)
|
||||
@@ -983,3 +988,21 @@ class Market:
|
||||
metatab = self.META_MAP_REVERSE_GROUPED.get(metagrpid)
|
||||
metalvl = item.metaLevel or 0
|
||||
return catname, mktgrpid, parentname, metatab, metalvl, item.name
|
||||
|
||||
def printAllItems(self):
|
||||
items = set()
|
||||
|
||||
def handleMg(marketGroup, path=()):
|
||||
marketGroup = self.getMarketGroup(marketGroup, eager=("items", "items.metaGroup", "children"))
|
||||
path = path + (marketGroup.name,)
|
||||
print(' > '.join(path))
|
||||
for item in self.getItemsByMarketGroup(marketGroup):
|
||||
items.add(item.ID)
|
||||
for mgc in self.getMarketGroupChildren(marketGroup):
|
||||
handleMg(mgc, path=path)
|
||||
|
||||
for mg in self.ROOT_MARKET_GROUPS:
|
||||
handleMg(mg)
|
||||
print(sorted(items))
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__all__ = ['evemarketer', 'evepraisal', 'evemarketdata', 'fuzzwork', 'cevemarket']
|
||||
__all__ = ['evetycoon', 'evemarketdata', 'fuzzwork', 'cevemarket']
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# =============================================================================
|
||||
# Copyright (C) 2010 Diego Duclos
|
||||
#
|
||||
# This file is part of pyfa.
|
||||
#
|
||||
# pyfa is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# pyfa 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from xml.dom import minidom
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
from eos.saveddata.price import PriceStatus
|
||||
from service.network import Network
|
||||
from service.price import Price
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class EveMarketer:
|
||||
|
||||
name = 'evemarketer'
|
||||
group = 'tranquility'
|
||||
|
||||
def __init__(self, priceMap, system, fetchTimeout):
|
||||
# Try selected system first
|
||||
self.fetchPrices(priceMap, max(2 * fetchTimeout / 3, 2), system)
|
||||
# If price was not available - try globally
|
||||
if priceMap:
|
||||
self.fetchPrices(priceMap, max(fetchTimeout / 3, 2))
|
||||
|
||||
@staticmethod
|
||||
def fetchPrices(priceMap, fetchTimeout, system=None):
|
||||
params = {'typeid': {typeID for typeID in priceMap}}
|
||||
if system is not None:
|
||||
params['usesystem'] = system
|
||||
baseurl = 'https://api.evemarketer.com/ec/marketstat'
|
||||
network = Network.getInstance()
|
||||
data = network.get(url=baseurl, type=network.PRICES, params=params, timeout=fetchTimeout)
|
||||
xml = minidom.parseString(data.text)
|
||||
types = xml.getElementsByTagName('marketstat').item(0).getElementsByTagName('type')
|
||||
# Cycle through all types we've got from request
|
||||
for type_ in types:
|
||||
# Get data out of each typeID details tree
|
||||
typeID = int(type_.getAttribute('id'))
|
||||
sell = type_.getElementsByTagName('sell').item(0)
|
||||
# If price data wasn't there, skip the item
|
||||
try:
|
||||
percprice = float(sell.getElementsByTagName('percentile').item(0).firstChild.data)
|
||||
except (TypeError, ValueError):
|
||||
pyfalog.warning('Failed to get price for: {0}', type_)
|
||||
continue
|
||||
|
||||
# Price is 0 if evemarketer has info on this item, but it is not available
|
||||
# for current scope limit. If we provided scope limit - make sure to skip
|
||||
# such items to check globally, and do not skip if requested globally
|
||||
if percprice == 0 and system is not None:
|
||||
continue
|
||||
priceMap[typeID].update(PriceStatus.fetchSuccess, percprice)
|
||||
del priceMap[typeID]
|
||||
|
||||
|
||||
Price.register(EveMarketer)
|
||||
@@ -17,7 +17,6 @@
|
||||
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
from eos.saveddata.price import PriceStatus
|
||||
@@ -26,58 +25,41 @@ from service.price import Price
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
systemAliases = {
|
||||
None: 'universe',
|
||||
30000142: 'jita',
|
||||
30002187: 'amarr',
|
||||
30002659: 'dodixie',
|
||||
30002510: 'rens',
|
||||
30002053: 'hek'}
|
||||
locations = {
|
||||
30000142: (10000002, 60003760), # Jita 4-4 CNAP
|
||||
30002187: (10000043, 60008494), # Amarr VIII
|
||||
30002659: (10000032, 60011866), # Dodixie
|
||||
30002510: (10000030, 60004588), # Rens
|
||||
30002053: (10000042, 60005686)} # Hek
|
||||
|
||||
|
||||
class EvePraisal:
|
||||
class EveTycoon:
|
||||
|
||||
name = 'evepraisal'
|
||||
name = 'evetycoon'
|
||||
group = 'tranquility'
|
||||
|
||||
def __init__(self, priceMap, system, fetchTimeout):
|
||||
# Try selected system first
|
||||
self.fetchPrices(priceMap, max(2 * fetchTimeout / 3, 2), system)
|
||||
# If price was not available - try globally
|
||||
if priceMap:
|
||||
self.fetchPrices(priceMap, max(fetchTimeout / 3, 2))
|
||||
|
||||
@staticmethod
|
||||
def fetchPrices(priceMap, fetchTimeout, system=None):
|
||||
if system not in systemAliases:
|
||||
return
|
||||
jsonData = {
|
||||
'market_name': systemAliases[system],
|
||||
'items': [{'type_id': typeID} for typeID in priceMap]}
|
||||
baseurl = 'https://evepraisal.com/appraisal/structured.json'
|
||||
# Default to jita when system is not found
|
||||
regionID, stationID = locations.get(system, locations[30000142])
|
||||
baseurl = 'https://evetycoon.com/api/v1/market/stats'
|
||||
network = Network.getInstance()
|
||||
resp = network.post(baseurl, network.PRICES, jsonData=jsonData, timeout=fetchTimeout)
|
||||
data = resp.json()
|
||||
try:
|
||||
itemsData = data['appraisal']['items']
|
||||
except (KeyError, TypeError):
|
||||
return
|
||||
# Cycle through all types we've got from request
|
||||
for itemData in itemsData:
|
||||
try:
|
||||
typeID = int(itemData['typeID'])
|
||||
price = itemData['prices']['sell']['min']
|
||||
orderCount = itemData['prices']['sell']['order_count']
|
||||
except (KeyError, TypeError):
|
||||
for typeID in tuple(priceMap):
|
||||
url = f'{baseurl}/{regionID}/{typeID}'
|
||||
resp = network.get(url=url, params={'locationId': stationID}, type=network.PRICES, timeout=fetchTimeout)
|
||||
if resp.status_code != 200:
|
||||
continue
|
||||
# evepraisal returns 0 if price data doesn't even exist for the item
|
||||
price = resp.json()['sellAvgFivePercent']
|
||||
# Price is 0 - no data
|
||||
if price == 0:
|
||||
continue
|
||||
# evepraisal seems to provide price for some items despite having no orders up
|
||||
if orderCount < 1:
|
||||
continue
|
||||
priceMap[typeID].update(PriceStatus.fetchSuccess, price)
|
||||
del priceMap[typeID]
|
||||
|
||||
|
||||
Price.register(EvePraisal)
|
||||
Price.register(EveTycoon)
|
||||
@@ -1,2 +1,2 @@
|
||||
from .efs import EfsPort
|
||||
from .port import Port, IPortUser
|
||||
from .port import Port
|
||||
|
||||
@@ -423,7 +423,8 @@ class EfsPort:
|
||||
else:
|
||||
maxRange = stats.maxRange
|
||||
|
||||
dps_spread_dict = stats.getDps(spoolOptions=spoolOptions, getSpreadDPS=True)
|
||||
dps = stats.getDps(spoolOptions=spoolOptions)
|
||||
dps_spread_dict = {'em': dps.em, 'therm': dps.thermal, 'kin': dps.kinetic, 'exp': dps.explosive, 'pure': dps.pure}
|
||||
dps_spread_dict.update((x, y*n) for x, y in dps_spread_dict.items())
|
||||
|
||||
statDict = {
|
||||
@@ -637,7 +638,7 @@ class EfsPort:
|
||||
bsGroupNames = ["Battleship", "Elite Battleship", "Black Ops", "Marauder"]
|
||||
capitalGroupNames = ["Titan", "Dreadnought", "Freighter", "Carrier", "Supercarrier",
|
||||
"Capital Industrial Ship", "Jump Freighter", "Force Auxiliary"]
|
||||
indyGroupNames = ["Industrial", "Deep Space Transport", "Blockade Runner",
|
||||
indyGroupNames = ["Hauler", "Deep Space Transport", "Blockade Runner",
|
||||
"Mining Barge", "Exhumer", "Industrial Command Ship"]
|
||||
miscGroupNames = ["Capsule", "Prototype Exploration Ship"]
|
||||
shipSizes = [
|
||||
|
||||
@@ -38,7 +38,7 @@ from service.const import PortEftOptions
|
||||
from service.fit import Fit as svcFit
|
||||
from service.market import Market
|
||||
from service.port.muta import parseMutant, renderMutant
|
||||
from service.port.shared import IPortUser, fetchItem, processing_notify
|
||||
from service.port.shared import fetchItem
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -46,7 +46,7 @@ pyfalog = Logger(__name__)
|
||||
MODULE_CATS = ('Module', 'Subsystem', 'Structure Module')
|
||||
SLOT_ORDER = (FittingSlot.LOW, FittingSlot.MED, FittingSlot.HIGH, FittingSlot.RIG, FittingSlot.SUBSYSTEM, FittingSlot.SERVICE)
|
||||
OFFLINE_SUFFIX = '/OFFLINE'
|
||||
NAME_CHARS = '[^,/\[\]]' # Characters which are allowed to be used in name
|
||||
NAME_CHARS = r'[^,/\[\]]' # Characters which are allowed to be used in name
|
||||
|
||||
|
||||
class MutationExportData:
|
||||
@@ -176,7 +176,11 @@ def exportDrones(drones, exportMutants=True, mutaData=None, standAlone=True):
|
||||
return drone.item.typeName
|
||||
|
||||
def droneSorter(drone):
|
||||
groupName = Market.getInstance().getMarketGroupByItem(drone.item).marketGroupName
|
||||
if drone.isMutated:
|
||||
item = drone.baseItem
|
||||
else:
|
||||
item = drone.item
|
||||
groupName = Market.getInstance().getMarketGroupByItem(item).marketGroupName
|
||||
return (DRONE_ORDER.index(groupName), drone.isMutated, drone.fullName)
|
||||
|
||||
if mutaData is None:
|
||||
@@ -200,7 +204,10 @@ def exportDrones(drones, exportMutants=True, mutaData=None, standAlone=True):
|
||||
|
||||
def exportFighters(fighters):
|
||||
# Same as in drone additions panel
|
||||
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
|
||||
FIGHTER_ORDER = (
|
||||
'Light Fighter', 'Structure Light Fighter',
|
||||
'Heavy Fighter', 'Structure Heavy Fighter',
|
||||
'Support Fighter', 'Structure Support Fighter')
|
||||
|
||||
def fighterSorter(fighter):
|
||||
groupName = Market.getInstance().getGroupByItem(fighter.item).name
|
||||
@@ -243,9 +250,9 @@ def importEft(lines):
|
||||
aFit = AbstractFit()
|
||||
aFit.mutations = importGetMutationData(lines)
|
||||
|
||||
stubPattern = '^\[.+?\]$'
|
||||
modulePattern = '^(?P<typeName>{0}+?)(,\s*(?P<chargeName>{0}+?))?(?P<offline>\s*{1})?(\s*\[(?P<mutation>\d+?)\])?$'.format(NAME_CHARS, OFFLINE_SUFFIX)
|
||||
droneCargoPattern = '^(?P<typeName>{}+?) x(?P<amount>\d+?)(\s*\[(?P<mutation>\d+?)\])?$'.format(NAME_CHARS)
|
||||
stubPattern = r'^\[.+?\]$'
|
||||
modulePattern = r'^(?P<typeName>{0}+?)(,\s*(?P<chargeName>{0}+?))?(?P<offline>\s*{1})?(\s*\[(?P<mutation>\d+?)\])?$'.format(NAME_CHARS, OFFLINE_SUFFIX)
|
||||
droneCargoPattern = r'^(?P<typeName>{}+?) x(?P<amount>\d+?)(\s*\[(?P<mutation>\d+?)\])?$'.format(NAME_CHARS)
|
||||
|
||||
sections = []
|
||||
for section in _importSectionIter(lines):
|
||||
@@ -365,7 +372,7 @@ def importEft(lines):
|
||||
return fit
|
||||
|
||||
|
||||
def importEftCfg(shipname, lines, iportuser):
|
||||
def importEftCfg(shipname, lines, progress):
|
||||
"""Handle import from EFT config store file"""
|
||||
|
||||
# Check if we have such ship in database, bail if we don't
|
||||
@@ -388,6 +395,8 @@ def importEftCfg(shipname, lines, iportuser):
|
||||
fitIndices.append(startPos)
|
||||
|
||||
for i, startPos in enumerate(fitIndices):
|
||||
if progress and progress.userCancelled:
|
||||
return []
|
||||
# End position is last file line if we're trying to get it for last fit,
|
||||
# or start position of next fit minus 1
|
||||
endPos = len(lines) if i == len(fitIndices) - 1 else fitIndices[i + 1]
|
||||
@@ -413,17 +422,17 @@ def importEftCfg(shipname, lines, iportuser):
|
||||
continue
|
||||
|
||||
# Parse line into some data we will need
|
||||
misc = re.match("(Drones|Implant|Booster)_(Active|Inactive)=(.+)", line)
|
||||
cargo = re.match("Cargohold=(.+)", line)
|
||||
misc = re.match(r"(Drones|Implant|Booster)_(Active|Inactive)=(.+)", line)
|
||||
cargo = re.match(r"Cargohold=(.+)", line)
|
||||
# 2017/03/27 NOTE: store description from EFT
|
||||
description = re.match("Description=(.+)", line)
|
||||
description = re.match(r"Description=(.+)", line)
|
||||
|
||||
if misc:
|
||||
entityType = misc.group(1)
|
||||
entityState = misc.group(2)
|
||||
entityData = misc.group(3)
|
||||
if entityType == "Drones":
|
||||
droneData = re.match("(.+),([0-9]+)", entityData)
|
||||
droneData = re.match(r"(.+),([0-9]+)", entityData)
|
||||
# Get drone name and attempt to detect drone number
|
||||
droneName = droneData.group(1) if droneData else entityData
|
||||
droneAmount = int(droneData.group(2)) if droneData else 1
|
||||
@@ -489,7 +498,7 @@ def importEftCfg(shipname, lines, iportuser):
|
||||
fitobj.boosters.append(b)
|
||||
# If we don't have any prefixes, then it's a module
|
||||
elif cargo:
|
||||
cargoData = re.match("(.+),([0-9]+)", cargo.group(1))
|
||||
cargoData = re.match(r"(.+),([0-9]+)", cargo.group(1))
|
||||
cargoName = cargoData.group(1) if cargoData else cargo.group(1)
|
||||
cargoAmount = int(cargoData.group(2)) if cargoData else 1
|
||||
# Bail if we can't get item
|
||||
@@ -508,7 +517,7 @@ def importEftCfg(shipname, lines, iportuser):
|
||||
elif description:
|
||||
fitobj.notes = description.group(1).replace("|", "\n")
|
||||
else:
|
||||
withCharge = re.match("(.+),(.+)", line)
|
||||
withCharge = re.match(r"(.+),(.+)", line)
|
||||
modName = withCharge.group(1) if withCharge else line
|
||||
chargeName = withCharge.group(2) if withCharge else None
|
||||
# If we can't get module item, skip it
|
||||
@@ -558,11 +567,8 @@ def importEftCfg(shipname, lines, iportuser):
|
||||
# Append fit to list of fits
|
||||
fits.append(fitobj)
|
||||
|
||||
if iportuser: # NOTE: Send current processing status
|
||||
processing_notify(
|
||||
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
|
||||
"%s:\n%s" % (fitobj.ship.name, fitobj.name)
|
||||
)
|
||||
if progress:
|
||||
progress.message = "%s:\n%s" % (fitobj.ship.name, fitobj.name)
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
@@ -585,7 +591,7 @@ def _importPrepare(lines):
|
||||
return lines
|
||||
|
||||
|
||||
mutantHeaderPattern = re.compile('^\[(?P<ref>\d+)\](?P<tail>.*)')
|
||||
mutantHeaderPattern = re.compile(r'^\[(?P<ref>\d+)\](?P<tail>.*)')
|
||||
|
||||
|
||||
def importGetMutationData(lines):
|
||||
@@ -646,7 +652,7 @@ def _importCreateFit(lines):
|
||||
"""Create fit and set top-level entity (ship or citadel)."""
|
||||
fit = Fit()
|
||||
header = lines.pop(0)
|
||||
m = re.match('\[(?P<shipType>[^,]+),\s*(?P<fitName>.+)\]', header)
|
||||
m = re.match(r'\[(?P<shipType>[^,]+),\s*(?P<fitName>.+)\]', header)
|
||||
if not m:
|
||||
pyfalog.warning('service.port.eft.importEft: corrupted fit header')
|
||||
raise EftImportError
|
||||
@@ -969,7 +975,7 @@ def lineIter(text):
|
||||
def parseAdditions(text, mutaData=None):
|
||||
items = []
|
||||
sMkt = Market.getInstance()
|
||||
pattern = '^(?P<typeName>{}+?)( x(?P<amount>\d+?))?(\s*\[(?P<mutaref>\d+?)\])?$'.format(NAME_CHARS)
|
||||
pattern = r'^(?P<typeName>{}+?)( x(?P<amount>\d+?))?(\s*\[(?P<mutaref>\d+?)\])?$'.format(NAME_CHARS)
|
||||
for line in lineIter(text):
|
||||
m = re.match(pattern, line)
|
||||
if not m:
|
||||
@@ -992,7 +998,7 @@ def isValidDroneImport(text):
|
||||
lines = list(lineIter(text))
|
||||
mutaData = importGetMutationData(lines)
|
||||
text = '\n'.join(lines)
|
||||
pattern = 'x\d+(\s*\[\d+\])?$'
|
||||
pattern = r'x\d+(\s*\[\d+\])?$'
|
||||
for line in lineIter(text):
|
||||
if not re.search(pattern, line):
|
||||
return False, ()
|
||||
@@ -1006,7 +1012,7 @@ def isValidDroneImport(text):
|
||||
|
||||
|
||||
def isValidFighterImport(text):
|
||||
pattern = 'x\d+$'
|
||||
pattern = r'x\d+$'
|
||||
for line in lineIter(text):
|
||||
if not re.search(pattern, line):
|
||||
return False, ()
|
||||
@@ -1020,7 +1026,7 @@ def isValidFighterImport(text):
|
||||
|
||||
|
||||
def isValidCargoImport(text):
|
||||
pattern = 'x\d+$'
|
||||
pattern = r'x\d+$'
|
||||
for line in lineIter(text):
|
||||
if not re.search(pattern, line):
|
||||
return False, ()
|
||||
@@ -1034,7 +1040,7 @@ def isValidCargoImport(text):
|
||||
|
||||
|
||||
def isValidImplantImport(text):
|
||||
pattern = 'x\d+$'
|
||||
pattern = r'x\d+$'
|
||||
for line in lineIter(text):
|
||||
if re.search(pattern, line):
|
||||
return False, ()
|
||||
@@ -1048,7 +1054,7 @@ def isValidImplantImport(text):
|
||||
|
||||
|
||||
def isValidBoosterImport(text):
|
||||
pattern = 'x\d+$'
|
||||
pattern = r'x\d+$'
|
||||
for line in lineIter(text):
|
||||
if re.search(pattern, line):
|
||||
return False, ()
|
||||
|
||||
@@ -56,7 +56,7 @@ INV_FLAG_DRONEBAY = 87
|
||||
INV_FLAG_FIGHTER = 158
|
||||
|
||||
|
||||
def exportESI(ofit, exportCharges, callback):
|
||||
def exportESI(ofit, exportCharges, exportImplants, exportBoosters, callback):
|
||||
# A few notes:
|
||||
# max fit name length is 50 characters
|
||||
# Most keys are created simply because they are required, but bogus data is okay
|
||||
@@ -133,6 +133,22 @@ def exportESI(ofit, exportCharges, callback):
|
||||
item['type_id'] = fighter.item.ID
|
||||
fit['items'].append(item)
|
||||
|
||||
if exportImplants:
|
||||
for implant in ofit.implants:
|
||||
item = nested_dict()
|
||||
item['flag'] = INV_FLAG_CARGOBAY
|
||||
item['quantity'] = 1
|
||||
item['type_id'] = implant.item.ID
|
||||
fit['items'].append(item)
|
||||
|
||||
if exportBoosters:
|
||||
for booster in ofit.boosters:
|
||||
item = nested_dict()
|
||||
item['flag'] = INV_FLAG_CARGOBAY
|
||||
item['quantity'] = 1
|
||||
item['type_id'] = booster.item.ID
|
||||
fit['items'].append(item)
|
||||
|
||||
if len(fit['items']) == 0:
|
||||
raise ESIExportException("Cannot export fitting: module list cannot be empty.")
|
||||
|
||||
|
||||
@@ -28,17 +28,20 @@ from service.esiAccess import EsiAccess
|
||||
|
||||
def renderMutant(mutant, firstPrefix='', prefix=''):
|
||||
exportLines = []
|
||||
exportLines.append('{}{}'.format(firstPrefix, mutant.baseItem.name))
|
||||
exportLines.append('{}{}'.format(prefix, mutant.mutaplasmid.item.name))
|
||||
exportLines.append('{}{}'.format(prefix, renderMutantAttrs(mutant)))
|
||||
return '\n'.join(exportLines)
|
||||
|
||||
|
||||
def renderMutantAttrs(mutant):
|
||||
mutatedAttrs = {}
|
||||
for attrID, mutator in mutant.mutators.items():
|
||||
attrName = getAttributeInfo(attrID).name
|
||||
mutatedAttrs[attrName] = mutator.value
|
||||
exportLines.append('{}{}'.format(firstPrefix, mutant.baseItem.name))
|
||||
exportLines.append('{}{}'.format(prefix, mutant.mutaplasmid.item.name))
|
||||
customAttrsLine = ', '.join(
|
||||
return ', '.join(
|
||||
'{} {}'.format(a, floatUnerr(mutatedAttrs[a]))
|
||||
for a in sorted(mutatedAttrs))
|
||||
exportLines.append('{}{}'.format(prefix, customAttrsLine))
|
||||
return '\n'.join(exportLines)
|
||||
|
||||
|
||||
def parseMutant(lines):
|
||||
@@ -64,8 +67,13 @@ def parseMutant(lines):
|
||||
mutationsLine = lines[2]
|
||||
except IndexError:
|
||||
return baseItem, mutaplasmidItem, {}
|
||||
mutations = parseMutantAttrs(mutationsLine)
|
||||
return baseItem, mutaplasmidItem, mutations
|
||||
|
||||
|
||||
def parseMutantAttrs(line):
|
||||
mutations = {}
|
||||
pairs = [p.strip() for p in mutationsLine.split(',')]
|
||||
pairs = [p.strip() for p in line.split(',')]
|
||||
for pair in pairs:
|
||||
try:
|
||||
attrName, value = pair.split(' ')
|
||||
@@ -79,7 +87,7 @@ def parseMutant(lines):
|
||||
if attrInfo is None:
|
||||
continue
|
||||
mutations[attrInfo.ID] = value
|
||||
return baseItem, mutaplasmidItem, mutations
|
||||
return mutations
|
||||
|
||||
|
||||
def parseDynamicItemString(text):
|
||||
|
||||
@@ -38,7 +38,6 @@ from service.port.eft import (
|
||||
isValidImplantImport, isValidBoosterImport)
|
||||
from service.port.esi import exportESI, importESI
|
||||
from service.port.multibuy import exportMultiBuy
|
||||
from service.port.shared import IPortUser, UserCancelException, processing_notify
|
||||
from service.port.shipstats import exportFitStats
|
||||
from service.port.xml import importXml, exportXml
|
||||
from service.port.muta import parseMutant, parseDynamicItemString, fetchDynamicItem
|
||||
@@ -73,53 +72,48 @@ class Port:
|
||||
return cls.__tag_replace_flag
|
||||
|
||||
@staticmethod
|
||||
def backupFits(path, iportuser):
|
||||
def backupFits(path, progress):
|
||||
pyfalog.debug("Starting backup fits thread.")
|
||||
|
||||
def backupFitsWorkerFunc(path, iportuser):
|
||||
success = True
|
||||
def backupFitsWorkerFunc(path, progress):
|
||||
try:
|
||||
iportuser.on_port_process_start()
|
||||
backedUpFits = Port.exportXml(svcFit.getInstance().getAllFits(), iportuser)
|
||||
backupFile = open(path, "w", encoding="utf-8")
|
||||
backupFile.write(backedUpFits)
|
||||
backupFile.close()
|
||||
except UserCancelException:
|
||||
success = False
|
||||
# Send done signal to GUI
|
||||
# wx.CallAfter(callback, -1, "Done.")
|
||||
flag = IPortUser.ID_ERROR if not success else IPortUser.ID_DONE
|
||||
iportuser.on_port_processing(IPortUser.PROCESS_EXPORT | flag,
|
||||
"User canceled or some error occurrence." if not success else "Done.")
|
||||
backedUpFits = Port.exportXml(svcFit.getInstance().getAllFits(), progress)
|
||||
if backedUpFits:
|
||||
progress.message = f'writing {path}'
|
||||
backupFile = open(path, "w", encoding="utf-8")
|
||||
backupFile.write(backedUpFits)
|
||||
backupFile.close()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception as e:
|
||||
progress.error = f'{e}'
|
||||
finally:
|
||||
progress.current += 1
|
||||
progress.workerWorking = False
|
||||
|
||||
threading.Thread(
|
||||
target=backupFitsWorkerFunc,
|
||||
args=(path, iportuser)
|
||||
args=(path, progress)
|
||||
).start()
|
||||
|
||||
@staticmethod
|
||||
def importFitsThreaded(paths, iportuser):
|
||||
# type: (tuple, IPortUser) -> None
|
||||
def importFitsThreaded(paths, progress):
|
||||
"""
|
||||
:param paths: fits data file path list.
|
||||
:param iportuser: IPortUser implemented class.
|
||||
:rtype: None
|
||||
"""
|
||||
pyfalog.debug("Starting import fits thread.")
|
||||
|
||||
def importFitsFromFileWorkerFunc(paths, iportuser):
|
||||
iportuser.on_port_process_start()
|
||||
success, result = Port.importFitFromFiles(paths, iportuser)
|
||||
flag = IPortUser.ID_ERROR if not success else IPortUser.ID_DONE
|
||||
iportuser.on_port_processing(IPortUser.PROCESS_IMPORT | flag, result)
|
||||
def importFitsFromFileWorkerFunc(paths, progress):
|
||||
Port.importFitFromFiles(paths, progress)
|
||||
|
||||
threading.Thread(
|
||||
target=importFitsFromFileWorkerFunc,
|
||||
args=(paths, iportuser)
|
||||
args=(paths, progress)
|
||||
).start()
|
||||
|
||||
@staticmethod
|
||||
def importFitFromFiles(paths, iportuser=None):
|
||||
def importFitFromFiles(paths, progress=None):
|
||||
"""
|
||||
Imports fits from file(s). First processes all provided paths and stores
|
||||
assembled fits into a list. This allows us to call back to the GUI as
|
||||
@@ -132,11 +126,13 @@ class Port:
|
||||
fit_list = []
|
||||
try:
|
||||
for path in paths:
|
||||
if iportuser: # Pulse
|
||||
if progress:
|
||||
if progress and progress.userCancelled:
|
||||
progress.workerWorking = False
|
||||
return False, "Cancelled by user"
|
||||
msg = "Processing file:\n%s" % path
|
||||
progress.message = msg
|
||||
pyfalog.debug(msg)
|
||||
processing_notify(iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, msg)
|
||||
# wx.CallAfter(callback, 1, msg)
|
||||
|
||||
with open(path, "rb") as file_:
|
||||
srcString = file_.read()
|
||||
@@ -148,15 +144,21 @@ class Port:
|
||||
continue
|
||||
|
||||
try:
|
||||
importType, makesNewFits, fitsImport = Port.importAuto(srcString, path, iportuser=iportuser)
|
||||
importType, makesNewFits, fitsImport = Port.importAuto(srcString, path, progress=progress)
|
||||
fit_list += fitsImport
|
||||
except xml.parsers.expat.ExpatError:
|
||||
pyfalog.warning("Malformed XML in:\n{0}", path)
|
||||
return False, "Malformed XML in %s" % path
|
||||
msg = "Malformed XML in %s" % path
|
||||
if progress:
|
||||
progress.error = msg
|
||||
progress.workerWorking = False
|
||||
return False, msg
|
||||
|
||||
# IDs = [] # NOTE: what use for IDs?
|
||||
numFits = len(fit_list)
|
||||
for idx, fit in enumerate(fit_list):
|
||||
if progress and progress.userCancelled:
|
||||
progress.workerWorking = False
|
||||
return False, "Cancelled by user"
|
||||
# Set some more fit attributes and save
|
||||
fit.character = sFit.character
|
||||
fit.damagePattern = sFit.pattern
|
||||
@@ -168,25 +170,23 @@ class Port:
|
||||
fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT
|
||||
db.save(fit)
|
||||
# IDs.append(fit.ID)
|
||||
if iportuser: # Pulse
|
||||
if progress:
|
||||
pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", idx + 1, numFits)
|
||||
processing_notify(
|
||||
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
|
||||
"Processing complete, saving fits to database\n(%d/%d) %s" % (idx + 1, numFits, fit.ship.name)
|
||||
)
|
||||
|
||||
except UserCancelException:
|
||||
return False, "Processing has been canceled.\n"
|
||||
progress.message = "Processing complete, saving fits to database\n(%d/%d) %s" % (idx + 1, numFits, fit.ship.name)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception as e:
|
||||
pyfalog.critical("Unknown exception processing: {0}", path)
|
||||
pyfalog.critical("Unknown exception processing: {0}", paths)
|
||||
pyfalog.critical(e)
|
||||
# TypeError: not all arguments converted during string formatting
|
||||
# return False, "Unknown Error while processing {0}" % path
|
||||
if progress:
|
||||
progress.error = f'{e}'
|
||||
progress.workerWorking = False
|
||||
return False, "Unknown error while processing {}\n\n Error: {} {}".format(
|
||||
path, type(e).__name__, getattr(e, 'message', ''))
|
||||
paths, type(e).__name__, getattr(e, 'message', ''))
|
||||
|
||||
if progress:
|
||||
progress.cbArgs.append(fit_list[:])
|
||||
progress.workerWorking = False
|
||||
return True, fit_list
|
||||
|
||||
@staticmethod
|
||||
@@ -211,8 +211,7 @@ class Port:
|
||||
return importType, importData
|
||||
|
||||
@classmethod
|
||||
def importAuto(cls, string, path=None, activeFit=None, iportuser=None):
|
||||
# type: (Port, str, str, object, IPortUser) -> object
|
||||
def importAuto(cls, string, path=None, activeFit=None, progress=None):
|
||||
lines = string.splitlines()
|
||||
# Get first line and strip space symbols of it to avoid possible detection errors
|
||||
firstLine = ''
|
||||
@@ -224,7 +223,7 @@ class Port:
|
||||
|
||||
# If XML-style start of tag encountered, detect as XML
|
||||
if re.search(RE_XML_START, firstLine):
|
||||
return "XML", True, cls.importXml(string, iportuser)
|
||||
return "XML", True, cls.importXml(string, progress)
|
||||
|
||||
# If JSON-style start, parse as CREST/JSON
|
||||
if firstLine[0] == '{':
|
||||
@@ -232,21 +231,21 @@ class Port:
|
||||
|
||||
# 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("^\s*\[.*\]", firstLine) and path is not None:
|
||||
if re.match(r"^\s*\[.*\]", firstLine) and path is not None:
|
||||
filename = os.path.split(path)[1]
|
||||
shipName = filename.rsplit('.')[0]
|
||||
return "EFT Config", True, cls.importEftCfg(shipName, lines, iportuser)
|
||||
return "EFT Config", True, cls.importEftCfg(shipName, lines, progress)
|
||||
|
||||
# If no file is specified and there's comma between brackets,
|
||||
# consider that we have [ship, setup name] and detect like eft export format
|
||||
if re.match("^\s*\[.*,.*\]", firstLine):
|
||||
if re.match(r"^\s*\[.*,.*\]", firstLine):
|
||||
return "EFT", True, (cls.importEft(lines),)
|
||||
|
||||
# Check if string is in DNA format
|
||||
dnaPattern = "\d+(:\d+(;\d+))*::"
|
||||
dnaPattern = r"\d+(:\d+(;\d+))*::"
|
||||
if re.match(dnaPattern, firstLine):
|
||||
return "DNA", True, (cls.importDna(string),)
|
||||
dnaChatPattern = "<url=fitting:(?P<dna>{})>(?P<fitName>[^<>]+)</url>".format(dnaPattern)
|
||||
dnaChatPattern = r"<url=fitting:(?P<dna>{})>(?P<fitName>[^<>]+)</url>".format(dnaPattern)
|
||||
m = re.search(dnaChatPattern, firstLine)
|
||||
if m:
|
||||
return "DNA", True, (cls.importDna(m.group("dna"), fitName=m.group("fitName")),)
|
||||
@@ -297,8 +296,8 @@ class Port:
|
||||
return importEft(lines)
|
||||
|
||||
@staticmethod
|
||||
def importEftCfg(shipname, lines, iportuser=None):
|
||||
return importEftCfg(shipname, lines, iportuser)
|
||||
def importEftCfg(shipname, lines, progress=None):
|
||||
return importEftCfg(shipname, lines, progress)
|
||||
|
||||
@classmethod
|
||||
def exportEft(cls, fit, options, callback=None):
|
||||
@@ -323,17 +322,17 @@ class Port:
|
||||
return importESI(string)
|
||||
|
||||
@staticmethod
|
||||
def exportESI(fit, exportCharges, callback=None):
|
||||
return exportESI(fit, exportCharges, callback=callback)
|
||||
def exportESI(fit, exportCharges, exportImplants, exportBoosters, callback=None):
|
||||
return exportESI(fit, exportCharges, exportImplants, exportBoosters, callback=callback)
|
||||
|
||||
# XML-related methods
|
||||
@staticmethod
|
||||
def importXml(text, iportuser=None):
|
||||
return importXml(text, iportuser)
|
||||
def importXml(text, progress=None):
|
||||
return importXml(text, progress)
|
||||
|
||||
@staticmethod
|
||||
def exportXml(fits, iportuser=None, callback=None):
|
||||
return exportXml(fits, iportuser, callback=callback)
|
||||
def exportXml(fits, progress=None, callback=None):
|
||||
return exportXml(fits, progress, callback=callback)
|
||||
|
||||
# Multibuy-related methods
|
||||
@staticmethod
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
# =============================================================================
|
||||
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
from service.market import Market
|
||||
@@ -28,55 +26,6 @@ from service.market import Market
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
class UserCancelException(Exception):
|
||||
"""when user cancel on port processing."""
|
||||
pass
|
||||
|
||||
|
||||
class IPortUser(metaclass=ABCMeta):
|
||||
|
||||
ID_PULSE = 1
|
||||
# Pulse the progress bar
|
||||
ID_UPDATE = ID_PULSE << 1
|
||||
# Replace message with data: update messate
|
||||
ID_DONE = ID_PULSE << 2
|
||||
# open fits: import process done
|
||||
ID_ERROR = ID_PULSE << 3
|
||||
# display error: raise some error
|
||||
|
||||
PROCESS_IMPORT = ID_PULSE << 4
|
||||
# means import process.
|
||||
PROCESS_EXPORT = ID_PULSE << 5
|
||||
# means import process.
|
||||
|
||||
@abstractmethod
|
||||
def on_port_processing(self, action, data=None):
|
||||
"""
|
||||
While importing fits from file, the logic calls back to this function to
|
||||
update progress bar to show activity. XML files can contain multiple
|
||||
ships with multiple fits, whereas EFT cfg files contain many fits of
|
||||
a single ship. When iterating through the files, we update the message
|
||||
when we start a new file, and then Pulse the progress bar with every fit
|
||||
that is processed.
|
||||
|
||||
action : a flag that lets us know how to deal with :data
|
||||
None: Pulse the progress bar
|
||||
1: Replace message with data
|
||||
other: Close dialog and handle based on :action (-1 open fits, -2 display error)
|
||||
"""
|
||||
|
||||
"""return: True is continue process, False is cancel."""
|
||||
pass
|
||||
|
||||
def on_port_process_start(self):
|
||||
pass
|
||||
|
||||
|
||||
def processing_notify(iportuser, flag, data):
|
||||
if not iportuser.on_port_processing(flag, data):
|
||||
raise UserCancelException
|
||||
|
||||
|
||||
def fetchItem(typeName, eagerCat=False):
|
||||
sMkt = Market.getInstance()
|
||||
eager = 'group.category' if eagerCat else None
|
||||
|
||||
@@ -24,6 +24,7 @@ import xml.parsers.expat
|
||||
from logbook import Logger
|
||||
|
||||
from eos.const import FittingModuleState, FittingSlot
|
||||
from eos.db import getDynamicItem
|
||||
from eos.saveddata.cargo import Cargo
|
||||
from eos.saveddata.citadel import Citadel
|
||||
from eos.saveddata.drone import Drone
|
||||
@@ -34,7 +35,8 @@ from eos.saveddata.ship import Ship
|
||||
from gui.fitCommands.helpers import activeStateLimit
|
||||
from service.fit import Fit as svcFit
|
||||
from service.market import Market
|
||||
from service.port.shared import IPortUser, processing_notify
|
||||
from service.port.muta import renderMutantAttrs, parseMutantAttrs
|
||||
from service.port.shared import fetchItem
|
||||
from utils.strfunctions import replace_ltgt, sequential_rep
|
||||
|
||||
|
||||
@@ -115,7 +117,7 @@ def _resolve_ship(fitting, sMkt, b_localized):
|
||||
|
||||
def _resolve_module(hardware, sMkt, b_localized):
|
||||
# type: (xml.dom.minidom.Element, service.market.Market, bool) -> eos.saveddata.module.Module
|
||||
moduleName = hardware.getAttribute("type")
|
||||
moduleName = hardware.getAttribute("base_type") or hardware.getAttribute("type")
|
||||
emergency = None
|
||||
if b_localized:
|
||||
try:
|
||||
@@ -142,12 +144,18 @@ def _resolve_module(hardware, sMkt, b_localized):
|
||||
must_retry = True
|
||||
if not must_retry:
|
||||
break
|
||||
return item
|
||||
|
||||
mutaplasmidName = hardware.getAttribute("mutaplasmid")
|
||||
mutaplasmidItem = fetchItem(mutaplasmidName) if mutaplasmidName else None
|
||||
|
||||
mutatedAttrsText = hardware.getAttribute("mutated_attrs")
|
||||
mutatedAttrs = parseMutantAttrs(mutatedAttrsText) if mutatedAttrsText else None
|
||||
|
||||
return item, mutaplasmidItem, mutatedAttrs
|
||||
|
||||
|
||||
def importXml(text, iportuser):
|
||||
def importXml(text, progress):
|
||||
from .port import Port
|
||||
# type: (str, IPortUser) -> list[eos.saveddata.fit.Fit]
|
||||
sMkt = Market.getInstance()
|
||||
doc = xml.dom.minidom.parseString(text)
|
||||
# NOTE:
|
||||
@@ -160,6 +168,9 @@ def importXml(text, iportuser):
|
||||
failed = 0
|
||||
|
||||
for fitting in fittings:
|
||||
if progress and progress.userCancelled:
|
||||
return []
|
||||
|
||||
try:
|
||||
fitobj = _resolve_ship(fitting, sMkt, b_localized)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
@@ -185,12 +196,25 @@ def importXml(text, iportuser):
|
||||
moduleList = []
|
||||
for hardware in hardwares:
|
||||
try:
|
||||
item = _resolve_module(hardware, sMkt, b_localized)
|
||||
item, mutaItem, mutaAttrs = _resolve_module(hardware, sMkt, b_localized)
|
||||
if not item or not item.published:
|
||||
continue
|
||||
|
||||
if item.category.name == "Drone":
|
||||
d = Drone(item)
|
||||
d = None
|
||||
if mutaItem:
|
||||
mutaplasmid = getDynamicItem(mutaItem.ID)
|
||||
if mutaplasmid:
|
||||
try:
|
||||
d = Drone(mutaplasmid.resultingItem, item, mutaplasmid)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
for attrID, mutator in d.mutators.items():
|
||||
if attrID in mutaAttrs:
|
||||
mutator.value = mutaAttrs[attrID]
|
||||
if d is None:
|
||||
d = Drone(item)
|
||||
d.amount = int(hardware.getAttribute("qty"))
|
||||
fitobj.drones.append(d)
|
||||
elif item.category.name == "Fighter":
|
||||
@@ -205,8 +229,21 @@ def importXml(text, iportuser):
|
||||
c.amount = int(hardware.getAttribute("qty"))
|
||||
fitobj.cargo.append(c)
|
||||
else:
|
||||
m = None
|
||||
try:
|
||||
m = Module(item)
|
||||
if mutaItem:
|
||||
mutaplasmid = getDynamicItem(mutaItem.ID)
|
||||
if mutaplasmid:
|
||||
try:
|
||||
m = Module(mutaplasmid.resultingItem, item, mutaplasmid)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
for attrID, mutator in m.mutators.items():
|
||||
if attrID in mutaAttrs:
|
||||
mutator.value = mutaAttrs[attrID]
|
||||
if m is None:
|
||||
m = Module(item)
|
||||
# When item can't be added to any slot (unknown item or just charge), ignore it
|
||||
except ValueError:
|
||||
pyfalog.warning("item can't be added to any slot (unknown item or just charge), ignore it")
|
||||
@@ -237,16 +274,13 @@ def importXml(text, iportuser):
|
||||
fitobj.modules.append(module)
|
||||
|
||||
fit_list.append(fitobj)
|
||||
if iportuser: # NOTE: Send current processing status
|
||||
processing_notify(
|
||||
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
|
||||
"Processing %s\n%s" % (fitobj.ship.name, fitobj.name)
|
||||
)
|
||||
if progress:
|
||||
progress.message = "Processing %s\n%s" % (fitobj.ship.name, fitobj.name)
|
||||
|
||||
return fit_list
|
||||
|
||||
|
||||
def exportXml(fits, iportuser, callback):
|
||||
def exportXml(fits, progress, callback):
|
||||
doc = xml.dom.minidom.Document()
|
||||
fittings = doc.createElement("fittings")
|
||||
# fit count
|
||||
@@ -254,7 +288,18 @@ def exportXml(fits, iportuser, callback):
|
||||
fittings.setAttribute("count", "%s" % fit_count)
|
||||
doc.appendChild(fittings)
|
||||
|
||||
def addMutantAttributes(node, mutant):
|
||||
node.setAttribute("base_type", mutant.baseItem.name)
|
||||
node.setAttribute("mutaplasmid", mutant.mutaplasmid.item.name)
|
||||
node.setAttribute("mutated_attrs", renderMutantAttrs(mutant))
|
||||
|
||||
for i, fit in enumerate(fits):
|
||||
if progress:
|
||||
if progress.userCancelled:
|
||||
return None
|
||||
processedFits = i + 1
|
||||
progress.current = processedFits
|
||||
progress.message = "converting to xml (%s/%s) %s" % (processedFits, fit_count, fit.ship.name)
|
||||
try:
|
||||
fitting = doc.createElement("fitting")
|
||||
fitting.setAttribute("name", fit.name)
|
||||
@@ -303,6 +348,9 @@ def exportXml(fits, iportuser, callback):
|
||||
slotName = FittingSlot(slot).name.lower()
|
||||
slotName = slotName if slotName != "high" else "hi"
|
||||
hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId))
|
||||
if module.isMutated:
|
||||
addMutantAttributes(hardware, module)
|
||||
|
||||
fitting.appendChild(hardware)
|
||||
|
||||
if module.charge:
|
||||
@@ -316,6 +364,9 @@ def exportXml(fits, iportuser, callback):
|
||||
hardware.setAttribute("qty", "%d" % drone.amount)
|
||||
hardware.setAttribute("slot", "drone bay")
|
||||
hardware.setAttribute("type", drone.item.name)
|
||||
if drone.isMutated:
|
||||
addMutantAttributes(hardware, drone)
|
||||
|
||||
fitting.appendChild(hardware)
|
||||
|
||||
for fighter in fit.fighters:
|
||||
@@ -341,12 +392,6 @@ def exportXml(fits, iportuser, callback):
|
||||
except Exception as e:
|
||||
pyfalog.error("Failed on fitID: %d, message: %s" % e.message)
|
||||
continue
|
||||
finally:
|
||||
if iportuser:
|
||||
processing_notify(
|
||||
iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE,
|
||||
(i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name))
|
||||
)
|
||||
text = doc.toprettyxml()
|
||||
|
||||
if callback:
|
||||
|
||||
@@ -55,7 +55,7 @@ def version_precheck():
|
||||
|
||||
try:
|
||||
import sqlalchemy
|
||||
saMatch = re.match("([0-9]+).([0-9]+).([0-9]+)(([b\.])([0-9]+))?", sqlalchemy.__version__)
|
||||
saMatch = re.match(r"([0-9]+).([0-9]+).([0-9]+)(([b\.])([0-9]+))?", sqlalchemy.__version__)
|
||||
version_block += "\nSQLAlchemy version: {}".format(sqlalchemy.__version__)
|
||||
|
||||
if (int(saMatch.group(1)), int(saMatch.group(2)), int(saMatch.group(3))) < (1, 0, 5):
|
||||
|
||||
@@ -276,4 +276,4 @@ class PriceWorkerThread(threading.Thread):
|
||||
|
||||
|
||||
# Import market sources only to initialize price source modules, they register on their own
|
||||
from service.marketSources import evemarketer, evemarketdata, evepraisal, fuzzwork, cevemarket # noqa: E402
|
||||
from service.marketSources import evemarketdata, fuzzwork, cevemarket, evetycoon # noqa: E402
|
||||
|
||||
@@ -222,9 +222,9 @@ class NetworkSettings:
|
||||
if prefix not in proxydict:
|
||||
continue
|
||||
proxyline = proxydict[prefix]
|
||||
proto = "{0}://".format(prefix)
|
||||
if proxyline[:len(proto)] == proto:
|
||||
proxyline = proxyline[len(proto):]
|
||||
proto_pos = proxyline.find('://')
|
||||
if proto_pos != -1:
|
||||
proxyline = proxyline[proto_pos+3:]
|
||||
# sometimes proxyline contains "user:password@" section before proxy address
|
||||
# remove it if present, so later split by ":" works
|
||||
if '@' in proxyline:
|
||||
@@ -376,6 +376,8 @@ class EsiSettings:
|
||||
"timeout": 60,
|
||||
"server": "Tranquility",
|
||||
"exportCharges": True,
|
||||
"exportImplants": True,
|
||||
"exportBoosters": True,
|
||||
"enforceJwtExpiration": True
|
||||
}
|
||||
|
||||
@@ -390,6 +392,9 @@ class EsiSettings:
|
||||
def set(self, type, value):
|
||||
self.settings[type] = value
|
||||
|
||||
def keys(self):
|
||||
return config.supported_servers.keys()
|
||||
|
||||
|
||||
class StatViewSettings:
|
||||
_instance = None
|
||||
@@ -558,7 +563,7 @@ class LocaleSettings:
|
||||
with open(os.path.join(config.pyfaPath, 'locale', 'progress.json'), "r") as f:
|
||||
self.progress_data = json.load(f)
|
||||
except FileNotFoundError:
|
||||
self.progress_data = None
|
||||
self.progress_data = {}
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
@@ -567,14 +572,14 @@ class LocaleSettings:
|
||||
return cls._instance
|
||||
|
||||
def get_progress(self, lang):
|
||||
if self.progress_data is None:
|
||||
if not self.progress_data:
|
||||
return None
|
||||
if lang == self.defaults['locale']:
|
||||
return None
|
||||
return self.progress_data[lang]
|
||||
return self.progress_data.get(lang)
|
||||
|
||||
@classmethod
|
||||
def supported_langauges(cls):
|
||||
def supported_languages(cls):
|
||||
"""Requires the application to be initialized, otherwise wx.Translation isn't set."""
|
||||
pyfalog.info(f'using "{config.CATALOG}" to fetch languages, relatively base path "{os.getcwd()}"')
|
||||
return {x: wx.Locale.FindLanguageInfo(x) for x in wx.Translations.Get().GetAvailableTranslations(config.CATALOG)}
|
||||
@@ -589,6 +594,6 @@ class LocaleSettings:
|
||||
return val if val != self.defaults['eos_locale'] else self.settings['locale'].split("_")[0]
|
||||
|
||||
def set(self, key, value):
|
||||
if key == 'locale' and value not in self.supported_langauges():
|
||||
if key == 'locale' and value not in self.supported_languages():
|
||||
self.settings[key] = self.DEFAULT
|
||||
self.settings[key] = value
|
||||
|
||||
Reference in New Issue
Block a user