Compare commits

...

4 Commits

Author SHA1 Message Date
dw-0
229f317025 fix(backup): do not create redundant subdirectory on single file backup 2025-10-27 09:47:09 +01:00
dw-0
48c0ae7227 fix(backup): allow reusing existing backup directory and enhance copy options 2025-10-27 09:30:33 +01:00
dw-0
9c7b5fcb10 fix: update scp submodule so duplicate sections are preserved while editing configs (#738)
* fix: improve repository parsing logic to handle empty lines and comments more effectively

* fix: update scp submodule so duplicate sections are preserved while editing configs (#735)

* Squashed 'kiauh/core/submodules/simple_config_parser/' changes from f5eee99..5bc9e0a

5bc9e0a docs: update README
394dd7b refactor!: improve parsing and writing for config (#5)

git-subtree-dir: kiauh/core/submodules/simple_config_parser
git-subtree-split: 5bc9e0a50947f1be2f4877a10ab3a632774f82ea

* fix(logging): change warning to error message for config creation failure

* fix(config): improve readability by using descriptive variable names for options

(cherry picked from commit ae0a6b697e)

* Squashed 'kiauh/core/submodules/simple_config_parser/' changes from 5bc9e0a..eef8861

eef8861 refactor: update type hint for fallback parameter to Any
5d04325 Revert "chore: use Optional instead of | and None instead of _UNSET"

git-subtree-dir: kiauh/core/submodules/simple_config_parser
git-subtree-split: eef8861f126ddf84012ac8bed77b467926016d3e

* Squashed 'kiauh/core/submodules/simple_config_parser/' changes from eef8861..9c89612

9c89612 fix: correct assignment of raw value in option handling

git-subtree-dir: kiauh/core/submodules/simple_config_parser
git-subtree-split: 9c896124cf624e25410714649d306001250482f1

* fix: remove unnecessary whitespace in trusted_clients formatting
2025-10-26 22:03:26 +01:00
dw-0
191bdd4874 Revert "fix: update scp submodule so duplicate sections are preserved… (#737)
Revert "fix: update scp submodule so duplicate sections are preserved while editing configs (#735)"

This reverts commit ae0a6b697e.
2025-10-26 18:58:33 +01:00
4 changed files with 51 additions and 39 deletions

View File

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

View File

@@ -62,16 +62,16 @@ class BackupService:
target_name target_name
or f"{source_path.stem}_{self.timestamp}{source_path.suffix}" or f"{source_path.stem}_{self.timestamp}{source_path.suffix}"
) )
if target_path is not None:
backup_path = self._backup_root.joinpath(target_path, filename)
else:
backup_path = self._backup_root.joinpath(filename)
backup_path.mkdir(parents=True, exist_ok=True) backup_dir = self._backup_root
shutil.copy2(source_path, backup_path) if target_path is not None:
backup_dir = self._backup_root.joinpath(target_path)
backup_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(source_path, backup_dir.joinpath(filename))
Logger.print_ok( Logger.print_ok(
f"Successfully backed up '{source_path}' to '{backup_path}'" f"Successfully backed up '{source_path}' to '{backup_dir}'"
) )
return True return True
@@ -109,7 +109,16 @@ class BackupService:
else: else:
backup_path = self._backup_root.joinpath(backup_dir_name) backup_path = self._backup_root.joinpath(backup_dir_name)
shutil.copytree(source_path, backup_path) if backup_path.exists():
Logger.print_info(f"Reusing existing backup directory '{backup_path}'")
shutil.copytree(
source_path,
backup_path,
dirs_exist_ok=True,
symlinks=True,
ignore_dangling_symlinks=True,
)
Logger.print_ok( Logger.print_ok(
f"Successfully backed up '{source_path}' to '{backup_path}'" f"Successfully backed up '{source_path}' to '{backup_path}'"

View File

@@ -254,32 +254,34 @@ class KiauhSettings:
section: str, section: str,
option: str, option: str,
getter: Callable[[str, str, T | None], T], getter: Callable[[str, str, T | None], T],
fallback: T = None, fallback: T | None = None,
silent: bool = False, silent: bool = False,
) -> T: ) -> T | None:
if not self.__check_option_exists(section, option, fallback, silent): if not self.__check_option_exists(section, option, fallback, silent):
return fallback return fallback
return getter(section, option, fallback) return getter(section, option, fallback)
def __set_repo_state(self, section: str, repos: List[str]) -> List[Repository]: def __set_repo_state(self, section: str, repos: List[str]) -> List[Repository]:
_repos: List[Repository] = [] _repos: List[Repository] = []
for repo in repos: for raw in repos:
try: line = raw.strip()
if repo.strip().startswith("#") or repo.strip().startswith(";"):
continue
if "," in repo:
url, branch = repo.strip().split(",")
if not branch: if not line or line.startswith("#") or line.startswith(";"):
branch = "master" continue
try:
if "," in line:
url_part, branch_part = line.split(",")
url = url_part.strip()
branch = branch_part.strip() or "master"
else: else:
url = repo.strip() url = line
branch = "master" branch = "master"
# url must not be empty otherwise it's considered # url must not be empty otherwise it's considered
# as an unrecoverable, invalid configuration # as an unrecoverable, invalid configuration
if not url: if not url:
raise InvalidValueError(section, "repositories", repo) raise InvalidValueError(section, "repositories", line)
_repos.append(Repository(url.strip(), branch.strip())) _repos.append(Repository(url.strip(), branch.strip()))

View File

@@ -12,7 +12,7 @@ import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Set, Union from typing import Any, Callable, Dict, List, Set, Union
# definition of section line: # definition of section line:
# - the line MUST start with an opening square bracket - it is the first section marker # - the line MUST start with an opening square bracket - it is the first section marker
@@ -91,6 +91,9 @@ class LineType(Enum):
BLANK = "blank" BLANK = "blank"
_UNSET = object()
class NoSectionError(Exception): class NoSectionError(Exception):
"""Raised when a section is not defined""" """Raised when a section is not defined"""
@@ -342,7 +345,7 @@ class SimpleConfigParser:
for line in file: for line in file:
self._parse_line(line) self._parse_line(line)
def write_file(self, path: Union[str, Path]) -> None: def write_file(self, path: str | Path) -> None:
"""Write the config to a file""" """Write the config to a file"""
if path is None: if path is None:
raise ValueError("File path cannot be None") raise ValueError("File path cannot be None")
@@ -418,9 +421,7 @@ class SimpleConfigParser:
"""Check if an option exists in a section""" """Check if an option exists in a section"""
return self.has_section(section) and option in self.get_options(section) return self.has_section(section) and option in self.get_options(section)
def set_option( def set_option(self, section: str, option: str, value: str | List[str]) -> None:
self, section: str, option: str, value: Union[str, List[str]]
) -> None:
""" """
Set the value of an option in a section. If the section does not exist, Set the value of an option in a section. If the section does not exist,
it is created. If the option does not exist, it is created. it is created. If the option does not exist, it is created.
@@ -467,8 +468,8 @@ class SimpleConfigParser:
elif opt and isinstance(opt, Option) and isinstance(value, str): elif opt and isinstance(opt, Option) and isinstance(value, str):
curr_val = opt.value curr_val = opt.value
new_val = value new_val = value
opt.value = value opt.value = new_val
opt.raw.replace(curr_val, new_val) opt.raw = opt.raw.replace(curr_val, new_val)
elif opt and isinstance(opt, MultiLineOption) and isinstance(value, list): elif opt and isinstance(opt, MultiLineOption) and isinstance(value, list):
# note: we completely replace the existing values # note: we completely replace the existing values
@@ -561,7 +562,7 @@ class SimpleConfigParser:
else self._find_option_by_name(option, section=sects[0]) else self._find_option_by_name(option, section=sects[0])
) )
def getval(self, section: str, option: str, fallback: Optional[str] = None) -> str: def getval(self, section: str, option: str, fallback: str | _UNSET = _UNSET) -> str:
""" """
Return the value of the given option in the given section Return the value of the given option in the given section
@@ -576,12 +577,12 @@ class SimpleConfigParser:
return opt.value if opt else "" return opt.value if opt else ""
except (NoSectionError, NoOptionError): except (NoSectionError, NoOptionError):
if fallback is None: if fallback is _UNSET:
raise raise
return fallback return fallback
def getvals( def getvals(
self, section: str, option: str, fallback: Optional[List[str]] = None self, section: str, option: str, fallback: List[str] | _UNSET = _UNSET
) -> List[str]: ) -> List[str]:
""" """
Return the values of the given multi-line option in the given section Return the values of the given multi-line option in the given section
@@ -597,22 +598,22 @@ class SimpleConfigParser:
return [v.value for v in opt.values] if opt else [] return [v.value for v in opt.values] if opt else []
except (NoSectionError, NoOptionError): except (NoSectionError, NoOptionError):
if fallback is None: if fallback is _UNSET:
raise raise
return fallback return fallback
def getint(self, section: str, option: str, fallback: Optional[int] = None) -> int: def getint(self, section: str, option: str, fallback: int | _UNSET = _UNSET) -> int:
"""Return the value of the given option in the given section as an int""" """Return the value of the given option in the given section as an int"""
return self._get_conv(section, option, int, fallback=fallback) return self._get_conv(section, option, int, fallback=fallback)
def getfloat( def getfloat(
self, section: str, option: str, fallback: Optional[float] = None self, section: str, option: str, fallback: float | _UNSET = _UNSET
) -> float: ) -> float:
"""Return the value of the given option in the given section as a float""" """Return the value of the given option in the given section as a float"""
return self._get_conv(section, option, float, fallback=fallback) return self._get_conv(section, option, float, fallback=fallback)
def getboolean( def getboolean(
self, section: str, option: str, fallback: Optional[bool] = None self, section: str, option: str, fallback: bool | _UNSET = _UNSET
) -> bool: ) -> bool:
"""Return the value of the given option in the given section as a boolean""" """Return the value of the given option in the given section as a boolean"""
return self._get_conv( return self._get_conv(
@@ -631,14 +632,14 @@ class SimpleConfigParser:
self, self,
section: str, section: str,
option: str, option: str,
conv: Callable[[str], Union[int, float, bool]], conv: Callable[[str], int | float | bool],
fallback: Optional[Any] = None, fallback: Any = _UNSET,
) -> Union[int, float, bool]: ) -> int | float | bool:
"""Return the value of the given option in the given section as a converted value""" """Return the value of the given option in the given section as a converted value"""
try: try:
return conv(self.getval(section, option, fallback)) return conv(self.getval(section, option, fallback))
except (ValueError, TypeError, AttributeError) as e: except (ValueError, TypeError, AttributeError) as e:
if fallback is not None: if fallback is not _UNSET:
return fallback return fallback
raise ValueError( raise ValueError(
f"Cannot convert {self.getval(section, option)} to {conv.__name__}" f"Cannot convert {self.getval(section, option)} to {conv.__name__}"