Compare commits

...

4 Commits

23 changed files with 249 additions and 98 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
.venv
.git
dist
build
__pycache__
*.pyc
.pytest_cache
*.zip
*.spec
imgs
dist_assets

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements-server.txt .
RUN pip install --no-cache-dir -r requirements-server.txt
COPY . .
EXPOSE 9123
CMD ["python", "pyfa_server.py"]

View File

@@ -23,10 +23,38 @@ rm -rf build dist
echo "Building binary with PyInstaller..." echo "Building binary with PyInstaller..."
uv run pyinstaller pyfa.spec uv run pyinstaller pyfa.spec
# Headless CLI exe (console) into main dist folder # Sim server exe (console) into main dist folder
if [ -f dist/pyfa_headless/pyfa-headless.exe ]; then if [ -f dist/pyfa_server/pyfa-server.exe ]; then
cp dist/pyfa_headless/pyfa-headless.exe dist/pyfa/ cp dist/pyfa_server/pyfa-server.exe dist/pyfa/
fi
# Docker image (Python server)
DOCKER_REPO="${DOCKER_REPO:-docker.site.quack-lab.dev}"
IMAGE_NAME="${IMAGE_NAME:-pyfa-server}"
COMMIT_SHA=$(git rev-parse --short HEAD)
IMAGE_BASE="${DOCKER_REPO}/${IMAGE_NAME}"
echo ""
echo "Building Docker image..."
docker build -t "${IMAGE_BASE}:${COMMIT_SHA}" .
docker tag "${IMAGE_BASE}:${COMMIT_SHA}" "${IMAGE_BASE}:latest"
TAGS=$(git tag --points-at HEAD 2>/dev/null || true)
if [ -n "$TAGS" ]; then
while IFS= read -r tag; do
[ -n "$tag" ] && docker tag "${IMAGE_BASE}:${COMMIT_SHA}" "${IMAGE_BASE}:${tag}"
done <<< "$TAGS"
fi fi
echo "" echo ""
echo "Build complete! dist/pyfa/pyfa.exe (GUI), dist/pyfa/pyfa-headless.exe (HTTP server POST /simulate :9123)" echo "Build complete! dist/pyfa/pyfa.exe (GUI), dist/pyfa/pyfa-server.exe (POST /simulate :9123)"
echo ""
echo "Docker image built as:"
echo " - ${IMAGE_BASE}:${COMMIT_SHA}"
echo " - ${IMAGE_BASE}:latest"
if [ -n "$TAGS" ]; then
while IFS= read -r tag; do
[ -n "$tag" ] && echo " - ${IMAGE_BASE}:${tag}"
done <<< "$TAGS"
fi

View File

@@ -1,7 +1,6 @@
import os import os
import sys import sys
import yaml import yaml
import wx
from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \ from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \
StreamHandler, TimedRotatingFileHandler, WARNING StreamHandler, TimedRotatingFileHandler, WARNING
@@ -67,20 +66,34 @@ LOGLEVEL_MAP = {
CATALOG = 'lang' CATALOG = 'lang'
slotColourMapDark = { def get_slotColourMapDark():
FittingSlot.LOW: wx.Colour(44, 36, 19), # yellow = low slots 24/13 import wx
FittingSlot.MED: wx.Colour(28, 39, 51), # blue = mid slots 8.1/9.5 return {
FittingSlot.HIGH: wx.Colour(53, 31, 34), # red = high slots 6.5/11.5 FittingSlot.LOW: wx.Colour(44, 36, 19), # yellow = low slots 24/13
FittingSlot.RIG: '', FittingSlot.MED: wx.Colour(28, 39, 51), # blue = mid slots 8.1/9.5
FittingSlot.SUBSYSTEM: ''} FittingSlot.HIGH: wx.Colour(53, 31, 34), # red = high slots 6.5/11.5
errColorDark = wx.Colour(70, 20, 20) FittingSlot.RIG: '',
slotColourMap = { FittingSlot.SUBSYSTEM: ''}
FittingSlot.LOW: wx.Colour(250, 235, 204), # yellow = low slots
FittingSlot.MED: wx.Colour(188, 215, 241), # blue = mid slots
FittingSlot.HIGH: wx.Colour(235, 204, 209), # red = high slots def get_errColorDark():
FittingSlot.RIG: '', import wx
FittingSlot.SUBSYSTEM: ''} return wx.Colour(70, 20, 20)
errColor = wx.Colour(204, 51, 51)
def get_slotColourMap():
import wx
return {
FittingSlot.LOW: wx.Colour(250, 235, 204), # yellow = low slots
FittingSlot.MED: wx.Colour(188, 215, 241), # blue = mid slots
FittingSlot.HIGH: wx.Colour(235, 204, 209), # red = high slots
FittingSlot.RIG: '',
FittingSlot.SUBSYSTEM: ''}
def get_errColor():
import wx
return wx.Colour(204, 51, 51)
def getClientSecret(): def getClientSecret():

