Implement a "simple" cli over pyfa for cli business
This commit is contained in:
@@ -5,9 +5,6 @@ import wx
|
||||
|
||||
import gui.mainFrame
|
||||
from gui.contextMenu import ContextMenuSingle
|
||||
from gui.fitCommands import (
|
||||
GuiConvertMutatedLocalModuleCommand, GuiRevertMutatedLocalModuleCommand,
|
||||
GuiConvertMutatedLocalDroneCommand, GuiRevertMutatedLocalDroneCommand)
|
||||
from service.fit import Fit
|
||||
|
||||
_t = wx.GetTranslation
|
||||
@@ -65,6 +62,8 @@ class ChangeItemMutation(ContextMenuSingle):
|
||||
return sub
|
||||
|
||||
def handleMenu(self, event):
|
||||
from gui.fitCommands import (
|
||||
GuiConvertMutatedLocalModuleCommand, GuiConvertMutatedLocalDroneCommand)
|
||||
mutaplasmid, item = self.eventIDs[event.Id]
|
||||
fitID = self.mainFrame.getActiveFit()
|
||||
fit = Fit.getInstance().getFit(fitID)
|
||||
@@ -78,6 +77,8 @@ class ChangeItemMutation(ContextMenuSingle):
|
||||
fitID=fitID, position=position, mutaplasmid=mutaplasmid))
|
||||
|
||||
def activate(self, callingWindow, fullContext, mainItem, i):
|
||||
from gui.fitCommands import (
|
||||
GuiRevertMutatedLocalModuleCommand, GuiRevertMutatedLocalDroneCommand)
|
||||
fitID = self.mainFrame.getActiveFit()
|
||||
fit = Fit.getInstance().getFit(fitID)
|
||||
if mainItem in fit.modules:
|
||||
|
||||
10
pyfa.py
10
pyfa.py
@@ -74,9 +74,19 @@ parser.add_option("-s", "--savepath", action="store", dest="savepath", help="Set
|
||||
parser.add_option("-l", "--logginglevel", action="store", dest="logginglevel", help="Set desired logging level [Critical|Error|Warning|Info|Debug]", default="Error")
|
||||
parser.add_option("-p", "--profile", action="store", dest="profile_path", help="Set location to save profileing.", default=None)
|
||||
parser.add_option("-i", "--language", action="store", dest="language", help="Sets the language for pyfa. Overrides user's saved settings. Format: xx_YY (eg: en_US). If translation doesn't exist, defaults to en_US", default=None)
|
||||
parser.add_option("--headless", action="store_true", dest="headless", help="Run CLI stats only: read JSON fit from first positional arg (or stdin), print stats JSON to stdout, then exit. No GUI.", default=False)
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if getattr(options, "headless", False):
|
||||
from scripts.pyfa_cli_stats import main as headless_main
|
||||
headless_argv = []
|
||||
if options.savepath:
|
||||
headless_argv.extend(["--savepath", options.savepath])
|
||||
if args:
|
||||
headless_argv.append(args[0])
|
||||
sys.exit(headless_main(headless_argv))
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
try:
|
||||
|
||||
@@ -22,3 +22,6 @@ dependencies = [
|
||||
"sqlalchemy==1.4.50",
|
||||
"wxpython==4.2.1",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest"]
|
||||
|
||||
411
scripts/pyfa_cli_stats.py
Normal file
411
scripts/pyfa_cli_stats.py
Normal file
@@ -0,0 +1,411 @@
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
import config
|
||||
import eos.config
|
||||
|
||||
from eos.const import FittingHardpoint
|
||||
from eos.utils.spoolSupport import SpoolOptions, SpoolType
|
||||
|
||||
|
||||
def _init_pyfa(savepath: str | None) -> None:
|
||||
config.debug = False
|
||||
config.loggingLevel = config.LOGLEVEL_MAP["error"]
|
||||
config.defPaths(savepath)
|
||||
config.defLogging()
|
||||
import eos.db # noqa: F401
|
||||
eos.db.saveddata_meta.create_all() # type: ignore[name-defined]
|
||||
import eos.events # noqa: F401
|
||||
from service import prefetch # noqa: F401
|
||||
|
||||
|
||||
def _parse_drone_markers(text: str) -> tuple[str, set[str]]:
|
||||
active_names: set[str] = set()
|
||||
cleaned_lines: list[str] = []
|
||||
for raw_line in text.splitlines():
|
||||
line = raw_line.rstrip()
|
||||
if " xx" in line:
|
||||
idx = line.find(" xx")
|
||||
marker_segment = line[idx + 1 :].strip()
|
||||
|
||||
if marker_segment == "xx":
|
||||
payload = line[:idx].rstrip()
|
||||
parts = payload.split(" x", 1)
|
||||
if len(parts) == 2:
|
||||
type_name = parts[0].strip()
|
||||
if type_name:
|
||||
active_names.add(type_name)
|
||||
line = payload
|
||||
|
||||
cleaned_lines.append(line)
|
||||
|
||||
return "\n".join(cleaned_lines), active_names
|
||||
|
||||
|
||||
def _import_single_fit(raw_text: str) -> tuple[object, set[str]]:
|
||||
from service.port.port import Port
|
||||
|
||||
cleaned_text, active_names = _parse_drone_markers(raw_text)
|
||||
import_type, import_data = Port.importFitFromBuffer(cleaned_text)
|
||||
|
||||
if not import_data or len(import_data) != 1:
|
||||
raise SystemExit("Expected exactly one fit in input; got %d" % (len(import_data) if import_data else 0))
|
||||
|
||||
fit = import_data[0]
|
||||
|
||||
if active_names:
|
||||
for drone in fit.drones:
|
||||
if getattr(drone.item, "typeName", None) in active_names:
|
||||
drone.amountActive = drone.amount
|
||||
for fighter in fit.fighters:
|
||||
if getattr(fighter.item, "typeName", None) in active_names:
|
||||
fighter.amount = fighter.amount
|
||||
|
||||
return fit, active_names
|
||||
|
||||
|
||||
def _add_projected_fit(s_fit, target_fit, projected_fit, amount: int) -> None:
|
||||
fit = s_fit.getFit(target_fit.ID)
|
||||
projected = s_fit.getFit(projected_fit.ID, projected=True)
|
||||
if projected is None:
|
||||
raise SystemExit("Projected fit %s is not available" % projected_fit.ID)
|
||||
|
||||
if projected in fit.projectedFits and projected.ID in fit.projectedFitDict:
|
||||
projection_info = projected.getProjectionInfo(fit.ID)
|
||||
if projection_info is None:
|
||||
raise SystemExit("Projection info missing for projected fit %s" % projected_fit.ID)
|
||||
else:
|
||||
fit.projectedFitDict[projected.ID] = projected
|
||||
eos.db.saveddata_session.flush()
|
||||
eos.db.saveddata_session.refresh(projected)
|
||||
projection_info = projected.getProjectionInfo(fit.ID)
|
||||
if projection_info is None:
|
||||
raise SystemExit("Projection info missing after linking projected fit %s" % projected_fit.ID)
|
||||
|
||||
projection_info.amount = amount
|
||||
projection_info.active = True
|
||||
|
||||
|
||||
def _add_command_fit(s_fit, target_fit, command_fit) -> None:
|
||||
fit = s_fit.getFit(target_fit.ID)
|
||||
command = s_fit.getFit(command_fit.ID)
|
||||
if command is None:
|
||||
raise SystemExit("Command fit %s is not available" % command_fit.ID)
|
||||
|
||||
if command in fit.commandFits or command.ID in fit.commandFitDict:
|
||||
return
|
||||
|
||||
fit.commandFitDict[command.ID] = command
|
||||
eos.db.saveddata_session.flush()
|
||||
eos.db.saveddata_session.refresh(command)
|
||||
info = command.getCommandInfo(fit.ID)
|
||||
if info is None:
|
||||
raise SystemExit("Command info missing for command fit %s" % command_fit.ID)
|
||||
info.active = True
|
||||
|
||||
|
||||
def _collect_resources(fit) -> dict:
|
||||
ship = fit.ship
|
||||
resources: dict = {}
|
||||
|
||||
resources["hardpoints"] = {
|
||||
"turret": {
|
||||
"used": fit.getHardpointsUsed(FittingHardpoint.TURRET),
|
||||
"total": ship.getModifiedItemAttr("turretSlotsLeft"),
|
||||
},
|
||||
"launcher": {
|
||||
"used": fit.getHardpointsUsed(FittingHardpoint.MISSILE),
|
||||
"total": ship.getModifiedItemAttr("launcherSlotsLeft"),
|
||||
},
|
||||
}
|
||||
|
||||
resources["drones"] = {
|
||||
"active": fit.activeDrones,
|
||||
"max_active": fit.extraAttributes.get("maxActiveDrones"),
|
||||
"bay_used": fit.droneBayUsed,
|
||||
"bay_capacity": ship.getModifiedItemAttr("droneCapacity"),
|
||||
"bandwidth_used": fit.droneBandwidthUsed,
|
||||
"bandwidth_capacity": ship.getModifiedItemAttr("droneBandwidth"),
|
||||
}
|
||||
|
||||
resources["fighters"] = {
|
||||
"tubes_used": fit.fighterTubesUsed,
|
||||
"tubes_total": fit.fighterTubesTotal,
|
||||
"bay_used": fit.fighterBayUsed,
|
||||
"bay_capacity": ship.getModifiedItemAttr("fighterCapacity"),
|
||||
}
|
||||
|
||||
resources["calibration"] = {
|
||||
"used": fit.calibrationUsed,
|
||||
"total": ship.getModifiedItemAttr("upgradeCapacity"),
|
||||
}
|
||||
|
||||
resources["powergrid"] = {
|
||||
"used": fit.pgUsed,
|
||||
"output": ship.getModifiedItemAttr("powerOutput"),
|
||||
}
|
||||
|
||||
resources["cpu"] = {
|
||||
"used": fit.cpuUsed,
|
||||
"output": ship.getModifiedItemAttr("cpuOutput"),
|
||||
}
|
||||
|
||||
resources["cargo"] = {
|
||||
"used": fit.cargoBayUsed,
|
||||
"capacity": ship.getModifiedItemAttr("capacity"),
|
||||
}
|
||||
|
||||
return resources
|
||||
|
||||
|
||||
def _collect_defense(fit) -> dict:
|
||||
ship = fit.ship
|
||||
|
||||
def _res(attr_name):
|
||||
return ship.getModifiedItemAttr(attr_name)
|
||||
|
||||
resonance = {
|
||||
"armor": {"em": _res("armorEmDamageResonance"), "exp": _res("armorExplosiveDamageResonance"), "kin": _res("armorKineticDamageResonance"), "therm": _res("armorThermalDamageResonance")},
|
||||
"shield": {"em": _res("shieldEmDamageResonance"), "exp": _res("shieldExplosiveDamageResonance"), "kin": _res("shieldKineticDamageResonance"), "therm": _res("shieldThermalDamageResonance")},
|
||||
"hull": {"em": _res("emDamageResonance"), "exp": _res("explosiveDamageResonance"), "kin": _res("kineticDamageResonance"), "therm": _res("thermalDamageResonance")},
|
||||
}
|
||||
defense: dict = {}
|
||||
defense["hp"] = fit.hp
|
||||
defense["ehp"] = fit.ehp
|
||||
defense["resonance"] = resonance
|
||||
defense["tank"] = fit.tank
|
||||
defense["effective_tank"] = fit.effectiveTank
|
||||
defense["sustainable_tank"] = fit.sustainableTank
|
||||
defense["effective_sustainable_tank"] = fit.effectiveSustainableTank
|
||||
return defense
|
||||
|
||||
|
||||
def _collect_capacitor(fit) -> dict:
|
||||
ship = fit.ship
|
||||
cap: dict = {}
|
||||
cap["capacity"] = ship.getModifiedItemAttr("capacitorCapacity")
|
||||
cap["recharge"] = fit.capRecharge
|
||||
cap["use"] = fit.capUsed
|
||||
cap["delta"] = fit.capDelta
|
||||
cap["stable"] = fit.capStable
|
||||
cap["state"] = fit.capState
|
||||
cap["neutralizer_resistance"] = ship.getModifiedItemAttr("energyWarfareResistance", 1)
|
||||
return cap
|
||||
|
||||
|
||||
def _collect_firepower(fit) -> dict:
|
||||
default_spool = eos.config.settings["globalDefaultSpoolupPercentage"]
|
||||
spool = SpoolOptions(SpoolType.SPOOL_SCALE, default_spool, False)
|
||||
wdps = fit.getWeaponDps(spoolOptions=spool)
|
||||
ddps = fit.getDroneDps()
|
||||
wvol = fit.getWeaponVolley(spoolOptions=spool)
|
||||
dvol = fit.getDroneVolley()
|
||||
return {
|
||||
"weapon_dps": wdps.total,
|
||||
"drone_dps": ddps.total,
|
||||
"total_dps": fit.getTotalDps(spoolOptions=spool).total,
|
||||
"weapon_volley": wvol.total,
|
||||
"drone_volley": dvol.total,
|
||||
"total_volley": fit.getTotalVolley(spoolOptions=spool).total,
|
||||
"weapons": [],
|
||||
}
|
||||
|
||||
|
||||
def _collect_remote_reps(fit) -> dict:
|
||||
default_spool = eos.config.settings["globalDefaultSpoolupPercentage"]
|
||||
pre = fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 0, True))
|
||||
full = fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, 1, True))
|
||||
current = fit.getRemoteReps(spoolOptions=SpoolOptions(SpoolType.SPOOL_SCALE, default_spool, False))
|
||||
return {
|
||||
"current": {
|
||||
"capacitor": current.capacitor,
|
||||
"shield": current.shield,
|
||||
"armor": current.armor,
|
||||
"hull": current.hull,
|
||||
},
|
||||
"pre_spool": {
|
||||
"capacitor": pre.capacitor,
|
||||
"shield": pre.shield,
|
||||
"armor": pre.armor,
|
||||
"hull": pre.hull,
|
||||
},
|
||||
"full_spool": {
|
||||
"capacitor": full.capacitor,
|
||||
"shield": full.shield,
|
||||
"armor": full.armor,
|
||||
"hull": full.hull,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _collect_targeting_misc(fit) -> dict:
|
||||
ship = fit.ship
|
||||
misc: dict = {}
|
||||
misc["targets_max"] = fit.maxTargets
|
||||
misc["target_range"] = fit.maxTargetRange
|
||||
misc["scan_resolution"] = ship.getModifiedItemAttr("scanResolution")
|
||||
misc["scan_strength"] = fit.scanStrength
|
||||
misc["scan_type"] = fit.scanType
|
||||
misc["jam_chance"] = fit.jamChance
|
||||
misc["drone_control_range"] = fit.extraAttributes.get("droneControlRange")
|
||||
misc["speed"] = fit.maxSpeed
|
||||
misc["align_time"] = fit.alignTime
|
||||
misc["signature_radius"] = ship.getModifiedItemAttr("signatureRadius")
|
||||
misc["warp_speed"] = fit.warpSpeed
|
||||
misc["max_warp_distance"] = fit.maxWarpDistance
|
||||
misc["probe_size"] = fit.probeSize
|
||||
misc["cargo_capacity"] = ship.getModifiedItemAttr("capacity")
|
||||
misc["cargo_used"] = fit.cargoBayUsed
|
||||
return misc
|
||||
|
||||
|
||||
def _collect_price(fit) -> dict:
|
||||
ship_price = 0.0
|
||||
module_price = 0.0
|
||||
drone_price = 0.0
|
||||
fighter_price = 0.0
|
||||
cargo_price = 0.0
|
||||
booster_price = 0.0
|
||||
implant_price = 0.0
|
||||
|
||||
if fit:
|
||||
ship_price = getattr(fit.ship.item.price, "price", 0.0)
|
||||
|
||||
for module in fit.modules:
|
||||
if not module.isEmpty:
|
||||
module_price += getattr(module.item.price, "price", 0.0)
|
||||
|
||||
for drone in fit.drones:
|
||||
drone_price += getattr(drone.item.price, "price", 0.0) * drone.amount
|
||||
|
||||
for fighter in fit.fighters:
|
||||
fighter_price += getattr(fighter.item.price, "price", 0.0) * fighter.amount
|
||||
|
||||
for cargo in fit.cargo:
|
||||
cargo_price += getattr(cargo.item.price, "price", 0.0) * cargo.amount
|
||||
|
||||
for booster in fit.boosters:
|
||||
booster_price += getattr(booster.item.price, "price", 0.0)
|
||||
|
||||
for implant in fit.appliedImplants:
|
||||
implant_price += getattr(implant.item.price, "price", 0.0)
|
||||
|
||||
total_price = ship_price + module_price + drone_price + fighter_price + cargo_price + booster_price + implant_price
|
||||
|
||||
return {
|
||||
"ship": ship_price,
|
||||
"fittings": module_price,
|
||||
"drones_and_fighters": drone_price + fighter_price,
|
||||
"cargo": cargo_price,
|
||||
"character": booster_price + implant_price,
|
||||
"total": total_price,
|
||||
}
|
||||
|
||||
|
||||
def _build_output(main_fit) -> dict:
|
||||
return {
|
||||
"fit": {
|
||||
"id": main_fit.ID,
|
||||
"name": main_fit.name,
|
||||
"ship_type": main_fit.ship.item.typeName,
|
||||
},
|
||||
"resources": _collect_resources(main_fit),
|
||||
"defense": _collect_defense(main_fit),
|
||||
"capacitor": _collect_capacitor(main_fit),
|
||||
"firepower": _collect_firepower(main_fit),
|
||||
"remote_reps_outgoing": _collect_remote_reps(main_fit),
|
||||
"targeting_misc": _collect_targeting_misc(main_fit),
|
||||
"price": _collect_price(main_fit),
|
||||
}
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Headless pyfa CLI fit statistics exporter")
|
||||
parser.add_argument(
|
||||
"data",
|
||||
nargs="?",
|
||||
help="JSON payload (inline). If omitted, read from stdin.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--savepath",
|
||||
help="Override pyfa save path (saveddata, DB, logs). Defaults to standard user location.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def _load_payload(data: str) -> dict:
|
||||
try:
|
||||
payload = json.loads(data)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise SystemExit("Invalid JSON: %s" % exc) from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise SystemExit("Top-level JSON must be an object")
|
||||
return payload
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
args = _parse_args(argv)
|
||||
_init_pyfa(args.savepath)
|
||||
|
||||
from service.fit import Fit as FitService
|
||||
|
||||
s_fit = FitService.getInstance()
|
||||
if s_fit.character is None:
|
||||
from eos.saveddata.character import Character as saveddata_Character
|
||||
s_fit.character = saveddata_Character.getAll5()
|
||||
|
||||
raw = args.data if args.data is not None else sys.stdin.read()
|
||||
payload = _load_payload(raw)
|
||||
|
||||
if "fit" not in payload or not isinstance(payload["fit"], str):
|
||||
raise SystemExit("Payload must contain a 'fit' field with EFT/text export")
|
||||
|
||||
main_fit, _ = _import_single_fit(payload["fit"])
|
||||
|
||||
projected_defs = payload.get("projected_fits", [])
|
||||
command_defs = payload.get("command_fits", [])
|
||||
|
||||
projected_fits: list[tuple[object, int]] = []
|
||||
for entry in projected_defs:
|
||||
if not isinstance(entry, dict):
|
||||
raise SystemExit("Each projected_fits entry must be an object")
|
||||
fit_text = entry.get("fit")
|
||||
count = entry.get("count")
|
||||
if not isinstance(fit_text, str):
|
||||
raise SystemExit("Each projected_fits entry must contain a string 'fit'")
|
||||
if not isinstance(count, int) or count <= 0:
|
||||
raise SystemExit("Each projected_fits entry must contain a positive integer 'count'")
|
||||
pf, _ = _import_single_fit(fit_text)
|
||||
projected_fits.append((pf, count))
|
||||
|
||||
command_fits: list[object] = []
|
||||
for entry in command_defs:
|
||||
if not isinstance(entry, dict):
|
||||
raise SystemExit("Each command_fits entry must be an object")
|
||||
fit_text = entry.get("fit")
|
||||
if not isinstance(fit_text, str):
|
||||
raise SystemExit("Each command_fits entry must contain a string 'fit'")
|
||||
cf, _ = _import_single_fit(fit_text)
|
||||
command_fits.append(cf)
|
||||
|
||||
for pf, count in projected_fits:
|
||||
_add_projected_fit(s_fit, main_fit, pf, count)
|
||||
for cf in command_fits:
|
||||
_add_command_fit(s_fit, main_fit, cf)
|
||||
|
||||
s_fit.recalc(main_fit)
|
||||
s_fit.fill(main_fit)
|
||||
|
||||
output = _build_output(main_fit)
|
||||
json.dump(output, sys.stdout, indent=2, sort_keys=True)
|
||||
sys.stdout.write("\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -13,11 +13,9 @@ 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
|
||||
from service.server import StoppableHTTPServer, AuthHandler
|
||||
from service.settings import EsiSettings
|
||||
from service.esiAccess import EsiAccess
|
||||
import gui.mainFrame
|
||||
|
||||
from requests import Session
|
||||
|
||||
@@ -140,6 +138,7 @@ class Esi(EsiAccess):
|
||||
self.fittings_deleted.add(fittingID)
|
||||
|
||||
def login(self):
|
||||
import gui.ssoLogin
|
||||
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:
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
from .efs import EfsPort
|
||||
from .port import Port
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name == "Port":
|
||||
from service.port.port import Port
|
||||
return Port
|
||||
raise AttributeError("module %r has no attribute %r" % (__name__, name))
|
||||
|
||||
@@ -31,7 +31,6 @@ from eos.saveddata.fighter import Fighter
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.saveddata.module import Module
|
||||
from eos.saveddata.ship import Ship
|
||||
from gui.fitCommands.helpers import activeStateLimit
|
||||
from service.const import PortDnaOptions
|
||||
from service.fit import Fit as svcFit
|
||||
from service.market import Market
|
||||
@@ -80,6 +79,7 @@ def importDnaAlt(string, fitName=None):
|
||||
return processImportInfo(info, fitName, "*")
|
||||
|
||||
def processImportInfo(info, fitName, amountSeparator):
|
||||
from gui.fitCommands.helpers import activeStateLimit
|
||||
sMkt = Market.getInstance()
|
||||
f = Fit()
|
||||
try:
|
||||
|
||||
@@ -16,10 +16,6 @@ from eos.effectHandlerHelpers import HandledList
|
||||
from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup
|
||||
from eos.gamedata import Attribute, Effect, Group, Item, ItemEffect
|
||||
from eos.utils.spoolSupport import SpoolType, SpoolOptions
|
||||
from gui.fitCommands.calc.module.localAdd import CalcAddLocalModuleCommand
|
||||
from gui.fitCommands.calc.module.localRemove import CalcRemoveLocalModulesCommand
|
||||
from gui.fitCommands.calc.module.changeCharges import CalcChangeModuleChargesCommand
|
||||
from gui.fitCommands.helpers import ModuleInfo
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
@@ -68,6 +64,9 @@ class EfsPort:
|
||||
|
||||
if propID is None:
|
||||
return None
|
||||
from gui.fitCommands.calc.module.localAdd import CalcAddLocalModuleCommand
|
||||
from gui.fitCommands.calc.module.localRemove import CalcRemoveLocalModulesCommand
|
||||
from gui.fitCommands.helpers import ModuleInfo
|
||||
cmd = CalcAddLocalModuleCommand(fitID, ModuleInfo(itemID=propID))
|
||||
cmd.Do()
|
||||
if cmd.needsGuiRecalc:
|
||||
@@ -137,6 +136,7 @@ class EfsPort:
|
||||
EfsPort.attrDirectMap(["reloadTime"], stats, mod)
|
||||
c = mod.charge
|
||||
if c:
|
||||
from gui.fitCommands.calc.module.changeCharges import CalcChangeModuleChargesCommand
|
||||
sFit.recalc(fit)
|
||||
CalcChangeModuleChargesCommand(
|
||||
fit.ID,
|
||||
|
||||
@@ -33,7 +33,6 @@ from eos.saveddata.fit import Fit
|
||||
from eos.saveddata.implant import Implant
|
||||
from eos.saveddata.module import Module
|
||||
from eos.saveddata.ship import Ship
|
||||
from gui.fitCommands.helpers import activeStateLimit
|
||||
from service.const import PortEftOptions
|
||||
from service.fit import Fit as svcFit
|
||||
from service.market import Market
|
||||
@@ -241,6 +240,7 @@ def exportCargo(cargos):
|
||||
|
||||
|
||||
def importEft(lines):
|
||||
from gui.fitCommands.helpers import activeStateLimit
|
||||
lines = _importPrepare(lines)
|
||||
try:
|
||||
fit = _importCreateFit(lines)
|
||||
@@ -877,6 +877,7 @@ class AbstractFit:
|
||||
self.getContainerBySlot(m.slot).append(m)
|
||||
|
||||
def __makeModule(self, itemSpec):
|
||||
from gui.fitCommands.helpers import activeStateLimit
|
||||
# Mutate item if needed
|
||||
m = None
|
||||
if itemSpec.mutationIdx in self.mutations:
|
||||
|
||||
@@ -31,7 +31,6 @@ from eos.saveddata.fighter import Fighter
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.saveddata.module import Module
|
||||
from eos.saveddata.ship import Ship
|
||||
from gui.fitCommands.helpers import activeStateLimit
|
||||
from service.fit import Fit as svcFit
|
||||
from service.market import Market
|
||||
|
||||
@@ -161,6 +160,7 @@ def exportESI(ofit, exportCharges, exportImplants, exportBoosters, callback):
|
||||
|
||||
|
||||
def importESI(string):
|
||||
from gui.fitCommands.helpers import activeStateLimit
|
||||
|
||||
sMkt = Market.getInstance()
|
||||
fitobj = Fit()
|
||||
|
||||
@@ -195,10 +195,14 @@ class Port:
|
||||
# TODO: catch the exception?
|
||||
# activeFit is reserved?, bufferStr is unicode? (assume only clipboard string?
|
||||
sFit = svcFit.getInstance()
|
||||
if sFit.character is None:
|
||||
from eos.saveddata.character import Character as saveddata_Character
|
||||
sFit.character = saveddata_Character.getAll5()
|
||||
importType, makesNewFits, importData = Port.importAuto(bufferStr, activeFit=activeFit)
|
||||
|
||||
if makesNewFits:
|
||||
for fit in importData:
|
||||
fits = [f for f in importData if f is not None]
|
||||
for fit in fits:
|
||||
fit.character = sFit.character
|
||||
fit.damagePattern = sFit.pattern
|
||||
fit.targetProfile = sFit.targetProfile
|
||||
@@ -208,6 +212,7 @@ class Port:
|
||||
useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"]
|
||||
fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT
|
||||
db.save(fit)
|
||||
return importType, fits
|
||||
return importType, importData
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -32,7 +32,6 @@ from eos.saveddata.fighter import Fighter
|
||||
from eos.saveddata.fit import Fit
|
||||
from eos.saveddata.module import Module
|
||||
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.muta import renderMutantAttrs, parseMutantAttrs
|
||||
@@ -155,6 +154,7 @@ def _resolve_module(hardware, sMkt, b_localized):
|
||||
|
||||
|
||||
def importXml(text, progress):
|
||||
from gui.fitCommands.helpers import activeStateLimit
|
||||
from .port import Port
|
||||
sMkt = Market.getInstance()
|
||||
doc = xml.dom.minidom.parseString(text)
|
||||
|
||||
62
tests/test_pyfa_cli_stats.py
Normal file
62
tests/test_pyfa_cli_stats.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from io import StringIO
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(os.path.realpath(os.path.join(script_dir, "..")))
|
||||
|
||||
from scripts import pyfa_cli_stats # noqa: E402
|
||||
|
||||
|
||||
ISHTAR_SPIDER_FIT = """[Ishtar, Spider]
|
||||
|
||||
Capacitor Power Relay II
|
||||
Drone Damage Amplifier II
|
||||
Explosive Armor Hardener II
|
||||
Multispectrum Energized Membrane II
|
||||
Reactive Armor Hardener
|
||||
Shadow Serpentis EM Armor Hardener
|
||||
|
||||
Cap Recharger II
|
||||
Omnidirectional Tracking Link II, Tracking Speed Script
|
||||
Medium Compact Pb-Acid Cap Battery
|
||||
Republic Fleet Large Cap Battery
|
||||
|
||||
Medium Remote Armor Repairer II
|
||||
Medium Remote Armor Repairer II
|
||||
Medium Remote Armor Repairer II
|
||||
Medium Remote Armor Repairer II
|
||||
|
||||
Medium Explosive Armor Reinforcer II
|
||||
Medium Thermal Armor Reinforcer II
|
||||
|
||||
|
||||
Valkyrie II x5
|
||||
Berserker II x5
|
||||
"""
|
||||
|
||||
|
||||
def test_ishtar_spider_remote_armor_reps():
|
||||
payload = {
|
||||
"fit": ISHTAR_SPIDER_FIT,
|
||||
}
|
||||
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
|
||||
argv = ["--savepath", tmp, json.dumps(payload)]
|
||||
|
||||
buf = StringIO()
|
||||
with redirect_stdout(buf):
|
||||
rc = pyfa_cli_stats.main(argv)
|
||||
|
||||
assert rc == 0
|
||||
|
||||
out = buf.getvalue()
|
||||
data = json.loads(out)
|
||||
|
||||
armor_rps = data["remote_reps_outgoing"]["current"]["armor"]
|
||||
|
||||
assert armor_rps is not None
|
||||
assert int(round(armor_rps)) == 171
|
||||
59
uv.lock
generated
59
uv.lock
generated
@@ -159,6 +159,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "contourpy"
|
||||
version = "1.3.3"
|
||||
@@ -369,6 +378,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
version = "1.4.9"
|
||||
@@ -593,6 +611,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.1"
|
||||
@@ -634,6 +661,11 @@ dependencies = [
|
||||
{ name = "wxpython" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "beautifulsoup4", specifier = "==4.12.2" },
|
||||
@@ -643,6 +675,7 @@ requires-dist = [
|
||||
{ name = "matplotlib", specifier = "==3.8.2" },
|
||||
{ name = "numpy", specifier = "==1.26.2" },
|
||||
{ name = "packaging", specifier = "==23.2" },
|
||||
{ name = "pytest", marker = "extra == 'dev'" },
|
||||
{ name = "python-dateutil", specifier = "==2.8.2" },
|
||||
{ name = "python-jose", specifier = "==3.3.0" },
|
||||
{ name = "pyyaml", specifier = "==6.0.1" },
|
||||
@@ -653,6 +686,16 @@ requires-dist = [
|
||||
{ name = "sqlalchemy", specifier = "==1.4.50" },
|
||||
{ name = "wxpython", specifier = "==4.2.1" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
@@ -663,6 +706,22 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.8.2"
|
||||
|
||||
Reference in New Issue
Block a user