Files
KX-Bridge-Release/orca_filaments.py
viewit 0322ade606 refactor: translate all logs, comments and API error strings to English
Logs are user-facing across all locales; comments and docstrings switch
to English for external contributors. No behavior change.
2026-07-06 22:33:25 +02:00

148 lines
5.7 KiB
Python

"""OrcaSlicer Filament-Profil Parser.
Shared between the generator (tools/gen_orca_filament_list.py) and the
Custom-Profile-Import-Endpoint (bridge/kobrax_moonraker_bridge.py).
Reads Orca filament JSON files (system or user profiles) and returns
them as a normalized list with (id, name, vendor, type, color).
"""
from __future__ import annotations
import json
import re
def first_str(value, default: str = "") -> str:
"""Orca profiles store some fields as ['value']. Returns the first
element as a string."""
if isinstance(value, list):
return str(value[0]) if value else default
if isinstance(value, str):
return value
return default
def clean_name(raw: str) -> str:
"""Strippt printer-/varianten-spezifische Suffixe:
'PolyTerra PLA @base''PolyTerra PLA'
'Anycubic PLA @Anycubic Kobra X 0.4 nozzle''Anycubic PLA'
'Anker Generic PLA 0.4 nozzle''Anker Generic PLA'
"""
name = re.sub(r"\s*@.*$", "", raw).strip()
name = re.sub(r"\s+\d+(\.\d+)?\s*nozzle\s*$", "", name, flags=re.IGNORECASE).strip()
return name or raw
def parse_profile(data: dict, by_name: dict | None = None,
path_vendor: str | None = None,
source_path: str = "",
system_index: list | None = None) -> dict | None:
"""Parses a single Orca filament profile into the bridge schema.
`by_name` is optionally a {name: [profile, ...]} index for inherits resolution
from the raw source tree (generator). For single-file imports (user file
from the OrcaSlicer user dir) we pass `system_index` instead -
the finished system profile list from orca_filaments.json. This lets
us derive filament_id/vendor/type/color via the `inherits` chain from
the system parent even when the user profile does not set these
fields itself (typically: user override profiles only contain tweaks).
Returns {id, name, vendor, type, color} or None when the profile
has no filament_id (e.g. abstract @base templates).
"""
if not isinstance(data, dict):
return None
# User profiles from the OrcaSlicer user dir often set NO "type" field -
# it comes from the system parent. We accept that when either "type"
# is explicitly "filament" OR an "inherits" points to another profile.
if data.get("type") not in (None, "filament") and not data.get("inherits"):
return None
if data.get("type") == "filament" and data.get("inherits") is None and not data.get("filament_id"):
# type=filament but no parent + no ID -> worthless stub
return None
inst = data.get("instantiation", "true")
if isinstance(inst, str) and inst.lower() == "false":
return None
# Build the system name index for the fallback lookup when system_index is set.
sys_by_name: dict[str, dict] = {}
if system_index:
for p in system_index:
if isinstance(p, dict) and p.get("name"):
sys_by_name[p["name"]] = p
def _resolve(key: str, depth: int = 5):
cur_list = [data]
for _ in range(depth):
for cur in cur_list:
v = cur.get(key)
if v not in ("", [], None, [""]) and v is not None:
return v
# Erst raw-Inherits via by_name (Generator-Pfad)
if by_name:
next_list: list[dict] = []
for cur in cur_list:
parent_name = cur.get("inherits")
if parent_name and parent_name in by_name:
next_list.extend(by_name[parent_name])
if next_list:
cur_list = next_list
continue
break
return None
def _resolve_via_system_index(key: str):
"""Inherits chain via system_index (clean_name match)."""
parent_raw = data.get("inherits")
if not parent_raw or not sys_by_name:
return None
parent_clean = clean_name(parent_raw)
sys_p = sys_by_name.get(parent_clean)
if not sys_p:
return None
# The system JSON already uses the normalized schema
mapping = {
"filament_id": "id",
"filament_vendor": "vendor",
"filament_type": "type",
"default_filament_colour": "color",
}
return sys_p.get(mapping.get(key, key))
def _resolve_full(key: str):
v = _resolve(key)
if v not in ("", [], None, [""]) and v is not None:
return v
return _resolve_via_system_index(key)
fid = _resolve_full("filament_id")
if not fid or not isinstance(fid, str):
return None
name_raw = data.get("name", fid)
name = clean_name(name_raw)
vendor = first_str(_resolve_full("filament_vendor")) or (path_vendor or "Generic")
ftype = first_str(_resolve_full("filament_type"), "")
color = first_str(_resolve_full("default_filament_colour"), "")
return {
"id": fid,
"name": name,
"vendor": vendor,
"type": ftype,
"color": color,
}
def parse_profile_bytes(blob: bytes, source_name: str = "",
system_index: list | None = None) -> dict | None:
"""Reads a single profile from JSON bytes. For the file upload path.
`system_index` is optionally the finished list from orca_filaments.json -
used for the inherits resolution of user profiles that do not carry the full
schema from the system parent."""
try:
data = json.loads(blob.decode("utf-8", errors="replace"))
except Exception:
return None
return parse_profile(data, source_path=source_name, system_index=system_index)