5
docker-compose.yml Normal file
View File

@@ -0,0 +1,5 @@
services:
pyfa-server:
image: docker.site.quack-lab.dev/pyfa-server:latest
ports:
- "9123:9123"

View File

@@ -3,7 +3,7 @@ from logbook import Logger
import gui.builtinMarketBrowser.pfSearchBox as SBox import gui.builtinMarketBrowser.pfSearchBox as SBox
import gui.globalEvents as GE import gui.globalEvents as GE
from config import slotColourMap, slotColourMapDark from config import get_slotColourMap, get_slotColourMapDark
from eos.saveddata.module import Module from eos.saveddata.module import Module
from eos.const import FittingSlot from eos.const import FittingSlot
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT
@@ -412,7 +412,7 @@ class ItemView(Display):
def columnBackground(self, colItem, item): def columnBackground(self, colItem, item):
if self.sFit.serviceFittingOptions["colorFitBySlot"]: if self.sFit.serviceFittingOptions["colorFitBySlot"]:
colorMap = slotColourMapDark if isDark() else slotColourMap colorMap = get_slotColourMapDark() if isDark() else get_slotColourMap()
return colorMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour() return colorMap.get(Module.calculateSlot(item)) or self.GetBackgroundColour()
else: else:
return self.GetBackgroundColour() return self.GetBackgroundColour()

View File

@@ -42,7 +42,7 @@ from gui.utils.staticHelpers import DragDropHelper
from gui.utils.dark import isDark from gui.utils.dark import isDark
from service.fit import Fit from service.fit import Fit
from service.market import Market from service.market import Market
from config import slotColourMap, slotColourMapDark, errColor, errColorDark from config import get_slotColourMap, get_slotColourMapDark, get_errColor, get_errColorDark
from gui.fitCommands.helpers import getSimilarModPositions from gui.fitCommands.helpers import getSimilarModPositions
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -766,9 +766,9 @@ class FittingView(d.Display):
def slotColour(self, slot): def slotColour(self, slot):
if isDark(): if isDark():
return slotColourMapDark.get(slot) or self.GetBackgroundColour() return get_slotColourMapDark().get(slot) or self.GetBackgroundColour()
else: else:
return slotColourMap.get(slot) or self.GetBackgroundColour() return get_slotColourMap().get(slot) or self.GetBackgroundColour()
def refresh(self, stuff): def refresh(self, stuff):
""" """
@@ -812,7 +812,7 @@ class FittingView(d.Display):
hasRestrictionOverriden = not hasRestrictionOverriden hasRestrictionOverriden = not hasRestrictionOverriden
if slotMap[mod.slot] or hasRestrictionOverriden: # Color too many modules as red if slotMap[mod.slot] or hasRestrictionOverriden: # Color too many modules as red
self.SetItemBackgroundColour(i, errColorDark if isDark() else errColor) self.SetItemBackgroundColour(i, get_errColorDark() if isDark() else get_errColor())
elif sFit.serviceFittingOptions["colorFitBySlot"]: # Color by slot it enabled elif sFit.serviceFittingOptions["colorFitBySlot"]: # Color by slot it enabled
self.SetItemBackgroundColour(i, self.slotColour(mod.slot)) self.SetItemBackgroundColour(i, self.slotColour(mod.slot))

View File

