Compare commits
33
Commits
v6.0.3
...
b90a8f13b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b90a8f13b1 | ||
|
|
ea1b794afd | ||
|
|
647a7d9400 | ||
|
|
aa0609d143 | ||
|
|
2175bd55f9 | ||
|
|
a935e67431 | ||
|
|
2f41e52189 | ||
|
|
757344128a | ||
|
|
7ca08f9b30 | ||
|
|
308079a821 | ||
|
|
09a5d96b63 | ||
|
|
1f9d4c823a | ||
|
|
c8df9427b3 | ||
|
|
5414aba299 | ||
|
|
80948edbb4 | ||
|
|
a455edba93 | ||
|
|
810ab3a2fa | ||
|
|
6c9a78496a | ||
|
|
123ccde378 | ||
|
|
45fde808d2 | ||
|
|
8ba134f574 | ||
|
|
f951936b20 | ||
|
|
657d919378 | ||
|
|
24c9b9daa9 | ||
|
|
4c511017f1 | ||
|
|
372bab8847 | ||
|
|
d5062d41de | ||
|
|
e9459bd68e | ||
|
|
ee460663c9 | ||
|
|
6f0e0146ef | ||
|
|
229f317025 | ||
|
|
48c0ae7227 | ||
|
|
9c7b5fcb10 |
No files matched your search
@@ -7,6 +7,7 @@
|
||||
*.tmp
|
||||
__pycache__
|
||||
.kiauh-env
|
||||
.venv
|
||||
*.code-workspace
|
||||
*.iml
|
||||
kiauh.cfg
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# AGENTS.md - KIAUH Development Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
KIAUH (Klipper Installation And Update Helper) is a Python-based installation script for Klipper 3D printer firmware and related components written in Python 3.8+.
|
||||
|
||||
## Running KIAUH
|
||||
|
||||
```bash
|
||||
./kiauh.sh
|
||||
```
|
||||
|
||||
**Important:** Must NOT run as root. The script will exit if EUID is 0.
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Install dev dependencies
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
# Lint (ruff)
|
||||
ruff check .
|
||||
|
||||
# Format
|
||||
ruff format .
|
||||
|
||||
# Typecheck
|
||||
mypy kiauh
|
||||
|
||||
# Run tests
|
||||
pytest
|
||||
|
||||
# Run specific test file
|
||||
pytest kiauh/core/simple_config_parser/tests/public_api/test_options_api.py
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- New tests should be placed near their corresponding components/modules (e.g., `kiauh/components/klipper/*/test_*.py`)
|
||||
- Always use a `tests/` subdirectory
|
||||
- Existing pytest setup in `kiauh/core/simple_config_parser/tests/` serves as reference
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `kiauh.sh` - Bash entry point, sets PYTHONPATH and calls main.py
|
||||
- `kiauh/main.py` - Python entry point
|
||||
- `kiauh/core/` - Core functionality (menus, services, settings, types)
|
||||
- `kiauh/components/` - Klipper components (klipper, moonraker, webui_client, etc.)
|
||||
- `kiauh/extensions/` - Extension system for optional addons (obico, octoprint, spoolman, etc.)
|
||||
- `kiauh/core/simple_config_parser/` - Custom INI-style config parser for Klipper configs
|
||||
- `kiauh/core/simple_config_parser/src/simple_config_parser/` - Submodule (git subtree)
|
||||
|
||||
## Key Quirks
|
||||
|
||||
1. **Python version:** Requires Python 3.8+ (checked in kiauh.sh)
|
||||
2. **Config files:** KIAUH uses `kiauh.cfg` in project root (not .ini format - it's parsed by simple_config_parser)
|
||||
3. **Submodule:** `kiauh/core/simple_config_parser/` is a git subtree, not a submodule
|
||||
4. **Branch check:** KIAUH only checks for updates on master branch (not develop)
|
||||
5. **Target:** Designed to run on Raspberry Pi OS / Debian-based distros
|
||||
|
||||
## Code Style
|
||||
|
||||
- 4-space indentation
|
||||
- 88 character line length
|
||||
- Double quotes
|
||||
- LF line endings
|
||||
- Type hints required (mypy checks)
|
||||
- Ruff with I (isort) enabled
|
||||
@@ -143,7 +143,7 @@ changes!**
|
||||
|
||||
<tr>
|
||||
<th><h3><a href="https://github.com/fluidd-core/fluidd">Fluidd</a></h3></th>
|
||||
<th><h3><a href="https://github.com/jordanruthe/KlipperScreen">KlipperScreen</a></h3></th>
|
||||
<th><h3><a href="https://github.com/KlipperScreen/KlipperScreen">KlipperScreen</a></h3></th>
|
||||
<th><h3><a href="https://github.com/OctoPrint/OctoPrint">OctoPrint</a></h3></th>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -153,7 +153,7 @@ changes!**
|
||||
</tr>
|
||||
<tr>
|
||||
<th>by <a href="https://github.com/fluidd-core">fluidd-core</a></th>
|
||||
<th>by <a href="https://github.com/jordanruthe">jordanruthe</a></th>
|
||||
<th>by <a href="https://github.com/alfrix">alfrix</a></th>
|
||||
<th>by <a href="https://github.com/OctoPrint">OctoPrint</a></th>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#=======================================================================#
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -15,18 +15,30 @@ clear -x
|
||||
# make sure we have the correct permissions while running the script
|
||||
umask 022
|
||||
|
||||
# gets the path where this script is located
|
||||
KIAUH_SRCDIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd)
|
||||
|
||||
# colors
|
||||
white="\033[37m"
|
||||
cyan="\033[96m"
|
||||
red="\033[91m"
|
||||
yellow="\033[93m"
|
||||
green="\033[92m"
|
||||
|
||||
#===================================================#
|
||||
#=================== UPDATE KIAUH ==================#
|
||||
#===================================================#
|
||||
|
||||
function update_kiauh() {
|
||||
status_msg "Updating KIAUH ..."
|
||||
echo "Updating KIAUH ..."
|
||||
|
||||
cd "${KIAUH_SRCDIR}"
|
||||
git reset --hard && git pull
|
||||
|
||||
ok_msg "Update complete! Please restart KIAUH."
|
||||
exit 0
|
||||
echo "Update complete! Restarting..."
|
||||
sleep 1
|
||||
exec "$0" "$@" # restarts the script
|
||||
|
||||
}
|
||||
|
||||
#===================================================#
|
||||
@@ -65,16 +77,18 @@ function kiauh_update_dialog() {
|
||||
echo -e "\-------------------------------------------------------/"
|
||||
|
||||
local yn
|
||||
read -p "${cyan}###### Do you want to update now? (Y/n):${white} " yn
|
||||
echo -ne "${cyan}###### Do you want to update now? (Y/n):${white} "
|
||||
read yn
|
||||
while true; do
|
||||
case "${yn}" in
|
||||
Y|y|Yes|yes|"")
|
||||
do_action "update_kiauh"
|
||||
update_kiauh
|
||||
break;;
|
||||
N|n|No|no)
|
||||
break;;
|
||||
*)
|
||||
deny_action "kiauh_update_dialog";;
|
||||
echo -e "${red}Invalid input. Please try again.${white}"
|
||||
kiauh_update_dialog;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -47,7 +47,7 @@ from utils.sys_utils import (
|
||||
|
||||
def install_crowsnest() -> None:
|
||||
# Step 1: Clone crowsnest repo
|
||||
git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master")
|
||||
git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR)
|
||||
|
||||
# Step 2: Install dependencies
|
||||
check_install_dependencies({"make"})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -34,7 +34,7 @@ from core.constants import CURRENT_USER
|
||||
from core.instance_manager.base_instance import SUFFIX_BLACKLIST
|
||||
from core.logger import DialogType, Logger
|
||||
from core.services.backup_service import BackupService
|
||||
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
|
||||
from core.simple_config_parser.simple_config_parser import (
|
||||
SimpleConfigParser,
|
||||
)
|
||||
from core.types.component_status import ComponentStatus
|
||||
@@ -113,7 +113,7 @@ def check_user_groups() -> None:
|
||||
|
||||
if not get_confirm(f"Add user '{CURRENT_USER}' to group(s) now?"):
|
||||
log = "Skipped adding user to required groups. You might encounter issues."
|
||||
Logger.warn(log)
|
||||
Logger.print_warn(log)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -237,7 +237,6 @@ def install_input_shaper_deps() -> None:
|
||||
"If you agree, the following additional system packages will be installed:",
|
||||
"● python3-numpy",
|
||||
"● python3-matplotlib",
|
||||
"● libatlas-base-dev",
|
||||
"● libopenblas-dev",
|
||||
"\n\n",
|
||||
"Also, the following Python package will be installed:",
|
||||
@@ -253,7 +252,6 @@ def install_input_shaper_deps() -> None:
|
||||
apt_deps = (
|
||||
"python3-numpy",
|
||||
"python3-matplotlib",
|
||||
"libatlas-base-dev",
|
||||
"libopenblas-dev",
|
||||
)
|
||||
check_install_dependencies({*apt_deps})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -236,9 +236,11 @@ class KlipperSelectMcuConnectionMenu(BaseMenu):
|
||||
if len(self.flash_options.mcu_list) < 1:
|
||||
Logger.print_warn("No MCUs found!")
|
||||
Logger.print_warn("Make sure they are connected and repeat this step.")
|
||||
time.sleep(3)
|
||||
return
|
||||
|
||||
# if standalone is True, we only display the MCUs to the user and return
|
||||
if self.__standalone and len(self.flash_options.mcu_list) > 0:
|
||||
if self.__standalone:
|
||||
Logger.print_ok("The following MCUs were found:", prefix=False)
|
||||
for i, mcu in enumerate(self.flash_options.mcu_list):
|
||||
print(f" ● MCU #{i}: {Color.CYAN}{mcu}{Color.RST}")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -25,7 +25,7 @@ from components.moonraker import (
|
||||
from core.constants import CURRENT_USER
|
||||
from core.instance_manager.base_instance import BaseInstance
|
||||
from core.logger import Logger
|
||||
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
|
||||
from core.simple_config_parser.simple_config_parser import (
|
||||
SimpleConfigParser,
|
||||
)
|
||||
from utils.fs_utils import create_folders
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -25,7 +25,7 @@ from components.moonraker.utils.sysdeps_parser import SysDepsParser
|
||||
from components.webui_client.base_data import BaseWebClient
|
||||
from core.logger import Logger
|
||||
from core.services.backup_service import BackupService
|
||||
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
|
||||
from core.simple_config_parser.simple_config_parser import (
|
||||
SimpleConfigParser,
|
||||
)
|
||||
from core.types.component_status import ComponentStatus
|
||||
@@ -123,7 +123,7 @@ def create_example_moonraker_conf(
|
||||
scp = SimpleConfigParser()
|
||||
scp.read_file(target)
|
||||
trusted_clients: List[str] = [
|
||||
f" {'.'.join(ip)}\n",
|
||||
f"{'.'.join(ip)}",
|
||||
*scp.getvals("authorization", "trusted_clients"),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -8,7 +8,7 @@
|
||||
# ======================================================================= #
|
||||
|
||||
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from components.klipper.klipper import Klipper
|
||||
from components.moonraker.moonraker import Moonraker
|
||||
@@ -27,6 +27,7 @@ def run_client_config_removal(
|
||||
client_config: BaseWebClientConfig,
|
||||
kl_instances: List[Klipper],
|
||||
mr_instances: List[Moonraker],
|
||||
svc: Optional[BackupService] = None,
|
||||
) -> Message:
|
||||
completion_msg = Message(
|
||||
title=f"{client_config.display_name} Removal Process completed",
|
||||
@@ -36,12 +37,15 @@ def run_client_config_removal(
|
||||
if run_remove_routines(client_config.config_dir):
|
||||
completion_msg.text.append(f"● {client_config.display_name} removed")
|
||||
|
||||
BackupService().backup_printer_config_dir()
|
||||
if svc is None:
|
||||
svc = BackupService()
|
||||
|
||||
svc.backup_moonraker_conf()
|
||||
completion_msg = remove_moonraker_config_section(
|
||||
completion_msg, client_config, mr_instances
|
||||
)
|
||||
|
||||
svc.backup_printer_cfg()
|
||||
completion_msg = remove_printer_config_section(
|
||||
completion_msg, client_config, kl_instances
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -41,6 +41,7 @@ def run_client_removal(
|
||||
)
|
||||
mr_instances: List[Moonraker] = get_instances(Moonraker)
|
||||
kl_instances: List[Klipper] = get_instances(Klipper)
|
||||
svc = BackupService()
|
||||
|
||||
if backup_config:
|
||||
version = ""
|
||||
@@ -49,7 +50,6 @@ def run_client_removal(
|
||||
with open(src.joinpath(".version"), "r") as v:
|
||||
version = v.readlines()[0]
|
||||
|
||||
svc = BackupService()
|
||||
target_path = svc.backup_root.joinpath(f"{client.client_dir.name}_{version}")
|
||||
success = svc.backup_file(
|
||||
source_path=client.config_file,
|
||||
@@ -67,7 +67,7 @@ def run_client_removal(
|
||||
if remove_client_nginx_logs(client, kl_instances):
|
||||
completion_msg.text.append("● NGINX logs removed")
|
||||
|
||||
BackupService().backup_moonraker_conf()
|
||||
svc.backup_moonraker_conf()
|
||||
section = f"update_manager {client_name}"
|
||||
handled_instances: List[Moonraker] = remove_config_section(
|
||||
section, mr_instances
|
||||
@@ -83,6 +83,7 @@ def run_client_removal(
|
||||
client.client_config,
|
||||
kl_instances,
|
||||
mr_instances,
|
||||
svc,
|
||||
)
|
||||
if cfg_completion_msg.color == Color.GREEN:
|
||||
completion_msg.text.extend(cfg_completion_msg.text[1:])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
from json import JSONDecodeError
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, CalledProcessError, run
|
||||
from typing import List, get_args
|
||||
@@ -32,7 +33,7 @@ from core.constants import (
|
||||
from core.logger import Logger
|
||||
from core.services.backup_service import BackupService
|
||||
from core.settings.kiauh_settings import KiauhSettings, WebUiSettings
|
||||
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
|
||||
from core.simple_config_parser.simple_config_parser import (
|
||||
SimpleConfigParser,
|
||||
)
|
||||
from core.types.color import Color
|
||||
@@ -77,10 +78,10 @@ def get_current_client_config() -> str:
|
||||
installed = [c for c in clients if c.client_config.config_dir.exists()]
|
||||
|
||||
if not installed:
|
||||
return Color.apply("-", Color.CYAN)
|
||||
return str(Color.apply("-", Color.CYAN))
|
||||
elif len(installed) == 1:
|
||||
cfg = installed[0].client_config
|
||||
return Color.apply(cfg.display_name, Color.CYAN)
|
||||
return str(Color.apply(cfg.display_name, Color.CYAN))
|
||||
|
||||
# at this point, both client config folders exists, so we need to check
|
||||
# which are actually included in the printer.cfg of all klipper instances
|
||||
@@ -99,18 +100,18 @@ def get_current_client_config() -> str:
|
||||
|
||||
# if both are included in the same file, we have a potential conflict
|
||||
if includes_mainsail and includes_fluidd:
|
||||
return Color.apply("Conflict", Color.YELLOW)
|
||||
return str(Color.apply("Conflict", Color.YELLOW))
|
||||
|
||||
if not mainsail_includes and not fluidd_includes:
|
||||
# there are no includes at all, even though the client config folders exist
|
||||
return Color.apply("-", Color.CYAN)
|
||||
return str(Color.apply("-", Color.CYAN))
|
||||
elif len(fluidd_includes) > len(mainsail_includes):
|
||||
# there are more instances that include fluidd than mainsail
|
||||
return Color.apply(fluidd.client_config.display_name, Color.CYAN)
|
||||
return str(Color.apply(fluidd.client_config.display_name, Color.CYAN))
|
||||
else:
|
||||
# there are the same amount of non-conflicting includes for each config
|
||||
# or more instances include mainsail than fluidd
|
||||
return Color.apply(mainsail.client_config.display_name, Color.CYAN)
|
||||
return str(Color.apply(mainsail.client_config.display_name, Color.CYAN))
|
||||
|
||||
|
||||
def enable_mainsail_remotemode() -> None:
|
||||
@@ -154,15 +155,32 @@ def get_local_client_version(client: BaseWebClient) -> str | None:
|
||||
|
||||
if not client.client_dir.exists():
|
||||
return None
|
||||
if not relinfo_file.is_file() and not version_file.is_file():
|
||||
return "n/a"
|
||||
|
||||
# try to get version from release_info.json first
|
||||
if relinfo_file.is_file():
|
||||
with open(relinfo_file, "r") as f:
|
||||
return str(json.load(f)["version"])
|
||||
else:
|
||||
with open(version_file, "r") as f:
|
||||
return f.readlines()[0]
|
||||
try:
|
||||
if relinfo_file.stat().st_size == 0:
|
||||
raise JSONDecodeError("Empty file", "", 0)
|
||||
with open(relinfo_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
raw_version = data.get("version")
|
||||
if raw_version is not None:
|
||||
parsed = str(raw_version).strip()
|
||||
if parsed:
|
||||
return parsed
|
||||
except (JSONDecodeError, OSError):
|
||||
Logger.print_error("Invalid 'release_info.json'")
|
||||
|
||||
# fallback to .version file
|
||||
if version_file.is_file():
|
||||
try:
|
||||
with open(version_file, "r") as f:
|
||||
line = f.readline().strip()
|
||||
return line or None
|
||||
except OSError:
|
||||
Logger.print_error("Unable to read '.version'")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_remote_client_version(client: BaseWebClient) -> str | None:
|
||||
@@ -427,9 +445,9 @@ def get_client_port_selection(
|
||||
while True:
|
||||
_type = "Reconfigure" if reconfigure else "Configure"
|
||||
question = f"{_type} {client.display_name} for port"
|
||||
port_input = get_number_input(question, min_value=80, default=port)
|
||||
port_input: int | None = get_number_input(question, min_value=80, default=port)
|
||||
|
||||
if port_input not in ports_in_use:
|
||||
if port_input and port_input not in ports_in_use:
|
||||
client_settings: WebUiSettings = settings[client.name]
|
||||
client_settings.port = port_input
|
||||
settings.save()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -97,7 +97,7 @@ class ClientInstallMenu(BaseMenu):
|
||||
self.message_service.set_message(message)
|
||||
|
||||
def _get_current_port(self) -> int:
|
||||
curr_port = get_nginx_listen_port(self.client.nginx_config)
|
||||
curr_port: int | None = get_nginx_listen_port(self.client.nginx_config)
|
||||
if curr_port is None:
|
||||
# if the port is not found in the config file we use
|
||||
# the default port from the kiauh settings as fallback
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -58,7 +58,7 @@ class BackupMenu(BaseMenu):
|
||||
|
||||
def print_menu(self) -> None:
|
||||
line1 = Color.apply(
|
||||
"INFO: Backups are located in '~/kiauh-backups'", Color.YELLOW
|
||||
"INFO: Backups are located in '~/kiauh_backups'", Color.YELLOW
|
||||
)
|
||||
menu = textwrap.dedent(
|
||||
f"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -95,8 +95,8 @@ class MainMenu(BaseMenu):
|
||||
status_data: ComponentStatus = status_fn(*args)
|
||||
code: int = status_data.status
|
||||
status: StatusText = StatusMap[code]
|
||||
owner: str = trunc_string(status_data.owner, 23)
|
||||
repo: str = trunc_string(status_data.repo, 23)
|
||||
owner: str = trunc_string(status_data.owner, 23) if status_data.owner else '-'
|
||||
repo: str = trunc_string(status_data.repo, 23) if status_data.repo else '-'
|
||||
instance_count: int = status_data.instances
|
||||
|
||||
count_txt: str = ""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -100,11 +100,11 @@ class SettingsMenu(BaseMenu):
|
||||
def trim_repo_url(repo: str) -> str:
|
||||
return repo.replace(".git", "").replace("https://", "").replace("git@", "")
|
||||
|
||||
if not klipper_status.repo == "-":
|
||||
if klipper_status.repo:
|
||||
url = trim_repo_url(klipper_status.repo_url)
|
||||
self.kl_repo_url = Color.apply(url, Color.CYAN)
|
||||
self.kl_branch = Color.apply(klipper_status.branch, Color.CYAN)
|
||||
if not moonraker_status.repo == "-":
|
||||
if moonraker_status.repo:
|
||||
url = trim_repo_url(moonraker_status.repo_url)
|
||||
self.mr_repo_url = Color.apply(url, Color.CYAN)
|
||||
self.mr_branch = Color.apply(moonraker_status.branch, Color.CYAN)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -251,20 +251,23 @@ class UpdateMenu(BaseMenu):
|
||||
self._set_status_data("klipperscreen", get_klipperscreen_status)
|
||||
self._set_status_data("crowsnest", get_crowsnest_status)
|
||||
|
||||
self._fetch_system_package_update_status()
|
||||
|
||||
def _fetch_system_package_update_status(self) -> None:
|
||||
update_system_package_lists(silent=True)
|
||||
self.packages = get_upgradable_packages()
|
||||
self.package_count = len(self.packages)
|
||||
|
||||
def _format_local_status(self, local_version, remote_version) -> str:
|
||||
color = Color.RED
|
||||
if not local_version:
|
||||
if local_version is None:
|
||||
color = Color.RED
|
||||
elif local_version == remote_version:
|
||||
color = Color.GREEN
|
||||
elif local_version != remote_version:
|
||||
color = Color.YELLOW
|
||||
|
||||
return Color.apply(local_version or "-", color)
|
||||
return str(Color.apply(local_version or '-', color))
|
||||
|
||||
def _set_status_data(self, name: str, status_fn: Callable, *args) -> None:
|
||||
comp_status: ComponentStatus = status_fn(*args)
|
||||
@@ -290,7 +293,13 @@ class UpdateMenu(BaseMenu):
|
||||
return self.status_data[name]["installed"]
|
||||
|
||||
def _is_update_available(self, name: str) -> bool:
|
||||
return self.status_data[name]["local"] != self.status_data[name]["remote"]
|
||||
local = self.status_data[name]["local"]
|
||||
remote = self.status_data[name]["remote"]
|
||||
|
||||
if local is None or remote is None:
|
||||
return False
|
||||
|
||||
return local != remote
|
||||
|
||||
def _run_update_routine(self, name: str, update_fn: Callable, *args) -> None:
|
||||
display_name = self.status_data[name]["display_name"]
|
||||
@@ -306,6 +315,27 @@ class UpdateMenu(BaseMenu):
|
||||
|
||||
update_fn(*args)
|
||||
|
||||
self._refresh_component_status(name)
|
||||
|
||||
def _refresh_component_status(self, name: str) -> None:
|
||||
"""Refresh the status data for a component after an update."""
|
||||
if name == "klipper":
|
||||
self._set_status_data("klipper", get_klipper_status)
|
||||
elif name == "moonraker":
|
||||
self._set_status_data("moonraker", get_moonraker_status)
|
||||
elif name == "mainsail":
|
||||
self._set_status_data("mainsail", get_client_status, self.mainsail_data, True)
|
||||
elif name == "mainsail_config":
|
||||
self._set_status_data("mainsail_config", get_client_config_status, self.mainsail_data)
|
||||
elif name == "fluidd":
|
||||
self._set_status_data("fluidd", get_client_status, self.fluidd_data, True)
|
||||
elif name == "fluidd_config":
|
||||
self._set_status_data("fluidd_config", get_client_config_status, self.fluidd_data)
|
||||
elif name == "klipperscreen":
|
||||
self._set_status_data("klipperscreen", get_klipperscreen_status)
|
||||
elif name == "crowsnest":
|
||||
self._set_status_data("crowsnest", get_crowsnest_status)
|
||||
|
||||
def _run_system_updates(self) -> None:
|
||||
if not self.packages:
|
||||
Logger.print_info("No system upgrades available!")
|
||||
@@ -313,15 +343,20 @@ class UpdateMenu(BaseMenu):
|
||||
|
||||
try:
|
||||
pkgs: str = ", ".join(self.packages)
|
||||
|
||||
Logger.print_dialog(
|
||||
DialogType.CUSTOM,
|
||||
["The following packages will be upgraded:", "\n\n", pkgs],
|
||||
custom_title="UPGRADABLE SYSTEM UPDATES",
|
||||
)
|
||||
if not get_confirm("Continue?"):
|
||||
|
||||
if not get_confirm("Upgrade packages?"):
|
||||
return
|
||||
|
||||
Logger.print_status("Upgrading system packages ...")
|
||||
|
||||
upgrade_system_packages(self.packages)
|
||||
self._fetch_system_package_update_status()
|
||||
except Exception as e:
|
||||
Logger.print_error(f"Error upgrading system packages:\n{e}")
|
||||
raise
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -22,6 +22,7 @@ from utils.instance_utils import get_instances
|
||||
class BackupService:
|
||||
def __init__(self):
|
||||
self._backup_root = Path.home().joinpath("kiauh_backups")
|
||||
self._timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
@property
|
||||
def backup_root(self) -> Path:
|
||||
@@ -29,7 +30,7 @@ class BackupService:
|
||||
|
||||
@property
|
||||
def timestamp(self) -> str:
|
||||
return datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
return self._timestamp
|
||||
|
||||
################################################
|
||||
# GENERIC BACKUP METHODS
|
||||
@@ -62,16 +63,21 @@ class BackupService:
|
||||
target_name
|
||||
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)
|
||||
shutil.copy2(source_path, backup_path)
|
||||
backup_dir = self._backup_root
|
||||
if target_path is not None:
|
||||
backup_dir = self._backup_root.joinpath(target_path)
|
||||
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
target_path = backup_dir.joinpath(filename)
|
||||
if target_path.exists():
|
||||
Logger.print_info(f"File '{target_path}' already exists. Skipping ...")
|
||||
return True
|
||||
|
||||
shutil.copy2(source_path, target_path)
|
||||
|
||||
Logger.print_ok(
|
||||
f"Successfully backed up '{source_path}' to '{backup_path}'"
|
||||
f"Successfully backed up '{source_path}' to '{target_path}'"
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -109,7 +115,27 @@ class BackupService:
|
||||
else:
|
||||
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}'")
|
||||
for item in source_path.rglob("*"):
|
||||
relative_path = item.relative_to(source_path)
|
||||
target_item = backup_path.joinpath(relative_path)
|
||||
if item.is_file():
|
||||
if not target_item.exists():
|
||||
target_item.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(item, target_item)
|
||||
else:
|
||||
Logger.print_info(f"File '{target_item}' already exists. Skipping...")
|
||||
elif item.is_dir():
|
||||
target_item.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
shutil.copytree(
|
||||
source_path,
|
||||
backup_path,
|
||||
dirs_exist_ok=True,
|
||||
symlinks=True,
|
||||
ignore_dangling_symlinks=True,
|
||||
)
|
||||
|
||||
Logger.print_ok(
|
||||
f"Successfully backed up '{source_path}' to '{backup_path}'"
|
||||
@@ -125,27 +151,29 @@ class BackupService:
|
||||
################################################
|
||||
|
||||
def backup_printer_cfg(self):
|
||||
"""Backup printer.cfg files of all Klipper instances.
|
||||
Files are backed up to:
|
||||
{backup_root}/{instance_data_dir_name}/printer_{timestamp}.cfg
|
||||
"""
|
||||
klipper_instances: List[Klipper] = get_instances(Klipper)
|
||||
for instance in klipper_instances:
|
||||
target_path: Path = self._backup_root.joinpath(
|
||||
instance.data_dir.name, f"config_{self.timestamp}"
|
||||
)
|
||||
target_path: Path = self._backup_root.joinpath(instance.data_dir.name)
|
||||
self.backup_file(
|
||||
source_path=instance.cfg_file,
|
||||
target_path=target_path,
|
||||
target_name=instance.cfg_file.name,
|
||||
)
|
||||
|
||||
def backup_moonraker_conf(self):
|
||||
"""Backup moonraker.conf files of all Moonraker instances.
|
||||
Files are backed up to:
|
||||
{backup_root}/{instance_data_dir_name}/moonraker_{timestamp}.conf
|
||||
"""
|
||||
moonraker_instances: List[Moonraker] = get_instances(Moonraker)
|
||||
for instance in moonraker_instances:
|
||||
target_path: Path = self._backup_root.joinpath(
|
||||
instance.data_dir.name, f"config_{self.timestamp}"
|
||||
)
|
||||
target_path: Path = self._backup_root.joinpath(instance.data_dir.name)
|
||||
self.backup_file(
|
||||
source_path=instance.cfg_file,
|
||||
target_path=target_path,
|
||||
target_name=instance.cfg_file.name,
|
||||
)
|
||||
|
||||
def backup_printer_config_dir(self) -> None:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2020 - 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# Copyright (C) 2020 - 2026 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||
# https://github.com/dw-0/kiauh #
|
||||
@@ -16,7 +16,7 @@ from components.klipper import KLIPPER_REPO_URL
|
||||
from components.moonraker import MOONRAKER_REPO_URL
|
||||
from core.logger import DialogType, Logger
|
||||
from core.services.backup_service import BackupService
|
||||
from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import (
|
||||
from core.simple_config_parser.simple_config_parser import (
|
||||
SimpleConfigParser,
|
||||
)
|
||||
from utils.input_utils import get_confirm
|
||||
@@ -254,32 +254,34 @@ class KiauhSettings:
|
||||
section: str,
|
||||
option: str,
|
||||
getter: Callable[[str, str, T | None], T],
|
||||
fallback: T = None,
|
||||
fallback: T | None = None,
|
||||
silent: bool = False,
|
||||
) -> T:
|
||||
) -> T | None:
|
||||
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:
|
||||
try:
|
||||
if repo.strip().startswith("#") or repo.strip().startswith(";"):
|
||||
continue
|
||||
if "," in repo:
|
||||
url, branch = repo.strip().split(",")
|
||||
for raw in repos:
|
||||
line = raw.strip()
|
||||
|
||||
if not branch:
|
||||
branch = "master"
|
||||
if not line or line.startswith("#") or line.startswith(";"):
|
||||
continue
|
||||
|
||||
try:
|
||||
if "," in line:
|
||||
url_part, branch_part = line.split(",")
|
||||
url = url_part.strip()
|
||||
branch = branch_part.strip() or "master"
|
||||
else:
|
||||
url = repo.strip()
|
||||
url = line
|
||||
branch = "master"
|
||||
|
||||
# url must not be empty otherwise it's considered
|
||||
# as an unrecoverable, invalid configuration
|
||||
if not url:
|
||||
raise InvalidValueError(section, "repositories", repo)
|
||||
raise InvalidValueError(section, "repositories", line)
|
||||
|
||||
_repos.append(Repository(url.strip(), branch.strip()))
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Simple Config Parser
|
||||
|
||||
A custom config parser inspired by Python's configparser module.
|
||||
Specialized for handling Klipper style config files.
|
||||
|
||||
---
|
||||
|
||||
### When parsing a config file, it will be split into the following elements:
|
||||
- Header: All lines before the first section
|
||||
- Section: A section is defined by a line starting with a `[` and ending with a `]`
|
||||
- Option: A line starting with a word, followed by a `:` or `=` and a value
|
||||
- Option Block: A line starting with a word, followed by a `:` or `=` and a newline
|
||||
- The word `gcode` is excluded from being treated as an option block
|
||||
- Gcode Block: A line starting with the word `gcode`, followed by a `:` or `=` and a newline
|
||||
- All indented lines following the gcode line are considered part of the gcode block
|
||||
- Comment: A line starting with a `#` or `;`
|
||||
- Blank: A line containing only whitespace characters
|
||||
- SaveConfig Block: Klippers auto-generated SAVE_CONFIG section that can be found at the very end of the config file
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,646 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2025 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# https://github.com/dw-0/simple-config-parser #
|
||||
# #
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||
# ======================================================================= #
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Set, Union
|
||||
|
||||
# definition of section line:
|
||||
# - the line MUST start with an opening square bracket - it is the first section marker
|
||||
# - the section marker MUST be followed by at least one character - it is the section name
|
||||
# - the section name MUST be followed by a closing square bracket - it is the second section marker
|
||||
# - the second section marker MAY be followed by any amount of whitespace characters
|
||||
# - the second section marker MAY be followed by a # or ; - it is the comment marker
|
||||
# - the inline comment MAY be of any length and character
|
||||
SECTION_RE = re.compile(r"^\[(\S.*\S|\S)]\s*([#;].*)?$")
|
||||
|
||||
# definition of option line:
|
||||
# - the line MUST start with a word - it is the option name
|
||||
# - the option name MUST be followed by a colon or an equal sign - it is the separator
|
||||
# - the separator MUST be followed by a value
|
||||
# - the separator MAY have any amount of leading or trailing whitespaces
|
||||
# - the separator MUST NOT be directly followed by a colon or equal sign
|
||||
# - the value MAY be of any length and character
|
||||
# - the value MAY contain any amount of trailing whitespaces
|
||||
# - the value MAY be followed by a # or ; - it is the comment marker
|
||||
# - the inline comment MAY be of any length and character
|
||||
OPTION_RE = re.compile(r"^([^;#:=\s]+)\s?[:=]\s*([^;#:=\s][^;#]*?)\s*([#;].*)?$")
|
||||
|
||||
# definition of options block start line:
|
||||
# - the line MUST start with a word - it is the option name
|
||||
# - the option name MUST NOT be "gcode"
|
||||
# - the option name MUST be followed by a colon or an equal sign - it is the separator
|
||||
# - the separator MUST NOT be followed by a value
|
||||
# - the separator MAY have any amount of leading or trailing whitespaces
|
||||
# - the separator MUST NOT be directly followed by a colon or equal sign
|
||||
# - the separator MAY be followed by a # or ; - it is the comment marker
|
||||
# - the inline comment MAY be of any length and character
|
||||
OPTIONS_BLOCK_START_RE = re.compile(
|
||||
r"^(?!\s*gcode\s*[:=])([^;#:=\s]+)\s*[:=]\s*([#;].*)?$"
|
||||
)
|
||||
|
||||
# definition of gcode block start line:
|
||||
# - the line MUST start with the word "gcode"
|
||||
# - the word "gcode" MUST be followed by a colon or an equal sign - it is the separator
|
||||
# - the separator MUST NOT be followed by a value
|
||||
# - the separator MAY have any amount of leading or trailing whitespaces
|
||||
# - the separator MUST NOT be directly followed by a colon or equal sign
|
||||
# - the separator MAY be followed by a # or ; - it is the comment marker
|
||||
# - the inline comment MAY be of any length and character
|
||||
GCODE_BLOCK_START_RE = re.compile(r"^\s*gcode\s*[:=]\s*(?:[#;].*)?$")
|
||||
|
||||
# definition of comment line:
|
||||
# - the line MAY start with any amount of whitespace characters
|
||||
# - the line MUST contain a # or ; - it is the comment marker
|
||||
# - the comment marker MAY be followed by any amount of whitespace characters
|
||||
# - the comment MAY be of any length and character
|
||||
LINE_COMMENT_RE = re.compile(r"^\s*[#;].*")
|
||||
|
||||
# definition of empty line:
|
||||
# - the line MUST contain only whitespace characters
|
||||
EMPTY_LINE_RE = re.compile(r"^\s*$")
|
||||
|
||||
SAVE_CONFIG_START_RE = re.compile(r"^#\*# <-+ SAVE_CONFIG -+>$")
|
||||
SAVE_CONFIG_CONTENT_RE = re.compile(r"^#\*#.*$")
|
||||
|
||||
BOOLEAN_STATES = {
|
||||
"1": True,
|
||||
"yes": True,
|
||||
"true": True,
|
||||
"on": True,
|
||||
"0": False,
|
||||
"no": False,
|
||||
"false": False,
|
||||
"off": False,
|
||||
}
|
||||
|
||||
|
||||
class LineType(Enum):
|
||||
OPTION = "option"
|
||||
OPTION_BLOCK = "option_block"
|
||||
COMMENT = "comment"
|
||||
BLANK = "blank"
|
||||
|
||||
|
||||
_UNSET = object()
|
||||
|
||||
|
||||
class NoSectionError(Exception):
|
||||
"""Raised when a section is not defined"""
|
||||
|
||||
def __init__(self, section: str):
|
||||
msg = f"Section '{section}' is not defined"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class DuplicateSectionError(Exception):
|
||||
"""Raised when a section is defined more than once"""
|
||||
|
||||
def __init__(self, section: str):
|
||||
msg = f"Section '{section}' is defined more than once"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class NoOptionError(Exception):
|
||||
"""Raised when an option is not defined in a section"""
|
||||
|
||||
def __init__(self, option: str, section: str):
|
||||
msg = f"Option '{option}' in section '{section}' is not defined"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class UnknownLineError(Exception):
|
||||
"""Raised when a line is not recognized as any known type"""
|
||||
|
||||
def __init__(self, line: str):
|
||||
msg = f"Unknown line: '{line}'"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Option:
|
||||
"""Dataclass representing a (pseudo) config option"""
|
||||
|
||||
name: str
|
||||
raw: str
|
||||
value: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class MultiLineOption:
|
||||
"""Dataclass representing a multi-line config option"""
|
||||
|
||||
name: str
|
||||
raw: str
|
||||
values: List[MLOptionValue] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MLOptionValue:
|
||||
"""Dataclass representing a value in a multi-line option"""
|
||||
|
||||
raw: str
|
||||
indent: int
|
||||
value: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Gcode:
|
||||
"""Dataclass representing a gcode block"""
|
||||
|
||||
name: str
|
||||
raw: str
|
||||
gcode: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlankLine:
|
||||
"""Dataclass representing a blank line"""
|
||||
|
||||
raw: str = "\n"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommentLine:
|
||||
"""Dataclass representing a comment line"""
|
||||
|
||||
raw: str
|
||||
|
||||
|
||||
SectionItem = Union[Option, MultiLineOption, Gcode, BlankLine, CommentLine]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Section:
|
||||
"""Dataclass representing a config section"""
|
||||
|
||||
name: str
|
||||
raw: str
|
||||
items: List[SectionItem] = field(default_factory=list)
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
class SimpleConfigParser:
|
||||
"""A customized config parser targeted at handling Klipper style config files"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.config: Dict = {}
|
||||
|
||||
self._header: List[str] = []
|
||||
self._save_config_block: List[str] = []
|
||||
self._config: List[Section] = []
|
||||
self._curr_sect: Union[Section, None] = None
|
||||
self._curr_ml_opt: Union[MultiLineOption, None] = None
|
||||
self._curr_gcode: Union[Gcode, None] = None
|
||||
|
||||
def _match_section(self, line: str) -> bool:
|
||||
"""Whether the given line matches the definition of a section"""
|
||||
return SECTION_RE.match(line) is not None
|
||||
|
||||
def _match_option(self, line: str) -> bool:
|
||||
"""Whether the given line matches the definition of an option"""
|
||||
return OPTION_RE.match(line) is not None
|
||||
|
||||
def _match_options_block_start(self, line: str) -> bool:
|
||||
"""Whether the given line matches the definition of a multiline option"""
|
||||
return OPTIONS_BLOCK_START_RE.match(line) is not None
|
||||
|
||||
def _match_gcode_block_start(self, line: str) -> bool:
|
||||
"""Whether the given line matches the definition of a gcode block start"""
|
||||
return GCODE_BLOCK_START_RE.match(line) is not None
|
||||
|
||||
def _match_save_config_start(self, line: str) -> bool:
|
||||
"""Whether the given line matches the definition of a save config start"""
|
||||
return SAVE_CONFIG_START_RE.match(line) is not None
|
||||
|
||||
def _match_save_config_content(self, line: str) -> bool:
|
||||
"""Whether the given line matches the definition of a save config content"""
|
||||
return SAVE_CONFIG_CONTENT_RE.match(line) is not None
|
||||
|
||||
def _match_line_comment(self, line: str) -> bool:
|
||||
"""Whether the given line matches the definition of a comment"""
|
||||
return LINE_COMMENT_RE.match(line) is not None
|
||||
|
||||
def _match_empty_line(self, line: str) -> bool:
|
||||
"""Wheter or not the given line matches the definition of an empty line"""
|
||||
return EMPTY_LINE_RE.match(line) is not None
|
||||
|
||||
def _parse_line(self, line: str) -> None:
|
||||
"""Parses a line and determines its type"""
|
||||
if self._curr_sect is None and not self._match_section(line):
|
||||
# we are at the beginning of the file, so we consider the part
|
||||
# up to the first section as the file header and store it separately
|
||||
self._header.append(line)
|
||||
return
|
||||
|
||||
if self._match_section(line):
|
||||
self._reset_special_items()
|
||||
|
||||
sect_name: str = SECTION_RE.match(line).group(1)
|
||||
sect = Section(name=sect_name, raw=line)
|
||||
self._curr_sect = sect
|
||||
self._config.append(sect)
|
||||
return
|
||||
|
||||
if self._match_option(line):
|
||||
self._reset_special_items()
|
||||
|
||||
name: str = OPTION_RE.match(line).group(1)
|
||||
val: str = OPTION_RE.match(line).group(2)
|
||||
opt = Option(
|
||||
name=name,
|
||||
raw=line,
|
||||
value=val,
|
||||
)
|
||||
self._curr_sect.items.append(opt)
|
||||
return
|
||||
|
||||
if self._match_options_block_start(line):
|
||||
self._reset_special_items()
|
||||
|
||||
name: str = OPTIONS_BLOCK_START_RE.match(line).group(1)
|
||||
ml_opt = MultiLineOption(
|
||||
name=name,
|
||||
raw=line,
|
||||
)
|
||||
self._curr_ml_opt = ml_opt
|
||||
self._curr_sect.items.append(ml_opt)
|
||||
return
|
||||
|
||||
if self._curr_ml_opt is not None:
|
||||
# we are in an option block, so we consecutively add values
|
||||
# to the current multiline option until we hit a different line type
|
||||
|
||||
if "#" in line:
|
||||
value = line.split("#", 1)[0].strip()
|
||||
elif ";" in line:
|
||||
value = line.split(";", 1)[0].strip()
|
||||
else:
|
||||
value = line.strip()
|
||||
|
||||
ml_value = MLOptionValue(
|
||||
raw=line,
|
||||
indent=self._get_indent(line),
|
||||
value=value,
|
||||
)
|
||||
self._curr_ml_opt.values.append(ml_value)
|
||||
return
|
||||
|
||||
if self._match_gcode_block_start(line):
|
||||
self._curr_gcode = Gcode(
|
||||
name="gcode",
|
||||
raw=line,
|
||||
)
|
||||
self._curr_sect.items.append(self._curr_gcode)
|
||||
return
|
||||
|
||||
if self._curr_gcode is not None:
|
||||
# we are in a gcode block, so we add any following line
|
||||
# without further checks to the gcode block
|
||||
self._curr_gcode.gcode.append(line)
|
||||
return
|
||||
|
||||
if self._match_save_config_start(line):
|
||||
self._reset_special_items()
|
||||
self._save_config_block.append(line)
|
||||
return
|
||||
|
||||
if self._match_save_config_content(line):
|
||||
self._reset_special_items()
|
||||
self._save_config_block.append(line)
|
||||
return
|
||||
|
||||
if self._match_empty_line(line):
|
||||
self._reset_special_items()
|
||||
self._curr_sect.items.append(BlankLine(raw=line))
|
||||
return
|
||||
|
||||
if self._match_line_comment(line):
|
||||
self._reset_special_items()
|
||||
self._curr_sect.items.append(CommentLine(raw=line))
|
||||
return
|
||||
|
||||
def _reset_special_items(self) -> None:
|
||||
"""Reset special items like current multine option and gcode block"""
|
||||
self._curr_ml_opt = None
|
||||
self._curr_gcode = None
|
||||
|
||||
def _get_indent(self, line: str) -> int:
|
||||
"""Return the indentation level of a line"""
|
||||
return len(line) - len(line.lstrip())
|
||||
|
||||
def read_file(self, file: Path) -> None:
|
||||
"""Read and parse a config file"""
|
||||
self._config = []
|
||||
with open(file, "r", encoding="utf-8") as file:
|
||||
for line in file:
|
||||
self._parse_line(line)
|
||||
|
||||
def write_file(self, path: str | Path) -> None:
|
||||
"""Write the config to a file"""
|
||||
if path is None:
|
||||
raise ValueError("File path cannot be None")
|
||||
|
||||
# first write the header
|
||||
content: List[str] = list(self._header)
|
||||
|
||||
# then write all sections
|
||||
for i in self._config:
|
||||
content.append(i.raw)
|
||||
for item in i.items:
|
||||
content.append(item.raw)
|
||||
if isinstance(item, MultiLineOption):
|
||||
content.extend(val.raw for val in item.values)
|
||||
elif isinstance(item, Gcode):
|
||||
content.extend(item.gcode)
|
||||
|
||||
# then write the save config block
|
||||
content.extend(self._save_config_block)
|
||||
|
||||
# ensure file ends with a newline
|
||||
if content and not content[-1].endswith("\n"):
|
||||
content.append("\n")
|
||||
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.writelines(content)
|
||||
|
||||
def get_sections(self) -> Set[str]:
|
||||
"""Return a set of all section names"""
|
||||
return {s.name for s in self._config} if self._config else set()
|
||||
|
||||
def has_section(self, section: str) -> bool:
|
||||
"""Check if a section exists"""
|
||||
return section in self.get_sections()
|
||||
|
||||
def add_section(self, section: str) -> Section:
|
||||
"""Add a new section to the config"""
|
||||
if section in self.get_sections():
|
||||
raise DuplicateSectionError(section)
|
||||
|
||||
if not self._config:
|
||||
new_sect = Section(name=section, raw=f"[{section}]\n")
|
||||
self._config.append(new_sect)
|
||||
return new_sect
|
||||
|
||||
last_sect: Section = self._config[-1]
|
||||
if not last_sect.items or (
|
||||
last_sect.items and not isinstance(last_sect.items[-1], BlankLine)
|
||||
):
|
||||
last_sect.items.append(BlankLine())
|
||||
|
||||
new_sect = Section(name=section, raw=f"[{section}]\n")
|
||||
self._config.append(new_sect)
|
||||
return new_sect
|
||||
|
||||
def remove_section(self, section: str) -> None:
|
||||
"""Remove a section from the config
|
||||
|
||||
This will remove ALL occurences of sections with the given name.
|
||||
"""
|
||||
self._config = [s for s in self._config if s.name != section]
|
||||
|
||||
def get_options(self, section: str) -> Set[str]:
|
||||
"""Return a set of all option names for a given section"""
|
||||
sections: List[Section] = [s for s in self._config if s.name == section]
|
||||
all_items: List[SectionItem] = [
|
||||
item for section in sections for item in section.items
|
||||
]
|
||||
|
||||
return {o.name for o in all_items if isinstance(o, (Option, MultiLineOption))}
|
||||
|
||||
def has_option(self, section: str, option: str) -> bool:
|
||||
"""Check if an option exists in a section"""
|
||||
return self.has_section(section) and option in self.get_options(section)
|
||||
|
||||
def set_option(self, section: str, option: str, value: str | List[str]) -> None:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
# when adding options, we add them to the first matching section
|
||||
# if the section does not exist, we create it
|
||||
section: Section = (
|
||||
self.add_section(section)
|
||||
if not self.has_section(section)
|
||||
else next(s for s in self._config if s.name == section)
|
||||
)
|
||||
|
||||
opt = self._find_option_by_name(option, section=section)
|
||||
if opt is None:
|
||||
if isinstance(value, list):
|
||||
indent = 4
|
||||
_opt = MultiLineOption(
|
||||
name=option,
|
||||
raw=f"{option}:\n",
|
||||
values=[
|
||||
MLOptionValue(
|
||||
raw=f"{' ' * indent}{val}\n",
|
||||
indent=indent,
|
||||
value=val,
|
||||
)
|
||||
for val in value
|
||||
],
|
||||
)
|
||||
else:
|
||||
_opt = Option(
|
||||
name=option,
|
||||
raw=f"{option}: {value}\n",
|
||||
value=value,
|
||||
)
|
||||
|
||||
last_opt_idx: int = 0
|
||||
for idx, item in enumerate(section.items):
|
||||
if isinstance(item, (Option, MultiLineOption)):
|
||||
last_opt_idx = idx
|
||||
# insert the new option after the last existing option
|
||||
section.items.insert(last_opt_idx + 1, _opt)
|
||||
|
||||
elif opt and isinstance(opt, Option) and isinstance(value, str):
|
||||
curr_val = opt.value
|
||||
new_val = value
|
||||
opt.value = new_val
|
||||
opt.raw = opt.raw.replace(curr_val, new_val)
|
||||
|
||||
elif opt and isinstance(opt, MultiLineOption) and isinstance(value, list):
|
||||
# note: we completely replace the existing values
|
||||
# so any existing indentation, comments, etc. will be lost
|
||||
indent = 4
|
||||
opt.values = [
|
||||
MLOptionValue(
|
||||
raw=f"{' ' * indent}{val}\n",
|
||||
indent=indent,
|
||||
value=val,
|
||||
)
|
||||
for val in value
|
||||
]
|
||||
|
||||
def _find_section_by_name(
|
||||
self, sect_name: str
|
||||
) -> Union[None, Section, List[Section]]:
|
||||
"""Find a section by name"""
|
||||
_sects = [s for s in self._config if s.name == sect_name]
|
||||
if len(_sects) > 1:
|
||||
return _sects
|
||||
elif len(_sects) == 1:
|
||||
return _sects[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def _find_option_by_name(
|
||||
self,
|
||||
opt_name: str,
|
||||
section: Union[Section, None] = None,
|
||||
sections: Union[List[Section], None] = None,
|
||||
) -> Union[None, Option, MultiLineOption]:
|
||||
"""Find an option or multi-line option by name in a section"""
|
||||
|
||||
# if a single section is provided, search its items for the option
|
||||
if section is not None:
|
||||
for item in section.items:
|
||||
if (
|
||||
isinstance(item, (Option, MultiLineOption))
|
||||
and item.name == opt_name
|
||||
):
|
||||
return item
|
||||
|
||||
# if multiple sections with the same name are provided, merge their
|
||||
# items and search for the option
|
||||
if sections is not None:
|
||||
all_items: List[SectionItem] = [
|
||||
item for sect in sections for item in sect.items
|
||||
]
|
||||
for item in all_items:
|
||||
if (
|
||||
isinstance(item, (Option, MultiLineOption))
|
||||
and item.name == opt_name
|
||||
):
|
||||
return item
|
||||
|
||||
return None
|
||||
|
||||
def remove_option(self, section: str, option: str) -> None:
|
||||
"""Remove an option from a section
|
||||
|
||||
This will remove the option from ALL occurences of sections with the given name.
|
||||
Other non-option items (comments, blank lines, etc.) are preserved.
|
||||
"""
|
||||
sections: List[Section] = [s for s in self._config if s.name == section]
|
||||
if not sections:
|
||||
return
|
||||
|
||||
for sect in sections:
|
||||
sect.items = [
|
||||
item
|
||||
for item in sect.items
|
||||
if not (
|
||||
isinstance(item, (Option, MultiLineOption)) and item.name == option
|
||||
)
|
||||
]
|
||||
|
||||
def _get_option(
|
||||
self, section: str, option: str
|
||||
) -> Union[Option, MultiLineOption, None]:
|
||||
"""Internal helper to resolve an option or multi-line option."""
|
||||
if section not in self.get_sections():
|
||||
raise NoSectionError(section)
|
||||
if option not in self.get_options(section):
|
||||
raise NoOptionError(option, section)
|
||||
sects: List[Section] = [s for s in self._config if s.name == section]
|
||||
return (
|
||||
self._find_option_by_name(option, sections=sects)
|
||||
if len(sects) > 1
|
||||
else self._find_option_by_name(option, section=sects[0])
|
||||
)
|
||||
|
||||
def getval(self, section: str, option: str, fallback: str | _UNSET = _UNSET) -> str:
|
||||
"""
|
||||
Return the value of the given option in the given section
|
||||
|
||||
If the key is not found and 'fallback' is provided, it is used as
|
||||
a fallback value.
|
||||
"""
|
||||
try:
|
||||
opt = self._get_option(section, option)
|
||||
if not isinstance(opt, Option):
|
||||
raise NoOptionError(option, section)
|
||||
|
||||
return opt.value if opt else ""
|
||||
|
||||
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:
|
||||
opt = self._get_option(section, option)
|
||||
if not isinstance(opt, MultiLineOption):
|
||||
raise NoOptionError(option, section)
|
||||
|
||||
return [v.value for v in opt.values] if opt else []
|
||||
|
||||
except (NoSectionError, NoOptionError):
|
||||
if fallback is _UNSET:
|
||||
raise
|
||||
return fallback
|
||||
|
||||
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 self._get_conv(section, option, int, fallback=fallback)
|
||||
|
||||
def getfloat(
|
||||
self, section: str, option: str, fallback: float | _UNSET = _UNSET
|
||||
) -> float:
|
||||
"""Return the value of the given option in the given section as a float"""
|
||||
return self._get_conv(section, option, float, fallback=fallback)
|
||||
|
||||
def getboolean(
|
||||
self, section: str, option: str, fallback: bool | _UNSET = _UNSET
|
||||
) -> bool:
|
||||
"""Return the value of the given option in the given section as a boolean"""
|
||||
return self._get_conv(
|
||||
section, option, self._convert_to_boolean, fallback=fallback
|
||||
)
|
||||
|
||||
def _convert_to_boolean(self, value: str) -> bool:
|
||||
"""Convert a string to a boolean"""
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if value.lower() not in BOOLEAN_STATES:
|
||||
raise ValueError("Not a boolean: %s" % value)
|
||||
return BOOLEAN_STATES[value.lower()]
|
||||
|
||||
def _get_conv(
|
||||
self,
|
||||
section: str,
|
||||
option: str,
|
||||
conv: Callable[[str], int | float | bool],
|
||||
fallback: Any = _UNSET,
|
||||
) -> int | float | bool:
|
||||
"""Return the value of the given option in the given section as a converted value"""
|
||||
try:
|
||||
return conv(self.getval(section, option, fallback))
|
||||
except (ValueError, TypeError, AttributeError) as e:
|
||||
if fallback is not _UNSET:
|
||||
return fallback
|
||||
raise ValueError(
|
||||
f"Cannot convert {self.getval(section, option)} to {conv.__name__}"
|
||||
) from e
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,14 @@
|
||||
# Header line
|
||||
# Another header comment
|
||||
[toolhead]
|
||||
option_a: 1
|
||||
option_b: true
|
||||
|
||||
[gcode_macro test]
|
||||
gcode: # start gcode block
|
||||
G28 ; home all
|
||||
M118 Done ; echo
|
||||
G1 X10 Y10 F3000
|
||||
|
||||
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -10,8 +10,8 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
|
||||
from tests.utils import load_testdata_from_file
|
||||
from core.simple_config_parser.simple_config_parser import SimpleConfigParser
|
||||
from core.simple_config_parser.tests.utils import load_testdata_from_file
|
||||
|
||||
BASE_DIR = Path(__file__).parent.joinpath("test_data")
|
||||
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
|
||||
File renamed without changes.
+16
@@ -0,0 +1,16 @@
|
||||
gcode:
|
||||
gcode:
|
||||
gcode: # comment
|
||||
gcode: ; comment
|
||||
gcode :
|
||||
gcode :
|
||||
gcode : # comment
|
||||
gcode : ; comment
|
||||
gcode=
|
||||
gcode=
|
||||
gcode= # comment
|
||||
gcode= ; comment
|
||||
gcode =
|
||||
gcode =
|
||||
gcode = # comment
|
||||
gcode = ; comment
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
type: jsonfile
|
||||
path: /dev/shm/drying_box.json
|
||||
baud: 250000
|
||||
minimum_cruise_ratio: 0.5
|
||||
square_corner_velocity: 5.0
|
||||
full_steps_per_rotation: 200
|
||||
position_min: 0
|
||||
homing_speed: 5.0
|
||||
# baud: 250000
|
||||
# minimum_cruise_ratio: 0.5
|
||||
# square_corner_velocity: 5.0
|
||||
# full_steps_per_rotation: 200
|
||||
# position_min: 0
|
||||
# homing_speed: 5.0
|
||||
|
||||
option:
|
||||
option :
|
||||
option :
|
||||
option=
|
||||
option =
|
||||
option =
|
||||
|
||||
|
||||
### this is a comment
|
||||
; this is also a comment
|
||||
;
|
||||
#
|
||||
homing_speed::
|
||||
homing_speed::
|
||||
homing_speed ::
|
||||
homing_speed ::
|
||||
homing_speed==
|
||||
homing_speed==
|
||||
homing_speed ==
|
||||
homing_speed ==
|
||||
homing_speed :=
|
||||
homing_speed :=
|
||||
homing_speed =:
|
||||
homing_speed =:
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# ======================================================================= #
|
||||
# Copyright (C) 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||
# #
|
||||
# https://github.com/dw-0/simple-config-parser #
|
||||
# #
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||
# ======================================================================= #
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from core.simple_config_parser.simple_config_parser import SimpleConfigParser
|
||||
from core.simple_config_parser.tests.utils import load_testdata_from_file
|
||||
|
||||
BASE_DIR = Path(__file__).parent.joinpath("test_data")
|
||||
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
|
||||
NON_MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("non_matching_data.txt")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser():
|
||||
return SimpleConfigParser()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("line", load_testdata_from_file(MATCHING_TEST_DATA_PATH))
|
||||
def test_match_gcode_block_start(parser, line):
|
||||
"""Test that a line matches the definition of an options block start"""
|
||||
assert parser._match_gcode_block_start(line) is True, (
|
||||
f"Expected line '{line}' to match gcode block start definition!"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH))
|
||||
def test_non_matching_gcode_block_start(parser, line):
|
||||
"""Test that a line does not match the definition of an options block start"""
|
||||
assert parser._match_gcode_block_start(line) is False, (
|
||||
f"Expected line '{line}' to not match gcode block start definition!"
|
||||
)
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -10,8 +10,8 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
|
||||
from tests.utils import load_testdata_from_file
|
||||
from core.simple_config_parser.simple_config_parser import SimpleConfigParser
|
||||
from core.simple_config_parser.tests.utils import load_testdata_from_file
|
||||
|
||||
BASE_DIR = Path(__file__).parent.joinpath("test_data")
|
||||
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -10,8 +10,8 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.simple_config_parser.simple_config_parser import SimpleConfigParser
|
||||
from tests.utils import load_testdata_from_file
|
||||
from core.simple_config_parser.simple_config_parser import SimpleConfigParser
|
||||
from core.simple_config_parser.tests.utils import load_testdata_from_file
|
||||
|
||||
BASE_DIR = Path(__file__).parent.joinpath("test_data")
|
||||
MATCHING_TEST_DATA_PATH = BASE_DIR.joinpath("matching_data.txt")
|
||||
File renamed without changes.
-1
@@ -1,5 +1,4 @@
|
||||
trusted_clients:
|
||||
gcode:
|
||||
cors_domains:
|
||||
an_options_block_start_with_comment: ; this is a comment
|
||||
an_options_block_start_with_comment: # this is a comment
|
||||
+6
@@ -29,3 +29,9 @@ homing_speed :=
|
||||
homing_speed :=
|
||||
homing_speed =:
|
||||
homing_speed =:
|
||||
gcode:
|
||||
gcode :
|
||||
gcode :
|
||||
gcode=
|
||||
gcode =
|
||||
gcode =
|
||||
Loaded 100 of 190 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user