first commit

This commit is contained in:
2025-08-27 18:55:45 +02:00
commit 1dce2ebf2c
34 changed files with 52590 additions and 0 deletions

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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)

View 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
View 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}