@@ -346,20 +346,7 @@ class CargoInfo:
return makeReprStr(self, ['itemID', 'amount']) return makeReprStr(self, ['itemID', 'amount'])
def activeStateLimit(itemIdentity): from service.port.active_state import activeStateLimit
item = Market.getInstance().getItem(itemIdentity)
if {
'moduleBonusAssaultDamageControl', 'moduleBonusIndustrialInvulnerability',
'microJumpDrive', 'microJumpPortalDrive', 'emergencyHullEnergizer',
'cynosuralGeneration', 'jumpPortalGeneration', 'jumpPortalGenerationBO',
'cloneJumpAccepting', 'cloakingWarpSafe', 'cloakingPrototype', 'cloaking',
'massEntanglerEffect5', 'electronicAttributeModifyOnline', 'targetPassively',
'cargoScan', 'shipScan', 'surveyScan', 'targetSpectrumBreakerBonus',
'interdictionNullifierBonus', 'warpCoreStabilizerActive',
'industrialItemCompression'
}.intersection(item.effects):
return FittingModuleState.ONLINE
return FittingModuleState.ACTIVE
def droneStackLimit(fit, itemIdentity): def droneStackLimit(fit, itemIdentity):

View File

@@ -79,7 +79,7 @@ a = Analysis(['pyfa.py'],
win_private_assemblies=False, win_private_assemblies=False,
cipher=block_cipher) cipher=block_cipher)
a_headless = Analysis(['pyfa_headless.py'], a_server = Analysis(['pyfa_server.py'],
pathex=pathex, pathex=pathex,
binaries=[], binaries=[],
datas=added_files, datas=added_files,
@@ -92,7 +92,7 @@ a_headless = Analysis(['pyfa_headless.py'],
cipher=block_cipher) cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
pyz_headless = PYZ(a_headless.pure, a_headless.zipped_data, cipher=block_cipher) pyz_server = PYZ(a_server.pure, a_server.zipped_data, cipher=block_cipher)
exe = EXE( exe = EXE(
pyz, pyz,
@@ -108,12 +108,12 @@ exe = EXE(
contents_directory='app', contents_directory='app',
) )
# Headless: server only. POST /simulate on port 9123. # Sim server. POST /simulate on port 9123.
exe_headless = EXE( exe_server = EXE(
pyz_headless, pyz_server,
a_headless.scripts, a_server.scripts,
exclude_binaries=True, exclude_binaries=True,
name='pyfa-headless', name='pyfa-server',
debug=debug, debug=debug,
strip=False, strip=False,
upx=upx, upx=upx,
@@ -131,14 +131,14 @@ coll = COLLECT(
name='pyfa', name='pyfa',
) )
coll_headless = COLLECT( coll_server = COLLECT(
exe_headless, exe_server,
a_headless.binaries, a_server.binaries,
a_headless.zipfiles, a_server.zipfiles,
a_headless.datas, a_server.datas,
strip=False, strip=False,
upx=upx, upx=upx,
name='pyfa_headless', name='pyfa_server',
) )
if platform.system() == 'Darwin': if platform.system() == 'Darwin':

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env python3
# Headless sim daemon only. POST /simulate with JSON body.
from scripts.pyfa_cli_stats import _run_http_server
_run_http_server(9123, None)

5
pyfa_server.py Normal file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env python3
# Sim server. POST /simulate with JSON body.
from scripts.pyfa_sim import _run_http_server
_run_http_server(9123, None)

View File

@@ -57,4 +57,32 @@ curl -X POST \
rm "${ZIP}" rm "${ZIP}"
# Push Docker image
DOCKER_REPO="${DOCKER_REPO:-docker.site.quack-lab.dev}"
IMAGE_NAME="${IMAGE_NAME:-pyfa-server}"
COMMIT_SHA=$(git rev-parse --short HEAD)
IMAGE_BASE="${DOCKER_REPO}/${IMAGE_NAME}"
echo ""
echo "Pushing Docker images..."
docker push "${IMAGE_BASE}:${COMMIT_SHA}"
docker push "${IMAGE_BASE}:latest"
TAGS=$(git tag --points-at HEAD 2>/dev/null || true)
if [ -n "$TAGS" ]; then
while IFS= read -r tag; do
[ -n "$tag" ] && docker push "${IMAGE_BASE}:${tag}"
done <<< "$TAGS"
fi
echo ""
echo "Docker image pushed as:"
echo " - ${IMAGE_BASE}:${COMMIT_SHA}"
echo " - ${IMAGE_BASE}:latest"
if [ -n "$TAGS" ]; then
while IFS= read -r tag; do
[ -n "$tag" ] && echo " - ${IMAGE_BASE}:${tag}"
done <<< "$TAGS"
fi
echo ""
echo "Release complete! ${ZIP} uploaded to ${TAG}" echo "Release complete! ${ZIP} uploaded to ${TAG}"

14
requirements-server.txt Normal file
View File

@@ -0,0 +1,14 @@
logbook==1.7.0.post0
numpy==1.26.2
matplotlib==3.8.2
python-dateutil==2.8.2
requests==2.31.0
sqlalchemy==1.4.50
cryptography==42.0.4
markdown2==2.4.11
packaging==23.2
roman==4.1
beautifulsoup4==4.12.2
pyyaml==6.0.1
python-jose==3.3.0
requests-cache==1.1.1

View File

@@ -8,6 +8,25 @@ import eos.config
from eos.const import FittingHardpoint from eos.const import FittingHardpoint
from eos.utils.spoolSupport import SpoolOptions, SpoolType from eos.utils.spoolSupport import SpoolOptions, SpoolType
# POST /simulate request and response shape; included in 400 response body
SIMULATE_SCHEMA = {
"request": {
"fit": "string (required). EFT or multi-line text export of a single fit, e.g. [ShipName, FitName] followed by modules/drones/etc.",
"projected_fits": "array (optional). Each element: { \"fit\": string, \"count\": positive integer }.",
"command_fits": "array (optional). Each element: { \"fit\": string }.",
},
"response_200": {
"fit": {"id": "int", "name": "string", "ship_type": "string"},
"resources": {"hardpoints": {}, "drones": {}, "fighters": {}, "calibration": {}, "powergrid": {}, "cpu": {}, "cargo": {}},
"defense": {"hp": {}, "ehp": {}, "resonance": {}, "tank": {}, "effective_tank": {}, "sustainable_tank": {}, "effective_sustainable_tank": {}},
"capacitor": {"capacity": {}, "recharge": {}, "use": {}, "delta": {}, "stable": {}, "state": {}, "neutralizer_resistance": {}},
"firepower": {"weapon_dps": {}, "drone_dps": {}, "total_dps": {}, "weapon_volley": {}, "drone_volley": {}, "total_volley": {}, "weapons": []},
"remote_reps_outgoing": {"current": {}, "pre_spool": {}, "full_spool": {}},
"targeting_misc": {"targets_max": {}, "target_range": {}, "scan_resolution": {}, "scan_strength": {}, "scan_type": {}, "jam_chance": {}, "drone_control_range": {}, "speed": {}, "align_time": {}, "signature_radius": {}, "warp_speed": {}, "max_warp_distance": {}, "probe_size": {}, "cargo_capacity": {}, "cargo_used": {}},
"price": {"ship": {}, "fittings": {}, "drones_and_fighters": {}, "cargo": {}, "character": {}, "total": {}},
},
}
def _init_pyfa(savepath: str | None) -> None: def _init_pyfa(savepath: str | None) -> None:
config.debug = False config.debug = False
@@ -363,41 +382,57 @@ def compute_stats(payload: dict, savepath: str | None = None) -> dict:
def _run_http_server(port: int, savepath: str | None) -> None: def _run_http_server(port: int, savepath: str | None) -> None:
def send_error_json(code: int, msg: str, traceback_str: str | None = None):
body = {"error": msg, "schema": SIMULATE_SCHEMA}
if traceback_str:
body["traceback"] = traceback_str
return json.dumps(body, indent=2) + "\n"
class SimulateHandler(BaseHTTPRequestHandler): class SimulateHandler(BaseHTTPRequestHandler):
def send_error(self, code: int, message: str | None = None, explain: str | None = None):
msg = message or explain or "Error"
self.send_response(code)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(send_error_json(code, msg).encode("utf-8"))
def _reply_error(self, code: int, msg: str, traceback_str: str | None = None):
self.send_response(code)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(send_error_json(code, msg, traceback_str).encode("utf-8"))
def do_GET(self):
if self.path == "/simulate":
self._reply_error(405, "Method not allowed. Use POST.")
else:
self._reply_error(404, "Not found. POST /simulate with JSON body.")
def do_POST(self): def do_POST(self):
if self.path != "/simulate": if self.path != "/simulate":
self.send_response(404) self._reply_error(404, "Not found. POST /simulate with JSON body.")
self.end_headers()
return return
content_length = int(self.headers.get("Content-Length", 0)) content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode("utf-8") body = self.rfile.read(content_length).decode("utf-8")
try: try:
payload = json.loads(body) payload = json.loads(body)
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
self.send_response(400) self._reply_error(400, "Invalid JSON: %s" % exc)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(("Invalid JSON: %s" % exc).encode("utf-8"))
return return
if not isinstance(payload, dict): if not isinstance(payload, dict):
self.send_response(400) self._reply_error(400, "Top-level JSON must be an object")
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(b"Top-level JSON must be an object")
return return
try: try:
output = compute_stats(payload, savepath) output = compute_stats(payload, savepath)
except ValueError as e: except ValueError as e:
self.send_response(400) self._reply_error(400, str(e))
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(str(e).encode("utf-8"))
return return
except Exception as e: except Exception as e:
self.send_response(500) import traceback
self.send_header("Content-Type", "text/plain; charset=utf-8") tb = traceback.format_exc()
self.end_headers() sys.stderr.write(tb)
self.wfile.write(str(e).encode("utf-8")) sys.stderr.flush()
self._reply_error(500, str(e), tb)
return return
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Type", "application/json; charset=utf-8")

View File

@@ -28,12 +28,8 @@ from xml.etree import ElementTree
from xml.dom import minidom from xml.dom import minidom
import gzip import gzip
# noinspection PyPackageRequirements
import wx
import config import config
import eos.db import eos.db
from service.esi import Esi
from eos.saveddata.implant import Implant as es_Implant from eos.saveddata.implant import Implant as es_Implant
from eos.saveddata.character import Character as es_Character, Skill from eos.saveddata.character import Character as es_Character, Skill
@@ -42,7 +38,12 @@ from eos.const import FittingSlot as es_Slot
from eos.saveddata.fighter import Fighter as es_Fighter from eos.saveddata.fighter import Fighter as es_Fighter
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
_t = wx.GetTranslation
def _t(s):
import wx
return wx.GetTranslation(s)
class CharacterImportThread(threading.Thread): class CharacterImportThread(threading.Thread):
@@ -97,6 +98,7 @@ class CharacterImportThread(threading.Thread):
pyfalog.error(e) pyfalog.error(e)
continue continue
import wx
wx.CallAfter(self.callback) wx.CallAfter(self.callback)
def stop(self): def stop(self):
@@ -132,6 +134,7 @@ class SkillBackupThread(threading.Thread):
with open(path, mode='w', encoding='utf-8') as backupFile: with open(path, mode='w', encoding='utf-8') as backupFile:
backupFile.write(backupData) backupFile.write(backupData)
import wx
wx.CallAfter(self.callback) wx.CallAfter(self.callback)
def stop(self): def stop(self):
@@ -386,6 +389,7 @@ class Character:
def apiFetchCallback(self, guiCallback, e=None): def apiFetchCallback(self, guiCallback, e=None):
eos.db.commit() eos.db.commit()
import wx
wx.CallAfter(guiCallback, e) wx.CallAfter(guiCallback, e)
@staticmethod @staticmethod
@@ -501,6 +505,7 @@ class UpdateAPIThread(threading.Thread):
try: try:
char = eos.db.getCharacter(self.charID) char = eos.db.getCharacter(self.charID)
from service.esi import Esi
sEsi = Esi.getInstance() sEsi = Esi.getInstance()
sChar = Character.getInstance() sChar = Character.getInstance()
ssoChar = sChar.getSsoCharacter(char.ID) ssoChar = sChar.getSsoCharacter(char.ID)

View File

@@ -22,7 +22,6 @@ import datetime
from time import time from time import time
from weakref import WeakSet from weakref import WeakSet
import wx
from logbook import Logger from logbook import Logger
import eos.db import eos.db
@@ -235,6 +234,7 @@ class Fit:
@classmethod @classmethod
def getCommandProcessor(cls, fitID): def getCommandProcessor(cls, fitID):
if fitID not in cls.processors: if fitID not in cls.processors:
import wx
cls.processors[fitID] = wx.CommandProcessor(maxCommands=100) cls.processors[fitID] = wx.CommandProcessor(maxCommands=100)
return cls.processors[fitID] return cls.processors[fitID]

View File

@@ -23,8 +23,6 @@ import threading
from collections import OrderedDict from collections import OrderedDict
from itertools import chain from itertools import chain
# noinspection PyPackageRequirements
import wx
from logbook import Logger from logbook import Logger
from sqlalchemy.sql import or_ from sqlalchemy.sql import or_
@@ -38,7 +36,12 @@ from service.settings import SettingsProvider
from utils.cjk import isStringCjk from utils.cjk import isStringCjk
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
_t = wx.GetTranslation
def _t(s):
import wx
return wx.GetTranslation(s)
# Event which tells threads dependent on Market that it's initialized # Event which tells threads dependent on Market that it's initialized
mktRdy = threading.Event() mktRdy = threading.Event()
@@ -77,6 +80,7 @@ class ShipBrowserWorkerThread(threading.Thread):
set_ = sMkt.getShipList(id_) set_ = sMkt.getShipList(id_)
cache[id_] = set_ cache[id_] = set_
import wx
wx.CallAfter(callback, (id_, set_)) wx.CallAfter(callback, (id_, set_))
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
@@ -170,6 +174,7 @@ class SearchWorkerThread(threading.Thread):
for item in all_results: for item in all_results:
if sMkt.getPublicityByItem(item): if sMkt.getPublicityByItem(item):
item_IDs.add(item.ID) item_IDs.add(item.ID)
import wx
wx.CallAfter(callback, sorted(item_IDs)) wx.CallAfter(callback, sorted(item_IDs))
def scheduleSearch(self, text, callback, filterName=None): def scheduleSearch(self, text, callback, filterName=None):
@@ -268,7 +273,7 @@ class Market:
self.les_grp = types_Group() self.les_grp = types_Group()
self.les_grp.ID = -1 self.les_grp.ID = -1
self.les_grp.name = "Limited Issue Ships" self.les_grp.name = "Limited Issue Ships"
self.les_grp.displayName = _t("Limited Issue Ships") self.les_grp.displayName = "Limited Issue Ships"
self.les_grp.published = True self.les_grp.published = True
ships = self.getCategory("Ship") ships = self.getCategory("Ship")
self.les_grp.category = ships self.les_grp.category = ships

View File

@@ -0,0 +1,12 @@
# Module-state logic shared by port (EFT import) and gui (fit commands).
# Uses gamedata (item type + attributes), no hardcoded effect names.
from eos.const import FittingModuleState
from service.market import Market
def activeStateLimit(itemIdentity):
item = Market.getInstance().getItem(itemIdentity)
if not item.isType("active") or item.getAttribute("activationBlocked", 0) > 0:
return FittingModuleState.ONLINE
return FittingModuleState.ACTIVE

View File

@@ -47,6 +47,8 @@ SLOT_ORDER = (FittingSlot.LOW, FittingSlot.MED, FittingSlot.HIGH, FittingSlot.RI
OFFLINE_SUFFIX = '/offline' OFFLINE_SUFFIX = '/offline'
NAME_CHARS = r'[^,/\[\]]' # Characters which are allowed to be used in name NAME_CHARS = r'[^,/\[\]]' # Characters which are allowed to be used in name
from service.port.active_state import activeStateLimit
class MutationExportData: class MutationExportData:
@@ -240,7 +242,6 @@ def exportCargo(cargos):
def importEft(lines): def importEft(lines):
from gui.fitCommands.helpers import activeStateLimit
lines = _importPrepare(lines) lines = _importPrepare(lines)
try: try:
fit = _importCreateFit(lines) fit = _importCreateFit(lines)
@@ -877,7 +878,6 @@ class AbstractFit:
self.getContainerBySlot(m.slot).append(m) self.getContainerBySlot(m.slot).append(m)
def __makeModule(self, itemSpec): def __makeModule(self, itemSpec):
from gui.fitCommands.helpers import activeStateLimit
# Mutate item if needed # Mutate item if needed
m = None m = None
if itemSpec.mutationIdx in self.mutations: if itemSpec.mutationIdx in self.mutations:

View File

@@ -38,7 +38,6 @@ from service.port.eft import (
isValidImplantImport, isValidBoosterImport) isValidImplantImport, isValidBoosterImport)
from service.port.esi import exportESI, importESI from service.port.esi import exportESI, importESI
from service.port.multibuy import exportMultiBuy from service.port.multibuy import exportMultiBuy
from service.port.shipstats import exportFitStats
from service.port.xml import importXml, exportXml from service.port.xml import importXml, exportXml
from service.port.muta import parseMutant, parseDynamicItemString, fetchDynamicItem from service.port.muta import parseMutant, parseDynamicItemString, fetchDynamicItem
@@ -346,4 +345,5 @@ class Port:
@staticmethod @staticmethod
def exportFitStats(fit, callback=None): def exportFitStats(fit, callback=None):
return exportFitStats(fit, callback=callback) from service.port.shipstats import exportFitStats as _exportFitStats
return _exportFitStats(fit, callback=callback)

View File

@@ -24,7 +24,6 @@ import timeit
from itertools import chain from itertools import chain
import math import math
import wx
from logbook import Logger from logbook import Logger
from eos import db from eos import db
@@ -51,6 +50,8 @@ class Price:
sources = {} sources = {}
def __init__(self): def __init__(self):
# Import market sources so they register; avoid at module level so server path never loads them
from service.marketSources import evemarketdata, fuzzwork, cevemarket, evetycoon # noqa: F401
# Start price fetcher # Start price fetcher
self.priceWorkerThread = PriceWorkerThread() self.priceWorkerThread = PriceWorkerThread()
self.priceWorkerThread.daemon = True self.priceWorkerThread.daemon = True
@@ -253,6 +254,7 @@ class PriceWorkerThread(threading.Thread):
if len(requests) > 0: if len(requests) > 0:
Price.fetchPrices(requests, fetchTimeout, validityOverride) Price.fetchPrices(requests, fetchTimeout, validityOverride)
import wx
wx.CallAfter(callback) wx.CallAfter(callback)
queue.task_done() queue.task_done()
@@ -273,7 +275,3 @@ class PriceWorkerThread(threading.Thread):
def stop(self): def stop(self):
self.running = False self.running = False
# Import market sources only to initialize price source modules, they register on their own
from service.marketSources import evemarketdata, fuzzwork, cevemarket, evetycoon # noqa: E402

View File

@@ -24,7 +24,6 @@ import urllib.error
import urllib.parse import urllib.parse
import json import json
from collections import namedtuple from collections import namedtuple
import wx
from logbook import Logger from logbook import Logger
@@ -582,6 +581,7 @@ class LocaleSettings:
@classmethod @classmethod
def supported_languages(cls): def supported_languages(cls):
"""Requires the application to be initialized, otherwise wx.Translation isn't set.""" """Requires the application to be initialized, otherwise wx.Translation isn't set."""
import wx
pyfalog.info(f'using "{config.CATALOG}" to fetch languages, relatively base path "{os.getcwd()}"') 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)} return {x: wx.Locale.FindLanguageInfo(x) for x in wx.Translations.Get().GetAvailableTranslations(config.CATALOG)}

View File

@@ -1,4 +1,3 @@
import json
import os import os
import sys import sys
import tempfile import tempfile
@@ -7,7 +6,7 @@ import tempfile
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.realpath(os.path.join(script_dir, ".."))) sys.path.append(os.path.realpath(os.path.join(script_dir, "..")))
from scripts import pyfa_cli_stats # noqa: E402 from scripts import pyfa_sim # noqa: E402
ISHTAR_SPIDER_FIT = """[Ishtar, Spider] ISHTAR_SPIDER_FIT = """[Ishtar, Spider]
@@ -41,7 +40,7 @@ Berserker II x5
def test_ishtar_spider_remote_armor_reps(): def test_ishtar_spider_remote_armor_reps():
payload = {"fit": ISHTAR_SPIDER_FIT} payload = {"fit": ISHTAR_SPIDER_FIT}
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp: with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
data = pyfa_cli_stats.compute_stats(payload, tmp) data = pyfa_sim.compute_stats(payload, tmp)
armor_rps = data["remote_reps_outgoing"]["current"]["armor"] armor_rps = data["remote_reps_outgoing"]["current"]["armor"]
assert armor_rps is not None assert armor_rps is not None
assert int(round(armor_rps)) == 171 assert int(round(armor_rps)) == 171