first commit
This commit is contained in:
0
webapp/services/__init__.py
Normal file
0
webapp/services/__init__.py
Normal file
BIN
webapp/services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
webapp/services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
webapp/services/__pycache__/blueprints.cpython-313.pyc
Normal file
BIN
webapp/services/__pycache__/blueprints.cpython-313.pyc
Normal file
Binary file not shown.
BIN
webapp/services/__pycache__/ccookbook.cpython-313.pyc
Normal file
BIN
webapp/services/__pycache__/ccookbook.cpython-313.pyc
Normal file
Binary file not shown.
108
webapp/services/blueprints.py
Normal file
108
webapp/services/blueprints.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
import csv
|
||||
import os
|
||||
import json
|
||||
from typing import Optional, Dict
|
||||
from flask import current_app
|
||||
|
||||
# Cache: "name in lowercase" -> type_id
|
||||
_BP_NAME2ID: Dict[str, int] = {}
|
||||
# Cache: product type id -> (product name)
|
||||
_ID2NAME: Dict[int, str] = {}
|
||||
# Optional benutzerdefinierte Überschreibungen
|
||||
# Format: { "Structure Market Network": 123456, ... }
|
||||
_USER_MAP: Dict[str, int] = {}
|
||||
|
||||
|
||||
def _data_path(filename: str) -> str:
|
||||
return os.path.join(current_app.root_path, "data", filename)
|
||||
|
||||
|
||||
def _ensure_loaded() -> None:
|
||||
"""Liest typeIDs.csv einmalig ein und baut schnelle Nachschlage-Maps auf."""
|
||||
global _BP_NAME2ID, _ID2NAME, _USER_MAP
|
||||
if _BP_NAME2ID and _ID2NAME:
|
||||
return
|
||||
|
||||
# Benutzer-Overrides (optional)
|
||||
try:
|
||||
override = _data_path("blueprints.json")
|
||||
if os.path.isfile(override):
|
||||
with open(override, "r", encoding="utf-8") as f:
|
||||
_USER_MAP = json.load(f) or {}
|
||||
except Exception:
|
||||
_USER_MAP = {}
|
||||
|
||||
csv_path = _data_path("typeIDs.csv")
|
||||
if not os.path.isfile(csv_path):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(csv_path, "r", encoding="utf-8", newline="") as f:
|
||||
# Datei ist ;-separiert: id;name;group_id;iconID;graphicID
|
||||
reader = csv.reader(f, delimiter=";")
|
||||
header = next(reader, None) # "id;name;group_id;iconID;graphicID"
|
||||
for row in reader:
|
||||
if not row or len(row) < 2:
|
||||
continue
|
||||
try:
|
||||
tid = int(row[0])
|
||||
except Exception:
|
||||
continue
|
||||
name = (row[1] or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
_ID2NAME[tid] = name
|
||||
key = name.lower()
|
||||
# Merke auch die Blaupause separat (Name endet exakt auf " Blueprint")
|
||||
if key.endswith(" blueprint"):
|
||||
_BP_NAME2ID[key] = tid
|
||||
except Exception:
|
||||
# Map bleibt ggf. leer; Aufrufer bekommen dann None zurück
|
||||
pass
|
||||
|
||||
|
||||
def product_name_by_id(type_id: int) -> Optional[str]:
|
||||
"""Gibt den Produktnamen zur TypeID zurück (aus typeIDs.csv)."""
|
||||
_ensure_loaded()
|
||||
return _ID2NAME.get(type_id)
|
||||
|
||||
|
||||
def resolve_blueprint_id(product_name: str) -> Optional[int]:
|
||||
"""
|
||||
Ermittelt die Blueprint-TypeID zu einem *Produktnamen*.
|
||||
Regeln:
|
||||
1) Benutzer-Override (blueprints.json)
|
||||
2) Exakter Treffer "<Produktname> Blueprint" in typeIDs.csv
|
||||
3) Weicher Treffer (enthält Produktname & 'blueprint')
|
||||
"""
|
||||
if not product_name:
|
||||
return None
|
||||
|
||||
_ensure_loaded()
|
||||
|
||||
# 1) User-Override
|
||||
if product_name in _USER_MAP:
|
||||
return int(_USER_MAP[product_name])
|
||||
|
||||
wanted_exact = f"{product_name} Blueprint".lower()
|
||||
|
||||
# 2) Exakter Treffer
|
||||
if wanted_exact in _BP_NAME2ID:
|
||||
return _BP_NAME2ID[wanted_exact]
|
||||
|
||||
# 3) Weiche Suche
|
||||
prod_l = product_name.lower()
|
||||
for k, tid in _BP_NAME2ID.items():
|
||||
if prod_l in k and "blueprint" in k:
|
||||
return tid
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_blueprint_id_by_product_id(product_type_id: int) -> Optional[int]:
|
||||
"""Ermittelt die Blueprint-TypeID aus der *Produkt*-TypeID."""
|
||||
name = product_name_by_id(int(product_type_id))
|
||||
if not name:
|
||||
return None
|
||||
return resolve_blueprint_id(name)
|
||||
145
webapp/services/ccookbook.py
Normal file
145
webapp/services/ccookbook.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
from urllib.parse import urljoin
|
||||
import requests
|
||||
|
||||
__all__ = ["call_cookbook_build_cost", "call_eve_ref_materials"]
|
||||
|
||||
# ---------- HTTP Session mit Retries (für evecookbook + everef) ---------------
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
_SESSION = requests.Session()
|
||||
_RETRY = Retry(
|
||||
total=3, connect=3, read=3,
|
||||
backoff_factor=0.3,
|
||||
status_forcelist=(429, 500, 502, 503, 504),
|
||||
allowed_methods=frozenset(["GET", "POST"]),
|
||||
)
|
||||
_ADAPTER = HTTPAdapter(max_retries=_RETRY)
|
||||
_SESSION.mount("http://", _ADAPTER)
|
||||
_SESSION.mount("https://", _ADAPTER)
|
||||
|
||||
# ---------- EVE Cookbook: Build Cost ------------------------------------------
|
||||
|
||||
def call_cookbook_build_cost(
|
||||
blueprint_type_ids: Union[List[str], List[int]],
|
||||
*,
|
||||
quantity: int = 1,
|
||||
price_mode: str = "buy",
|
||||
additional_costs: int = 0,
|
||||
base_me: int = 0,
|
||||
components_me: int = 0,
|
||||
system: str = "Jita",
|
||||
facility_tax: int = 0,
|
||||
industry_structure_type: str = "Station",
|
||||
industry_rig: str = "None",
|
||||
reaction_structure_type: str = "Athanor",
|
||||
reaction_rig: str = "None",
|
||||
reaction_flag: str = "",
|
||||
blueprint_version: str = "tq",
|
||||
timeout: Tuple[float, float] = (5.0, 25.0),
|
||||
) -> Dict[str, Any]:
|
||||
base = "https://evecookbook.com/api/"
|
||||
url = urljoin(base, "buildCost")
|
||||
|
||||
params: List[Tuple[str, Any]] = []
|
||||
for bid in blueprint_type_ids:
|
||||
params.append(("blueprintTypeId", str(bid)))
|
||||
params.extend([
|
||||
("quantity", str(int(quantity))),
|
||||
("priceMode", price_mode),
|
||||
("additionalCosts", str(int(additional_costs))),
|
||||
("baseMe", str(int(base_me))),
|
||||
("componentsMe", str(int(components_me))),
|
||||
("system", system),
|
||||
("facilityTax", str(int(facility_tax))),
|
||||
("industryStructureType", industry_structure_type),
|
||||
("industryRig", industry_rig),
|
||||
("reactionStructureType", reaction_structure_type),
|
||||
("reactionRig", reaction_rig),
|
||||
("reactionFlag", reaction_flag),
|
||||
("blueprintVersion", blueprint_version),
|
||||
])
|
||||
|
||||
headers = {"Accept": "application/json"}
|
||||
resp = _SESSION.get(url, params=params, headers=headers, timeout=timeout)
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except requests.HTTPError as e:
|
||||
try:
|
||||
detail = resp.json()
|
||||
except Exception:
|
||||
detail = resp.text[:600]
|
||||
raise RuntimeError(f"Cookbook API {resp.status_code}: {detail}") from e
|
||||
|
||||
try:
|
||||
return resp.json() if resp.content else {}
|
||||
except Exception:
|
||||
return {"raw": resp.text}
|
||||
|
||||
# ---------- EVE Ref: Materials (BOM) ------------------------------------------
|
||||
|
||||
EVEREF_COST_URL = "https://api.everef.net/v1/industry/cost"
|
||||
REFDATA_TYPE_URL = "https://ref-data.everef.net/types/{type_id}"
|
||||
|
||||
_TYPE_NAME_CACHE: Dict[int, str] = {}
|
||||
|
||||
def _type_name(type_id: int) -> str:
|
||||
if type_id in _TYPE_NAME_CACHE:
|
||||
return _TYPE_NAME_CACHE[type_id]
|
||||
try:
|
||||
r = _SESSION.get(REFDATA_TYPE_URL.format(type_id=type_id), timeout=10)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
name = (data.get("name", {}) or {}).get("en") or data.get("name") or str(type_id)
|
||||
except Exception:
|
||||
name = str(type_id)
|
||||
_TYPE_NAME_CACHE[type_id] = name
|
||||
return name
|
||||
|
||||
def call_eve_ref_materials(params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Holt Materialliste (Manufacturing) zu einer Blueprint-ID.
|
||||
Erwartet mindestens: blueprintTypeId; optional: quantity, baseMe, industryStructureType.
|
||||
"""
|
||||
bp_id = int(params.get("blueprintTypeId"))
|
||||
runs = int(params.get("quantity", 1))
|
||||
me = int(params.get("baseMe", 0))
|
||||
te = int(params.get("baseTe", 0)) if params.get("baseTe") is not None else 0
|
||||
|
||||
structure_map = {"Station": None, "Raitaru": 35825, "Azbel": 35826, "Sotiyo": 35827}
|
||||
st_name = params.get("industryStructureType", "Station")
|
||||
st_id = structure_map.get(st_name)
|
||||
|
||||
q: Dict[str, Any] = {"blueprint_id": bp_id, "runs": runs, "me": me, "te": te}
|
||||
if st_id:
|
||||
q["structure_type_id"] = st_id
|
||||
|
||||
r = _SESSION.get(EVEREF_COST_URL, params=q, timeout=20)
|
||||
r.raise_for_status()
|
||||
payload = r.json()
|
||||
|
||||
manuf = (payload.get("manufacturing") or {})
|
||||
if not manuf:
|
||||
return {"materials": []}
|
||||
|
||||
first_key = next(iter(manuf.keys()))
|
||||
mats = manuf[first_key].get("materials", {}) or {}
|
||||
|
||||
out: List[Dict[str, Any]] = []
|
||||
for _, row in mats.items():
|
||||
try:
|
||||
tid = int(row.get("type_id"))
|
||||
except Exception:
|
||||
continue
|
||||
out.append({
|
||||
"type_id": tid,
|
||||
"name": _type_name(tid),
|
||||
"quantity": row.get("quantity"),
|
||||
"cost_per_unit": row.get("cost_per_unit"),
|
||||
"cost": row.get("cost"),
|
||||
})
|
||||
|
||||
out.sort(key=lambda x: (x.get("cost") or 0), reverse=True)
|
||||
return {"materials": out}
|
||||
121
webapp/services/cookbook.py
Normal file
121
webapp/services/cookbook.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
from urllib.parse import urljoin
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
__all__ = ["call_cookbook_build_cost", "call_eve_ref_materials"]
|
||||
|
||||
_SESSION = requests.Session()
|
||||
_RETRY = Retry(total=3, connect=3, read=3, backoff_factor=0.3,
|
||||
status_forcelist=(429,500,502,503,504),
|
||||
allowed_methods=frozenset(["GET","POST"]))
|
||||
_ADAPTER = HTTPAdapter(max_retries=_RETRY)
|
||||
_SESSION.mount("http://", _ADAPTER)
|
||||
_SESSION.mount("https://", _ADAPTER)
|
||||
|
||||
def call_cookbook_build_cost(
|
||||
blueprint_type_ids: Union[List[str], List[int]],
|
||||
*,
|
||||
quantity: int = 1,
|
||||
price_mode: str = "buy",
|
||||
additional_costs: int = 0,
|
||||
base_me: int = 0,
|
||||
components_me: int = 0,
|
||||
system: str = "Jita",
|
||||
facility_tax: int = 0,
|
||||
industry_structure_type: str = "Station",
|
||||
industry_rig: str = "None",
|
||||
reaction_structure_type: str = "Athanor",
|
||||
reaction_rig: str = "None",
|
||||
reaction_flag: str = "",
|
||||
blueprint_version: str = "tq",
|
||||
timeout: Tuple[float, float] = (5.0, 25.0),
|
||||
) -> Dict[str, Any]:
|
||||
base = "https://evecookbook.com/api/"
|
||||
url = urljoin(base, "buildCost")
|
||||
|
||||
params: List[Tuple[str, Any]] = []
|
||||
for bid in blueprint_type_ids:
|
||||
params.append(("blueprintTypeId", str(bid)))
|
||||
params.extend([
|
||||
("quantity", str(int(quantity))),
|
||||
("priceMode", price_mode),
|
||||
("additionalCosts", str(int(additional_costs))),
|
||||
("baseMe", str(int(base_me))),
|
||||
("componentsMe", str(int(components_me))),
|
||||
("system", system),
|
||||
("facilityTax", str(int(facility_tax))),
|
||||
("industryStructureType", industry_structure_type),
|
||||
("industryRig", industry_rig),
|
||||
("reactionStructureType", reaction_structure_type),
|
||||
("reactionRig", reaction_rig),
|
||||
("reactionFlag", reaction_flag),
|
||||
("blueprintVersion", blueprint_version),
|
||||
])
|
||||
|
||||
headers = {"Accept": "application/json"}
|
||||
resp = _SESSION.get(url, params=params, headers=headers, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
try:
|
||||
return resp.json() if resp.content else {}
|
||||
except Exception:
|
||||
return {"raw": resp.text}
|
||||
|
||||
EVEREF_COST_URL = "https://api.everef.net/v1/industry/cost"
|
||||
REFDATA_TYPE_URL = "https://ref-data.everef.net/types/{type_id}"
|
||||
_TYPE_NAME_CACHE: Dict[int, str] = {}
|
||||
|
||||
def _type_name(type_id: int) -> str:
|
||||
if type_id in _TYPE_NAME_CACHE:
|
||||
return _TYPE_NAME_CACHE[type_id]
|
||||
try:
|
||||
r = _SESSION.get(REFDATA_TYPE_URL.format(type_id=type_id), timeout=10)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
name = (data.get("name", {}) or {}).get("en") or data.get("name") or str(type_id)
|
||||
except Exception:
|
||||
name = str(type_id)
|
||||
_TYPE_NAME_CACHE[type_id] = name
|
||||
return name
|
||||
|
||||
def call_eve_ref_materials(params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
bp_id = int(params.get("blueprintTypeId"))
|
||||
runs = int(params.get("quantity", 1))
|
||||
me = int(params.get("baseMe", 0))
|
||||
te = int(params.get("baseTe", 0)) if params.get("baseTe") is not None else 0
|
||||
|
||||
structure_map = {"Station": None, "Raitaru": 35825, "Azbel": 35826, "Sotiyo": 35827}
|
||||
st_name = params.get("industryStructureType", "Station")
|
||||
st_id = structure_map.get(st_name)
|
||||
|
||||
q: Dict[str, Any] = {"blueprint_id": bp_id, "runs": runs, "me": me, "te": te}
|
||||
if st_id:
|
||||
q["structure_type_id"] = st_id
|
||||
|
||||
r = _SESSION.get(EVEREF_COST_URL, params=q, timeout=20)
|
||||
r.raise_for_status()
|
||||
payload = r.json()
|
||||
|
||||
manuf = (payload.get("manufacturing") or {})
|
||||
if not manuf:
|
||||
return {"materials": []}
|
||||
|
||||
first_key = next(iter(manuf.keys()))
|
||||
mats = manuf[first_key].get("materials", {}) or {}
|
||||
|
||||
out: List[Dict[str, Any]] = []
|
||||
for _, row in mats.items():
|
||||
try:
|
||||
tid = int(row.get("type_id"))
|
||||
except Exception:
|
||||
continue
|
||||
out.append({
|
||||
"type_id": tid,
|
||||
"name": _type_name(tid),
|
||||
"quantity": row.get("quantity"),
|
||||
"cost_per_unit": row.get("cost_per_unit"),
|
||||
"cost": row.get("cost"),
|
||||
})
|
||||
out.sort(key=lambda x: (x.get("cost") or 0), reverse=True)
|
||||
return {"materials": out}
|
||||
Reference in New Issue
Block a user