Compare commits

..

2 Commits

Author SHA1 Message Date
Pavel Sorejs
2c5427c81e Merge 0d971c70a0 into c91816d13f 2025-04-03 00:05:25 +02:00
Pavel Sorejs
0d971c70a0 Add option to cusomize python binary for klipper and moonraker. Add option to not install moonraker speedups. 2025-04-02 23:53:36 +02:00
16 changed files with 195 additions and 225 deletions

View File

@@ -13,10 +13,6 @@ repositories:
https://github.com/Klipper3d/klipper
[moonraker]
# Moonraker supports two optional Python packages that can be used to reduce its CPU load
# If set to true, those packages will be installed during the Moonraker installation
optional_speedups: True
# add custom repositories here, if at least one is given, the first in the list will be used by default
# otherwise the official repository is used
#

View File

@@ -126,7 +126,7 @@ def create_example_moonraker_conf(
scp.read_file(target)
trusted_clients: List[str] = [
f" {'.'.join(ip)}\n",
*scp.getvals("authorization", "trusted_clients"),
*scp.getval("authorization", "trusted_clients"),
]
scp.set_option("server", "port", str(port))

View File

@@ -102,7 +102,6 @@ def install_client(
section=f"update_manager {client.name}",
instances=mr_instances,
options=[
("persistent_files", ["config.json"]),
("type", "web"),
("channel", "stable"),
("repo", str(client.repo_path)),

View File

@@ -118,8 +118,8 @@ def enable_mainsail_remotemode() -> None:
c_json = MainsailData().client_dir.joinpath("config.json")
with open(c_json, "r") as f:
config_data = json.load(f)
if config_data["instancesDB"] == "browser" or config_data["instancesDB"] == "json":
if config_data["instancesDB"] == "browser":
Logger.print_info("Remote mode already configured. Skipped ...")
return

View File

@@ -8,15 +8,14 @@
# ======================================================================= #
from __future__ import annotations
import shutil
from dataclasses import dataclass, field
from typing import Any, Callable, List, TypeVar
from typing import Any, List
from components.klipper import KLIPPER_REPO_URL
from components.moonraker import MOONRAKER_REPO_URL
from core.backup_manager.backup_manager import BackupManager
from core.logger import DialogType, Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
NoOptionError,
NoSectionError,
SimpleConfigParser,
)
from utils.input_utils import get_confirm
@@ -27,7 +26,13 @@ from kiauh import PROJECT_ROOT
DEFAULT_CFG = PROJECT_ROOT.joinpath("default.kiauh.cfg")
CUSTOM_CFG = PROJECT_ROOT.joinpath("kiauh.cfg")
T = TypeVar("T")
class NoValueError(Exception):
"""Raised when a required value is not defined for an option"""
def __init__(self, section: str, option: str):
msg = f"Missing value for option '{option}' in section '{section}'"
super().__init__(msg)
class InvalidValueError(Exception):
@@ -48,20 +53,17 @@ class Repository:
url: str
branch: str
@dataclass
class KlipperSettings:
repositories: List[Repository] | None = field(default=None)
use_python_binary: str | None = field(default=None)
@dataclass
class MoonrakerSettings:
optional_speedups: bool | None = field(default=None)
repositories: List[Repository] | None = field(default=None)
use_python_binary: str | None = field(default=None)
@dataclass
class WebUiSettings:
port: int | None = field(default=None)
@@ -72,7 +74,6 @@ class WebUiSettings:
# noinspection PyMethodMayBeStatic
class KiauhSettings:
__instance = None
__initialized = False
def __new__(cls, *args, **kwargs) -> "KiauhSettings":
if cls.__instance is None:
@@ -90,10 +91,11 @@ class KiauhSettings:
return getattr(self, item)
def __init__(self) -> None:
if not hasattr(self, "__initialized"):
self.__initialized = False
if self.__initialized:
return
self.__initialized = True
self.config = SimpleConfigParser()
self.kiauh = AppSettings()
self.klipper = KlipperSettings()
@@ -101,9 +103,8 @@ class KiauhSettings:
self.mainsail = WebUiSettings()
self.fluidd = WebUiSettings()
self.__read_config_set_internal_state()
self._load_config()
# todo: refactor this, at least rename to something else!
def get(self, section: str, option: str) -> str | int | bool:
"""
Get a value from the settings state by providing the section and option name as
@@ -121,175 +122,135 @@ class KiauhSettings:
raise
def save(self) -> None:
self.__write_internal_state_to_cfg()
self.__read_config_set_internal_state()
self._set_config_options_state()
self.config.write_file(CUSTOM_CFG)
self._load_config()
def __read_config_set_internal_state(self) -> None:
def _load_config(self) -> None:
if not CUSTOM_CFG.exists() and not DEFAULT_CFG.exists():
Logger.print_dialog(
DialogType.ERROR,
[
"No KIAUH configuration file found! Please make sure you have at least "
"one of the following configuration files in KIAUH's root directory:",
"● default.kiauh.cfg",
"● kiauh.cfg",
],
)
self.__kill()
cfg = CUSTOM_CFG if CUSTOM_CFG.exists() else DEFAULT_CFG
self.config.read_file(cfg)
needs_migration = self._check_deprecated_repo_config()
if needs_migration:
self._prompt_migration_dialog()
return
else:
# Only validate if no migration was needed
self._validate_cfg()
self.__set_internal_state()
def _validate_cfg(self) -> None:
def __err_and_kill(error: str) -> None:
Logger.print_error(f"Error validating kiauh.cfg: {error}")
kill()
# copy default config to custom config if it does not exist
if not CUSTOM_CFG.exists():
shutil.copyfile(DEFAULT_CFG, CUSTOM_CFG)
try:
self._validate_bool("kiauh", "backup_before_update")
self.config.read_file(CUSTOM_CFG)
self._validate_repositories("klipper", "repositories")
self._validate_repositories("moonraker", "repositories")
# check if there are deprecated repo_url and branch options in the kiauh.cfg
if self._check_deprecated_repo_config():
self._prompt_migration_dialog()
self._validate_int("mainsail", "port")
self._validate_bool("mainsail", "unstable_releases")
self.__set_internal_state()
self._validate_int("fluidd", "port")
self._validate_bool("fluidd", "unstable_releases")
self._validate_bool("moonraker", "optional_speedups")
def __set_internal_state(self) -> None:
# parse Kiauh options
self.kiauh.backup_before_update = self.__read_from_cfg(
"kiauh",
"backup_before_update",
self.config.getboolean,
False,
)
except ValueError:
err = f"Invalid value for option '{self._v_option}' in section '{self._v_section}'"
__err_and_kill(err)
except NoSectionError:
err = f"Missing section '{self._v_section}' in config file"
__err_and_kill(err)
except NoOptionError:
err = f"Missing option '{self._v_option}' in section '{self._v_section}'"
__err_and_kill(err)
except NoValueError:
err = f"Missing value for option '{self._v_option}' in section '{self._v_section}'"
__err_and_kill(err)
# parse Klipper options
self.klipper.use_python_binary = self.__read_from_cfg(
"klipper",
"use_python_binary",
self.config.getval,
None,
True,
)
kl_repos: List[str] = self.__read_from_cfg(
"klipper",
"repositories",
self.config.getvals,
[KLIPPER_REPO_URL],
)
self.klipper.repositories = self.__set_repo_state("klipper", kl_repos)
def _validate_bool(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option)
(bool(self.config.getboolean(section, option)))
# parse Moonraker options
self.moonraker.use_python_binary = self.__read_from_cfg(
"moonraker",
"use_python_binary",
self.config.getval,
None,
True,
)
self.moonraker.optional_speedups = self.__read_from_cfg(
"moonraker",
"optional_speedups",
self.config.getboolean,
True,
)
mr_repos: List[str] = self.__read_from_cfg(
"moonraker",
"repositories",
self.config.getvals,
[MOONRAKER_REPO_URL],
)
self.moonraker.repositories = self.__set_repo_state("moonraker", mr_repos)
def _validate_int(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option)
int(self.config.getint(section, option))
# parse Mainsail options
self.mainsail.port = self.__read_from_cfg(
"mainsail",
"port",
self.config.getint,
80,
)
self.mainsail.unstable_releases = self.__read_from_cfg(
"mainsail",
"unstable_releases",
self.config.getboolean,
False,
)
def _validate_str(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option)
v = self.config.getval(section, option)
# parse Fluidd options
self.fluidd.port = self.__read_from_cfg(
"fluidd",
"port",
self.config.getint,
80,
)
self.fluidd.unstable_releases = self.__read_from_cfg(
"fluidd",
"unstable_releases",
self.config.getboolean,
False,
)
if not v:
raise ValueError
def __check_option_exists(
self, section: str, option: str, fallback: Any, silent: bool = False
) -> bool:
has_section = self.config.has_section(section)
has_option = self.config.has_option(section, option)
def _validate_repositories(self, section: str, option: str) -> None:
self._v_section, self._v_option = (section, option)
repos = self.config.getval(section, option)
if not repos:
raise NoValueError(section, option)
if not (has_section and has_option):
if not silent:
Logger.print_warn(
f"Option '{option}' in section '{section}' not defined. Falling back to '{fallback}'."
)
return False
return True
def __read_bool_from_cfg(
self,
section: str,
option: str,
fallback: bool | None = None,
silent: bool = False,
) -> bool | None:
if not self.__check_option_exists(section, option, fallback, silent):
return fallback
return self.config.getboolean(section, option, fallback)
def __read_from_cfg(
self,
section: str,
option: str,
getter: Callable[[str, str, T | None], T],
fallback: T = None,
silent: bool = False,
) -> T:
if not self.__check_option_exists(section, option, fallback, silent):
return fallback
return getter(section, option, fallback)
def __set_repo_state(self, section: str, repos: List[str]) -> List[Repository]:
_repos: List[Repository] = []
for repo in repos:
if repo.strip().startswith("#") or repo.strip().startswith(";"):
continue
try:
if repo.strip().startswith("#") or repo.strip().startswith(";"):
continue
if "," in repo:
url, branch = repo.strip().split(",")
if not branch:
branch = "master"
if not url:
raise InvalidValueError(section, option, repo)
else:
url = repo.strip()
if not url:
raise InvalidValueError(section, option, repo)
except ValueError:
raise InvalidValueError(section, option, repo)
def __set_internal_state(self) -> None:
self.kiauh.backup_before_update = self.config.getboolean(
"kiauh", "backup_before_update"
)
self.moonraker.optional_speedups = self.config.getboolean("moonraker", "optional_speedups", True)
kl_repos = self.config.getval("klipper", "repositories")
self.klipper.repositories = self.__set_repo_state(kl_repos)
mr_repos = self.config.getval("moonraker", "repositories")
self.moonraker.repositories = self.__set_repo_state(mr_repos)
self.klipper.use_python_binary = self.config.getval("klipper", "use_python_binary", None)
self.moonraker.use_python_binary = self.config.getval("moonraker", "use_python_binary", None)
self.mainsail.port = self.config.getint("mainsail", "port")
self.mainsail.unstable_releases = self.config.getboolean(
"mainsail", "unstable_releases"
)
self.fluidd.port = self.config.getint("fluidd", "port")
self.fluidd.unstable_releases = self.config.getboolean(
"fluidd", "unstable_releases"
)
def __set_repo_state(self, repos: List[str]) -> List[Repository]:
_repos: List[Repository] = []
for repo in repos:
if repo.strip().startswith("#") or repo.strip().startswith(";"):
continue
if "," in repo:
url, branch = repo.strip().split(",")
if not branch:
branch = "master"
# url must not be empty otherwise it's considered
# as an unrecoverable, invalid configuration
if not url:
raise InvalidValueError(section, "repositories", repo)
_repos.append(Repository(url.strip(), branch.strip()))
except InvalidValueError as e:
Logger.print_error(f"Error parsing kiauh.cfg: {e}")
kill()
else:
url = repo.strip()
branch = "master"
_repos.append(Repository(url.strip(), branch.strip()))
return _repos
def __write_internal_state_to_cfg(self) -> None:
def _set_config_options_state(self) -> None:
"""Updates the config with current settings, preserving values that haven't been modified"""
if self.kiauh.backup_before_update is not None:
self.config.set_option(
@@ -327,8 +288,6 @@ class KiauhSettings:
"fluidd", "unstable_releases", str(self.fluidd.unstable_releases)
)
self.config.write_file(CUSTOM_CFG)
def _check_deprecated_repo_config(self) -> bool:
# repo_url and branch are deprecated - 2025.03.23
for section in ["klipper", "moonraker"]:
@@ -340,23 +299,22 @@ class KiauhSettings:
def _prompt_migration_dialog(self) -> None:
migration_1: List[str] = [
"Options 'repo_url' and 'branch' are now combined into a 'repositories' option.",
"The old 'repo_url' and 'branch' options are now combined under 'repositories'.",
"\n\n",
"● Old format:",
" [klipper]",
" repo_url: https://github.com/Klipper3d/klipper",
" branch: master",
"Example format:",
"[klipper]",
"repositories:",
" https://github.com/Klipper3d/klipper, master",
"\n\n",
"● New format:",
" [klipper]",
" repositories:",
" https://github.com/Klipper3d/klipper, master",
"[moonraker]",
"repositories:",
" https://github.com/Arksine/moonraker, master",
]
Logger.print_dialog(
DialogType.ATTENTION,
[
"Deprecated kiauh.cfg configuration found!",
"KAIUH can now attempt to automatically migrate the configuration.",
"Deprecated repository configuration found!",
"KAIUH can now attempt to automatically migrate your configuration.",
"\n\n",
*migration_1,
],
@@ -367,7 +325,7 @@ class KiauhSettings:
Logger.print_dialog(
DialogType.ERROR,
[
"Please update the configuration file manually.",
"Please update your configuration file manually.",
],
center_content=True,
)
@@ -408,7 +366,23 @@ class KiauhSettings:
self.config.write_file(CUSTOM_CFG)
self.config.read_file(CUSTOM_CFG) # reload config
# Validate the migrated config
self._validate_cfg()
self.__set_internal_state()
except Exception as e:
Logger.print_error(f"Error migrating configuration: {e}")
Logger.print_error("Please migrate manually.")
kill()
def __kill(self) -> None:
Logger.print_dialog(
DialogType.ERROR,
[
"No KIAUH configuration file found! Please make sure you have at least "
"one of the following configuration files in KIAUH's root directory:",
"● default.kiauh.cfg",
"● kiauh.cfg",
],
)
kill()

View File

@@ -314,7 +314,9 @@ class SimpleConfigParser:
elements.pop(i)
break
def getval(self, section: str, option: str, fallback: str | _UNSET = _UNSET) -> str:
def getval(
self, section: str, option: str, fallback: str | _UNSET = _UNSET
) -> str | List[str]:
"""
Return the value of the given option in the given section
@@ -327,34 +329,22 @@ class SimpleConfigParser:
if option not in self.get_options(section):
raise NoOptionError(option, section)
# Find the option in the elements list
for element in self.config[section]["elements"]:
if element["type"] is LineType.OPTION.value and element["name"] == option:
return str(element["value"].strip().replace("\n", ""))
return ""
except (NoSectionError, NoOptionError):
if fallback is _UNSET:
raise
return fallback
def getvals(self, section: str, option: str, fallback: List[str] | _UNSET = _UNSET) -> List[str]:
"""
Return the values of the given multi-line option in the given section
If the key is not found and 'fallback' is provided, it is used as
a fallback value.
"""
try:
if section not in self.get_sections():
raise NoSectionError(section)
if option not in self.get_options(section):
raise NoOptionError(option, section)
for element in self.config[section]["elements"]:
if element["type"] is LineType.OPTION_BLOCK.value and element["name"] == option:
return [val.strip() for val in element["value"] if val.strip()]
return []
if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value] and element["name"] == option:
raw_value = element["value"]
if isinstance(raw_value, str) and raw_value.endswith("\n"):
return raw_value[:-1].strip()
elif isinstance(raw_value, list):
values: List[str] = []
for i, val in enumerate(raw_value):
val = val.strip().strip("\n")
if len(val) < 1:
continue
values.append(val.strip())
return values
return str(raw_value)
raise NoOptionError(option, section)
except (NoSectionError, NoOptionError):
if fallback is _UNSET:
raise

View File

@@ -51,7 +51,7 @@ def test_getval(parser):
assert parser.getval("section_2", "option_2") == "value_2"
# test multiline option values
ml_val = parser.getvals("section number 5", "multi_option")
ml_val = parser.getval("section number 5", "multi_option")
assert isinstance(ml_val, list)
assert len(ml_val) > 0
@@ -164,7 +164,7 @@ def test_set_new_option(parser):
assert parser.getval("new_section", "very_new_option") == "very_new_value"
parser.set_option("section_2", "array_option", ["value_1", "value_2", "value_3"])
assert parser.getvals("section_2", "array_option") == [
assert parser.getval("section_2", "array_option") == [
"value_1",
"value_2",
"value_3",

View File

@@ -11,7 +11,7 @@ from __future__ import annotations
import shutil
import tempfile
from pathlib import Path
from typing import List, Tuple, Union
from typing import List, Tuple
from core.logger import Logger
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
@@ -19,7 +19,7 @@ from core.submodules.simple_config_parser.src.simple_config_parser.simple_config
)
from utils.instance_type import InstanceType
ConfigOption = Tuple[str, Union[str, List[str]]]
ConfigOption = Tuple[str, str]
def add_config_section(

View File

@@ -184,6 +184,9 @@ def install_python_requirements(target: Path, requirements: Path) -> None:
:return: None
"""
try:
# always update pip before installing requirements
update_python_pip(target)
Logger.print_status("Installing Python requirements ...")
command = [
target.joinpath("bin/pip").as_posix(),
@@ -193,7 +196,7 @@ def install_python_requirements(target: Path, requirements: Path) -> None:
]
result = run(command, stderr=PIPE, text=True)
if result.returncode != 0:
if result.returncode != 0 or result.stderr:
Logger.print_error(f"{result.stderr}", False)
raise VenvCreationFailedException("Installing Python requirements failed!")
@@ -213,6 +216,9 @@ def install_python_packages(target: Path, packages: List[str]) -> None:
:return: None
"""
try:
# always update pip before installing requirements
update_python_pip(target)
Logger.print_status("Installing Python requirements ...")
command = [
target.joinpath("bin/pip").as_posix(),
@@ -222,7 +228,7 @@ def install_python_packages(target: Path, packages: List[str]) -> None:
command.append(pkg)
result = run(command, stderr=PIPE, text=True)
if result.returncode != 0:
if result.returncode != 0 or result.stderr:
Logger.print_error(f"{result.stderr}", False)
raise VenvCreationFailedException("Installing Python requirements failed!")

View File

@@ -2,7 +2,7 @@
requires-python = ">=3.8"
[project.optional-dependencies]
dev=["ruff", "pyright"]
dev=["ruff", "mypy"]
[tool.ruff]
required-version = ">=0.9.10"
@@ -20,3 +20,14 @@ quote-style = "double"
[tool.ruff.lint]
extend-select = ["I"]
[tool.mypy]
python_version = "3.8"
platform = "linux"
# strict = true # TODO: enable this once everything is else is handled
check_untyped_defs = true
ignore_missing_imports = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
warn_unreachable = true

View File

@@ -1,6 +0,0 @@
{
"pythonVersion": "3.8",
"pythonPlatform": "Linux",
"typeCheckingMode": "standard",
"venvPath": "./.kiauh-env"
}

View File

@@ -1,2 +0,0 @@
ruff (>=0.9.10)
pyright

View File

@@ -280,6 +280,7 @@ function create_klipper_virtualenv() {
status_msg "Installing $("python${python_version}" -V) virtual environment..."
if virtualenv -p "python${python_version}" "${KLIPPY_ENV}"; then
(( python_version == 3 )) && "${KLIPPY_ENV}"/bin/pip install -U pip
"${KLIPPY_ENV}"/bin/pip install -r "${KLIPPER_DIR}"/scripts/klippy-requirements.txt
else
log_error "failure while creating python3 klippy-env"

View File

@@ -126,7 +126,7 @@ function update_klipperscreen() {
git checkout -f master && ok_msg "Checkout successfull"
if [[ $(md5sum "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt" | cut -d " " -f1) != "${old_md5}" ]]; then
status_msg "New dependencies detected..."
status_msg "New dependecies detected..."
"${KLIPPERSCREEN_ENV}"/bin/pip install -r "${KLIPPERSCREEN_DIR}/scripts/KlipperScreen-requirements.txt"
ok_msg "Dependencies have been installed!"
fi

View File

@@ -133,7 +133,7 @@ function update_mobileraker() {
git checkout -f main && ok_msg "Checkout successfull"
if [[ $(md5sum "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt" | cut -d " " -f1) != "${old_md5}" ]]; then
status_msg "New dependencies detected..."
status_msg "New dependecies detected..."
"${MOBILERAKER_ENV}"/bin/pip install -r "${MOBILERAKER_DIR}/scripts/mobileraker-requirements.txt"
ok_msg "Dependencies have been installed!"
fi

View File

@@ -336,6 +336,7 @@ function create_moonraker_virtualenv() {
[[ -d ${MOONRAKER_ENV} ]] && rm -rf "${MOONRAKER_ENV}"
if virtualenv -p /usr/bin/python3 "${MOONRAKER_ENV}"; then
"${MOONRAKER_ENV}"/bin/pip install -U pip
"${MOONRAKER_ENV}"/bin/pip install -r "${MOONRAKER_DIR}/scripts/moonraker-requirements.txt"
else
log_error "failure while creating python3 moonraker-env"