From d0854c63ad8b4b27add9f3bf89773c0e5e68dd47 Mon Sep 17 00:00:00 2001 From: dw-0 Date: Sat, 11 Oct 2025 16:35:16 +0200 Subject: [PATCH] chore: this is the last commit for KIAUH v5 --- default.kiauh.cfg | 36 - kiauh.py | 15 - kiauh.sh | 108 +- kiauh/__init__.py | 15 - kiauh/components/__init__.py | 0 kiauh/components/crowsnest/__init__.py | 28 - kiauh/components/crowsnest/crowsnest.py | 176 --- kiauh/components/klipper/__init__.py | 36 - kiauh/components/klipper/assets/klipper.env | 1 - .../components/klipper/assets/klipper.service | 18 - kiauh/components/klipper/assets/printer.cfg | 11 - kiauh/components/klipper/klipper.py | 142 -- kiauh/components/klipper/klipper_dialogs.py | 113 -- kiauh/components/klipper/klipper_utils.py | 263 ---- kiauh/components/klipper/menus/__init__.py | 0 .../klipper/menus/klipper_remove_menu.py | 102 -- kiauh/components/klipper/services/__init__.py | 0 .../services/klipper_instance_service.py | 46 - .../klipper/services/klipper_setup_service.py | 366 ----- kiauh/components/klipper_firmware/__init__.py | 12 - .../klipper_firmware/firmware_utils.py | 213 --- .../klipper_firmware/flash_options.py | 115 -- .../menus/klipper_build_menu.py | 274 ---- .../menus/klipper_flash_error_menu.py | 107 -- .../menus/klipper_flash_help_menu.py | 177 --- .../menus/klipper_flash_menu.py | 484 ------ kiauh/components/klipperscreen/__init__.py | 32 - .../components/klipperscreen/klipperscreen.py | 207 --- kiauh/components/log_uploads/__init__.py | 14 - .../log_uploads/log_upload_utils.py | 55 - .../log_uploads/menus/log_upload_menu.py | 67 - kiauh/components/moonraker/__init__.py | 43 - .../moonraker/assets/moonraker.conf | 30 - .../components/moonraker/assets/moonraker.env | 1 - .../moonraker/assets/moonraker.service | 19 - kiauh/components/moonraker/menus/__init__.py | 0 .../moonraker/menus/moonraker_remove_menu.py | 110 -- kiauh/components/moonraker/moonraker.py | 146 -- .../components/moonraker/moonraker_dialogs.py | 75 - .../components/moonraker/services/__init__.py | 0 .../services/moonraker_instance_service.py | 49 - .../services/moonraker_setup_service.py | 408 ----- kiauh/components/moonraker/utils/__init__.py | 0 .../moonraker/utils/sysdeps_parser.py | 179 --- kiauh/components/moonraker/utils/utils.py | 228 --- kiauh/components/webui_client/__init__.py | 12 - .../webui_client/assets/common_vars.conf | 6 - .../components/webui_client/assets/nginx_cfg | 95 -- .../webui_client/assets/upstreams.conf | 25 - kiauh/components/webui_client/base_data.py | 55 - .../webui_client/client_config/__init__.py | 0 .../client_config/client_config_remove.py | 94 -- .../client_config/client_config_setup.py | 126 -- .../components/webui_client/client_dialogs.py | 93 -- .../components/webui_client/client_remove.py | 124 -- kiauh/components/webui_client/client_setup.py | 188 --- kiauh/components/webui_client/client_utils.py | 466 ------ kiauh/components/webui_client/fluidd_data.py | 55 - .../components/webui_client/mainsail_data.py | 55 - .../components/webui_client/menus/__init__.py | 0 .../webui_client/menus/client_install_menu.py | 105 -- .../webui_client/menus/client_remove_menu.py | 114 -- kiauh/core/__init__.py | 0 kiauh/core/constants.py | 27 - kiauh/core/decorators.py | 24 - kiauh/core/instance_manager/__init__.py | 0 kiauh/core/instance_manager/base_instance.py | 59 - .../core/instance_manager/instance_manager.py | 108 -- kiauh/core/logger.py | 169 --- kiauh/core/menus/__init__.py | 37 - kiauh/core/menus/advanced_menu.py | 106 -- kiauh/core/menus/backup_menu.py | 107 -- kiauh/core/menus/base_menu.py | 239 --- kiauh/core/menus/install_menu.py | 109 -- kiauh/core/menus/main_menu.py | 179 --- kiauh/core/menus/remove_menu.py | 86 -- kiauh/core/menus/repo_select_menu.py | 162 -- kiauh/core/menus/settings_menu.py | 140 -- kiauh/core/menus/update_menu.py | 327 ---- kiauh/core/services/__init__.py | 0 kiauh/core/services/backup_service.py | 189 --- kiauh/core/services/message_service.py | 61 - kiauh/core/settings/__init__.py | 0 kiauh/core/settings/kiauh_settings.py | 414 ----- kiauh/core/spinner.py | 42 - kiauh/core/submodules/__init__.py | 0 .../simple_config_parser/.editorconfig | 13 - .../simple_config_parser/.gitignore | 13 - .../submodules/simple_config_parser/LICENSE | 674 --------- .../submodules/simple_config_parser/README.md | 51 - .../simple_config_parser/pyproject.toml | 66 - .../simple_config_parser/requirements-dev.txt | 3 - .../src/simple_config_parser/__init__.py | 0 .../src/simple_config_parser/constants.py | 74 - .../simple_config_parser.py | 426 ------ .../simple_config_parser/tests/__init__.py | 0 .../tests/assets/klipper_config.txt | 1337 ----------------- .../tests/assets/test_config_1.cfg | 32 - .../tests/assets/test_config_2.cfg | 33 - .../tests/assets/test_config_3.cfg | 94 -- .../tests/assets/test_config_4.cfg | 116 -- .../write_tests/add_option/expected.cfg | 8 - .../assets/write_tests/add_option/input.cfg | 7 - .../write_tests/remove_option/expected.cfg | 7 - .../write_tests/remove_option/input.cfg | 8 - .../write_tests/remove_section/expected.cfg | 7 - .../write_tests/remove_section/input.cfg | 11 - .../tests/line_matching/__init__.py | 0 .../match_empty_line/__init__.py | 0 .../test_data/matching_data.txt | 6 - .../test_data/non_matching_data.txt | 7 - .../match_empty_line/test_match_empty_line.py | 39 - .../match_line_comment/__init__.py | 0 .../test_data/matching_data.txt | 28 - .../test_data/non_matching_data.txt | 5 - .../test_match_line_comment.py | 39 - .../line_matching/match_option/__init__.py | 0 .../match_option/test_data/matching_data.txt | 461 ------ .../test_data/non_matching_data.txt | 37 - .../match_option/test_match_option.py | 39 - .../match_option_block_start/__init__.py | 0 .../test_data/matching_data.txt | 15 - .../test_data/non_matching_data.txt | 31 - .../test_match_options_block_start.py | 39 - .../test_data/matching_data.txt | 22 - .../test_data/non_matching_data.txt | 6 - .../test_match_save_config_content.py | 37 - .../test_data/matching_data.txt | 6 - .../test_data/non_matching_data.txt | 13 - .../test_match_save_config_start.py | 37 - .../match_section/__init__,py.py | 0 .../match_section/test_data/matching_data.txt | 127 -- .../test_data/non_matching_data.txt | 19 - .../match_section/test_match_section.py | 39 - .../tests/line_parsing/__init__.py | 0 .../tests/line_parsing/test_line_parsing.py | 79 - .../tests/public_api/__init__.py | 0 .../tests/public_api/conftest.py | 26 - .../tests/public_api/test_options_api.py | 186 --- .../tests/public_api/test_read_file.py | 22 - .../tests/public_api/test_sections_api.py | 65 - .../tests/public_api/test_write_file.py | 119 -- .../simple_config_parser/tests/utils.py | 15 - .../tests/value_conversion/__init__.py | 0 .../tests/value_conversion/test_get_conv.py | 89 -- kiauh/core/types/__init__.py | 0 kiauh/core/types/color.py | 29 - kiauh/core/types/component_status.py | 32 - kiauh/extensions/__init__.py | 12 - kiauh/extensions/base_extension.py | 29 - kiauh/extensions/extensions_menu.py | 185 --- kiauh/extensions/gcode_shell_cmd/__init__.py | 19 - .../assets/gcode_shell_command.py | 94 -- .../gcode_shell_cmd/assets/shell_command.cfg | 7 - .../gcode_shell_cmd_extension.py | 134 -- .../extensions/gcode_shell_cmd/metadata.json | 10 - kiauh/extensions/klipper_backup/__init__.py | 19 - .../klipper_backup_extension.py | 160 -- kiauh/extensions/klipper_backup/metadata.json | 12 - .../mainsail_theme_installer/__init__.py | 0 .../mainsail_theme_installer_extension.py | 188 --- .../mainsail_theme_installer/metadata.json | 11 - kiauh/extensions/mobileraker/__init__.py | 29 - kiauh/extensions/mobileraker/metadata.json | 13 - .../mobileraker/mobileraker_extension.py | 193 --- kiauh/extensions/obico/__init__.py | 34 - .../obico/assets/moonraker-obico.env | 1 - .../obico/assets/moonraker-obico.service | 16 - kiauh/extensions/obico/metadata.json | 18 - kiauh/extensions/obico/moonraker_obico.py | 145 -- .../obico/moonraker_obico_extension.py | 380 ----- kiauh/extensions/octoapp/__init__.py | 28 - kiauh/extensions/octoapp/metadata.json | 18 - kiauh/extensions/octoapp/octoapp.py | 73 - kiauh/extensions/octoapp/octoapp_extension.py | 206 --- kiauh/extensions/octoeverywhere/__init__.py | 29 - kiauh/extensions/octoeverywhere/metadata.json | 18 - .../octoeverywhere/octoeverywhere.py | 75 - .../octoeverywhere_extension.py | 193 --- kiauh/extensions/octoprint/__init__.py | 22 - kiauh/extensions/octoprint/metadata.json | 18 - kiauh/extensions/octoprint/octoprint.py | 116 -- .../octoprint/octoprint_extension.py | 286 ---- kiauh/extensions/pretty_gcode/__init__.py | 0 .../pretty_gcode/assets/pgcode.local.conf | 19 - kiauh/extensions/pretty_gcode/metadata.json | 11 - .../pretty_gcode/pretty_gcode_extension.py | 101 -- kiauh/extensions/simply_print/__init__.py | 0 kiauh/extensions/simply_print/metadata.json | 16 - .../simply_print/simply_print_extension.py | 132 -- kiauh/extensions/spoolman/__init__.py | 16 - .../spoolman/assets/docker-compose.yml | 14 - kiauh/extensions/spoolman/metadata.json | 19 - kiauh/extensions/spoolman/spoolman.py | 190 --- .../extensions/spoolman/spoolman_extension.py | 345 ----- kiauh/extensions/telegram_bot/__init__.py | 29 - .../assets/moonraker-telegram-bot.env | 1 - .../assets/moonraker-telegram-bot.service | 16 - kiauh/extensions/telegram_bot/metadata.json | 11 - .../telegram_bot/moonraker_telegram_bot.py | 124 -- .../moonraker_telegram_bot_extension.py | 229 --- kiauh/main.py | 29 - kiauh/procedures/__init__.py | 0 kiauh/procedures/switch_repo.py | 145 -- kiauh/procedures/system.py | 103 -- kiauh/utils/__init__.py | 12 - kiauh/utils/common.py | 181 --- kiauh/utils/config_utils.py | 105 -- kiauh/utils/fs_utils.py | 162 -- kiauh/utils/git_utils.py | 360 ----- kiauh/utils/input_utils.py | 177 --- kiauh/utils/instance_type.py | 29 - kiauh/utils/instance_utils.py | 58 - kiauh/utils/sys_utils.py | 638 -------- pyproject.toml | 33 - pyrightconfig.json | 6 - requirements-dev.txt | 2 - 217 files changed, 1 insertion(+), 19516 deletions(-) delete mode 100644 default.kiauh.cfg delete mode 100644 kiauh.py delete mode 100644 kiauh/__init__.py delete mode 100644 kiauh/components/__init__.py delete mode 100644 kiauh/components/crowsnest/__init__.py delete mode 100644 kiauh/components/crowsnest/crowsnest.py delete mode 100644 kiauh/components/klipper/__init__.py delete mode 100644 kiauh/components/klipper/assets/klipper.env delete mode 100644 kiauh/components/klipper/assets/klipper.service delete mode 100644 kiauh/components/klipper/assets/printer.cfg delete mode 100644 kiauh/components/klipper/klipper.py delete mode 100644 kiauh/components/klipper/klipper_dialogs.py delete mode 100644 kiauh/components/klipper/klipper_utils.py delete mode 100644 kiauh/components/klipper/menus/__init__.py delete mode 100644 kiauh/components/klipper/menus/klipper_remove_menu.py delete mode 100644 kiauh/components/klipper/services/__init__.py delete mode 100644 kiauh/components/klipper/services/klipper_instance_service.py delete mode 100644 kiauh/components/klipper/services/klipper_setup_service.py delete mode 100644 kiauh/components/klipper_firmware/__init__.py delete mode 100644 kiauh/components/klipper_firmware/firmware_utils.py delete mode 100644 kiauh/components/klipper_firmware/flash_options.py delete mode 100644 kiauh/components/klipper_firmware/menus/klipper_build_menu.py delete mode 100644 kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py delete mode 100644 kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py delete mode 100644 kiauh/components/klipper_firmware/menus/klipper_flash_menu.py delete mode 100644 kiauh/components/klipperscreen/__init__.py delete mode 100644 kiauh/components/klipperscreen/klipperscreen.py delete mode 100644 kiauh/components/log_uploads/__init__.py delete mode 100644 kiauh/components/log_uploads/log_upload_utils.py delete mode 100644 kiauh/components/log_uploads/menus/log_upload_menu.py delete mode 100644 kiauh/components/moonraker/__init__.py delete mode 100644 kiauh/components/moonraker/assets/moonraker.conf delete mode 100644 kiauh/components/moonraker/assets/moonraker.env delete mode 100644 kiauh/components/moonraker/assets/moonraker.service delete mode 100644 kiauh/components/moonraker/menus/__init__.py delete mode 100644 kiauh/components/moonraker/menus/moonraker_remove_menu.py delete mode 100644 kiauh/components/moonraker/moonraker.py delete mode 100644 kiauh/components/moonraker/moonraker_dialogs.py delete mode 100644 kiauh/components/moonraker/services/__init__.py delete mode 100644 kiauh/components/moonraker/services/moonraker_instance_service.py delete mode 100644 kiauh/components/moonraker/services/moonraker_setup_service.py delete mode 100644 kiauh/components/moonraker/utils/__init__.py delete mode 100644 kiauh/components/moonraker/utils/sysdeps_parser.py delete mode 100644 kiauh/components/moonraker/utils/utils.py delete mode 100644 kiauh/components/webui_client/__init__.py delete mode 100644 kiauh/components/webui_client/assets/common_vars.conf delete mode 100644 kiauh/components/webui_client/assets/nginx_cfg delete mode 100644 kiauh/components/webui_client/assets/upstreams.conf delete mode 100644 kiauh/components/webui_client/base_data.py delete mode 100644 kiauh/components/webui_client/client_config/__init__.py delete mode 100644 kiauh/components/webui_client/client_config/client_config_remove.py delete mode 100644 kiauh/components/webui_client/client_config/client_config_setup.py delete mode 100644 kiauh/components/webui_client/client_dialogs.py delete mode 100644 kiauh/components/webui_client/client_remove.py delete mode 100644 kiauh/components/webui_client/client_setup.py delete mode 100644 kiauh/components/webui_client/client_utils.py delete mode 100644 kiauh/components/webui_client/fluidd_data.py delete mode 100644 kiauh/components/webui_client/mainsail_data.py delete mode 100644 kiauh/components/webui_client/menus/__init__.py delete mode 100644 kiauh/components/webui_client/menus/client_install_menu.py delete mode 100644 kiauh/components/webui_client/menus/client_remove_menu.py delete mode 100644 kiauh/core/__init__.py delete mode 100644 kiauh/core/constants.py delete mode 100644 kiauh/core/decorators.py delete mode 100644 kiauh/core/instance_manager/__init__.py delete mode 100644 kiauh/core/instance_manager/base_instance.py delete mode 100644 kiauh/core/instance_manager/instance_manager.py delete mode 100644 kiauh/core/logger.py delete mode 100644 kiauh/core/menus/__init__.py delete mode 100644 kiauh/core/menus/advanced_menu.py delete mode 100644 kiauh/core/menus/backup_menu.py delete mode 100644 kiauh/core/menus/base_menu.py delete mode 100644 kiauh/core/menus/install_menu.py delete mode 100644 kiauh/core/menus/main_menu.py delete mode 100644 kiauh/core/menus/remove_menu.py delete mode 100644 kiauh/core/menus/repo_select_menu.py delete mode 100644 kiauh/core/menus/settings_menu.py delete mode 100644 kiauh/core/menus/update_menu.py delete mode 100644 kiauh/core/services/__init__.py delete mode 100644 kiauh/core/services/backup_service.py delete mode 100644 kiauh/core/services/message_service.py delete mode 100644 kiauh/core/settings/__init__.py delete mode 100644 kiauh/core/settings/kiauh_settings.py delete mode 100644 kiauh/core/spinner.py delete mode 100644 kiauh/core/submodules/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/.editorconfig delete mode 100644 kiauh/core/submodules/simple_config_parser/.gitignore delete mode 100644 kiauh/core/submodules/simple_config_parser/LICENSE delete mode 100644 kiauh/core/submodules/simple_config_parser/README.md delete mode 100644 kiauh/core/submodules/simple_config_parser/pyproject.toml delete mode 100644 kiauh/core/submodules/simple_config_parser/requirements-dev.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/src/simple_config_parser/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py delete mode 100644 kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/klipper_config.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/test_config_1.cfg delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/test_config_2.cfg delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/test_config_3.cfg delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/test_config_4.cfg delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/expected.cfg delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/input.cfg delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/expected.cfg delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/input.cfg delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/expected.cfg delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/input.cfg delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/non_matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_match_empty_line.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/non_matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_match_line_comment.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/non_matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_match_option.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/non_matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_match_options_block_start.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/non_matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_match_save_config_content.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/non_matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_match_save_config_start.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/__init__,py.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/non_matching_data.txt delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_match_section.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_parsing/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/line_parsing/test_line_parsing.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/public_api/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/public_api/test_options_api.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/public_api/test_read_file.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/public_api/test_sections_api.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/public_api/test_write_file.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/utils.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/value_conversion/__init__.py delete mode 100644 kiauh/core/submodules/simple_config_parser/tests/value_conversion/test_get_conv.py delete mode 100644 kiauh/core/types/__init__.py delete mode 100644 kiauh/core/types/color.py delete mode 100644 kiauh/core/types/component_status.py delete mode 100644 kiauh/extensions/__init__.py delete mode 100644 kiauh/extensions/base_extension.py delete mode 100644 kiauh/extensions/extensions_menu.py delete mode 100644 kiauh/extensions/gcode_shell_cmd/__init__.py delete mode 100644 kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py delete mode 100644 kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg delete mode 100644 kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py delete mode 100644 kiauh/extensions/gcode_shell_cmd/metadata.json delete mode 100644 kiauh/extensions/klipper_backup/__init__.py delete mode 100644 kiauh/extensions/klipper_backup/klipper_backup_extension.py delete mode 100644 kiauh/extensions/klipper_backup/metadata.json delete mode 100644 kiauh/extensions/mainsail_theme_installer/__init__.py delete mode 100644 kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py delete mode 100644 kiauh/extensions/mainsail_theme_installer/metadata.json delete mode 100644 kiauh/extensions/mobileraker/__init__.py delete mode 100644 kiauh/extensions/mobileraker/metadata.json delete mode 100644 kiauh/extensions/mobileraker/mobileraker_extension.py delete mode 100644 kiauh/extensions/obico/__init__.py delete mode 100644 kiauh/extensions/obico/assets/moonraker-obico.env delete mode 100644 kiauh/extensions/obico/assets/moonraker-obico.service delete mode 100644 kiauh/extensions/obico/metadata.json delete mode 100644 kiauh/extensions/obico/moonraker_obico.py delete mode 100644 kiauh/extensions/obico/moonraker_obico_extension.py delete mode 100644 kiauh/extensions/octoapp/__init__.py delete mode 100644 kiauh/extensions/octoapp/metadata.json delete mode 100644 kiauh/extensions/octoapp/octoapp.py delete mode 100644 kiauh/extensions/octoapp/octoapp_extension.py delete mode 100644 kiauh/extensions/octoeverywhere/__init__.py delete mode 100644 kiauh/extensions/octoeverywhere/metadata.json delete mode 100644 kiauh/extensions/octoeverywhere/octoeverywhere.py delete mode 100644 kiauh/extensions/octoeverywhere/octoeverywhere_extension.py delete mode 100644 kiauh/extensions/octoprint/__init__.py delete mode 100644 kiauh/extensions/octoprint/metadata.json delete mode 100644 kiauh/extensions/octoprint/octoprint.py delete mode 100644 kiauh/extensions/octoprint/octoprint_extension.py delete mode 100644 kiauh/extensions/pretty_gcode/__init__.py delete mode 100644 kiauh/extensions/pretty_gcode/assets/pgcode.local.conf delete mode 100644 kiauh/extensions/pretty_gcode/metadata.json delete mode 100644 kiauh/extensions/pretty_gcode/pretty_gcode_extension.py delete mode 100644 kiauh/extensions/simply_print/__init__.py delete mode 100644 kiauh/extensions/simply_print/metadata.json delete mode 100644 kiauh/extensions/simply_print/simply_print_extension.py delete mode 100644 kiauh/extensions/spoolman/__init__.py delete mode 100644 kiauh/extensions/spoolman/assets/docker-compose.yml delete mode 100644 kiauh/extensions/spoolman/metadata.json delete mode 100644 kiauh/extensions/spoolman/spoolman.py delete mode 100644 kiauh/extensions/spoolman/spoolman_extension.py delete mode 100644 kiauh/extensions/telegram_bot/__init__.py delete mode 100644 kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env delete mode 100644 kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service delete mode 100644 kiauh/extensions/telegram_bot/metadata.json delete mode 100644 kiauh/extensions/telegram_bot/moonraker_telegram_bot.py delete mode 100644 kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py delete mode 100644 kiauh/main.py delete mode 100644 kiauh/procedures/__init__.py delete mode 100644 kiauh/procedures/switch_repo.py delete mode 100644 kiauh/procedures/system.py delete mode 100644 kiauh/utils/__init__.py delete mode 100644 kiauh/utils/common.py delete mode 100644 kiauh/utils/config_utils.py delete mode 100644 kiauh/utils/fs_utils.py delete mode 100644 kiauh/utils/git_utils.py delete mode 100644 kiauh/utils/input_utils.py delete mode 100644 kiauh/utils/instance_type.py delete mode 100644 kiauh/utils/instance_utils.py delete mode 100644 kiauh/utils/sys_utils.py delete mode 100644 pyproject.toml delete mode 100644 pyrightconfig.json delete mode 100644 requirements-dev.txt diff --git a/default.kiauh.cfg b/default.kiauh.cfg deleted file mode 100644 index d3ac149..0000000 --- a/default.kiauh.cfg +++ /dev/null @@ -1,36 +0,0 @@ -[kiauh] -backup_before_update: False - -[klipper] -# add custom repositories here, if at least one is given, the first in the list will be used by default -# otherwise the official repository is used -# -# format: https://github.com/username/repository, branch -# example: https://github.com/Klipper3d/klipper, master -# -# branch is optional, if given, it must be preceded by a comma, if not given, 'master' is used -repositories: - https://github.com/Klipper3d/klipper - -[moonraker] -# Moonraker supports two optional Python packages that can be used to reduce its CPU load -# If set to true, those packages will be installed during the Moonraker installation -optional_speedups: True - -# add custom repositories here, if at least one is given, the first in the list will be used by default -# otherwise the official repository is used -# -# format: https://github.com/username/repository, branch -# example: https://github.com/Arksine/moonraker, master -# -# branch is optional, if given, it must be preceded by a comma, if not given, 'master' is used -repositories: - https://github.com/Arksine/moonraker - -[mainsail] -port: 80 -unstable_releases: False - -[fluidd] -port: 80 -unstable_releases: False diff --git a/kiauh.py b/kiauh.py deleted file mode 100644 index 6f99540..0000000 --- a/kiauh.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from kiauh.main import main - -if __name__ == "__main__": - main() diff --git a/kiauh.sh b/kiauh.sh index 1e4bc9c..8d981d7 100755 --- a/kiauh.sh +++ b/kiauh.sh @@ -57,121 +57,15 @@ function kiauh_update_avail() { fi } -function save_startup_version() { - local launch_version - - echo "${1}" - - sed -i "/^version_to_launch=/d" "${INI_FILE}" - sed -i '$a'"version_to_launch=${1}" "${INI_FILE}" -} - -function kiauh_update_dialog() { - [[ ! $(kiauh_update_avail) == "true" ]] && return - top_border - echo -e "|${green} New KIAUH update available! ${white}|" - hr - echo -e "|${green} View Changelog: https://git.io/JnmlX ${white}|" - blank_line - echo -e "|${yellow} It is recommended to keep KIAUH up to date. Updates ${white}|" - echo -e "|${yellow} usually contain bugfixes, important changes or new ${white}|" - echo -e "|${yellow} features. Please consider updating! ${white}|" - bottom_border - - local yn - read -p "${cyan}###### Do you want to update now? (Y/n):${white} " yn - while true; do - case "${yn}" in - Y|y|Yes|yes|"") - do_action "update_kiauh" - break;; - N|n|No|no) - break;; - *) - deny_action "kiauh_update_dialog";; - esac - done -} - -function launch_kiauh_v5() { - main_menu -} - -function launch_kiauh_v6() { - local entrypoint - - if ! command -v python3 &>/dev/null || [[ $(python3 -V | cut -d " " -f2 | cut -d "." -f2) -lt 8 ]]; then - echo "Python 3.8 or higher is not installed!" - echo "Please install Python 3.8 or higher and try again." - exit 1 - fi - - entrypoint=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") - - export PYTHONPATH="${entrypoint}" - - clear -x - python3 "${entrypoint}/kiauh.py" -} - function main() { read_kiauh_ini "${FUNCNAME[0]}" - - if [[ ${version_to_launch} -eq 5 ]]; then - launch_kiauh_v5 - elif [[ ${version_to_launch} -eq 6 ]]; then - launch_kiauh_v6 - else - top_border - echo -e "| ${green}KIAUH v6.0.0-rc.2 is available now!${white} |" - hr - echo -e "| View Changelog: ${magenta}https://git.io/JnmlX${white} |" - blank_line - echo -e "| KIAUH v6 was completely rewritten from the ground up. |" - echo -e "| It's based on Python 3.8 and has many improvements. |" - blank_line - echo -e "| ${yellow}NOTE: Version 6 is still not final yet, but most bugs${white} |" - echo -e "| ${yellow}should be fixed by now. Still, if you encounter any${white} |" - echo -e "| ${yellow}issues, please report them so they can get fixed.${white} |" - hr - echo -e "| Would you like to try out KIAUH v6? |" - echo -e "| 1) Yes (recommended - v5 is sunsetting soon) |" - echo -e "| 2) No |" - echo -e "| 3) Yes, remember my choice for next time |" - echo -e "| 4) No, remember my choice for next time |" - quit_footer - while true; do - read -p "${cyan}###### Select action:${white} " -e input - case "${input}" in - 1) - launch_kiauh_v6 - break;; - 2) - launch_kiauh_v5 - break;; - 3) - save_startup_version 6 - launch_kiauh_v6 - break;; - 4) - save_startup_version 5 - launch_kiauh_v5 - break;; - Q|q) - echo -e "${green}###### Happy printing! ######${white}"; echo - exit 0;; - *) - error_msg "Invalid Input!\n";; - esac - done && input="" - fi + main_menu } check_if_ratos check_euid init_logfile set_globals -kiauh_update_dialog read_kiauh_ini init_ini main diff --git a/kiauh/__init__.py b/kiauh/__init__.py deleted file mode 100644 index bbf131c..0000000 --- a/kiauh/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -import sys -from pathlib import Path - -PROJECT_ROOT = Path(__file__).resolve().parent.parent -APPLICATION_ROOT = Path(__file__).resolve().parent -sys.path.append(str(APPLICATION_ROOT)) diff --git a/kiauh/components/__init__.py b/kiauh/components/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/crowsnest/__init__.py b/kiauh/components/crowsnest/__init__.py deleted file mode 100644 index 7f545ee..0000000 --- a/kiauh/components/crowsnest/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -from core.constants import SYSTEMD - -# repo -CROWSNEST_REPO = "https://github.com/mainsail-crew/crowsnest.git" - -# names -CROWSNEST_SERVICE_NAME = "crowsnest.service" - -# directories -CROWSNEST_DIR = Path.home().joinpath("crowsnest") - -# files -CROWSNEST_MULTI_CONFIG = CROWSNEST_DIR.joinpath("tools/.config") -CROWSNEST_INSTALL_SCRIPT = CROWSNEST_DIR.joinpath("tools/install.sh") -CROWSNEST_BIN_FILE = Path("/usr/local/bin/crowsnest") -CROWSNEST_LOGROTATE_FILE = Path("/etc/logrotate.d/crowsnest") -CROWSNEST_SERVICE_FILE = SYSTEMD.joinpath(CROWSNEST_SERVICE_NAME) diff --git a/kiauh/components/crowsnest/crowsnest.py b/kiauh/components/crowsnest/crowsnest.py deleted file mode 100644 index 1fa9a5e..0000000 --- a/kiauh/components/crowsnest/crowsnest.py +++ /dev/null @@ -1,176 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import shutil -import time -from pathlib import Path -from subprocess import CalledProcessError, run -from typing import List - -from components.crowsnest import ( - CROWSNEST_BIN_FILE, - CROWSNEST_DIR, - CROWSNEST_INSTALL_SCRIPT, - CROWSNEST_LOGROTATE_FILE, - CROWSNEST_MULTI_CONFIG, - CROWSNEST_REPO, - CROWSNEST_SERVICE_FILE, - CROWSNEST_SERVICE_NAME, -) -from components.klipper.klipper import Klipper -from core.logger import DialogType, Logger -from core.services.backup_service import BackupService -from core.settings.kiauh_settings import KiauhSettings -from core.types.component_status import ComponentStatus -from utils.common import ( - check_install_dependencies, - get_install_status, -) -from utils.git_utils import ( - git_clone_wrapper, - git_pull_wrapper, -) -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - cmd_sysctl_service, - parse_packages_from_file, -) - - -def install_crowsnest() -> None: - # Step 1: Clone crowsnest repo - git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master") - - # Step 2: Install dependencies - check_install_dependencies({"make"}) - - # Step 3: Check for Multi Instance - instances: List[Klipper] = get_instances(Klipper) - - if len(instances) > 1: - print_multi_instance_warning(instances) - - if not get_confirm("Do you want to continue with the installation?"): - Logger.print_info("Crowsnest installation aborted!") - return - - Logger.print_status("Launching crowsnest's install configurator ...") - time.sleep(3) - configure_multi_instance() - - # Step 4: Launch crowsnest installer - Logger.print_status("Launching crowsnest installer ...") - Logger.print_info("Installer will prompt you for sudo password!") - try: - run( - "sudo make install", - cwd=CROWSNEST_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Something went wrong! Please try again...\n{e}") - return - - -def print_multi_instance_warning(instances: List[Klipper]) -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - "Multi instance install detected!", - "\n\n", - "Crowsnest is NOT designed to support multi instances. A workaround " - "for this is to choose the most used instance as a 'master' and use " - "this instance to set up your 'crowsnest.conf' and steering it's service.", - "\n\n", - "The following instances were found:", - *[f"● {instance.data_dir.name}" for instance in instances], - ], - ) - - -def configure_multi_instance() -> None: - try: - run( - "make config", - cwd=CROWSNEST_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Something went wrong! Please try again...\n{e}") - if CROWSNEST_MULTI_CONFIG.exists(): - Path.unlink(CROWSNEST_MULTI_CONFIG) - return - - if not CROWSNEST_MULTI_CONFIG.exists(): - Logger.print_error("Generating .config failed, installation aborted") - - -def update_crowsnest() -> None: - try: - cmd_sysctl_service(CROWSNEST_SERVICE_NAME, "stop") - - if not CROWSNEST_DIR.exists(): - git_clone_wrapper(CROWSNEST_REPO, CROWSNEST_DIR, "master") - else: - Logger.print_status("Updating Crowsnest ...") - - settings = KiauhSettings() - if settings.kiauh.backup_before_update: - svc = BackupService() - svc.backup_directory( - source_path=CROWSNEST_DIR, - target_path="crowsnest", - backup_name="crowsnest", - ) - - git_pull_wrapper(CROWSNEST_DIR) - - deps = parse_packages_from_file(CROWSNEST_INSTALL_SCRIPT) - check_install_dependencies({*deps}) - - cmd_sysctl_service(CROWSNEST_SERVICE_NAME, "restart") - - Logger.print_ok("Crowsnest updated successfully.", end="\n\n") - except CalledProcessError as e: - Logger.print_error(f"Something went wrong! Please try again...\n{e}") - return - - -def get_crowsnest_status() -> ComponentStatus: - files = [ - CROWSNEST_BIN_FILE, - CROWSNEST_LOGROTATE_FILE, - CROWSNEST_SERVICE_FILE, - ] - return get_install_status(CROWSNEST_DIR, files=files) - - -def remove_crowsnest() -> None: - if not CROWSNEST_DIR.exists(): - Logger.print_info("Crowsnest does not seem to be installed! Skipping ...") - return - - try: - run( - "make uninstall", - cwd=CROWSNEST_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Something went wrong! Please try again...\n{e}") - return - - Logger.print_status("Removing crowsnest directory ...") - shutil.rmtree(CROWSNEST_DIR) - Logger.print_ok("Directory removed!") diff --git a/kiauh/components/klipper/__init__.py b/kiauh/components/klipper/__init__.py deleted file mode 100644 index f5bc5ae..0000000 --- a/kiauh/components/klipper/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent - -KLIPPER_REPO_URL = "https://github.com/Klipper3d/klipper.git" - -# names -KLIPPER_LOG_NAME = "klippy.log" -KLIPPER_CFG_NAME = "printer.cfg" -KLIPPER_SERIAL_NAME = "klippy.serial" -KLIPPER_UDS_NAME = "klippy.sock" -KLIPPER_ENV_FILE_NAME = "klipper.env" -KLIPPER_SERVICE_NAME = "klipper.service" - -# directories -KLIPPER_DIR = Path.home().joinpath("klipper") -KLIPPER_KCONFIGS_DIR = Path.home().joinpath("klipper-kconfigs") -KLIPPER_ENV_DIR = Path.home().joinpath("klippy-env") - -# files -KLIPPER_REQ_FILE = KLIPPER_DIR.joinpath("scripts/klippy-requirements.txt") -KLIPPER_INSTALL_SCRIPT = KLIPPER_DIR.joinpath("scripts/install-ubuntu-22.04.sh") -KLIPPER_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{KLIPPER_SERVICE_NAME}") -KLIPPER_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{KLIPPER_ENV_FILE_NAME}") - - -EXIT_KLIPPER_SETUP = "Exiting Klipper setup ..." diff --git a/kiauh/components/klipper/assets/klipper.env b/kiauh/components/klipper/assets/klipper.env deleted file mode 100644 index b56553e..0000000 --- a/kiauh/components/klipper/assets/klipper.env +++ /dev/null @@ -1 +0,0 @@ -KLIPPER_ARGS="%KLIPPER_DIR%/klippy/klippy.py %CFG% -I %SERIAL% -l %LOG% -a %UDS%" diff --git a/kiauh/components/klipper/assets/klipper.service b/kiauh/components/klipper/assets/klipper.service deleted file mode 100644 index b41788f..0000000 --- a/kiauh/components/klipper/assets/klipper.service +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=Klipper 3D Printer Firmware SV1 -Documentation=https://www.klipper3d.org/ -After=network-online.target -Wants=udev.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -RemainAfterExit=yes -WorkingDirectory=%KLIPPER_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python $KLIPPER_ARGS -Restart=always -RestartSec=10 diff --git a/kiauh/components/klipper/assets/printer.cfg b/kiauh/components/klipper/assets/printer.cfg deleted file mode 100644 index 88fe7df..0000000 --- a/kiauh/components/klipper/assets/printer.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[mcu] -serial: /dev/serial/by-id/ - -[virtual_sdcard] -path: %GCODES_DIR% -on_error_gcode: CANCEL_PRINT - -[printer] -kinematics: none -max_velocity: 1000 -max_accel: 1000 diff --git a/kiauh/components/klipper/klipper.py b/kiauh/components/klipper/klipper.py deleted file mode 100644 index 8615186..0000000 --- a/kiauh/components/klipper/klipper.py +++ /dev/null @@ -1,142 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError - -from components.klipper import ( - KLIPPER_CFG_NAME, - KLIPPER_DIR, - KLIPPER_ENV_DIR, - KLIPPER_ENV_FILE_NAME, - KLIPPER_ENV_FILE_TEMPLATE, - KLIPPER_LOG_NAME, - KLIPPER_SERIAL_NAME, - KLIPPER_SERVICE_TEMPLATE, - KLIPPER_UDS_NAME, -) -from core.constants import CURRENT_USER -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from utils.fs_utils import create_folders, get_data_dir -from utils.sys_utils import get_service_file_path - - -# noinspection PyMethodMayBeStatic -@dataclass(repr=True) -class Klipper: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name: str = KLIPPER_LOG_NAME - klipper_dir: Path = KLIPPER_DIR - env_dir: Path = KLIPPER_ENV_DIR - data_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - env_file: Path = field(init=False) - serial: Path = field(init=False) - uds: Path = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Klipper, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path(Klipper, self.suffix) - self.data_dir: Path = get_data_dir(Klipper, self.suffix) - self.cfg_file: Path = self.base.cfg_dir.joinpath(KLIPPER_CFG_NAME) - self.env_file: Path = self.base.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME) - self.serial: Path = self.base.comms_dir.joinpath(KLIPPER_SERIAL_NAME) - self.uds: Path = self.base.comms_dir.joinpath(KLIPPER_UDS_NAME) - - def create(self) -> None: - from utils.sys_utils import create_env_file, create_service_file - - Logger.print_status("Creating new Klipper Instance ...") - - try: - create_folders(self.base.base_folders) - - create_service_file( - name=self.service_file_path.name, - content=self._prep_service_file_content(), - ) - - create_env_file( - path=self.base.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME), - content=self._prep_env_file_content(), - ) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - except OSError as e: - Logger.print_error(f"Error creating env file: {e}") - raise - - def _prep_service_file_content(self) -> str: - template = KLIPPER_SERVICE_TEMPLATE - - try: - with open(template, "r") as template_file: - template_content = template_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - service_content = template_content.replace( - "%USER%", - CURRENT_USER, - ) - service_content = service_content.replace( - "%KLIPPER_DIR%", - self.klipper_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV%", - self.env_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV_FILE%", - self.base.sysd_dir.joinpath(KLIPPER_ENV_FILE_NAME).as_posix(), - ) - return service_content - - def _prep_env_file_content(self) -> str: - template = KLIPPER_ENV_FILE_TEMPLATE - - try: - with open(template, "r") as env_file: - env_template_file_content = env_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - env_file_content = env_template_file_content.replace( - "%KLIPPER_DIR%", self.klipper_dir.as_posix() - ) - env_file_content = env_file_content.replace( - "%CFG%", - f"{self.base.cfg_dir}/{KLIPPER_CFG_NAME}", - ) - env_file_content = env_file_content.replace( - "%SERIAL%", - self.serial.as_posix() if self.serial else "", - ) - env_file_content = env_file_content.replace( - "%LOG%", - self.base.log_dir.joinpath(self.log_file_name).as_posix(), - ) - env_file_content = env_file_content.replace( - "%UDS%", - self.uds.as_posix() if self.uds else "", - ) - - return env_file_content diff --git a/kiauh/components/klipper/klipper_dialogs.py b/kiauh/components/klipper/klipper_dialogs.py deleted file mode 100644 index 87ccefa..0000000 --- a/kiauh/components/klipper/klipper_dialogs.py +++ /dev/null @@ -1,113 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -import textwrap -from enum import Enum, unique -from typing import List - -from core.menus.base_menu import print_back_footer -from core.types.color import Color -from utils.instance_type import InstanceType - - -@unique -class DisplayType(Enum): - SERVICE_NAME = "SERVICE_NAME" - PRINTER_NAME = "PRINTER_NAME" - - -def print_instance_overview( - instances: List[InstanceType], - display_type: DisplayType = DisplayType.SERVICE_NAME, - show_headline=True, - show_index=False, - start_index=0, - show_select_all=False, -) -> None: - dialog = "╔═══════════════════════════════════════════════════════╗\n" - if show_headline: - d_type = ( - "Klipper instances" - if display_type is DisplayType.SERVICE_NAME - else "printer directories" - ) - headline = Color.apply(f"The following {d_type} were found:", Color.GREEN) - dialog += f"║{headline:^64}║\n" - dialog += "╟───────────────────────────────────────────────────────╢\n" - - if show_select_all: - select_all = Color.apply("a) Select all", Color.YELLOW) - dialog += f"║ {select_all:<63}║\n" - dialog += "║ ║\n" - - for i, s in enumerate(instances): - if display_type is DisplayType.SERVICE_NAME: - name = s.service_file_path.stem - else: - name = s.data_dir - line = Color.apply( - f"{f'{i + start_index})' if show_index else '●'} {name}", Color.CYAN - ) - dialog += f"║ {line:<63}║\n" - dialog += "╟───────────────────────────────────────────────────────╢\n" - - print(dialog, end="") - print_back_footer() - - -def print_select_instance_count_dialog() -> None: - line1 = Color.apply("WARNING:", Color.YELLOW) - line2 = Color.apply( - "Setting up too many instances may crash your system.", Color.YELLOW - ) - dialog = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║ Please select the number of Klipper instances to set ║ - ║ up. The number of Klipper instances will determine ║ - ║ the amount of printers you can run from this host. ║ - ║ ║ - ║ {line1:<63}║ - ║ {line2:<63}║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - - print(dialog, end="") - print_back_footer() - - -def print_select_custom_name_dialog() -> None: - line1 = Color.apply("INFO:", Color.YELLOW) - line2 = Color.apply("Only alphanumeric characters are allowed!", Color.YELLOW) - dialog = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║ Do you want to assign a custom name to each instance? ║ - ║ ║ - ║ Assigning a custom name will create a Klipper service ║ - ║ and a printer directory with the chosen name. ║ - ║ ║ - ║ Example for custom name 'kiauh': ║ - ║ ● Klipper service: klipper-kiauh.service ║ - ║ ● Printer directory: printer_kiauh_data ║ - ║ ║ - ║ If skipped, each instance will get an index assigned ║ - ║ in ascending order, starting at '1' in case of a new ║ - ║ installation. Otherwise, the index will be derived ║ - ║ from amount of already existing instances. ║ - ║ ║ - ║ {line1:<63}║ - ║ {line2:<63}║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - - print(dialog, end="") - print_back_footer() diff --git a/kiauh/components/klipper/klipper_utils.py b/kiauh/components/klipper/klipper_utils.py deleted file mode 100644 index a67d683..0000000 --- a/kiauh/components/klipper/klipper_utils.py +++ /dev/null @@ -1,263 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import grp -import os -import shutil -from pathlib import Path -from subprocess import CalledProcessError, run -from typing import Dict, List - -from components.klipper import ( - KLIPPER_DIR, - KLIPPER_ENV_DIR, - KLIPPER_INSTALL_SCRIPT, - MODULE_PATH, -) -from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import ( - print_instance_overview, - print_select_instance_count_dialog, -) -from components.webui_client.base_data import BaseWebClient -from components.webui_client.client_config.client_config_setup import ( - create_client_config_symlink, -) -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 ( - SimpleConfigParser, -) -from core.types.component_status import ComponentStatus -from utils.common import check_install_dependencies, get_install_status -from utils.fs_utils import check_file_exist -from utils.input_utils import get_confirm, get_number_input, get_string_input -from utils.instance_utils import get_instances -from utils.sys_utils import ( - cmd_sysctl_service, - install_python_packages, - parse_packages_from_file, -) - - -def get_klipper_status() -> ComponentStatus: - return get_install_status(KLIPPER_DIR, KLIPPER_ENV_DIR, Klipper) - - -def add_to_existing() -> bool | None: - kl_instances: List[Klipper] = get_instances(Klipper) - print_instance_overview(kl_instances) - _input: bool | None = get_confirm("Add new instances?", allow_go_back=True) - return _input - - -def get_install_count() -> int | None: - """ - Print a dialog for selecting the amount of Klipper instances - to set up with an option to navigate back. Returns None if the - user selected to go back, otherwise an integer greater or equal than 1 | - :return: Integer >= 1 or None - """ - kl_instances = get_instances(Klipper) - print_select_instance_count_dialog() - question = ( - f"Number of" - f"{' additional' if len(kl_instances) > 0 else ''} " - f"Klipper instances to set up" - ) - _input: int | None = get_number_input(question, 1, default=1, allow_go_back=True) - return _input - - -def assign_custom_name(key: int, name_dict: Dict[int, str]) -> None: - existing_names = [] - existing_names.extend(SUFFIX_BLACKLIST) - existing_names.extend(name_dict[n] for n in name_dict) - pattern = r"^[a-zA-Z0-9]+$" - - question = f"Enter name for instance {key}" - name_dict[key] = get_string_input(question, exclude=existing_names, regex=pattern) - - -def check_user_groups() -> None: - user_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()] - missing_groups = [g for g in ["tty", "dialout"] if g not in user_groups] - - if not missing_groups: - return - - Logger.print_dialog( - DialogType.ATTENTION, - [ - "Your current user is not in group:", - *[f"● {g}" for g in missing_groups], - "\n\n", - "It is possible that you won't be able to successfully connect and/or " - "flash the controller board without your user being a member of that " - "group. If you want to add the current user to the group(s) listed above, " - "answer with 'Y'. Else skip with 'n'.", - "\n\n", - "INFO:", - "Relog required for group assignments to take effect!", - ], - ) - - 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) - return - - try: - for group in missing_groups: - Logger.print_status(f"Adding user '{CURRENT_USER}' to group {group} ...") - command = ["sudo", "usermod", "-a", "-G", group, CURRENT_USER] - run(command, check=True) - Logger.print_ok(f"Group {group} assigned to user '{CURRENT_USER}'.") - except CalledProcessError as e: - Logger.print_error(f"Unable to add user to usergroups: {e}") - raise - - log = "Remember to relog/restart this machine for the group(s) to be applied!" - Logger.print_warn(log) - - -def handle_disruptive_system_packages() -> None: - services = [] - - command = ["systemctl", "is-enabled", "brltty"] - brltty_status = run(command, capture_output=True, text=True) - - command = ["systemctl", "is-enabled", "brltty-udev"] - brltty_udev_status = run(command, capture_output=True, text=True) - - command = ["systemctl", "is-enabled", "ModemManager"] - modem_manager_status = run(command, capture_output=True, text=True) - - if "enabled" in brltty_status.stdout: - services.append("brltty") - if "enabled" in brltty_udev_status.stdout: - services.append("brltty-udev") - if "enabled" in modem_manager_status.stdout: - services.append("ModemManager") - - for service in services if services else []: - try: - cmd_sysctl_service(service, "mask") - except CalledProcessError: - Logger.print_dialog( - DialogType.WARNING, - [ - f"KIAUH was unable to mask the {service} system service. " - "Please fix the problem manually. Otherwise, this may have " - "undesirable effects on the operation of Klipper." - ], - ) - - -def create_example_printer_cfg( - instance: Klipper, clients: List[BaseWebClient] | None = None -) -> None: - Logger.print_status(f"Creating example printer.cfg in '{instance.base.cfg_dir}'") - if instance.cfg_file.is_file(): - Logger.print_info(f"'{instance.cfg_file}' already exists.") - return - - source = MODULE_PATH.joinpath("assets/printer.cfg") - target = instance.cfg_file - try: - shutil.copy(source, target) - except OSError as e: - Logger.print_error(f"Unable to create example printer.cfg:\n{e}") - return - - scp = SimpleConfigParser() - scp.read_file(target) - scp.set_option("virtual_sdcard", "path", str(instance.base.gcodes_dir)) - - # include existing client configs in the example config - if clients is not None and len(clients) > 0: - for c in clients: - client_config = c.client_config - section = client_config.config_section - scp.add_section(section=section) - create_client_config_symlink(client_config, [instance]) - - scp.write_file(target) - - Logger.print_ok(f"Example printer.cfg created in '{instance.base.cfg_dir}'") - - -def backup_klipper_dir() -> None: - svc = BackupService() - svc.backup_directory( - source_path=KLIPPER_DIR, - backup_name="klipper", - target_path="klipper", - ) - svc.backup_directory( - source_path=KLIPPER_ENV_DIR, - backup_name="klippy-env", - target_path="klipper", - ) - - -def install_klipper_packages() -> None: - script = KLIPPER_INSTALL_SCRIPT - packages = parse_packages_from_file(script) - - # Add pkg-config for rp2040 build - packages.append("pkg-config") - - # Add dbus requirement for DietPi distro - if check_file_exist(Path("/boot/dietpi/.version")): - packages.append("dbus") - - check_install_dependencies({*packages}) - - -def install_input_shaper_deps() -> None: - if not KLIPPER_ENV_DIR.exists(): - Logger.print_warn("Required Klipper python environment not found!") - return - - Logger.print_dialog( - DialogType.CUSTOM, - [ - "Resonance measurements and shaper auto-calibration require additional " - "software dependencies which are not installed by default. " - "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:", - "● numpy", - ], - custom_title="Install Input Shaper Dependencies", - ) - if not get_confirm( - "Do you want to install the required packages?", default_choice=False - ): - return - - apt_deps = ( - "python3-numpy", - "python3-matplotlib", - "libatlas-base-dev", - "libopenblas-dev", - ) - check_install_dependencies({*apt_deps}) - - py_deps = ("numpy",) - - install_python_packages(KLIPPER_ENV_DIR, {*py_deps}) diff --git a/kiauh/components/klipper/menus/__init__.py b/kiauh/components/klipper/menus/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/klipper/menus/klipper_remove_menu.py b/kiauh/components/klipper/menus/klipper_remove_menu.py deleted file mode 100644 index 8ec3bcd..0000000 --- a/kiauh/components/klipper/menus/klipper_remove_menu.py +++ /dev/null @@ -1,102 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.klipper.services.klipper_setup_service import KlipperSetupService -from core.menus import FooterType, Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color - - -# noinspection PyUnusedLocal -class KlipperRemoveMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - - self.title = "Remove Klipper" - self.title_color = Color.RED - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.footer_type = FooterType.BACK - - self.rm_svc = False - self.rm_dir = False - self.rm_env = False - self.select_state = False - - self.klsvc = KlipperSetupService() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.remove_menu import RemoveMenu - - self.previous_menu = previous_menu if previous_menu is not None else RemoveMenu - - def set_options(self) -> None: - self.options = { - "a": Option(method=self.toggle_all), - "1": Option(method=self.toggle_remove_klipper_service), - "2": Option(method=self.toggle_remove_klipper_dir), - "3": Option(method=self.toggle_remove_klipper_env), - "c": Option(method=self.run_removal_process), - } - - def print_menu(self) -> None: - checked = f"[{Color.apply('x', Color.CYAN)}]" - unchecked = "[ ]" - o1 = checked if self.rm_svc else unchecked - o2 = checked if self.rm_dir else unchecked - o3 = checked if self.rm_env else unchecked - sel_state = f"{'Select' if not self.select_state else 'Deselect'} everything" - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ Enter a number and hit enter to select / deselect ║ - ║ the specific option for removal. ║ - ╟───────────────────────────────────────────────────────╢ - ║ a) {sel_state:49} ║ - ╟───────────────────────────────────────────────────────╢ - ║ 1) {o1} Remove Service ║ - ║ 2) {o2} Remove Local Repository ║ - ║ 3) {o3} Remove Python Environment ║ - ╟───────────────────────────────────────────────────────╢ - ║ C) Continue ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def toggle_all(self, **kwargs) -> None: - self.select_state = not self.select_state - self.rm_svc = self.select_state - self.rm_dir = self.select_state - self.rm_env = self.select_state - - def toggle_remove_klipper_service(self, **kwargs) -> None: - self.rm_svc = not self.rm_svc - - def toggle_remove_klipper_dir(self, **kwargs) -> None: - self.rm_dir = not self.rm_dir - - def toggle_remove_klipper_env(self, **kwargs) -> None: - self.rm_env = not self.rm_env - - def run_removal_process(self, **kwargs) -> None: - if not self.rm_svc and not self.rm_dir and not self.rm_env: - msg = "Nothing selected! Select options to remove first." - print(Color.apply(msg, Color.RED)) - return - - self.klsvc.remove(self.rm_svc, self.rm_dir, self.rm_env) - - self.rm_svc = False - self.rm_dir = False - self.rm_env = False - self.select_state = False diff --git a/kiauh/components/klipper/services/__init__.py b/kiauh/components/klipper/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/klipper/services/klipper_instance_service.py b/kiauh/components/klipper/services/klipper_instance_service.py deleted file mode 100644 index 593e173..0000000 --- a/kiauh/components/klipper/services/klipper_instance_service.py +++ /dev/null @@ -1,46 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from typing import List - -from components.klipper.klipper import Klipper -from utils.instance_utils import get_instances - - -class KlipperInstanceService: - __cls_instance = None - __instances: List[Klipper] = [] - - def __new__(cls) -> "KlipperInstanceService": - if cls.__cls_instance is None: - cls.__cls_instance = super(KlipperInstanceService, cls).__new__(cls) - return cls.__cls_instance - - def __init__(self) -> None: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - - def load_instances(self) -> None: - self.__instances = get_instances(Klipper) - - def create_new_instance(self, suffix: str) -> Klipper: - instance = Klipper(suffix) - self.__instances.append(instance) - return instance - - def get_all_instances(self) -> List[Klipper]: - return self.__instances - - def get_instance_by_suffix(self, suffix: str) -> Klipper | None: - instances: List[Klipper] = [i for i in self.__instances if i.suffix == suffix] - return instances[0] if instances else None diff --git a/kiauh/components/klipper/services/klipper_setup_service.py b/kiauh/components/klipper/services/klipper_setup_service.py deleted file mode 100644 index 2a80b94..0000000 --- a/kiauh/components/klipper/services/klipper_setup_service.py +++ /dev/null @@ -1,366 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from copy import copy -from typing import Dict, List, Tuple - -from components.klipper import ( - EXIT_KLIPPER_SETUP, - KLIPPER_DIR, - KLIPPER_ENV_DIR, - KLIPPER_REPO_URL, - KLIPPER_REQ_FILE, -) -from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import ( - print_instance_overview, - print_select_custom_name_dialog, -) -from components.klipper.klipper_utils import ( - assign_custom_name, - backup_klipper_dir, - check_user_groups, - create_example_printer_cfg, - get_install_count, - handle_disruptive_system_packages, - install_klipper_packages, -) -from components.klipper.services.klipper_instance_service import KlipperInstanceService -from components.moonraker.moonraker import Moonraker -from components.moonraker.services.moonraker_instance_service import ( - MoonrakerInstanceService, -) -from components.webui_client.client_utils import ( - get_existing_clients, -) -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.message_service import Message, MessageService -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color -from utils.fs_utils import run_remove_routines -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import get_confirm, get_selection_input -from utils.sys_utils import ( - cmd_sysctl_manage, - create_python_venv, - install_python_requirements, - unit_file_exists, -) - - -# noinspection PyMethodMayBeStatic -class KlipperSetupService: - __cls_instance = None - - kisvc: KlipperInstanceService - misvc: MoonrakerInstanceService - msgsvc = MessageService - - settings: KiauhSettings - klipper_list: List[Klipper] - moonraker_list: List[Moonraker] - - def __new__(cls) -> "KlipperSetupService": - if cls.__cls_instance is None: - cls.__cls_instance = super(KlipperSetupService, cls).__new__(cls) - return cls.__cls_instance - - def __init__(self) -> None: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - self.__init_state() - - def __init_state(self) -> None: - self.settings = KiauhSettings() - - self.kisvc = KlipperInstanceService() - self.kisvc.load_instances() - self.klipper_list = self.kisvc.get_all_instances() - - self.misvc = MoonrakerInstanceService() - self.misvc.load_instances() - self.moonraker_list = self.misvc.get_all_instances() - - self.msgsvc = MessageService() - - def __refresh_state(self) -> None: - self.kisvc.load_instances() - self.klipper_list = self.kisvc.get_all_instances() - - self.misvc.load_instances() - self.moonraker_list = self.misvc.get_all_instances() - - def install(self) -> None: - self.__refresh_state() - - Logger.print_status("Installing Klipper ...") - - match_moonraker: bool = False - - # if there are more moonraker instances than klipper instances, ask the user to - # match the klipper instance count to the count of moonraker instances with the same suffix - if len(self.moonraker_list) > len(self.klipper_list): - is_confirmed = self.__display_moonraker_info() - if not is_confirmed: - Logger.print_status(EXIT_KLIPPER_SETUP) - return - match_moonraker = True - - install_count, name_dict = self.__get_install_count_and_name_dict() - - if install_count == 0: - Logger.print_status(EXIT_KLIPPER_SETUP) - return - - is_multi_install = install_count > 1 or ( - len(name_dict) >= 1 and install_count >= 1 - ) - if not name_dict and install_count == 1: - name_dict = {0: ""} - elif is_multi_install and not match_moonraker: - custom_names = self.__use_custom_names_or_go_back() - if custom_names is None: - Logger.print_status(EXIT_KLIPPER_SETUP) - return - - self.__handle_instance_names(install_count, name_dict, custom_names) - - create_example_cfg = get_confirm("Create example printer.cfg?") - # run the actual installation - try: - self.__run_setup(name_dict, create_example_cfg) - except Exception as e: - Logger.print_error(e) - Logger.print_error("Klipper installation failed!") - return - - def update(self) -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - "Do NOT continue if there are ongoing prints running!", - "All Klipper instances will be restarted during the update process and " - "ongoing prints WILL FAIL.", - ], - ) - - if not get_confirm("Update Klipper now?"): - return - - self.__refresh_state() - - if self.settings.kiauh.backup_before_update: - backup_klipper_dir() - - InstanceManager.stop_all(self.klipper_list) - git_pull_wrapper(KLIPPER_DIR) - install_klipper_packages() - install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE) - InstanceManager.start_all(self.klipper_list) - - def remove( - self, - remove_service: bool, - remove_dir: bool, - remove_env: bool, - ) -> None: - self.__refresh_state() - - completion_msg = Message( - title="Klipper Removal Process completed", - color=Color.GREEN, - ) - - if remove_service: - Logger.print_status("Removing Klipper instances ...") - if self.klipper_list: - instances_to_remove = self.__get_instances_to_remove() - self.__remove_instances(instances_to_remove) - if instances_to_remove: - instance_names = [ - i.service_file_path.stem for i in instances_to_remove - ] - txt = f"● Klipper instances removed: {', '.join(instance_names)}" - completion_msg.text.append(txt) - else: - Logger.print_info("No Klipper Services installed! Skipped ...") - - if (remove_dir or remove_env) and unit_file_exists("klipper", suffix="service"): - completion_msg.text = [ - "Some Klipper services are still installed:", - f"● '{KLIPPER_DIR}' was not removed, even though selected for removal.", - f"● '{KLIPPER_ENV_DIR}' was not removed, even though selected for removal.", - ] - else: - if remove_dir: - Logger.print_status("Removing Klipper local repository ...") - if run_remove_routines(KLIPPER_DIR): - completion_msg.text.append("● Klipper local repository removed") - if remove_env: - Logger.print_status("Removing Klipper Python environment ...") - if run_remove_routines(KLIPPER_ENV_DIR): - completion_msg.text.append("● Klipper Python environment removed") - - if completion_msg.text: - completion_msg.text.insert(0, "The following actions were performed:") - else: - completion_msg.color = Color.YELLOW - completion_msg.centered = True - completion_msg.text = ["Nothing to remove."] - - self.msgsvc.set_message(completion_msg) - - def __get_install_count_and_name_dict(self) -> Tuple[int, Dict[int, str]]: - install_count: int | None - if len(self.moonraker_list) > len(self.klipper_list): - install_count = len(self.moonraker_list) - name_dict = { - i: moonraker.suffix for i, moonraker in enumerate(self.moonraker_list) - } - else: - install_count = get_install_count() - name_dict = { - i: klipper.suffix for i, klipper in enumerate(self.klipper_list) - } - - if install_count is None: - Logger.print_status(EXIT_KLIPPER_SETUP) - return 0, {} - - return install_count, name_dict - - def __run_setup(self, name_dict: Dict[int, str], create_example_cfg: bool) -> None: - if not self.klipper_list: - self.__install_deps() - - for i in name_dict: - # skip this iteration if there is already an instance with the name - if name_dict[i] in [n.suffix for n in self.klipper_list]: - continue - - instance = Klipper(suffix=name_dict[i]) - instance.create() - InstanceManager.enable(instance) - - if create_example_cfg: - # if a client-config is installed, include it in the new example cfg - clients = get_existing_clients() - create_example_printer_cfg(instance, clients) - - InstanceManager.start(instance) - - cmd_sysctl_manage("daemon-reload") - - # step 4: check/handle conflicting packages/services - handle_disruptive_system_packages() - - # step 5: check for required group membership - check_user_groups() - - def __install_deps(self) -> None: - default_repo = (KLIPPER_REPO_URL, "master") - repo = self.settings.klipper.repositories - # pull the first repo defined in kiauh.cfg or fallback to the official Klipper repo - repo, branch = (repo[0].url, repo[0].branch) if repo else default_repo - git_clone_wrapper(repo, KLIPPER_DIR, branch) - - try: - install_klipper_packages() - if create_python_venv(KLIPPER_ENV_DIR, False, False, self.settings.klipper.use_python_binary): - install_python_requirements(KLIPPER_ENV_DIR, KLIPPER_REQ_FILE) - except Exception: - Logger.print_error("Error during installation of Klipper requirements!") - raise - - def __display_moonraker_info(self) -> bool: - # todo: only show the klipper instances that are not already installed - Logger.print_dialog( - DialogType.INFO, - [ - "Existing Moonraker instances detected:", - *[f"● {m.service_file_path.stem}" for m in self.moonraker_list], - "\n\n", - "The following Klipper instances will be installed:", - *[f"● klipper-{m.suffix}" for m in self.moonraker_list], - ], - ) - _input: bool = get_confirm("Proceed with installation?") - return _input - - def __handle_instance_names( - self, install_count: int, name_dict: Dict[int, str], custom_names: bool - ) -> None: - for i in range(install_count): # 3 - key: int = len(name_dict.keys()) + 1 - if custom_names: - assign_custom_name(key, name_dict) - else: - name_dict[key] = str(len(name_dict) + 1) - - def __use_custom_names_or_go_back(self) -> bool | None: - print_select_custom_name_dialog() - _input: bool | None = get_confirm( - "Assign custom names?", - False, - allow_go_back=True, - ) - return _input - - def __get_instances_to_remove(self) -> List[Klipper] | None: - start_index = 1 - curr_instances: List[Klipper] = self.klipper_list - instance_count = len(curr_instances) - - options = [str(i + start_index) for i in range(instance_count)] - options.extend(["a", "b"]) - instance_map = {options[i]: self.klipper_list[i] for i in range(instance_count)} - - print_instance_overview( - self.klipper_list, - start_index=start_index, - show_index=True, - show_select_all=True, - ) - selection = get_selection_input("Select Klipper instance to remove", options) - - if selection == "b": - return None - elif selection == "a": - return copy(self.klipper_list) - - return [instance_map[selection]] - - def __remove_instances( - self, - instance_list: List[Klipper] | None, - ) -> None: - if not instance_list: - return - - for instance in instance_list: - Logger.print_status( - f"Removing instance {instance.service_file_path.stem} ..." - ) - InstanceManager.remove(instance) - self.__delete_klipper_env_file(instance) - - self.__refresh_state() - - def __delete_klipper_env_file(self, instance: Klipper): - Logger.print_status(f"Remove '{instance.env_file}'") - if not instance.env_file.exists(): - msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..." - Logger.print_info(msg) - return - run_remove_routines(instance.env_file) diff --git a/kiauh/components/klipper_firmware/__init__.py b/kiauh/components/klipper_firmware/__init__.py deleted file mode 100644 index 3a39f20..0000000 --- a/kiauh/components/klipper_firmware/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from components.klipper import KLIPPER_DIR - -SD_FLASH_SCRIPT = KLIPPER_DIR.joinpath("scripts/flash-sdcard.sh") diff --git a/kiauh/components/klipper_firmware/firmware_utils.py b/kiauh/components/klipper_firmware/firmware_utils.py deleted file mode 100644 index ad35247..0000000 --- a/kiauh/components/klipper_firmware/firmware_utils.py +++ /dev/null @@ -1,213 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import re -from pathlib import Path -from subprocess import ( - DEVNULL, - PIPE, - STDOUT, - CalledProcessError, - Popen, - check_output, - run, -) -from typing import List - -from components.klipper import KLIPPER_DIR -from components.klipper.klipper import Klipper -from components.klipper_firmware import SD_FLASH_SCRIPT -from components.klipper_firmware.flash_options import ( - FlashMethod, - FlashOptions, -) -from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from utils.instance_utils import get_instances -from utils.sys_utils import log_process - - -def find_firmware_file() -> bool: - target = KLIPPER_DIR.joinpath("out") - target_exists: bool = target.exists() - - f1 = "klipper.elf.hex" - f2 = "klipper.elf" - f3 = "klipper.bin" - f4 = "klipper.uf2" - fw_file_exists: bool = ( - (target.joinpath(f1).exists() and target.joinpath(f2).exists()) - or target.joinpath(f3).exists() - or target.joinpath(f4).exists() - ) - - return target_exists and fw_file_exists - - -def find_usb_device_by_id() -> List[str]: - try: - command = "find /dev/serial/by-id/*" - output = check_output(command, shell=True, text=True, stderr=DEVNULL) - return output.splitlines() - except CalledProcessError as e: - Logger.print_error("Unable to find a USB device!") - Logger.print_error(e, prefix=False) - return [] - - -def find_uart_device() -> List[str]: - try: - cmd = "find /dev -maxdepth 1" - output = check_output(cmd, shell=True, text=True, stderr=DEVNULL) - device_list = [] - if output: - pattern = r"^/dev/tty(AMA0|S0)$" - devices = output.splitlines() - device_list = [d for d in devices if re.search(pattern, d)] - return device_list - except CalledProcessError as e: - Logger.print_error("Unable to find a UART device!") - Logger.print_error(e, prefix=False) - return [] - - -def find_usb_dfu_device() -> List[str]: - try: - output = check_output("lsusb", shell=True, text=True, stderr=DEVNULL) - device_list = [] - if output: - devices = output.splitlines() - device_list = [d.split(" ")[5] for d in devices if "DFU" in d] - return device_list - - except CalledProcessError as e: - Logger.print_error("Unable to find a USB DFU device!") - Logger.print_error(e, prefix=False) - return [] - - -def find_usb_rp2_boot_device() -> List[str]: - try: - output = check_output("lsusb", shell=True, text=True, stderr=DEVNULL) - device_list = [] - if output: - devices = output.splitlines() - device_list = [d.split(" ")[5] for d in devices if "RP2 Boot" in d] - return device_list - - except CalledProcessError as e: - Logger.print_error("Unable to find a USB RP2 Boot device!") - Logger.print_error(e, prefix=False) - return [] - - -def get_sd_flash_board_list() -> List[str]: - if not KLIPPER_DIR.exists() or not SD_FLASH_SCRIPT.exists(): - return [] - - try: - cmd = f"{SD_FLASH_SCRIPT} -l" - blist: List[str] = check_output(cmd, shell=True, text=True).splitlines()[1:] - return blist - except CalledProcessError as e: - Logger.print_error(f"An unexpected error occured:\n{e}") - return [] - - -def start_flash_process(flash_options: FlashOptions) -> None: - Logger.print_status(f"Flashing '{flash_options.selected_mcu}' ...") - try: - if not flash_options.flash_method: - raise Exception("Missing value for flash_method!") - if not flash_options.flash_command: - raise Exception("Missing value for flash_command!") - if not flash_options.selected_mcu: - raise Exception("Missing value for selected_mcu!") - if not flash_options.connection_type: - raise Exception("Missing value for connection_type!") - if ( - flash_options.flash_method == FlashMethod.SD_CARD - and not flash_options.selected_board - ): - raise Exception("Missing value for selected_board!") - - if flash_options.flash_method is FlashMethod.REGULAR: - cmd = [ - "make", - f"KCONFIG_CONFIG={flash_options.selected_kconfig}", - flash_options.flash_command.value, - f"FLASH_DEVICE={flash_options.selected_mcu}", - ] - elif flash_options.flash_method is FlashMethod.SD_CARD: - if not SD_FLASH_SCRIPT.exists(): - raise Exception("Unable to find Klippers sdcard flash script!") - cmd = [ - SD_FLASH_SCRIPT.as_posix(), - f"-b {flash_options.selected_baudrate}", - flash_options.selected_mcu, - flash_options.selected_board, - ] - else: - raise Exception("Invalid value for flash_method!") - - instances = get_instances(Klipper) - InstanceManager.stop_all(instances) - - process = Popen(cmd, cwd=KLIPPER_DIR, stdout=PIPE, stderr=STDOUT, text=True) - log_process(process) - - InstanceManager.start_all(instances) - - rc = process.returncode - if rc != 0: - raise Exception(f"Flashing failed with returncode: {rc}") - else: - Logger.print_ok("Flashing successful!", start="\n", end="\n\n") - - except (Exception, CalledProcessError): - Logger.print_error("Flashing failed!", start="\n") - Logger.print_error("See the console output above!", end="\n\n") - - -def run_make_clean(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None: - try: - run( - f"make KCONFIG_CONFIG={kconfig} clean", - cwd=KLIPPER_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Unexpected error:\n{e}") - raise - - -def run_make_menuconfig(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None: - try: - run( - f"make PYTHON=python3 KCONFIG_CONFIG={kconfig} menuconfig", - cwd=KLIPPER_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Unexpected error:\n{e}") - raise - - -def run_make(kconfig=Path(KLIPPER_DIR.joinpath(".config"))) -> None: - try: - run( - f"make PYTHON=python3 KCONFIG_CONFIG={kconfig}", - cwd=KLIPPER_DIR, - shell=True, - check=True, - ) - except CalledProcessError as e: - Logger.print_error(f"Unexpected error:\n{e}") - raise diff --git a/kiauh/components/klipper_firmware/flash_options.py b/kiauh/components/klipper_firmware/flash_options.py deleted file mode 100644 index d547681..0000000 --- a/kiauh/components/klipper_firmware/flash_options.py +++ /dev/null @@ -1,115 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import field -from enum import Enum -from typing import List - - -class FlashMethod(Enum): - REGULAR = "Regular" - SD_CARD = "SD Card" - - -class FlashCommand(Enum): - FLASH = "flash" - SERIAL_FLASH = "serialflash" - - -class ConnectionType(Enum): - USB = "USB" - USB_DFU = "USB (DFU)" - USB_RP2040 = "USB (RP2040)" - UART = "UART" - - -class FlashOptions: - _instance = None - _flash_method: FlashMethod | None = None - _flash_command: FlashCommand | None = None - _connection_type: ConnectionType | None = None - _mcu_list: List[str] = field(default_factory=list) - _selected_mcu: str = "" - _selected_board: str = "" - _selected_baudrate: int = 250000 - _selected_kconfig: str = ".config" - - def __new__(cls, *args, **kwargs): - if not cls._instance: - cls._instance = super(FlashOptions, cls).__new__(cls, *args, **kwargs) - return cls._instance - - @classmethod - def destroy(cls) -> None: - cls._instance = None - - @property - def flash_method(self) -> FlashMethod | None: - return self._flash_method - - @flash_method.setter - def flash_method(self, value: FlashMethod | None): - self._flash_method = value - - @property - def flash_command(self) -> FlashCommand | None: - return self._flash_command - - @flash_command.setter - def flash_command(self, value: FlashCommand | None): - self._flash_command = value - - @property - def connection_type(self) -> ConnectionType | None: - return self._connection_type - - @connection_type.setter - def connection_type(self, value: ConnectionType | None): - self._connection_type = value - - @property - def mcu_list(self) -> List[str]: - return self._mcu_list - - @mcu_list.setter - def mcu_list(self, value: List[str]) -> None: - self._mcu_list = value - - @property - def selected_mcu(self) -> str: - return self._selected_mcu - - @selected_mcu.setter - def selected_mcu(self, value: str) -> None: - self._selected_mcu = value - - @property - def selected_board(self) -> str: - return self._selected_board - - @selected_board.setter - def selected_board(self, value: str) -> None: - self._selected_board = value - - @property - def selected_baudrate(self) -> int: - return self._selected_baudrate - - @selected_baudrate.setter - def selected_baudrate(self, value: int) -> None: - self._selected_baudrate = value - - @property - def selected_kconfig(self) -> str: - return self._selected_kconfig - - @selected_kconfig.setter - def selected_kconfig(self, value: str) -> None: - self._selected_kconfig = value diff --git a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py b/kiauh/components/klipper_firmware/menus/klipper_build_menu.py deleted file mode 100644 index d0acf26..0000000 --- a/kiauh/components/klipper_firmware/menus/klipper_build_menu.py +++ /dev/null @@ -1,274 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from pathlib import Path -from shutil import copyfile -from typing import List, Set, Type - -from components.klipper import KLIPPER_DIR, KLIPPER_KCONFIGS_DIR -from components.klipper_firmware.firmware_utils import ( - run_make, - run_make_clean, - run_make_menuconfig, -) -from components.klipper_firmware.flash_options import FlashOptions -from core.logger import DialogType, Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from utils.input_utils import get_confirm, get_string_input -from utils.sys_utils import ( - check_package_install, - install_system_packages, - update_system_package_lists, -) - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperKConfigMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "Firmware Config Menu" - self.title_color = Color.CYAN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.flash_options = FlashOptions() - self.kconfigs_dirname = KLIPPER_KCONFIGS_DIR - self.kconfig_default = KLIPPER_DIR.joinpath(".config") - self.configs: List[Path] = [] - self.kconfig = ( - self.kconfig_default if not Path(self.kconfigs_dirname).is_dir() else None - ) - - def run(self) -> None: - if not self.kconfig: - super().run() - else: - self.flash_options.selected_kconfig = self.kconfig - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.advanced_menu import AdvancedMenu - - self.previous_menu = ( - previous_menu if previous_menu is not None else AdvancedMenu - ) - - def set_options(self) -> None: - if not Path(self.kconfigs_dirname).is_dir(): - return - - self.input_label_txt = "Select config or action to continue (default=N)" - self.default_option = Option( - method=self.select_config, opt_data=self.kconfig_default - ) - - option_index = 1 - for kconfig in Path(self.kconfigs_dirname).iterdir(): - if not kconfig.name.endswith(".config"): - continue - kconfig_path = self.kconfigs_dirname.joinpath(kconfig) - if Path(kconfig_path).is_file(): - self.configs += [kconfig] - self.options[str(option_index)] = Option( - method=self.select_config, opt_data=kconfig_path - ) - option_index += 1 - self.options["n"] = Option( - method=self.select_config, opt_data=self.kconfig_default - ) - - def print_menu(self) -> None: - cfg_found_str = Color.apply( - "Previously saved firmware configs found!", Color.GREEN - ) - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ {cfg_found_str:^62} ║ - ║ ║ - ║ Select an existing config or create a new one. ║ - ╟───────────────────────────────────────────────────────╢ - ║ Available firmware configs: ║ - """ - )[1:] - - start_index = 1 - for i, s in enumerate(self.configs): - line = f"{start_index + i}) {s.name}" - menu += f"║ {line:<54}║\n" - - new_config = Color.apply("N) Create new firmware config", Color.GREEN) - menu += "║ ║\n" - menu += f"║ {new_config:<62} ║\n" - - menu += "╟───────────────────────────────────────────────────────╢\n" - - print(menu, end="") - - def select_config(self, **kwargs) -> None: - selection: str | None = kwargs.get("opt_data", None) - if selection is None: - raise Exception("opt_data is None") - if not Path(selection).is_file() and selection != self.kconfig_default: - raise Exception("opt_data does not exists") - self.kconfig = selection - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperBuildFirmwareMenu(BaseMenu): - def __init__( - self, kconfig: str | None = None, previous_menu: Type[BaseMenu] | None = None - ): - super().__init__() - self.title = "Build Firmware Menu" - self.title_color = Color.CYAN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.deps: Set[str] = {"build-essential", "dpkg-dev", "make"} - self.missing_deps: List[str] = check_package_install(self.deps) - self.flash_options = FlashOptions() - self.kconfigs_dirname = KLIPPER_KCONFIGS_DIR - self.kconfig_default = KLIPPER_DIR.joinpath(".config") - self.kconfig = self.flash_options.selected_kconfig - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.advanced_menu import AdvancedMenu - - self.previous_menu = ( - previous_menu if previous_menu is not None else AdvancedMenu - ) - - def set_options(self) -> None: - self.input_label_txt = "Press ENTER to install dependencies" - self.default_option = Option(method=self.install_missing_deps) - - def run(self): - # immediately start the build process if all dependencies are met - if len(self.missing_deps) == 0: - self.start_build_process() - else: - super().run() - - def print_menu(self) -> None: - txt = Color.apply("Dependencies are missing!", Color.RED) - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ {txt:^62} ║ - ╟───────────────────────────────────────────────────────╢ - ║ The following dependencies are required: ║ - ║ ║ - """ - )[1:] - - for d in self.deps: - status_ok = Color.apply("*INSTALLED*", Color.GREEN) - status_missing = Color.apply("*MISSING*", Color.RED) - status = status_missing if d in self.missing_deps else status_ok - padding = 40 - len(d) + len(status) + (len(status_ok) - len(status)) - d = Color.apply(f"● {d}", Color.CYAN) - menu += f"║ {d}{status:>{padding}} ║\n" - - menu += "║ ║\n" - menu += "╟───────────────────────────────────────────────────────╢\n" - - print(menu, end="") - - def install_missing_deps(self, **kwargs) -> None: - try: - update_system_package_lists(silent=False) - Logger.print_status("Installing system packages...") - install_system_packages(self.missing_deps) - except Exception as e: - Logger.print_error(e) - Logger.print_error("Installing dependencies failed!") - finally: - # restart this menu - KlipperBuildFirmwareMenu().run() - - def start_build_process(self, **kwargs) -> None: - try: - run_make_clean(self.kconfig) - run_make_menuconfig(self.kconfig) - run_make(self.kconfig) - - Logger.print_ok("Firmware successfully built!") - Logger.print_ok(f"Firmware file located in '{KLIPPER_DIR}/out'!") - - if self.kconfig == self.kconfig_default: - self.save_firmware_config() - - except Exception as e: - Logger.print_error(e) - Logger.print_error("Building Klipper Firmware failed!") - - finally: - if self.previous_menu is not None: - self.previous_menu().run() - - def save_firmware_config(self) -> None: - Logger.print_dialog( - DialogType.CUSTOM, - [ - "You can save the firmware build configs for multiple MCUs," - " and use them to update the firmware after a Klipper version upgrade" - ], - custom_title="Save firmware config", - ) - if not get_confirm( - "Do you want to save firmware config?", default_choice=False - ): - return - - filename = self.kconfig_default - while True: - Logger.print_dialog( - DialogType.CUSTOM, - [ - "Allowed characters: a-z, 0-9 and '-'", - "The name must not contain the following:", - "\n\n", - "● Any special characters", - "● No leading or trailing '-'", - ], - ) - input_name = get_string_input( - "Enter the new firmware config name", - regex=r"^[a-z0-9]+([a-z0-9-]*[a-z0-9])?$", - ) - filename = self.kconfigs_dirname.joinpath(f"{input_name}.config") - - if Path(filename).is_file(): - if get_confirm( - f"Firmware config {input_name} already exists, overwrite?", - default_choice=False, - ): - break - - if Path(filename).is_dir(): - Logger.print_error(f"Path {filename} exists and it's a directory") - - if not Path(filename).exists(): - break - - if not get_confirm( - f"Save firmware config to '{filename}'?", default_choice=True - ): - Logger.print_info("Aborted saving firmware config ...") - return - - if not Path(self.kconfigs_dirname).exists(): - Path(self.kconfigs_dirname).mkdir() - - copyfile(self.kconfig_default, filename) - - Logger.print_ok() - Logger.print_ok(f"Firmware config successfully saved to {filename}") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py deleted file mode 100644 index 42f513c..0000000 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_error_menu.py +++ /dev/null @@ -1,107 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.klipper_firmware.flash_options import FlashMethod, FlashOptions -from core.menus import FooterType, Option -from core.menus.base_menu import BaseMenu, MenuTitleStyle -from core.types.color import Color - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperNoFirmwareErrorMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "!!! NO FIRMWARE FILE FOUND !!!" - self.title_color = Color.RED - self.title_style = MenuTitleStyle.PLAIN - self.previous_menu: Type[BaseMenu] | None = previous_menu - - self.flash_options = FlashOptions() - self.footer_type = FooterType.BLANK - self.input_label_txt = "Press ENTER to go back to [Advanced Menu]" - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = previous_menu - - def set_options(self) -> None: - self.default_option = Option(method=self.go_back) - - def print_menu(self) -> None: - line1 = "Unable to find a compiled firmware file!" - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ {Color.apply(line1, Color.RED):<62} ║ - ║ ║ - ║ Make sure, that: ║ - ║ ● the folder '~/klipper/out' and its content exist ║ - ║ ● the folder contains the following file: ║ - """ - )[1:] - - if self.flash_options.flash_method is FlashMethod.REGULAR: - menu += "║ ● 'klipper.elf' ║\n" - menu += "║ ● 'klipper.elf.hex' ║\n" - else: - menu += "║ ● 'klipper.bin' ║\n" - - print(menu, end="") - - def go_back(self, **kwargs) -> None: - from core.menus.advanced_menu import AdvancedMenu - - AdvancedMenu().run() - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperNoBoardTypesErrorMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "!!! ERROR GETTING BOARD LIST !!!" - self.title_color = Color.RED - self.title_style = MenuTitleStyle.PLAIN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.footer_type = FooterType.BLANK - self.input_label_txt = "Press ENTER to go back to [Main Menu]" - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = previous_menu - - def set_options(self) -> None: - self.default_option = Option(method=self.go_back) - - def print_menu(self) -> None: - line1 = "Reading the list of supported boards failed!" - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ {Color.apply(line1, Color.RED):<62} ║ - ║ ║ - ║ Make sure, that: ║ - ║ ● the folder '~/klipper' and all its content exist ║ - ║ ● the content of folder '~/klipper' is not currupted ║ - ║ ● the file '~/klipper/scripts/flash-sd.py' exist ║ - ║ ● your current user has access to those files/folders ║ - ║ ║ - ║ If in doubt or this process continues to fail, please ║ - ║ consider to download Klipper again. ║ - """ - )[1:] - print(menu, end="") - - def go_back(self, **kwargs) -> None: - from core.menus.main_menu import MainMenu - - MainMenu().run() diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py deleted file mode 100644 index 165117e..0000000 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_help_menu.py +++ /dev/null @@ -1,177 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Tuple, Type - -from core.menus.base_menu import BaseMenu, MenuTitleStyle -from core.types.color import Color - - -def __title_config__() -> Tuple[str, Color, MenuTitleStyle]: - return "< ? > Help: Flash MCU < ? >", Color.YELLOW, MenuTitleStyle.PLAIN - - -# noinspection DuplicatedCode -class KlipperFlashMethodHelpMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title, self.title_color, self.title_style = __title_config__() - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from components.klipper_firmware.menus.klipper_flash_menu import ( - KlipperFlashMethodMenu, - ) - - self.previous_menu = ( - previous_menu if previous_menu is not None else KlipperFlashMethodMenu - ) - - def set_options(self) -> None: - pass - - def print_menu(self) -> None: - subheader1 = Color.apply("Regular flashing method:", Color.CYAN) - subheader2 = Color.apply("Updating via SD-Card Update:", Color.CYAN) - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ {subheader1:<62} ║ - ║ The default method to flash controller boards which ║ - ║ are connected and updated over USB and not by placing ║ - ║ a compiled firmware file onto an internal SD-Card. ║ - ║ ║ - ║ Common controllers that get flashed that way are: ║ - ║ - Arduino Mega 2560 ║ - ║ - Fysetc F6 / S6 (used without a Display + SD-Slot) ║ - ║ ║ - ║ {subheader2:<62} ║ - ║ Many popular controller boards ship with a bootloader ║ - ║ capable of updating the firmware via SD-Card. ║ - ║ Choose this method if your controller board supports ║ - ║ this way of updating. This method ONLY works for up- ║ - ║ grading firmware. The initial flashing procedure must ║ - ║ be done manually per the instructions that apply to ║ - ║ your controller board. ║ - ║ ║ - ║ Common controllers that can be flashed that way are: ║ - ║ - BigTreeTech SKR 1.3 / 1.4 (Turbo) / E3 / Mini E3 ║ - ║ - Fysetc F6 / S6 (used with a Display + SD-Slot) ║ - ║ - Fysetc Spider ║ - ║ ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - print(menu, end="") - - -# noinspection DuplicatedCode -class KlipperFlashCommandHelpMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title, self.title_color, self.title_style = __title_config__() - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from components.klipper_firmware.menus.klipper_flash_menu import ( - KlipperFlashCommandMenu, - ) - - self.previous_menu = ( - previous_menu if previous_menu is not None else KlipperFlashCommandMenu - ) - - def set_options(self) -> None: - pass - - def print_menu(self) -> None: - subheader1 = Color.apply("make flash:", Color.CYAN) - subheader2 = Color.apply("make serialflash:", Color.CYAN) - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ {subheader1:<62} ║ - ║ The default command to flash controller board, it ║ - ║ will detect selected microcontroller and use suitable ║ - ║ tool for flashing it. ║ - ║ ║ - ║ {subheader2:<62} ║ - ║ Special command to flash STM32 microcontrollers in ║ - ║ DFU mode but connected via serial. stm32flash command ║ - ║ will be used internally. ║ - ║ ║ - """ - )[1:] - print(menu, end="") - - -# noinspection DuplicatedCode -class KlipperMcuConnectionHelpMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title, self.title_color, self.title_style = __title_config__() - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from components.klipper_firmware.menus.klipper_flash_menu import ( - KlipperSelectMcuConnectionMenu, - ) - - self.previous_menu = ( - previous_menu - if previous_menu is not None - else KlipperSelectMcuConnectionMenu - ) - - def set_options(self) -> None: - pass - - def print_menu(self) -> None: - subheader1 = Color.apply("USB:", Color.CYAN) - subheader2 = Color.apply("UART:", Color.CYAN) - subheader3 = Color.apply("USB DFU:", Color.CYAN) - subheader4 = Color.apply("USB RP2040 Boot:", Color.CYAN) - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ {subheader1:<62} ║ - ║ Selecting USB as the connection method will scan the ║ - ║ USB ports for connected controller boards. This will ║ - ║ be similar to the 'ls /dev/serial/by-id/*' command ║ - ║ suggested by the official Klipper documentation for ║ - ║ determining successfull USB connections! ║ - ║ ║ - ║ {subheader2:<62} ║ - ║ Selecting UART as the connection method will list all ║ - ║ possible UART serial ports. Note: This method ALWAYS ║ - ║ returns something as it seems impossible to determine ║ - ║ if a valid Klipper controller board is connected or ║ - ║ not. Because of that, you MUST know which UART serial ║ - ║ port your controller board is connected to when using ║ - ║ this connection method. ║ - ║ ║ - ║ {subheader3:<62} ║ - ║ Selecting USB DFU as the connection method will scan ║ - ║ the USB ports for connected controller boards in ║ - ║ STM32 DFU mode, which is usually done by holding down ║ - ║ the BOOT button or setting a special jumper on the ║ - ║ board before powering up. ║ - ║ ║ - ║ {subheader4:<62} ║ - ║ Selecting USB RP2 Boot as the connection method will ║ - ║ scan the USB ports for connected RP2040 controller ║ - ║ boards in Boot mode, which is usually done by holding ║ - ║ down the BOOT button before powering up. ║ - ║ ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - print(menu, end="") diff --git a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py b/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py deleted file mode 100644 index a9d1fe8..0000000 --- a/kiauh/components/klipper_firmware/menus/klipper_flash_menu.py +++ /dev/null @@ -1,484 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -import time -from pathlib import Path -from typing import Type - -from components.klipper_firmware.firmware_utils import ( - find_firmware_file, - find_uart_device, - find_usb_device_by_id, - find_usb_dfu_device, - find_usb_rp2_boot_device, - get_sd_flash_board_list, - start_flash_process, -) -from components.klipper_firmware.flash_options import ( - ConnectionType, - FlashCommand, - FlashMethod, - FlashOptions, -) -from components.klipper_firmware.menus.klipper_flash_error_menu import ( - KlipperNoBoardTypesErrorMenu, - KlipperNoFirmwareErrorMenu, -) -from components.klipper_firmware.menus.klipper_flash_help_menu import ( - KlipperFlashCommandHelpMenu, - KlipperFlashMethodHelpMenu, - KlipperMcuConnectionHelpMenu, -) -from core.logger import DialogType, Logger -from core.menus import FooterType, Option -from core.menus.base_menu import BaseMenu, MenuTitleStyle -from core.types.color import Color -from utils.input_utils import get_number_input - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperFlashMethodMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "MCU Flash Menu" - self.title_color = Color.CYAN - self.help_menu = KlipperFlashMethodHelpMenu - self.input_label_txt = "Select flash method" - self.footer_type = FooterType.BACK_HELP - self.flash_options = FlashOptions() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.advanced_menu import AdvancedMenu - - self.previous_menu = ( - previous_menu if previous_menu is not None else AdvancedMenu - ) - - def set_options(self) -> None: - self.options = { - "1": Option(self.select_regular), - "2": Option(self.select_sdcard), - } - - def print_menu(self) -> None: - subheader = Color.apply("ATTENTION:", Color.YELLOW) - subline1 = Color.apply( - "Make sure to select the correct method for the MCU!", Color.YELLOW - ) - subline2 = Color.apply("Not all MCUs support both methods!", Color.YELLOW) - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ Select the flash method for flashing the MCU. ║ - ║ ║ - ║ {subheader:<62} ║ - ║ {subline1:<62} ║ - ║ {subline2:<62} ║ - ╟───────────────────────────────────────────────────────╢ - ║ 1) Regular flashing method ║ - ║ 2) Updating via SD-Card Update ║ - ╟───────────────────────────┬───────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def select_regular(self, **kwargs): - self.flash_options.flash_method = FlashMethod.REGULAR - self.goto_next_menu() - - def select_sdcard(self, **kwargs): - self.flash_options.flash_method = FlashMethod.SD_CARD - self.goto_next_menu() - - def goto_next_menu(self, **kwargs): - if find_firmware_file(): - KlipperFlashCommandMenu(previous_menu=self.__class__).run() - else: - KlipperNoFirmwareErrorMenu().run() - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperFlashCommandMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "Which flash command to use for flashing the MCU?" - self.title_style = MenuTitleStyle.PLAIN - self.title_color = Color.YELLOW - self.help_menu = KlipperFlashCommandHelpMenu - self.input_label_txt = "Select flash command" - self.footer_type = FooterType.BACK_HELP - self.flash_options = FlashOptions() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = ( - previous_menu if previous_menu is not None else KlipperFlashMethodMenu - ) - - def set_options(self) -> None: - self.options = { - "1": Option(self.select_flash), - "2": Option(self.select_serialflash), - } - self.default_option = Option(self.select_flash) - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - ╟───────────────────────────────────────────────────────╢ - ║ 1) make flash (default) ║ - ║ 2) make serialflash (stm32flash) ║ - ╟───────────────────────────┬───────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def select_flash(self, **kwargs): - self.flash_options.flash_command = FlashCommand.FLASH - self.goto_next_menu() - - def select_serialflash(self, **kwargs): - self.flash_options.flash_command = FlashCommand.SERIAL_FLASH - self.goto_next_menu() - - def goto_next_menu(self, **kwargs): - KlipperSelectMcuConnectionMenu(previous_menu=self.__class__).run() - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperSelectMcuConnectionMenu(BaseMenu): - def __init__( - self, previous_menu: Type[BaseMenu] | None = None, standalone: bool = False - ): - super().__init__() - self.title = "Make sure that the controller board is connected now!" - self.title_style = MenuTitleStyle.PLAIN - self.title_color = Color.YELLOW - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.__standalone = standalone - self.help_menu = KlipperMcuConnectionHelpMenu - self.input_label_txt = "Select connection type" - self.footer_type = FooterType.BACK_HELP - self.flash_options = FlashOptions() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = ( - previous_menu if previous_menu is not None else KlipperFlashCommandMenu - ) - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.select_usb), - "2": Option(method=self.select_dfu), - "3": Option(method=self.select_usb_dfu), - "4": Option(method=self.select_usb_rp2040), - } - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - ╟───────────────────────────────────────────────────────╢ - ║ How is the controller board connected to the host? ║ - ╟───────────────────────────────────────────────────────╢ - ║ 1) USB ║ - ║ 2) UART ║ - ║ 3) USB (DFU mode) ║ - ║ 4) USB (RP2040 mode) ║ - ╟───────────────────────────┬───────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def select_usb(self, **kwargs): - self.flash_options.connection_type = ConnectionType.USB - self.get_mcu_list() - - def select_dfu(self, **kwargs): - self.flash_options.connection_type = ConnectionType.UART - self.get_mcu_list() - - def select_usb_dfu(self, **kwargs): - self.flash_options.connection_type = ConnectionType.USB_DFU - self.get_mcu_list() - - def select_usb_rp2040(self, **kwargs): - self.flash_options.connection_type = ConnectionType.USB_RP2040 - self.get_mcu_list() - - def get_mcu_list(self, **kwargs): - conn_type = self.flash_options.connection_type - - if conn_type is ConnectionType.USB: - Logger.print_status("Identifying MCU connected via USB ...") - self.flash_options.mcu_list = find_usb_device_by_id() - elif conn_type is ConnectionType.UART: - Logger.print_status("Identifying MCU possibly connected via UART ...") - self.flash_options.mcu_list = find_uart_device() - elif conn_type is ConnectionType.USB_DFU: - Logger.print_status("Identifying MCU connected via USB in DFU mode ...") - self.flash_options.mcu_list = find_usb_dfu_device() - elif conn_type is ConnectionType.USB_RP2040: - Logger.print_status( - "Identifying MCU connected via USB in RP2 Boot mode ..." - ) - self.flash_options.mcu_list = find_usb_rp2_boot_device() - - 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.") - - # 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: - 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}") - time.sleep(3) - return - - self.goto_next_menu() - - def goto_next_menu(self, **kwargs): - KlipperSelectMcuIdMenu(previous_menu=self.__class__).run() - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperSelectMcuIdMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "!!! ATTENTION !!!" - self.title_style = MenuTitleStyle.PLAIN - self.title_color = Color.RED - self.flash_options = FlashOptions() - self.mcu_list = self.flash_options.mcu_list - self.input_label_txt = "Select MCU to flash" - self.footer_type = FooterType.BACK - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = ( - previous_menu - if previous_menu is not None - else KlipperSelectMcuConnectionMenu - ) - - def set_options(self) -> None: - self.options = { - f"{i}": Option(self.flash_mcu, f"{i}") for i in range(len(self.mcu_list)) - } - - def print_menu(self) -> None: - header2 = f"[{Color.apply('List of detected MCUs', Color.CYAN)}]" - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ Make sure, to select the correct MCU! ║ - ║ ONLY flash a firmware created for the respective MCU! ║ - ║ ║ - ╟{header2:─^64}╢ - ║ ║ - """ - )[1:] - - for i, mcu in enumerate(self.mcu_list): - mcu = mcu.split("/")[-1] - menu += f"║ {i}) {Color.apply(f'{mcu:<51}', Color.CYAN)}║\n" - - menu += textwrap.dedent( - """ - ║ ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def flash_mcu(self, **kwargs): - try: - index: int | None = kwargs.get("opt_index", None) - if index is None: - raise Exception("opt_index is None") - - index = int(index) - selected_mcu = self.mcu_list[index] - self.flash_options.selected_mcu = selected_mcu - - if self.flash_options.flash_method == FlashMethod.SD_CARD: - KlipperSelectSDFlashBoardMenu(previous_menu=self.__class__).run() - elif self.flash_options.flash_method == FlashMethod.REGULAR: - KlipperFlashOverviewMenu(previous_menu=self.__class__).run() - except Exception as e: - Logger.print_error(e) - Logger.print_error("Flashing failed!") - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperSelectSDFlashBoardMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.flash_options = FlashOptions() - self.available_boards = get_sd_flash_board_list() - self.input_label_txt = "Select board type" - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = ( - previous_menu if previous_menu is not None else KlipperSelectMcuIdMenu - ) - - def set_options(self) -> None: - self.options = { - f"{i}": Option(self.board_select, f"{i}") - for i in range(len(self.available_boards)) - } - - def print_menu(self) -> None: - if len(self.available_boards) < 1: - KlipperNoBoardTypesErrorMenu().run() - else: - menu = textwrap.dedent( - """ - ║ Please select the type of board that corresponds to ║ - ║ the currently selected MCU ID you chose before. ║ - ║ ║ - ║ The following boards are currently supported: ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - - for i, board in enumerate(self.available_boards): - line = f" {i}) {board}" - menu += f"║{line:<55}║\n" - menu += "╟───────────────────────────────────────────────────────╢" - print(menu, end="") - - def board_select(self, **kwargs): - try: - index: int | None = kwargs.get("opt_index", None) - if index is None: - raise Exception("opt_index is None") - - index = int(index) - self.flash_options.selected_board = self.available_boards[index] - self.baudrate_select() - except Exception as e: - Logger.print_error(e) - Logger.print_error("Board selection failed!") - - def baudrate_select(self, **kwargs): - Logger.print_dialog( - DialogType.CUSTOM, - [ - "If your board is flashed with firmware that connects " - "at a custom baud rate, please change it now.", - "\n\n", - "If you are unsure, stick to the default 250000!", - ], - ) - self.flash_options.selected_baudrate = get_number_input( - question="Please set the baud rate", - default=250000, - min_value=0, - allow_go_back=True, - ) - KlipperFlashOverviewMenu(previous_menu=self.__class__).run() - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KlipperFlashOverviewMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "!!! ATTENTION !!!" - self.title_style = MenuTitleStyle.PLAIN - self.title_color = Color.RED - self.flash_options = FlashOptions() - self.input_label_txt = "Perform action (default=Y)" - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_options(self) -> None: - self.options = { - "y": Option(self.execute_flash), - "n": Option(self.abort_process), - } - - self.default_option = Option(self.execute_flash) - - def print_menu(self) -> None: - method = self.flash_options.flash_method.value - command = self.flash_options.flash_command.value - conn_type = self.flash_options.connection_type.value - mcu = self.flash_options.selected_mcu.split("/")[-1] - board = self.flash_options.selected_board - baudrate = self.flash_options.selected_baudrate - kconfig = Path(self.flash_options.selected_kconfig).name - color = Color.CYAN - subheader = f"[{Color.apply('Overview', color)}]" - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ Before contuining the flashing process, please check ║ - ║ if all parameters were set correctly! Once you made ║ - ║ sure everything is correct, start the process. If any ║ - ║ parameter needs to be changed, you can go back (B) ║ - ║ step by step or abort and start from the beginning. ║ - ║{subheader:─^64}║ - ║ ║ - """ - )[1:] - - menu += textwrap.dedent( - f""" - ║ MCU: {Color.apply(f"{mcu:<48}", color)} ║ - ║ Connection: {Color.apply(f"{conn_type:<41}", color)} ║ - ║ Flash method: {Color.apply(f"{method:<39}", color)} ║ - ║ Flash command: {Color.apply(f"{command:<38}", color)} ║ - """ - )[1:] - - if self.flash_options.flash_method is FlashMethod.SD_CARD: - menu += textwrap.dedent( - f""" - ║ Board type: {Color.apply(f"{board:<41}", color)} ║ - ║ Baudrate: {Color.apply(f"{baudrate:<43}", color)} ║ - """ - )[1:] - - if self.flash_options.flash_method is FlashMethod.REGULAR: - menu += textwrap.dedent( - f""" - ║ Firmware config: {Color.apply(f"{kconfig:<36}", color)} ║ - """ - )[1:] - - menu += textwrap.dedent( - """ - ║ ║ - ╟───────────────────────────────────────────────────────╢ - ║ Y) Start flash process ║ - ║ N) Abort - Return to Advanced Menu ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def execute_flash(self, **kwargs): - start_flash_process(self.flash_options) - Logger.print_info("Returning to MCU Flash Menu in 5 seconds ...") - time.sleep(5) - KlipperFlashMethodMenu().run() - - def abort_process(self, **kwargs): - from core.menus.advanced_menu import AdvancedMenu - - AdvancedMenu().run() diff --git a/kiauh/components/klipperscreen/__init__.py b/kiauh/components/klipperscreen/__init__.py deleted file mode 100644 index 8640b8a..0000000 --- a/kiauh/components/klipperscreen/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -from core.constants import SYSTEMD - -# repo -KLIPPERSCREEN_REPO = "https://github.com/KlipperScreen/KlipperScreen.git" - -# names -KLIPPERSCREEN_SERVICE_NAME = "KlipperScreen.service" -KLIPPERSCREEN_UPDATER_SECTION_NAME = "update_manager KlipperScreen" -KLIPPERSCREEN_LOG_NAME = "KlipperScreen.log" - -# directories -KLIPPERSCREEN_DIR = Path.home().joinpath("KlipperScreen") -KLIPPERSCREEN_ENV_DIR = Path.home().joinpath(".KlipperScreen-env") - -# files -KLIPPERSCREEN_REQ_FILE = KLIPPERSCREEN_DIR.joinpath( - "scripts/KlipperScreen-requirements.txt" -) -KLIPPERSCREEN_INSTALL_SCRIPT = KLIPPERSCREEN_DIR.joinpath( - "scripts/KlipperScreen-install.sh" -) -KLIPPERSCREEN_SERVICE_FILE = SYSTEMD.joinpath(KLIPPERSCREEN_SERVICE_NAME) diff --git a/kiauh/components/klipperscreen/klipperscreen.py b/kiauh/components/klipperscreen/klipperscreen.py deleted file mode 100644 index b62a3f5..0000000 --- a/kiauh/components/klipperscreen/klipperscreen.py +++ /dev/null @@ -1,207 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import shutil -from pathlib import Path -from subprocess import CalledProcessError, run -from typing import List - -from components.klipper.klipper import Klipper -from components.klipperscreen import ( - KLIPPERSCREEN_DIR, - KLIPPERSCREEN_ENV_DIR, - KLIPPERSCREEN_INSTALL_SCRIPT, - KLIPPERSCREEN_LOG_NAME, - KLIPPERSCREEN_REPO, - KLIPPERSCREEN_REQ_FILE, - KLIPPERSCREEN_SERVICE_FILE, - KLIPPERSCREEN_SERVICE_NAME, - KLIPPERSCREEN_UPDATER_SECTION_NAME, -) -from components.moonraker.moonraker import Moonraker -from core.constants import SYSTEMD -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.backup_service import BackupService -from core.settings.kiauh_settings import KiauhSettings -from core.types.component_status import ComponentStatus -from utils.common import ( - check_install_dependencies, - get_install_status, -) -from utils.config_utils import add_config_section, remove_config_section -from utils.fs_utils import remove_with_sudo -from utils.git_utils import ( - git_clone_wrapper, - git_pull_wrapper, -) -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - check_python_version, - cmd_sysctl_service, - install_python_requirements, - remove_system_service, -) - - -def install_klipperscreen() -> None: - Logger.print_status("Installing KlipperScreen ...") - - if not check_python_version(3, 7): - return - - mr_instances = get_instances(Moonraker) - if not mr_instances: - Logger.print_dialog( - DialogType.WARNING, - [ - "Moonraker not found! KlipperScreen will not properly work " - "without a working Moonraker installation.", - "\n\n", - "KlipperScreens update manager configuration for Moonraker " - "will not be added to any moonraker.conf.", - ], - ) - if not get_confirm( - "Continue KlipperScreen installation?", - default_choice=False, - allow_go_back=True, - ): - return - - check_install_dependencies() - - git_clone_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR) - - try: - run(KLIPPERSCREEN_INSTALL_SCRIPT.as_posix(), shell=True, check=True) - if mr_instances: - patch_klipperscreen_update_manager(mr_instances) - InstanceManager.restart_all(mr_instances) - else: - Logger.print_info( - "Moonraker is not installed! Cannot add " - "KlipperScreen to update manager!" - ) - Logger.print_ok("KlipperScreen successfully installed!") - except CalledProcessError as e: - Logger.print_error(f"Error installing KlipperScreen:\n{e}") - return - - -def patch_klipperscreen_update_manager(instances: List[Moonraker]) -> None: - BackupService().backup_moonraker_conf() - add_config_section( - section=KLIPPERSCREEN_UPDATER_SECTION_NAME, - instances=instances, - options=[ - ("type", "git_repo"), - ("path", KLIPPERSCREEN_DIR.as_posix()), - ("origin", KLIPPERSCREEN_REPO), - ("managed_services", "KlipperScreen"), - ("env", f"{KLIPPERSCREEN_ENV_DIR}/bin/python"), - ("requirements", KLIPPERSCREEN_REQ_FILE.as_posix()), - ("install_script", KLIPPERSCREEN_INSTALL_SCRIPT.as_posix()), - ], - ) - - -def update_klipperscreen() -> None: - if not KLIPPERSCREEN_DIR.exists(): - Logger.print_info("KlipperScreen does not seem to be installed! Skipping ...") - return - - try: - Logger.print_status("Updating KlipperScreen ...") - - cmd_sysctl_service(KLIPPERSCREEN_SERVICE_NAME, "stop") - - settings = KiauhSettings() - if settings.kiauh.backup_before_update: - backup_klipperscreen_dir() - - git_pull_wrapper(KLIPPERSCREEN_DIR) - - install_python_requirements(KLIPPERSCREEN_ENV_DIR, KLIPPERSCREEN_REQ_FILE) - - cmd_sysctl_service(KLIPPERSCREEN_SERVICE_NAME, "start") - - Logger.print_ok("KlipperScreen updated successfully.", end="\n\n") - except CalledProcessError as e: - Logger.print_error(f"Error updating KlipperScreen:\n{e}") - return - - -def get_klipperscreen_status() -> ComponentStatus: - return get_install_status( - KLIPPERSCREEN_DIR, - KLIPPERSCREEN_ENV_DIR, - files=[SYSTEMD.joinpath(KLIPPERSCREEN_SERVICE_NAME)], - ) - - -def remove_klipperscreen() -> None: - Logger.print_status("Removing KlipperScreen ...") - try: - if KLIPPERSCREEN_DIR.exists(): - Logger.print_status("Removing KlipperScreen directory ...") - shutil.rmtree(KLIPPERSCREEN_DIR) - Logger.print_ok("KlipperScreen directory successfully removed!") - else: - Logger.print_warn("KlipperScreen directory not found!") - - if KLIPPERSCREEN_ENV_DIR.exists(): - Logger.print_status("Removing KlipperScreen environment ...") - shutil.rmtree(KLIPPERSCREEN_ENV_DIR) - Logger.print_ok("KlipperScreen environment successfully removed!") - else: - Logger.print_warn("KlipperScreen environment not found!") - - if KLIPPERSCREEN_SERVICE_FILE.exists(): - remove_system_service(KLIPPERSCREEN_SERVICE_NAME) - - logfile = Path(f"/tmp/{KLIPPERSCREEN_LOG_NAME}") - if logfile.exists(): - Logger.print_status("Removing KlipperScreen log file ...") - remove_with_sudo(logfile) - Logger.print_ok("KlipperScreen log file successfully removed!") - - kl_instances: List[Klipper] = get_instances(Klipper) - for instance in kl_instances: - logfile = instance.base.log_dir.joinpath(KLIPPERSCREEN_LOG_NAME) - if logfile.exists(): - Logger.print_status(f"Removing {logfile} ...") - Path(logfile).unlink() - Logger.print_ok(f"{logfile} successfully removed!") - - mr_instances: List[Moonraker] = get_instances(Moonraker) - if mr_instances: - Logger.print_status("Removing KlipperScreen from update manager ...") - BackupService().backup_moonraker_conf() - remove_config_section("update_manager KlipperScreen", mr_instances) - Logger.print_ok("KlipperScreen successfully removed from update manager!") - - Logger.print_ok("KlipperScreen successfully removed!") - - except Exception as e: - Logger.print_error(f"Error removing KlipperScreen:\n{e}") - - -def backup_klipperscreen_dir() -> None: - svc = BackupService() - svc.backup_directory( - source_path=KLIPPERSCREEN_DIR, - backup_name="KlipperScreen", - target_path="KlipperScreen", - ) - svc.backup_directory( - source_path=KLIPPERSCREEN_ENV_DIR, - backup_name="KlipperScreen-env", - target_path="KlipperScreen", - ) diff --git a/kiauh/components/log_uploads/__init__.py b/kiauh/components/log_uploads/__init__.py deleted file mode 100644 index 2d4b133..0000000 --- a/kiauh/components/log_uploads/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path -from typing import Dict, Literal, Union - -FileKey = Literal["filepath", "display_name"] -LogFile = Dict[FileKey, Union[str, Path]] diff --git a/kiauh/components/log_uploads/log_upload_utils.py b/kiauh/components/log_uploads/log_upload_utils.py deleted file mode 100644 index 3047a11..0000000 --- a/kiauh/components/log_uploads/log_upload_utils.py +++ /dev/null @@ -1,55 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -import urllib.request -from pathlib import Path -from typing import List - -from components.klipper.klipper import Klipper -from components.log_uploads import LogFile -from core.logger import Logger -from utils.instance_utils import get_instances - - -def get_logfile_list() -> List[LogFile]: - log_dirs: List[Path] = [ - instance.base.log_dir for instance in get_instances(Klipper) - ] - - logfiles: List[LogFile] = [] - for _dir in log_dirs: - for f in _dir.iterdir(): - logfiles.append({"filepath": f, "display_name": get_display_name(f)}) - - return logfiles - - -def get_display_name(filepath: Path) -> str: - printer = " ".join(filepath.parts[-3].split("_")[:-1]) - name = filepath.name - - return f"{printer}: {name}" - - -def upload_logfile(logfile: LogFile) -> None: - file = logfile.get("filepath") - name = logfile.get("display_name") - Logger.print_status(f"Uploading the following logfile from {name} ...") - - with open(file, "rb") as f: - headers = {"x-random": ""} - req = urllib.request.Request("http://paste.c-net.org/", headers=headers, data=f) - try: - response = urllib.request.urlopen(req) - link = response.read().decode("utf-8") - Logger.print_ok("Upload successful! Access it via the following link:") - Logger.print_ok(f">>>> {link}", False) - except Exception as e: - Logger.print_error("Uploading logfile failed!") - Logger.print_error(str(e)) diff --git a/kiauh/components/log_uploads/menus/log_upload_menu.py b/kiauh/components/log_uploads/menus/log_upload_menu.py deleted file mode 100644 index 6c131fb..0000000 --- a/kiauh/components/log_uploads/menus/log_upload_menu.py +++ /dev/null @@ -1,67 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.log_uploads.log_upload_utils import get_logfile_list, upload_logfile -from core.logger import Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color - - -# noinspection PyMethodMayBeStatic -class LogUploadMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "Log Upload" - self.title_color = Color.YELLOW - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.logfile_list = get_logfile_list() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - f"{index}": Option(self.upload, opt_index=f"{index}") - for index in range(len(self.logfile_list)) - } - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - ╟───────────────────────────────────────────────────────╢ - ║ You can select the following logfiles for uploading: ║ - ║ ║ - """ - )[1:] - - for logfile in enumerate(self.logfile_list): - line = f"{logfile[0]}) {logfile[1].get('display_name')}" - menu += f"║ {line:<54}║\n" - menu += "╟───────────────────────────────────────────────────────╢\n" - - print(menu, end="") - - def upload(self, **kwargs): - try: - index: int | None = kwargs.get("opt_index", None) - if index is None: - raise Exception("opt_index is None") - - index = int(index) - upload_logfile(self.logfile_list[index]) - except Exception as e: - Logger.print_error(e) - Logger.print_error("Log upload failed!") diff --git a/kiauh/components/moonraker/__init__.py b/kiauh/components/moonraker/__init__.py deleted file mode 100644 index 50b98c6..0000000 --- a/kiauh/components/moonraker/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent - -MOONRAKER_REPO_URL = "https://github.com/Arksine/moonraker.git" - -# names -MOONRAKER_CFG_NAME = "moonraker.conf" -MOONRAKER_LOG_NAME = "moonraker.log" -MOONRAKER_SERVICE_NAME = "moonraker.service" -MOONRAKER_DEFAULT_PORT = 7125 -MOONRAKER_ENV_FILE_NAME = "moonraker.env" - -# directories -MOONRAKER_DIR = Path.home().joinpath("moonraker") -MOONRAKER_ENV_DIR = Path.home().joinpath("moonraker-env") - -# files -MOONRAKER_INSTALL_SCRIPT = MOONRAKER_DIR.joinpath("scripts/install-moonraker.sh") -MOONRAKER_REQ_FILE = MOONRAKER_DIR.joinpath("scripts/moonraker-requirements.txt") -MOONRAKER_SPEEDUPS_REQ_FILE = MOONRAKER_DIR.joinpath("scripts/moonraker-speedups.txt") -MOONRAKER_DEPS_JSON_FILE = MOONRAKER_DIR.joinpath("scripts/system-dependencies.json") -# introduced due to -# https://github.com/Arksine/moonraker/issues/349 -# https://github.com/Arksine/moonraker/pull/346 -POLKIT_LEGACY_FILE = Path("/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla") -POLKIT_FILE = Path("/etc/polkit-1/rules.d/moonraker.rules") -POLKIT_USR_FILE = Path("/usr/share/polkit-1/rules.d/moonraker.rules") -POLKIT_SCRIPT = MOONRAKER_DIR.joinpath("scripts/set-policykit-rules.sh") -MOONRAKER_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{MOONRAKER_SERVICE_NAME}") -MOONRAKER_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{MOONRAKER_ENV_FILE_NAME}") - - -EXIT_MOONRAKER_SETUP = "Exiting Moonraker setup ..." diff --git a/kiauh/components/moonraker/assets/moonraker.conf b/kiauh/components/moonraker/assets/moonraker.conf deleted file mode 100644 index 8e592f2..0000000 --- a/kiauh/components/moonraker/assets/moonraker.conf +++ /dev/null @@ -1,30 +0,0 @@ -[server] -host: 0.0.0.0 -port: %PORT% -klippy_uds_address: %UDS% - -[authorization] -trusted_clients: - 10.0.0.0/8 - 127.0.0.0/8 - 169.254.0.0/16 - 172.16.0.0/12 - 192.168.0.0/16 - FC00::/7 - FE80::/10 - ::1/128 -cors_domains: - *.lan - *.local - *://localhost - *://localhost:* - *://my.mainsail.xyz - *://app.fluidd.xyz - -[octoprint_compat] - -[history] - -[update_manager] -channel: dev -refresh_interval: 168 diff --git a/kiauh/components/moonraker/assets/moonraker.env b/kiauh/components/moonraker/assets/moonraker.env deleted file mode 100644 index bca6af5..0000000 --- a/kiauh/components/moonraker/assets/moonraker.env +++ /dev/null @@ -1 +0,0 @@ -MOONRAKER_ARGS="%MOONRAKER_DIR%/moonraker/moonraker.py -d %PRINTER_DATA%" \ No newline at end of file diff --git a/kiauh/components/moonraker/assets/moonraker.service b/kiauh/components/moonraker/assets/moonraker.service deleted file mode 100644 index 696d7ba..0000000 --- a/kiauh/components/moonraker/assets/moonraker.service +++ /dev/null @@ -1,19 +0,0 @@ -[Unit] -Description=API Server for Klipper SV1 -Documentation=https://moonraker.readthedocs.io/ -Requires=network-online.target -After=network-online.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -SupplementaryGroups=moonraker-admin -RemainAfterExit=yes -WorkingDirectory=%MOONRAKER_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python $MOONRAKER_ARGS -Restart=always -RestartSec=10 diff --git a/kiauh/components/moonraker/menus/__init__.py b/kiauh/components/moonraker/menus/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/moonraker/menus/moonraker_remove_menu.py b/kiauh/components/moonraker/menus/moonraker_remove_menu.py deleted file mode 100644 index 6729c3e..0000000 --- a/kiauh/components/moonraker/menus/moonraker_remove_menu.py +++ /dev/null @@ -1,110 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService -from core.menus import FooterType, Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color - - -# noinspection PyUnusedLocal -class MoonrakerRemoveMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - - self.title = "Remove Moonraker" - self.title_color = Color.RED - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.footer_type = FooterType.BACK - - self.rm_svc = False - self.rm_dir = False - self.rm_env = False - self.rm_pk = False - self.select_state = False - - self.mrsvc = MoonrakerSetupService() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.remove_menu import RemoveMenu - - self.previous_menu = previous_menu if previous_menu is not None else RemoveMenu - - def set_options(self) -> None: - self.options = { - "a": Option(method=self.toggle_all), - "1": Option(method=self.toggle_remove_moonraker_service), - "2": Option(method=self.toggle_remove_moonraker_dir), - "3": Option(method=self.toggle_remove_moonraker_env), - "4": Option(method=self.toggle_remove_moonraker_polkit), - "c": Option(method=self.run_removal_process), - } - - def print_menu(self) -> None: - checked = f"[{Color.apply('x', Color.CYAN)}]" - unchecked = "[ ]" - o1 = checked if self.rm_svc else unchecked - o2 = checked if self.rm_dir else unchecked - o3 = checked if self.rm_env else unchecked - o4 = checked if self.rm_pk else unchecked - sel_state = f"{'Select' if not self.select_state else 'Deselect'} everything" - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ Enter a number and hit enter to select / deselect ║ - ║ the specific option for removal. ║ - ╟───────────────────────────────────────────────────────╢ - ║ a) {sel_state:49} ║ - ╟───────────────────────────────────────────────────────╢ - ║ 1) {o1} Remove Service ║ - ║ 2) {o2} Remove Local Repository ║ - ║ 3) {o3} Remove Python Environment ║ - ║ 4) {o4} Remove Policy Kit Rules ║ - ╟───────────────────────────────────────────────────────╢ - ║ C) Continue ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def toggle_all(self, **kwargs) -> None: - self.select_state = not self.select_state - self.rm_svc = self.select_state - self.rm_dir = self.select_state - self.rm_env = self.select_state - self.rm_pk = self.select_state - - def toggle_remove_moonraker_service(self, **kwargs) -> None: - self.rm_svc = not self.rm_svc - - def toggle_remove_moonraker_dir(self, **kwargs) -> None: - self.rm_dir = not self.rm_dir - - def toggle_remove_moonraker_env(self, **kwargs) -> None: - self.rm_env = not self.rm_env - - def toggle_remove_moonraker_polkit(self, **kwargs) -> None: - self.rm_pk = not self.rm_pk - - def run_removal_process(self, **kwargs) -> None: - if not self.rm_svc and not self.rm_dir and not self.rm_env and not self.rm_pk: - msg = "Nothing selected! Select options to remove first." - print(Color.apply(msg, Color.RED)) - return - - self.mrsvc.remove(self.rm_svc, self.rm_dir, self.rm_env, self.rm_pk) - - self.rm_svc = False - self.rm_dir = False - self.rm_env = False - self.rm_pk = False diff --git a/kiauh/components/moonraker/moonraker.py b/kiauh/components/moonraker/moonraker.py deleted file mode 100644 index 81040c3..0000000 --- a/kiauh/components/moonraker/moonraker.py +++ /dev/null @@ -1,146 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError - -from components.klipper.klipper import Klipper -from components.moonraker import ( - MOONRAKER_CFG_NAME, - MOONRAKER_DIR, - MOONRAKER_ENV_DIR, - MOONRAKER_ENV_FILE_NAME, - MOONRAKER_ENV_FILE_TEMPLATE, - MOONRAKER_LOG_NAME, - MOONRAKER_SERVICE_TEMPLATE, -) -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 ( - SimpleConfigParser, -) -from utils.fs_utils import create_folders -from utils.sys_utils import get_service_file_path - - -# noinspection PyMethodMayBeStatic -@dataclass -class Moonraker: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name: str = MOONRAKER_LOG_NAME - moonraker_dir: Path = MOONRAKER_DIR - env_dir: Path = MOONRAKER_ENV_DIR - data_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - env_file: Path = field(init=False) - backup_dir: Path = field(init=False) - certs_dir: Path = field(init=False) - db_dir: Path = field(init=False) - port: int | None = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Klipper, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path(Moonraker, self.suffix) - self.data_dir: Path = self.base.data_dir - self.cfg_file: Path = self.base.cfg_dir.joinpath(MOONRAKER_CFG_NAME) - self.env_file: Path = self.base.sysd_dir.joinpath(MOONRAKER_ENV_FILE_NAME) - self.backup_dir: Path = self.base.data_dir.joinpath("backup") - self.certs_dir: Path = self.base.data_dir.joinpath("certs") - self.db_dir: Path = self.base.data_dir.joinpath("database") - self.port: int | None = self._get_port() - - def create(self) -> None: - from utils.sys_utils import create_env_file, create_service_file - - Logger.print_status("Creating new Moonraker Instance ...") - - try: - create_folders(self.base.base_folders) - - create_service_file( - name=self.service_file_path.name, - content=self._prep_service_file_content(), - ) - create_env_file( - path=self.base.sysd_dir.joinpath(MOONRAKER_ENV_FILE_NAME), - content=self._prep_env_file_content(), - ) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - except OSError as e: - Logger.print_error(f"Error creating env file: {e}") - raise - - def _prep_service_file_content(self) -> str: - template = MOONRAKER_SERVICE_TEMPLATE - - try: - with open(template, "r") as template_file: - template_content = template_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - service_content = template_content.replace( - "%USER%", - CURRENT_USER, - ) - service_content = service_content.replace( - "%MOONRAKER_DIR%", - self.moonraker_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV%", - self.env_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV_FILE%", - self.base.sysd_dir.joinpath(MOONRAKER_ENV_FILE_NAME).as_posix(), - ) - return service_content - - def _prep_env_file_content(self) -> str: - template = MOONRAKER_ENV_FILE_TEMPLATE - - try: - with open(template, "r") as env_file: - env_template_file_content = env_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - env_file_content = env_template_file_content.replace( - "%MOONRAKER_DIR%", - self.moonraker_dir.as_posix(), - ) - env_file_content = env_file_content.replace( - "%PRINTER_DATA%", - self.base.data_dir.as_posix(), - ) - - return env_file_content - - def _get_port(self) -> int | None: - if not self.cfg_file or not self.cfg_file.is_file(): - return None - - scp = SimpleConfigParser() - scp.read_file(self.cfg_file) - port: int | None = scp.getint("server", "port", fallback=None) - - return port diff --git a/kiauh/components/moonraker/moonraker_dialogs.py b/kiauh/components/moonraker/moonraker_dialogs.py deleted file mode 100644 index aa7acf5..0000000 --- a/kiauh/components/moonraker/moonraker_dialogs.py +++ /dev/null @@ -1,75 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -import textwrap -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.menus.base_menu import print_back_footer -from core.types.color import Color - - -def print_moonraker_overview( - klipper_instances: List[Klipper], - moonraker_instances: List[Moonraker], - show_index=False, - show_select_all=False, -): - headline = Color.apply("The following instances were found:", Color.GREEN) - dialog = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║{headline:^64}║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - - if show_select_all: - select_all = Color.apply("a) Select all", Color.YELLOW) - dialog += f"║ {select_all:<63}║\n" - dialog += "║ ║\n" - - instance_map = { - k.service_file_path.stem: ( - k.service_file_path.stem.replace("klipper", "moonraker") - if k.suffix in [m.suffix for m in moonraker_instances] - else "" - ) - for k in klipper_instances - } - - for i, k in enumerate(instance_map): - mr_name = instance_map.get(k) - m = f"<-> {mr_name}" if mr_name != "" else "" - line = Color.apply(f"{f'{i + 1})' if show_index else '●'} {k} {m}", Color.CYAN) - dialog += f"║ {line:<63}║\n" - - warn_l1 = Color.apply("PLEASE NOTE:", Color.YELLOW) - warn_l2 = Color.apply( - "If you select an instance with an existing Moonraker", Color.YELLOW - ) - warn_l3 = Color.apply( - "instance, that Moonraker instance will be re-created!", Color.YELLOW - ) - warning = textwrap.dedent( - f""" - ║ ║ - ╟───────────────────────────────────────────────────────╢ - ║ {warn_l1:<63}║ - ║ {warn_l2:<63}║ - ║ {warn_l3:<63}║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - - dialog += warning - - print(dialog, end="") - print_back_footer() diff --git a/kiauh/components/moonraker/services/__init__.py b/kiauh/components/moonraker/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/moonraker/services/moonraker_instance_service.py b/kiauh/components/moonraker/services/moonraker_instance_service.py deleted file mode 100644 index 86b8b19..0000000 --- a/kiauh/components/moonraker/services/moonraker_instance_service.py +++ /dev/null @@ -1,49 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from typing import Dict, List - -from components.moonraker.moonraker import Moonraker -from utils.instance_utils import get_instances - - -class MoonrakerInstanceService: - __cls_instance = None - __instances: List[Moonraker] = [] - - def __new__(cls) -> "MoonrakerInstanceService": - if cls.__cls_instance is None: - cls.__cls_instance = super(MoonrakerInstanceService, cls).__new__(cls) - return cls.__cls_instance - - def __init__(self) -> None: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - - def load_instances(self) -> None: - self.__instances = get_instances(Moonraker) - - def create_new_instance(self, suffix: str) -> Moonraker: - instance = Moonraker(suffix) - self.__instances.append(instance) - return instance - - def get_all_instances(self) -> List[Moonraker]: - return self.__instances - - def get_instance_by_suffix(self, suffix: str) -> Moonraker | None: - instances: List[Moonraker] = [i for i in self.__instances if i.suffix == suffix] - return instances[0] if instances else None - - def get_instance_port_map(self) -> Dict[str, int]: - return {i.suffix: i.port for i in self.__instances} diff --git a/kiauh/components/moonraker/services/moonraker_setup_service.py b/kiauh/components/moonraker/services/moonraker_setup_service.py deleted file mode 100644 index 45edd6f..0000000 --- a/kiauh/components/moonraker/services/moonraker_setup_service.py +++ /dev/null @@ -1,408 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from copy import copy -from subprocess import DEVNULL, PIPE, CalledProcessError, run -from typing import List - -from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import print_instance_overview -from components.klipper.services.klipper_instance_service import KlipperInstanceService -from components.moonraker import ( - EXIT_MOONRAKER_SETUP, - MOONRAKER_DIR, - MOONRAKER_ENV_DIR, - MOONRAKER_REPO_URL, - MOONRAKER_REQ_FILE, - MOONRAKER_SPEEDUPS_REQ_FILE, - POLKIT_FILE, - POLKIT_LEGACY_FILE, - POLKIT_SCRIPT, - POLKIT_USR_FILE, -) -from components.moonraker.moonraker import Moonraker -from components.moonraker.moonraker_dialogs import print_moonraker_overview -from components.moonraker.services.moonraker_instance_service import ( - MoonrakerInstanceService, -) -from components.moonraker.utils.utils import ( - backup_moonraker_dir, - create_example_moonraker_conf, - install_moonraker_packages, - remove_polkit_rules, -) -from components.webui_client.client_utils import ( - enable_mainsail_remotemode, - get_existing_clients, -) -from components.webui_client.mainsail_data import MainsailData -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.message_service import Message, MessageService -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color -from utils.common import check_install_dependencies -from utils.fs_utils import check_file_exist, run_remove_routines -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import ( - get_confirm, - get_selection_input, -) -from utils.sys_utils import ( - check_python_version, - cmd_sysctl_manage, - cmd_sysctl_service, - create_python_venv, - get_ipv4_addr, - install_python_requirements, - unit_file_exists, -) - - -# noinspection PyMethodMayBeStatic -class MoonrakerSetupService: - __cls_instance = None - - kisvc: KlipperInstanceService - misvc: MoonrakerInstanceService - msgsvc = MessageService - - settings: KiauhSettings - klipper_list: List[Klipper] - moonraker_list: List[Moonraker] - - def __new__(cls) -> "MoonrakerSetupService": - if cls.__cls_instance is None: - cls.__cls_instance = super(MoonrakerSetupService, cls).__new__(cls) - return cls.__cls_instance - - def __init__(self) -> None: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - self.__init_state() - - def __init_state(self) -> None: - self.settings = KiauhSettings() - - self.kisvc = KlipperInstanceService() - self.kisvc.load_instances() - self.klipper_list = self.kisvc.get_all_instances() - - self.misvc = MoonrakerInstanceService() - self.misvc.load_instances() - self.moonraker_list = self.misvc.get_all_instances() - - self.msgsvc = MessageService() - - def __refresh_state(self) -> None: - self.kisvc.load_instances() - self.klipper_list = self.kisvc.get_all_instances() - - self.misvc.load_instances() - self.moonraker_list = self.misvc.get_all_instances() - - def install(self) -> None: - self.__refresh_state() - - if not self.__check_requirements(self.klipper_list): - return - - new_instances: List[Moonraker] = [] - selected_option: str | Klipper - - if len(self.klipper_list) == 1: - suffix: str = self.klipper_list[0].suffix - new_inst = self.misvc.create_new_instance(suffix) - new_instances.append(new_inst) - - else: - print_moonraker_overview( - self.klipper_list, - self.moonraker_list, - show_index=True, - show_select_all=True, - ) - options = {str(i + 1): k for i, k in enumerate(self.klipper_list)} - additional_options = {"a": None, "b": None} - options = {**options, **additional_options} - question = "Select Klipper instance to setup Moonraker for" - selected_option = get_selection_input(question, options) - - if selected_option == "b": - Logger.print_status(EXIT_MOONRAKER_SETUP) - return - - if selected_option == "a": - new_inst_list: List[Moonraker] = [ - self.misvc.create_new_instance(k.suffix) for k in self.klipper_list - ] - new_instances.extend(new_inst_list) - else: - klipper_instance: Klipper | None = options.get(selected_option) - if klipper_instance is None: - raise Exception("Error selecting instance!") - new_inst = self.misvc.create_new_instance(klipper_instance.suffix) - new_instances.append(new_inst) - - create_example_cfg = get_confirm("Create example moonraker.conf?") - - try: - self.__run_setup(new_instances, create_example_cfg) - except Exception as e: - Logger.print_error(f"Error while installing Moonraker: {e}") - return - - def update(self) -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - "Be careful if there are ongoing prints running!", - "All Moonraker instances will be restarted during the update process and " - "ongoing prints COULD FAIL.", - ], - ) - - if not get_confirm("Update Moonraker now?"): - return - - self.__refresh_state() - - if self.settings.kiauh.backup_before_update: - backup_moonraker_dir() - - InstanceManager.stop_all(self.moonraker_list) - git_pull_wrapper(MOONRAKER_DIR) - install_moonraker_packages() - install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) - InstanceManager.start_all(self.moonraker_list) - - def remove( - self, - remove_service: bool, - remove_dir: bool, - remove_env: bool, - remove_polkit: bool, - ) -> None: - self.__refresh_state() - - completion_msg = Message( - title="Moonraker Removal Process completed", - color=Color.GREEN, - ) - - if remove_service: - Logger.print_status("Removing Moonraker instances ...") - if self.moonraker_list: - instances_to_remove = self.__get_instances_to_remove() - self.__remove_instances(instances_to_remove) - if instances_to_remove: - instance_names = [ - i.service_file_path.stem for i in instances_to_remove - ] - txt = f"● Moonraker instances removed: {', '.join(instance_names)}" - completion_msg.text.append(txt) - else: - Logger.print_info("No Moonraker Services installed! Skipped ...") - - if (remove_polkit or remove_dir or remove_env) and unit_file_exists( - "moonraker", suffix="service" - ): - completion_msg.text = [ - "Some Klipper services are still installed:", - "● Moonraker PolicyKit rules were not removed, even though selected for removal.", - f"● '{MOONRAKER_DIR}' was not removed, even though selected for removal.", - f"● '{MOONRAKER_ENV_DIR}' was not removed, even though selected for removal.", - ] - else: - if remove_polkit: - Logger.print_status("Removing all Moonraker policykit rules ...") - if remove_polkit_rules(): - completion_msg.text.append("● Moonraker policykit rules removed") - if remove_dir: - Logger.print_status("Removing Moonraker local repository ...") - if run_remove_routines(MOONRAKER_DIR): - completion_msg.text.append("● Moonraker local repository removed") - if remove_env: - Logger.print_status("Removing Moonraker Python environment ...") - if run_remove_routines(MOONRAKER_ENV_DIR): - completion_msg.text.append("● Moonraker Python environment removed") - - if completion_msg.text: - completion_msg.text.insert(0, "The following actions were performed:") - else: - completion_msg.color = Color.YELLOW - completion_msg.centered = True - completion_msg.text = ["Nothing to remove."] - - self.msgsvc.set_message(completion_msg) - - def __run_setup( - self, new_instances: List[Moonraker], create_example_cfg: bool - ) -> None: - check_install_dependencies() - self.__install_deps() - - ports_map = self.misvc.get_instance_port_map() - for i in new_instances: - i.create() - cmd_sysctl_service(i.service_file_path.name, "enable") - - if create_example_cfg: - # if a webclient and/or it's config is installed, patch - # its update section to the config - clients = get_existing_clients() - create_example_moonraker_conf(i, ports_map, clients) - - cmd_sysctl_service(i.service_file_path.name, "start") - - cmd_sysctl_manage("daemon-reload") - - # if mainsail is installed, and we installed - # multiple moonraker instances, we enable mainsails remote mode - if MainsailData().client_dir.exists() and len(self.moonraker_list) > 1: - enable_mainsail_remotemode() - - self.misvc.load_instances() - new_instances = [ - self.misvc.get_instance_by_suffix(i.suffix) for i in new_instances - ] - - ip: str = get_ipv4_addr() - # noinspection HttpUrlsUsage - url_list = [ - f"● {i.service_file_path.stem}: http://{ip}:{i.port}" - for i in new_instances - if i.port - ] - dialog_content = [] - if url_list: - dialog_content.append("You can access Moonraker via the following URL:") - dialog_content.extend(url_list) - - Logger.print_dialog( - DialogType.CUSTOM, - custom_title="Moonraker successfully installed!", - custom_color=Color.GREEN, - content=dialog_content, - ) - - def __check_requirements(self, klipper_list: List[Klipper]) -> bool: - is_klipper_installed = len(klipper_list) >= 1 - if not is_klipper_installed: - Logger.print_warn("Klipper not installed!") - Logger.print_warn("Moonraker cannot be installed! Install Klipper first.") - - is_python_ok = check_python_version(3, 7) - - return is_klipper_installed and is_python_ok - - def __install_deps(self) -> None: - default_repo = (MOONRAKER_REPO_URL, "master") - repo = self.settings.moonraker.repositories - # pull the first repo defined in kiauh.cfg or fallback to the official Moonraker repo - repo, branch = (repo[0].url, repo[0].branch) if repo else default_repo - git_clone_wrapper(repo, MOONRAKER_DIR, branch) - - try: - install_moonraker_packages() - if create_python_venv(MOONRAKER_ENV_DIR, False, False, self.settings.moonraker.use_python_binary): - install_python_requirements(MOONRAKER_ENV_DIR, MOONRAKER_REQ_FILE) - if self.settings.moonraker.optional_speedups: - install_python_requirements( - MOONRAKER_ENV_DIR, MOONRAKER_SPEEDUPS_REQ_FILE - ) - self.__install_polkit() - except Exception: - Logger.print_error("Error during installation of Moonraker requirements!") - raise - - def __install_polkit(self) -> None: - Logger.print_status("Installing Moonraker policykit rules ...") - - legacy_file_exists = check_file_exist(POLKIT_LEGACY_FILE, True) - polkit_file_exists = check_file_exist(POLKIT_FILE, True) - usr_file_exists = check_file_exist(POLKIT_USR_FILE, True) - - if legacy_file_exists or (polkit_file_exists and usr_file_exists): - Logger.print_info("Moonraker policykit rules are already installed.") - return - - try: - command = [POLKIT_SCRIPT, "--disable-systemctl"] - result = run( - command, - stderr=PIPE, - stdout=DEVNULL, - text=True, - ) - if result.returncode != 0 or result.stderr: - Logger.print_error(f"{result.stderr}", False) - Logger.print_error("Installing Moonraker policykit rules failed!") - return - - Logger.print_ok("Moonraker policykit rules successfully installed!") - except CalledProcessError as e: - log = ( - f"Error while installing Moonraker policykit rules: {e.stderr.decode()}" - ) - Logger.print_error(log) - - def __get_instances_to_remove(self) -> List[Moonraker] | None: - start_index = 1 - curr_instances: List[Moonraker] = self.moonraker_list - instance_count = len(curr_instances) - - options = [str(i + start_index) for i in range(instance_count)] - options.extend(["a", "b"]) - instance_map = { - options[i]: self.moonraker_list[i] for i in range(instance_count) - } - - print_instance_overview( - self.moonraker_list, - start_index=start_index, - show_index=True, - show_select_all=True, - ) - selection = get_selection_input("Select Moonraker instance to remove", options) - - if selection == "b": - return None - elif selection == "a": - return copy(self.moonraker_list) - - return [instance_map[selection]] - - def __remove_instances( - self, - instance_list: List[Moonraker] | None, - ) -> None: - if not instance_list: - return - - for instance in instance_list: - Logger.print_status( - f"Removing instance {instance.service_file_path.stem} ..." - ) - InstanceManager.remove(instance) - self.__delete_env_file(instance) - - self.__refresh_state() - - def __delete_env_file(self, instance: Moonraker): - Logger.print_status(f"Remove '{instance.env_file}'") - if not instance.env_file.exists(): - msg = f"Env file in {instance.base.sysd_dir} not found. Skipped ..." - Logger.print_info(msg) - return - run_remove_routines(instance.env_file) diff --git a/kiauh/components/moonraker/utils/__init__.py b/kiauh/components/moonraker/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/moonraker/utils/sysdeps_parser.py b/kiauh/components/moonraker/utils/sysdeps_parser.py deleted file mode 100644 index eae2a71..0000000 --- a/kiauh/components/moonraker/utils/sysdeps_parser.py +++ /dev/null @@ -1,179 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# It was modified by Dominik Willner # -# # -# The original file is part of Moonraker: # -# https://github.com/Arksine/moonraker # -# Copyright (C) 2025 Eric Callahan # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -import logging -import pathlib -import re -import shlex -from typing import Any, Dict, List, Tuple - - -def _get_distro_info() -> Dict[str, Any]: - release_file = pathlib.Path("/etc/os-release") - release_info: Dict[str, str] = {} - with release_file.open("r") as f: - lexer = shlex.shlex(f, posix=True) - lexer.whitespace_split = True - for item in list(lexer): - if "=" in item: - key, val = item.split("=", maxsplit=1) - release_info[key] = val - return dict( - distro_id=release_info.get("ID", ""), - distro_version=release_info.get("VERSION_ID", ""), - aliases=release_info.get("ID_LIKE", "").split(), - ) - - -def _convert_version(version: str) -> Tuple[str | int, ...]: - version = version.strip() - ver_match = re.match(r"\d+(\.\d+)*((?:-|\.).+)?", version) - if ver_match is not None: - return tuple( - [ - int(part) if part.isdigit() else part - for part in re.split(r"\.|-", version) - ] - ) - return (version,) - - -class SysDepsParser: - def __init__(self, distro_info: Dict[str, Any] | None = None) -> None: - if distro_info is None: - distro_info = _get_distro_info() - self.distro_id: str = distro_info.get("distro_id", "") - self.aliases: List[str] = distro_info.get("aliases", []) - self.distro_version: Tuple[int | str, ...] = tuple() - version = distro_info.get("distro_version") - if version: - self.distro_version = _convert_version(version) - self.vendor: str = "" - if pathlib.Path("/etc/rpi-issue").is_file(): - self.vendor = "raspberry-pi" - - def _parse_spec(self, full_spec: str) -> str | None: - parts = full_spec.split(";", maxsplit=1) - if len(parts) == 1: - return full_spec - pkg_name = parts[0].strip() - expressions = re.split(r"( and | or )", parts[1].strip()) - if not len(expressions) & 1: - # There should always be an odd number of expressions. Each - # expression is separated by an "and" or "or" operator - logging.info( - f"Requirement specifier is missing an expression " - f"between logical operators : {full_spec}" - ) - return None - last_result: bool = True - last_logical_op: str | None = "and" - for idx, exp in enumerate(expressions): - if idx & 1: - if last_logical_op is not None: - logging.info( - "Requirement specifier contains sequential logical " - f"operators: {full_spec}" - ) - return None - logical_op = exp.strip() - if logical_op not in ("and", "or"): - logging.info( - f"Invalid logical operator {logical_op} in requirement " - f"specifier: {full_spec}" - ) - return None - last_logical_op = logical_op - continue - elif last_logical_op is None: - logging.info( - f"Requirement specifier contains two seqential expressions " - f"without a logical operator: {full_spec}" - ) - return None - dep_parts = re.split(r"(==|!=|<=|>=|<|>)", exp.strip()) - req_var = dep_parts[0].strip().lower() - if len(dep_parts) != 3: - logging.info(f"Invalid comparison, must be 3 parts: {full_spec}") - return None - elif req_var == "distro_id": - left_op: str | Tuple[int | str, ...] = self.distro_id - right_op = dep_parts[2].strip().strip("\"'") - elif req_var == "vendor": - left_op = self.vendor - right_op = dep_parts[2].strip().strip("\"'") - elif req_var == "distro_version": - if not self.distro_version: - logging.info( - "Distro Version not detected, cannot satisfy requirement: " - f"{full_spec}" - ) - return None - left_op = self.distro_version - right_op = _convert_version(dep_parts[2].strip().strip("\"'")) - else: - logging.info(f"Invalid requirement specifier: {full_spec}") - return None - operator = dep_parts[1].strip() - try: - compfunc = { - "<": lambda x, y: x < y, - ">": lambda x, y: x > y, - "==": lambda x, y: x == y, - "!=": lambda x, y: x != y, - ">=": lambda x, y: x >= y, - "<=": lambda x, y: x <= y, - }.get(operator, lambda x, y: False) - result = compfunc(left_op, right_op) - if last_logical_op == "and": - last_result &= result - else: - last_result |= result - last_logical_op = None - except Exception: - logging.exception(f"Error comparing requirements: {full_spec}") - return None - if last_result: - return pkg_name - return None - - def parse_dependencies(self, sys_deps: Dict[str, List[str]]) -> List[str]: - if not self.distro_id: - logging.info( - "Failed to detect current distro ID, cannot parse dependencies" - ) - return [] - all_ids = [self.distro_id] + self.aliases - for distro_id in all_ids: - if distro_id in sys_deps: - if not sys_deps[distro_id]: - logging.info( - f"Dependency data contains an empty package definition " - f"for linux distro '{distro_id}'" - ) - continue - processed_deps: List[str] = [] - for dep in sys_deps[distro_id]: - parsed_dep = self._parse_spec(dep) - if parsed_dep is not None: - processed_deps.append(parsed_dep) - return processed_deps - else: - logging.info( - f"Dependency data has no package definition for linux " - f"distro '{self.distro_id}'" - ) - return [] diff --git a/kiauh/components/moonraker/utils/utils.py b/kiauh/components/moonraker/utils/utils.py deleted file mode 100644 index e01cf88..0000000 --- a/kiauh/components/moonraker/utils/utils.py +++ /dev/null @@ -1,228 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import json -import shutil -from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, run -from typing import Dict, List, Optional - -from components.moonraker import ( - MODULE_PATH, - MOONRAKER_DEFAULT_PORT, - MOONRAKER_DEPS_JSON_FILE, - MOONRAKER_DIR, - MOONRAKER_ENV_DIR, - MOONRAKER_INSTALL_SCRIPT, -) -from components.moonraker.moonraker import Moonraker -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 ( - SimpleConfigParser, -) -from core.types.component_status import ComponentStatus -from utils.common import check_install_dependencies, get_install_status -from utils.instance_utils import get_instances -from utils.sys_utils import ( - get_ipv4_addr, - parse_packages_from_file, -) - - -def get_moonraker_status() -> ComponentStatus: - return get_install_status(MOONRAKER_DIR, MOONRAKER_ENV_DIR, Moonraker) - - -def install_moonraker_packages() -> None: - Logger.print_status("Parsing Moonraker system dependencies ...") - - moonraker_deps = [] - if MOONRAKER_DEPS_JSON_FILE.exists(): - Logger.print_info( - f"Parsing system dependencies from {MOONRAKER_DEPS_JSON_FILE.name} ..." - ) - parser = SysDepsParser() - sysdeps = load_sysdeps_json(MOONRAKER_DEPS_JSON_FILE) - moonraker_deps.extend(parser.parse_dependencies(sysdeps)) - - elif MOONRAKER_INSTALL_SCRIPT.exists(): - Logger.print_warn(f"{MOONRAKER_DEPS_JSON_FILE.name} not found!") - Logger.print_info( - f"Parsing system dependencies from {MOONRAKER_INSTALL_SCRIPT.name} ..." - ) - moonraker_deps = parse_packages_from_file(MOONRAKER_INSTALL_SCRIPT) - - if not moonraker_deps: - raise ValueError("Error parsing Moonraker dependencies!") - - check_install_dependencies({*moonraker_deps}) - - -def remove_polkit_rules() -> bool: - if not MOONRAKER_DIR.exists(): - log = "Cannot remove policykit rules. Moonraker directory not found." - Logger.print_warn(log) - return False - - try: - cmd = [f"{MOONRAKER_DIR}/scripts/set-policykit-rules.sh", "--clear"] - run(cmd, stderr=PIPE, stdout=DEVNULL, check=True) - return True - except CalledProcessError as e: - Logger.print_error(f"Error while removing policykit rules: {e}") - return False - - -def create_example_moonraker_conf( - instance: Moonraker, - ports_map: Dict[str, int], - clients: Optional[List[BaseWebClient]] = None, -) -> None: - Logger.print_status(f"Creating example moonraker.conf in '{instance.base.cfg_dir}'") - if instance.cfg_file.is_file(): - Logger.print_info(f"'{instance.cfg_file}' already exists.") - return - - source = MODULE_PATH.joinpath("assets/moonraker.conf") - target = instance.cfg_file - try: - shutil.copy(source, target) - except OSError as e: - Logger.print_error(f"Unable to create example moonraker.conf:\n{e}") - return - - ports = [ - ports_map.get(instance) - for instance in ports_map - if ports_map.get(instance) is not None - ] - if ports_map.get(instance.suffix) is None: - # this could be improved to not increment the max value of the ports list and assign it as the port - # as it can lead to situation where the port for e.g. instance moonraker-2 becomes 7128 if the port - # of moonraker-1 is 7125 and moonraker-3 is 7127 and there are moonraker.conf files for moonraker-1 - # and moonraker-3 already. though, there does not seem to be a very reliable way of always assigning - # the correct port to each instance and the user will likely be required to correct the value manually. - port = max(ports) + 1 if ports else MOONRAKER_DEFAULT_PORT - else: - port = ports_map.get(instance.suffix) - - ports_map[instance.suffix] = port - - ip = get_ipv4_addr().split(".")[:2] - ip.extend(["0", "0/16"]) - uds = instance.base.comms_dir.joinpath("klippy.sock") - - scp = SimpleConfigParser() - scp.read_file(target) - trusted_clients: List[str] = [ - f" {'.'.join(ip)}\n", - *scp.getvals("authorization", "trusted_clients"), - ] - - scp.set_option("server", "port", str(port)) - scp.set_option("server", "klippy_uds_address", str(uds)) - scp.set_option("authorization", "trusted_clients", trusted_clients) - - # add existing client and client configs in the update section - if clients is not None and len(clients) > 0: - for c in clients: - # client part - c_section = f"update_manager {c.name}" - c_options = [ - ("type", "web"), - ("channel", "stable"), - ("repo", c.repo_path), - ("path", c.client_dir), - ] - scp.add_section(section=c_section) - for option in c_options: - scp.set_option(c_section, option[0], option[1]) - - # client config part - c_config = c.client_config - if c_config.config_dir.exists(): - c_config_section = f"update_manager {c_config.name}" - c_config_options = [ - ("type", "git_repo"), - ("primary_branch", "master"), - ("path", c_config.config_dir), - ("origin", c_config.repo_url), - ("managed_services", "klipper"), - ] - scp.add_section(section=c_config_section) - for option in c_config_options: - scp.set_option(c_config_section, option[0], option[1]) - - scp.write_file(target) - Logger.print_ok(f"Example moonraker.conf created in '{instance.base.cfg_dir}'") - - -def backup_moonraker_dir() -> None: - svc = BackupService() - svc.backup_directory( - source_path=MOONRAKER_DIR, backup_name="moonraker", target_path="moonraker" - ) - svc.backup_directory( - source_path=MOONRAKER_ENV_DIR, - backup_name="moonraker-env", - target_path="moonraker", - ) - - -def backup_moonraker_db_dir() -> None: - instances: List[Moonraker] = get_instances(Moonraker) - svc = BackupService() - - if not instances: - # fallback: search for printer data directories in the user's home directory - Logger.print_info("No Moonraker instances found via systemd services.") - Logger.print_info( - "Attempting to find printer data directories in home directory..." - ) - - home_dir = Path.home() - printer_data_dirs = [] - - for pattern in ["printer_data", "printer_*_data"]: - for data_dir in home_dir.glob(pattern): - if data_dir.is_dir(): - printer_data_dirs.append(data_dir) - - if not printer_data_dirs: - Logger.print_info("Unable to find directory to backup!") - Logger.print_info("No printer data directories found in home directory.") - return - - for data_dir in printer_data_dirs: - svc.backup_directory( - source_path=data_dir.joinpath("database"), - target_path=data_dir.name, - backup_name="database", - ) - - return - - for instance in instances: - svc.backup_directory( - source_path=instance.db_dir, - target_path=f"{instance.data_dir.name}", - backup_name="database", - ) - - -def load_sysdeps_json(file: Path) -> Dict[str, List[str]]: - try: - sysdeps: Dict[str, List[str]] = json.loads(file.read_bytes()) - except json.JSONDecodeError as e: - Logger.print_error(f"Unable to parse {file.name}:\n{e}") - return {} - else: - return sysdeps diff --git a/kiauh/components/webui_client/__init__.py b/kiauh/components/webui_client/__init__.py deleted file mode 100644 index 8bfcd7a..0000000 --- a/kiauh/components/webui_client/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent diff --git a/kiauh/components/webui_client/assets/common_vars.conf b/kiauh/components/webui_client/assets/common_vars.conf deleted file mode 100644 index 9c3f85e..0000000 --- a/kiauh/components/webui_client/assets/common_vars.conf +++ /dev/null @@ -1,6 +0,0 @@ -# /etc/nginx/conf.d/common_vars.conf - -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} \ No newline at end of file diff --git a/kiauh/components/webui_client/assets/nginx_cfg b/kiauh/components/webui_client/assets/nginx_cfg deleted file mode 100644 index d7aabf4..0000000 --- a/kiauh/components/webui_client/assets/nginx_cfg +++ /dev/null @@ -1,95 +0,0 @@ -server { - listen %PORT%; - # uncomment the next line to activate IPv6 - # listen [::]:%PORT%; - - access_log /var/log/nginx/%NAME%-access.log; - error_log /var/log/nginx/%NAME%-error.log; - - # disable this section on smaller hardware like a pi zero - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_proxied expired no-cache no-store private auth; - gzip_comp_level 4; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/json application/xml; - - # web_path from %NAME% static files - root %ROOT_DIR%; - - index index.html; - server_name _; - - # disable max upload size checks - client_max_body_size 0; - - # disable proxy request buffering - proxy_request_buffering off; - - location / { - try_files $uri $uri/ /index.html; - } - - location = /index.html { - add_header Cache-Control "no-store, no-cache, must-revalidate"; - } - - location /websocket { - proxy_pass http://apiserver/websocket; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_read_timeout 86400; - } - - location ~ ^/(printer|api|access|machine|server)/ { - proxy_pass http://apiserver$request_uri; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Scheme $scheme; - } - - location /webcam/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer1/; - } - - location /webcam2/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer2/; - } - - location /webcam3/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer3/; - } - - location /webcam4/ { - postpone_output 0; - proxy_buffering off; - proxy_ignore_headers X-Accel-Buffering; - access_log off; - error_log off; - proxy_pass http://mjpgstreamer4/; - } -} diff --git a/kiauh/components/webui_client/assets/upstreams.conf b/kiauh/components/webui_client/assets/upstreams.conf deleted file mode 100644 index d04e04a..0000000 --- a/kiauh/components/webui_client/assets/upstreams.conf +++ /dev/null @@ -1,25 +0,0 @@ -# /etc/nginx/conf.d/upstreams.conf -upstream apiserver { - ip_hash; - server 127.0.0.1:7125; -} - -upstream mjpgstreamer1 { - ip_hash; - server 127.0.0.1:8080; -} - -upstream mjpgstreamer2 { - ip_hash; - server 127.0.0.1:8081; -} - -upstream mjpgstreamer3 { - ip_hash; - server 127.0.0.1:8082; -} - -upstream mjpgstreamer4 { - ip_hash; - server 127.0.0.1:8083; -} \ No newline at end of file diff --git a/kiauh/components/webui_client/base_data.py b/kiauh/components/webui_client/base_data.py deleted file mode 100644 index cbf2554..0000000 --- a/kiauh/components/webui_client/base_data.py +++ /dev/null @@ -1,55 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -from abc import ABC -from dataclasses import dataclass -from enum import Enum -from pathlib import Path - - -class WebClientType(Enum): - MAINSAIL: str = "mainsail" - FLUIDD: str = "fluidd" - - -class WebClientConfigType(Enum): - MAINSAIL: str = "mainsail-config" - FLUIDD: str = "fluidd-config" - - -@dataclass() -class BaseWebClient(ABC): - """Base class for webclient data""" - - client: WebClientType - name: str - display_name: str - client_dir: Path - config_file: Path - repo_path: str - download_url: str - nginx_config: Path - nginx_access_log: Path - nginx_error_log: Path - client_config: BaseWebClientConfig - - -@dataclass() -class BaseWebClientConfig(ABC): - """Base class for webclient config data""" - - client_config: WebClientConfigType - name: str - display_name: str - config_filename: str - config_dir: Path - repo_url: str - config_section: str diff --git a/kiauh/components/webui_client/client_config/__init__.py b/kiauh/components/webui_client/client_config/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/webui_client/client_config/client_config_remove.py b/kiauh/components/webui_client/client_config/client_config_remove.py deleted file mode 100644 index 4c7ae89..0000000 --- a/kiauh/components/webui_client/client_config/client_config_remove.py +++ /dev/null @@ -1,94 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - - -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from components.webui_client.base_data import BaseWebClientConfig -from core.logger import Logger -from core.services.backup_service import BackupService -from core.services.message_service import Message -from core.types.color import Color -from utils.config_utils import remove_config_section -from utils.fs_utils import run_remove_routines -from utils.instance_type import InstanceType -from utils.instance_utils import get_instances - - -def run_client_config_removal( - client_config: BaseWebClientConfig, - kl_instances: List[Klipper], - mr_instances: List[Moonraker], -) -> Message: - completion_msg = Message( - title=f"{client_config.display_name} Removal Process completed", - color=Color.GREEN, - ) - Logger.print_status(f"Removing {client_config.display_name} ...") - if run_remove_routines(client_config.config_dir): - completion_msg.text.append(f"● {client_config.display_name} removed") - - BackupService().backup_printer_config_dir() - - completion_msg = remove_moonraker_config_section( - completion_msg, client_config, mr_instances - ) - - completion_msg = remove_printer_config_section( - completion_msg, client_config, kl_instances - ) - - if completion_msg.text: - completion_msg.text.insert(0, "The following actions were performed:") - else: - completion_msg.color = Color.YELLOW - completion_msg.centered = True - completion_msg.text = ["Nothing to remove."] - - return completion_msg - - -def remove_cfg_symlink(client_config: BaseWebClientConfig, message: Message) -> Message: - instances: List[Klipper] = get_instances(Klipper) - kl_instances = [] - for instance in instances: - cfg = instance.base.cfg_dir.joinpath(client_config.config_filename) - if run_remove_routines(cfg): - kl_instances.append(instance) - text = f"{client_config.display_name} removed from instance" - return update_msg(kl_instances, message, text) - - -def remove_printer_config_section( - message: Message, client_config: BaseWebClientConfig, kl_instances: List[Klipper] -) -> Message: - kl_section = client_config.config_section - kl_instances = remove_config_section(kl_section, kl_instances) - text = f"Klipper config section '{kl_section}' removed for instance" - return update_msg(kl_instances, message, text) - - -def remove_moonraker_config_section( - message: Message, client_config: BaseWebClientConfig, mr_instances: List[Moonraker] -) -> Message: - mr_section = f"update_manager {client_config.name}" - mr_instances = remove_config_section(mr_section, mr_instances) - text = f"Moonraker config section '{mr_section}' removed for instance" - return update_msg(mr_instances, message, text) - - -def update_msg(instances: List[InstanceType], message: Message, text: str) -> Message: - if not instances: - return message - - instance_names = [i.service_file_path.stem for i in instances] - message.text.append(f"● {text}: {', '.join(instance_names)}") - return message diff --git a/kiauh/components/webui_client/client_config/client_config_setup.py b/kiauh/components/webui_client/client_config/client_config_setup.py deleted file mode 100644 index 1037a72..0000000 --- a/kiauh/components/webui_client/client_config/client_config_setup.py +++ /dev/null @@ -1,126 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import shutil -import subprocess -from pathlib import Path -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from components.webui_client.base_data import BaseWebClient, BaseWebClientConfig -from components.webui_client.client_dialogs import ( - print_client_already_installed_dialog, -) -from components.webui_client.client_utils import ( - backup_client_config_data, - detect_client_cfg_conflict, -) -from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from core.services.backup_service import BackupService -from core.settings.kiauh_settings import KiauhSettings -from utils.config_utils import add_config_section, add_config_section_at_top -from utils.fs_utils import create_symlink -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances - - -def install_client_config(client_data: BaseWebClient, cfg_backup=True) -> None: - client_config: BaseWebClientConfig = client_data.client_config - display_name = client_config.display_name - - if detect_client_cfg_conflict(client_data): - Logger.print_info("Another Client-Config is already installed! Skipped ...") - return - - if client_config.config_dir.exists(): - print_client_already_installed_dialog(display_name) - if get_confirm(f"Re-install {display_name}?", allow_go_back=True): - shutil.rmtree(client_config.config_dir) - else: - return - - mr_instances: List[Moonraker] = get_instances(Moonraker) - kl_instances = get_instances(Klipper) - - try: - download_client_config(client_config) - create_client_config_symlink(client_config, kl_instances) - - if cfg_backup: - BackupService().backup_printer_config_dir() - - add_config_section( - section=f"update_manager {client_config.name}", - instances=mr_instances, - options=[ - ("type", "git_repo"), - ("primary_branch", "master"), - ("path", str(client_config.config_dir)), - ("origin", str(client_config.repo_url)), - ("managed_services", "klipper"), - ], - ) - add_config_section_at_top(client_config.config_section, kl_instances) - InstanceManager.restart_all(kl_instances) - - except Exception as e: - Logger.print_error(f"{display_name} installation failed!\n{e}") - return - - Logger.print_ok(f"{display_name} installation complete!", start="\n") - - -def download_client_config(client_config: BaseWebClientConfig) -> None: - try: - Logger.print_status(f"Downloading {client_config.display_name} ...") - repo = client_config.repo_url - target_dir = client_config.config_dir - git_clone_wrapper(repo, target_dir) - except Exception: - Logger.print_error(f"Downloading {client_config.display_name} failed!") - raise - - -def update_client_config(client: BaseWebClient) -> None: - client_config: BaseWebClientConfig = client.client_config - - Logger.print_status(f"Updating {client_config.display_name} ...") - - if not client_config.config_dir.exists(): - Logger.print_info( - f"Unable to update {client_config.display_name}. Directory does not exist! Skipping ..." - ) - return - - settings = KiauhSettings() - if settings.kiauh.backup_before_update: - backup_client_config_data(client) - - git_pull_wrapper(client_config.config_dir) - - Logger.print_ok(f"Successfully updated {client_config.display_name}.") - Logger.print_info("Restart Klipper to reload the configuration!") - - -def create_client_config_symlink( - client_config: BaseWebClientConfig, klipper_instances: List[Klipper] -) -> None: - for instance in klipper_instances: - Logger.print_status(f"Create symlink for {client_config.config_filename} ...") - source = Path(client_config.config_dir, client_config.config_filename) - target = instance.base.cfg_dir - Logger.print_status(f"Linking {source} to {target}") - try: - create_symlink(source, target) - except subprocess.CalledProcessError: - Logger.print_error("Creating symlink failed!") diff --git a/kiauh/components/webui_client/client_dialogs.py b/kiauh/components/webui_client/client_dialogs.py deleted file mode 100644 index a8e9468..0000000 --- a/kiauh/components/webui_client/client_dialogs.py +++ /dev/null @@ -1,93 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from typing import List - -from components.webui_client.base_data import BaseWebClient -from core.logger import DialogType, Logger - - -def print_moonraker_not_found_dialog(name: str) -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - "No local Moonraker installation was found!", - "\n\n", - f"It is possible to install {name} without a local Moonraker installation. " - "If you continue, you need to make sure, that Moonraker is installed on " - f"another machine in your network. Otherwise {name} will NOT work " - "correctly.", - ], - ) - - -def print_client_already_installed_dialog(name: str) -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - f"{name} seems to be already installed!", - f"If you continue, your current {name} installation will be overwritten.", - ], - ) - - -def print_client_port_select_dialog( - name: str, port: int, ports_in_use: List[int] -) -> None: - dialog_content: List[str] = [ - f"Please select the port, {name} should be served on. If your are unsure " - f"what to select, hit Enter to apply the suggested value of: {port}", - "\n\n", - f"In case you need {name} to be served on a specific port, you can set it " - f"now. Make sure that the port is not already used by another application " - f"on your system!", - ] - - if ports_in_use: - dialog_content.extend( - [ - "\n\n", - "The following ports were found to be already in use:", - *[f"● {p}" for p in ports_in_use if p != port], - ] - ) - - Logger.print_dialog(DialogType.CUSTOM, dialog_content) - - -def print_install_client_config_dialog(client: BaseWebClient) -> None: - name = client.display_name - url = client.client_config.repo_url.replace(".git", "") - Logger.print_dialog( - DialogType.INFO, - [ - f"It is recommended to use special macros in order to have {name} fully " - f"functional and working.", - "\n\n", - f"The recommended macros for {name} can be seen here:", - url, - "\n\n", - "If you already use these macros skip this step. Otherwise you should " - "consider to answer with 'Y' to download the recommended macros.", - ], - ) - - -def print_ipv6_warning_dialog() -> None: - Logger.print_dialog( - DialogType.WARNING, - [ - "It looks like IPv6 is enabled on this system!", - "This may cause issues with the installation of NGINX in the following " - "steps! It is recommended to disable IPv6 on your system to avoid this issue.", - "\n\n", - "If you think this warning is a false alarm, and you are sure that " - "IPv6 is disabled, you can continue with the installation.", - ], - ) diff --git a/kiauh/components/webui_client/client_remove.py b/kiauh/components/webui_client/client_remove.py deleted file mode 100644 index 4d64779..0000000 --- a/kiauh/components/webui_client/client_remove.py +++ /dev/null @@ -1,124 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from components.webui_client.base_data import ( - BaseWebClient, -) -from components.webui_client.client_config.client_config_remove import ( - run_client_config_removal, -) -from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from core.logger import Logger -from core.services.backup_service import BackupService -from core.services.message_service import Message -from core.types.color import Color -from utils.config_utils import remove_config_section -from utils.fs_utils import ( - remove_with_sudo, - run_remove_routines, -) -from utils.instance_utils import get_instances - - -def run_client_removal( - client: BaseWebClient, - remove_client: bool, - remove_client_cfg: bool, - backup_config: bool, -) -> Message: - completion_msg = Message( - title=f"{client.display_name} Removal Process completed", - color=Color.GREEN, - ) - mr_instances: List[Moonraker] = get_instances(Moonraker) - kl_instances: List[Klipper] = get_instances(Klipper) - - if backup_config: - version = "" - src = client.client_dir - if src.joinpath(".version").exists(): - 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, - target_path=target_path, - ) - if success: - completion_msg.text.append(f"● {client.config_file.name} backup created") - - if remove_client: - client_name = client.name - if remove_client_dir(client): - completion_msg.text.append(f"● {client.display_name} removed") - if remove_client_nginx_config(client_name): - completion_msg.text.append("● NGINX config removed") - if remove_client_nginx_logs(client, kl_instances): - completion_msg.text.append("● NGINX logs removed") - - BackupService().backup_moonraker_conf() - section = f"update_manager {client_name}" - handled_instances: List[Moonraker] = remove_config_section( - section, mr_instances - ) - if handled_instances: - names = [i.service_file_path.stem for i in handled_instances] - completion_msg.text.append( - f"● Moonraker config section '{section}' removed for instance: {', '.join(names)}" - ) - - if remove_client_cfg: - cfg_completion_msg = run_client_config_removal( - client.client_config, - kl_instances, - mr_instances, - ) - if cfg_completion_msg.color == Color.GREEN: - completion_msg.text.extend(cfg_completion_msg.text[1:]) - - if not completion_msg.text: - completion_msg.color = Color.YELLOW - completion_msg.centered = True - completion_msg.text.append("Nothing to remove.") - else: - completion_msg.text.insert(0, "The following actions were performed:") - - return completion_msg - - -def remove_client_dir(client: BaseWebClient) -> bool: - Logger.print_status(f"Removing {client.display_name} ...") - return run_remove_routines(client.client_dir) - - -def remove_client_nginx_config(name: str) -> bool: - Logger.print_status(f"Removing NGINX config for {name.capitalize()} ...") - return remove_with_sudo( - [ - NGINX_SITES_AVAILABLE.joinpath(name), - NGINX_SITES_ENABLED.joinpath(name), - ] - ) - - -def remove_client_nginx_logs(client: BaseWebClient, instances: List[Klipper]) -> bool: - Logger.print_status(f"Removing NGINX logs for {client.display_name} ...") - - files = [client.nginx_access_log, client.nginx_error_log] - if instances: - for instance in instances: - files.append(instance.base.log_dir.joinpath(client.nginx_access_log.name)) - files.append(instance.base.log_dir.joinpath(client.nginx_error_log.name)) - - return remove_with_sudo(files) diff --git a/kiauh/components/webui_client/client_setup.py b/kiauh/components/webui_client/client_setup.py deleted file mode 100644 index e5d7ca7..0000000 --- a/kiauh/components/webui_client/client_setup.py +++ /dev/null @@ -1,188 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import shutil -import tempfile -from pathlib import Path -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from components.webui_client import MODULE_PATH -from components.webui_client.base_data import ( - BaseWebClient, - BaseWebClientConfig, - WebClientType, -) -from components.webui_client.client_config.client_config_setup import ( - install_client_config, -) -from components.webui_client.client_dialogs import ( - print_install_client_config_dialog, - print_moonraker_not_found_dialog, -) -from components.webui_client.client_utils import ( - copy_common_vars_nginx_cfg, - copy_upstream_nginx_cfg, - create_nginx_cfg, - detect_client_cfg_conflict, - enable_mainsail_remotemode, - get_client_port_selection, - symlink_webui_nginx_log, -) -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.backup_service import BackupService -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color -from utils.common import check_install_dependencies -from utils.config_utils import add_config_section -from utils.fs_utils import unzip -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - cmd_sysctl_service, - download_file, - get_ipv4_addr, -) - - -def install_client( - client: BaseWebClient, - settings: KiauhSettings, - reinstall: bool = False, -) -> None: - mr_instances: List[Moonraker] = get_instances(Moonraker) - - enable_remotemode = False - if not mr_instances: - print_moonraker_not_found_dialog(client.display_name) - if not get_confirm(f"Continue {client.display_name} installation?"): - return - - # if moonraker is not installed or multiple instances - # are installed we enable mainsails remote mode - if ( - client.client == WebClientType.MAINSAIL - and not mr_instances - or len(mr_instances) > 1 - ): - enable_remotemode = True - - kl_instances = get_instances(Klipper) - install_client_cfg = False - client_config: BaseWebClientConfig = client.client_config - if ( - kl_instances - and not client_config.config_dir.exists() - and not detect_client_cfg_conflict(client) - ): - print_install_client_config_dialog(client) - question = f"Download the recommended {client_config.display_name}?" - install_client_cfg = get_confirm(question, allow_go_back=False) - - default_port: int = int(settings.get(client.name, "port")) - port: int = ( - default_port if reinstall else get_client_port_selection(client, settings) - ) - - check_install_dependencies({"nginx"}) - - try: - download_client(client) - if enable_remotemode and client.client == WebClientType.MAINSAIL: - enable_mainsail_remotemode() - - BackupService().backup_printer_config_dir() - add_config_section( - section=f"update_manager {client.name}", - instances=mr_instances, - options=[ - ("persistent_files", ["config.json"]), - ("type", "web"), - ("channel", "stable"), - ("repo", str(client.repo_path)), - ("path", str(client.client_dir)), - ], - ) - InstanceManager.restart_all(mr_instances) - - if install_client_cfg and kl_instances: - install_client_config(client, False) - - copy_upstream_nginx_cfg() - copy_common_vars_nginx_cfg() - create_nginx_cfg( - display_name=client.display_name, - cfg_name=client.name, - template_src=MODULE_PATH.joinpath("assets/nginx_cfg"), - PORT=port, - ROOT_DIR=client.client_dir, - NAME=client.name, - ) - - if kl_instances: - symlink_webui_nginx_log(client, kl_instances) - cmd_sysctl_service("nginx", "restart") - - except Exception as e: - Logger.print_error(e) - Logger.print_dialog( - DialogType.ERROR, - center_content=True, - content=[f"{client.display_name} installation failed!"], - ) - return - - # noinspection HttpUrlsUsage - Logger.print_dialog( - DialogType.CUSTOM, - custom_title=f"{client.display_name} installation complete!", - custom_color=Color.GREEN, - center_content=True, - content=[ - f"Open {client.display_name} now on: http://{get_ipv4_addr()}{'' if port == 80 else f':{port}'}", - ], - ) - - -def download_client(client: BaseWebClient) -> None: - zipfile = f"{client.name.lower()}.zip" - target = Path().home().joinpath(zipfile) - try: - Logger.print_status( - f"Downloading {client.display_name} from {client.download_url} ..." - ) - download_file(client.download_url, target, True) - Logger.print_ok("Download complete!") - - Logger.print_status(f"Extracting {zipfile} ...") - unzip(target, client.client_dir) - target.unlink(missing_ok=True) - Logger.print_ok("OK!") - - except Exception: - Logger.print_error(f"Downloading {client.display_name} failed!") - raise - - -def update_client(client: BaseWebClient) -> None: - Logger.print_status(f"Updating {client.display_name} ...") - if not client.client_dir.exists(): - Logger.print_info( - f"Unable to update {client.display_name}. Directory does not exist! Skipping ..." - ) - return - - with tempfile.NamedTemporaryFile(suffix=".json") as tmp_file: - Logger.print_status( - f"Creating temporary backup of {client.config_file} as {tmp_file.name} ..." - ) - shutil.copy(client.config_file, tmp_file.name) - download_client(client) - shutil.copy(tmp_file.name, client.config_file) diff --git a/kiauh/components/webui_client/client_utils.py b/kiauh/components/webui_client/client_utils.py deleted file mode 100644 index aea56a4..0000000 --- a/kiauh/components/webui_client/client_utils.py +++ /dev/null @@ -1,466 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import json -import re -import shutil -from pathlib import Path -from subprocess import PIPE, CalledProcessError, run -from typing import List, get_args - -from components.klipper.klipper import Klipper -from components.webui_client import MODULE_PATH -from components.webui_client.base_data import ( - BaseWebClient, - WebClientType, -) -from components.webui_client.client_dialogs import print_client_port_select_dialog -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from core.constants import ( - NGINX_CONFD, - NGINX_SITES_AVAILABLE, - NGINX_SITES_ENABLED, -) -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 ( - SimpleConfigParser, -) -from core.types.color import Color -from core.types.component_status import ComponentStatus -from utils.common import get_install_status -from utils.fs_utils import create_symlink, remove_file -from utils.git_utils import ( - get_latest_remote_tag, - get_latest_unstable_tag, -) -from utils.input_utils import get_number_input -from utils.instance_utils import get_instances - - -def get_client_status( - client: BaseWebClient, fetch_remote: bool = False -) -> ComponentStatus: - files = [ - NGINX_SITES_AVAILABLE.joinpath(client.name), - NGINX_CONFD.joinpath("upstreams.conf"), - NGINX_CONFD.joinpath("common_vars.conf"), - ] - comp_status: ComponentStatus = get_install_status(client.client_dir, files=files) - - # if the client dir does not exist, set the status to not - # installed even if the other files are present - if not client.client_dir.exists(): - comp_status.status = 0 - - comp_status.local = get_local_client_version(client) - comp_status.remote = get_remote_client_version(client) if fetch_remote else None - return comp_status - - -def get_client_config_status(client: BaseWebClient) -> ComponentStatus: - return get_install_status(client.client_config.config_dir) - - -def get_current_client_config() -> str: - mainsail, fluidd = MainsailData(), FluiddData() - clients: List[BaseWebClient] = [mainsail, fluidd] - installed = [c for c in clients if c.client_config.config_dir.exists()] - - if not installed: - return Color.apply("-", Color.CYAN) - elif len(installed) == 1: - cfg = installed[0].client_config - return 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 - mainsail_includes, fluidd_includes = [], [] - klipper_instances: List[Klipper] = get_instances(Klipper) - for instance in klipper_instances: - scp = SimpleConfigParser() - scp.read_file(instance.cfg_file) - includes_mainsail = scp.has_section(mainsail.client_config.config_section) - includes_fluidd = scp.has_section(fluidd.client_config.config_section) - - if includes_mainsail: - mainsail_includes.append(instance) - if includes_fluidd: - fluidd_includes.append(instance) - - # 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) - - 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) - 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) - 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) - - -def enable_mainsail_remotemode() -> None: - Logger.print_status("Enable Mainsails remote mode ...") - c_json = MainsailData().client_dir.joinpath("config.json") - with open(c_json, "r") as f: - config_data = json.load(f) - - if config_data["instancesDB"] == "browser" or config_data["instancesDB"] == "json": - Logger.print_info("Remote mode already configured. Skipped ...") - return - - Logger.print_status("Setting instance storage location to 'browser' ...") - config_data["instancesDB"] = "browser" - - with open(c_json, "w") as f: - json.dump(config_data, f, indent=4) - Logger.print_ok("Mainsails remote mode enabled!") - - -def symlink_webui_nginx_log( - client: BaseWebClient, klipper_instances: List[Klipper] -) -> None: - Logger.print_status("Link NGINX logs into log directory ...") - access_log = client.nginx_access_log - error_log = client.nginx_error_log - - for instance in klipper_instances: - desti_access = instance.base.log_dir.joinpath(access_log.name) - if not desti_access.exists(): - desti_access.symlink_to(access_log) - - desti_error = instance.base.log_dir.joinpath(error_log.name) - if not desti_error.exists(): - desti_error.symlink_to(error_log) - - -def get_local_client_version(client: BaseWebClient) -> str | None: - relinfo_file = client.client_dir.joinpath("release_info.json") - version_file = client.client_dir.joinpath(".version") - - if not client.client_dir.exists(): - return None - if not relinfo_file.is_file() and not version_file.is_file(): - return "n/a" - - 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] - - -def get_remote_client_version(client: BaseWebClient) -> str | None: - try: - if (tag := get_latest_remote_tag(client.repo_path)) != "": - return str(tag) - return None - except Exception: - return None - - -def backup_client_data(client: BaseWebClient) -> None: - version = "" - src = client.client_dir - if src.joinpath(".version").exists(): - 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}") - svc.backup_directory( - source_path=client.client_dir, - target_path=target_path, - backup_name=client.name, - ) - svc.backup_file( - source_path=client.config_file, - target_path=target_path, - ) - - -def backup_client_config_data(client: BaseWebClient) -> None: - version = "" - src = client.client_dir - if src.joinpath(".version").exists(): - 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}") - svc.backup_directory( - source_path=client.client_config.config_dir, - target_path=target_path, - backup_name=client.client_config.name, - ) - - -def get_existing_clients() -> List[BaseWebClient]: - clients = list(get_args(WebClientType)) - installed_clients: List[BaseWebClient] = [] - for client in clients: - if client.client_dir.exists(): - installed_clients.append(client) - - return installed_clients - - -def detect_client_cfg_conflict(curr_client: BaseWebClient) -> bool: - """ - Check if any other client configs are present on the system. - It is usually not harmful, but chances are they can conflict each other. - Multiple client configs are, at least, redundant to have them installed - :param curr_client: The client name to check for the conflict - :return: True, if other client configs were found, else False - """ - - mainsail_cfg_status: ComponentStatus = get_client_config_status(MainsailData()) - fluidd_cfg_status: ComponentStatus = get_client_config_status(FluiddData()) - - if curr_client.client == WebClientType.MAINSAIL and fluidd_cfg_status.status == 2: - return True - if curr_client.client == WebClientType.FLUIDD and mainsail_cfg_status.status == 2: - return True - - return False - - -def get_download_url(base_url: str, client: BaseWebClient) -> str: - settings = KiauhSettings() - use_unstable = settings.get(client.name, "unstable_releases") - stable_url = f"{base_url}/latest/download/{client.name}.zip" - - if not use_unstable: - return stable_url - - try: - unstable_tag = get_latest_unstable_tag(client.repo_path) - if unstable_tag == "": - raise Exception - return f"{base_url}/download/{unstable_tag}/{client.name}.zip" - except Exception: - return stable_url - - -################################################# -## NGINX RELATED FUNCTIONS -################################################# - - -def copy_upstream_nginx_cfg() -> None: - """ - Creates an upstream.conf in /etc/nginx/conf.d - :return: None - """ - source = MODULE_PATH.joinpath("assets/upstreams.conf") - target = NGINX_CONFD.joinpath("upstreams.conf") - try: - command = ["sudo", "cp", source, target] - run(command, stderr=PIPE, check=True) - except CalledProcessError as e: - log = f"Unable to create upstreams.conf: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def copy_common_vars_nginx_cfg() -> None: - """ - Creates a common_vars.conf in /etc/nginx/conf.d - :return: None - """ - source = MODULE_PATH.joinpath("assets/common_vars.conf") - target = NGINX_CONFD.joinpath("common_vars.conf") - try: - command = ["sudo", "cp", source, target] - run(command, stderr=PIPE, check=True) - except CalledProcessError as e: - log = f"Unable to create upstreams.conf: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def generate_nginx_cfg_from_template(name: str, template_src: Path, **kwargs) -> None: - """ - Creates an NGINX config from a template file and - replaces all placeholders passed as kwargs. A placeholder must be defined - in the template file as %{placeholder}%. - :param name: name of the config to create - :param template_src: the path to the template file - :return: None - """ - tmp = Path.home().joinpath(f"{name}.tmp") - shutil.copy(template_src, tmp) - with open(tmp, "r+") as f: - content = f.read() - - for key, value in kwargs.items(): - content = content.replace(f"%{key}%", str(value)) - - f.seek(0) - f.write(content) - f.truncate() - - target = NGINX_SITES_AVAILABLE.joinpath(name) - try: - command = ["sudo", "mv", tmp, target] - run(command, stderr=PIPE, check=True) - except CalledProcessError as e: - log = f"Unable to create '{target}': {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def create_nginx_cfg( - display_name: str, - cfg_name: str, - template_src: Path, - **kwargs, -) -> None: - from utils.sys_utils import set_nginx_permissions - - try: - Logger.print_status(f"Creating NGINX config for {display_name} ...") - - source = NGINX_SITES_AVAILABLE.joinpath(cfg_name) - target = NGINX_SITES_ENABLED.joinpath(cfg_name) - remove_file(Path("/etc/nginx/sites-enabled/default"), True) - generate_nginx_cfg_from_template(cfg_name, template_src=template_src, **kwargs) - create_symlink(source, target, True) - set_nginx_permissions() - - Logger.print_ok(f"NGINX config for {display_name} successfully created.") - except Exception: - Logger.print_error(f"Creating NGINX config for {display_name} failed!") - raise - - -def get_nginx_config_list() -> List[Path]: - """ - Get a list of all NGINX config files in /etc/nginx/sites-enabled - :return: List of NGINX config files - """ - configs: List[Path] = [] - for config in NGINX_SITES_ENABLED.iterdir(): - if not config.is_file(): - continue - configs.append(config) - return configs - - -def get_nginx_listen_port(config: Path) -> int | None: - """ - Get the listen port from an NGINX config file - :param config: The NGINX config file to read the port from - :return: The listen port as int or None if not found/parsable - """ - - # noinspection HttpUrlsUsage - pattern = r"default_server|http://|https://|[;\[\]]" - port = "" - with open(config, "r") as cfg: - for line in cfg.readlines(): - line = re.sub(pattern, "", line.strip()) - if line.startswith("listen"): - if ":" not in line: - port = line.split()[-1] - else: - port = line.split(":")[-1] - try: - return int(port) - except ValueError: - Logger.print_error( - f"Unable to parse listen port {port} from {config.name}!" - ) - return None - - -def read_ports_from_nginx_configs() -> List[int]: - """ - Helper function to iterate over all NGINX configs - and read all ports defined for listen - :return: A sorted list of listen ports - """ - if not NGINX_SITES_ENABLED.exists(): - return [] - - port_list: List[int] = [] - for config in get_nginx_config_list(): - port = get_nginx_listen_port(config) - if port is not None: - port_list.append(port) - - return sorted(port_list, key=lambda x: int(x)) - - -def get_client_port_selection( - client: BaseWebClient, - settings: KiauhSettings, - reconfigure=False, -) -> int: - default_port: int = int(settings.get(client.name, "port")) - ports_in_use: List[int] = read_ports_from_nginx_configs() - next_free_port: int = get_next_free_port(ports_in_use) - - port: int = ( - next_free_port - if not reconfigure and default_port in ports_in_use - else default_port - ) - - print_client_port_select_dialog(client.display_name, port, ports_in_use) - - 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) - - if port_input not in ports_in_use: - client_settings: WebUiSettings = settings[client.name] - client_settings.port = port_input - settings.save() - - return port_input - - Logger.print_error("This port is already in use. Please select another one.") - - -def get_next_free_port(ports_in_use: List[int]) -> int: - valid_ports = set(range(80, 7125)) - used_ports = set(map(int, ports_in_use)) - - return min(valid_ports - used_ports) - - -def set_listen_port(client: BaseWebClient, curr_port: int, new_port: int) -> None: - """ - Set the port the client should listen on in the NGINX config - :param curr_port: The current port the client listens on - :param new_port: The new port to set - :param client: The client to set the port for - :return: None - """ - config = NGINX_SITES_AVAILABLE.joinpath(client.name) - with open(config, "r") as f: - lines = f.readlines() - - for i, line in enumerate(lines): - if "listen" in line: - lines[i] = line.replace(str(curr_port), str(new_port)) - - with open(config, "w") as f: - f.writelines(lines) diff --git a/kiauh/components/webui_client/fluidd_data.py b/kiauh/components/webui_client/fluidd_data.py deleted file mode 100644 index f2f6ee2..0000000 --- a/kiauh/components/webui_client/fluidd_data.py +++ /dev/null @@ -1,55 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path - -from components.webui_client.base_data import ( - BaseWebClient, - BaseWebClientConfig, - WebClientConfigType, - WebClientType, -) -from core.constants import NGINX_SITES_AVAILABLE - - -@dataclass() -class FluiddConfigWeb(BaseWebClientConfig): - client_config: WebClientConfigType = WebClientConfigType.FLUIDD - name: str = client_config.value - display_name: str = name.title() - config_dir: Path = Path.home().joinpath("fluidd-config") - config_filename: str = "fluidd.cfg" - config_section: str = f"include {config_filename}" - repo_url: str = "https://github.com/fluidd-core/fluidd-config.git" - - -@dataclass() -class FluiddData(BaseWebClient): - BASE_DL_URL = "https://github.com/fluidd-core/fluidd/releases" - - client: WebClientType = WebClientType.FLUIDD - name: str = client.value - display_name: str = name.capitalize() - client_dir: Path = Path.home().joinpath("fluidd") - config_file: Path = client_dir.joinpath("config.json") - repo_path: str = "fluidd-core/fluidd" - nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("fluidd") - nginx_access_log: Path = Path("/var/log/nginx/fluidd-access.log") - nginx_error_log: Path = Path("/var/log/nginx/fluidd-error.log") - client_config: BaseWebClientConfig = None - download_url: str | None = None - - def __post_init__(self): - from components.webui_client.client_utils import get_download_url - - self.client_config = FluiddConfigWeb() - self.download_url = get_download_url(self.BASE_DL_URL, self) diff --git a/kiauh/components/webui_client/mainsail_data.py b/kiauh/components/webui_client/mainsail_data.py deleted file mode 100644 index bfb5062..0000000 --- a/kiauh/components/webui_client/mainsail_data.py +++ /dev/null @@ -1,55 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path - -from components.webui_client.base_data import ( - BaseWebClient, - BaseWebClientConfig, - WebClientConfigType, - WebClientType, -) -from core.constants import NGINX_SITES_AVAILABLE - - -@dataclass() -class MainsailConfigWeb(BaseWebClientConfig): - client_config: WebClientConfigType = WebClientConfigType.MAINSAIL - name: str = client_config.value - display_name: str = name.title() - config_dir: Path = Path.home().joinpath("mainsail-config") - config_filename: str = "mainsail.cfg" - config_section: str = f"include {config_filename}" - repo_url: str = "https://github.com/mainsail-crew/mainsail-config.git" - - -@dataclass() -class MainsailData(BaseWebClient): - BASE_DL_URL: str = "https://github.com/mainsail-crew/mainsail/releases" - - client: WebClientType = WebClientType.MAINSAIL - name: str = WebClientType.MAINSAIL.value - display_name: str = name.capitalize() - client_dir: Path = Path.home().joinpath("mainsail") - config_file: Path = client_dir.joinpath("config.json") - repo_path: str = "mainsail-crew/mainsail" - nginx_config: Path = NGINX_SITES_AVAILABLE.joinpath("mainsail") - nginx_access_log: Path = Path("/var/log/nginx/mainsail-access.log") - nginx_error_log: Path = Path("/var/log/nginx/mainsail-error.log") - client_config: BaseWebClientConfig = None - download_url: str | None = None - - def __post_init__(self): - from components.webui_client.client_utils import get_download_url - - self.client_config = MainsailConfigWeb() - self.download_url = get_download_url(self.BASE_DL_URL, self) diff --git a/kiauh/components/webui_client/menus/__init__.py b/kiauh/components/webui_client/menus/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/components/webui_client/menus/client_install_menu.py b/kiauh/components/webui_client/menus/client_install_menu.py deleted file mode 100644 index f9f67af..0000000 --- a/kiauh/components/webui_client/menus/client_install_menu.py +++ /dev/null @@ -1,105 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.webui_client.base_data import BaseWebClient -from components.webui_client.client_setup import install_client -from components.webui_client.client_utils import ( - get_client_port_selection, - get_nginx_listen_port, - set_listen_port, -) -from core.logger import Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.services.message_service import Message -from core.settings.kiauh_settings import KiauhSettings, WebUiSettings -from core.types.color import Color -from utils.sys_utils import cmd_sysctl_service, get_ipv4_addr - - -# noinspection PyUnusedLocal -class ClientInstallMenu(BaseMenu): - def __init__( - self, client: BaseWebClient, previous_menu: Type[BaseMenu] | None = None - ): - super().__init__() - self.title = f"Installation Menu > {client.display_name}" - self.title_color = Color.GREEN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.client: BaseWebClient = client - self.settings = KiauhSettings() - self.client_settings: WebUiSettings = self.settings[client.name] - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.install_menu import InstallMenu - - self.previous_menu = previous_menu if previous_menu is not None else InstallMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.reinstall_client), - "2": Option(method=self.change_listen_port), - } - - def print_menu(self) -> None: - client_name = self.client.display_name - port = f"(Current: {Color.apply(self._get_current_port(), Color.GREEN)})" - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ 1) Reinstall {client_name:16} ║ - ║ 2) Reconfigure Listen Port {port:<34} ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def reinstall_client(self, **kwargs) -> None: - install_client(self.client, settings=self.settings, reinstall=True) - - def change_listen_port(self, **kwargs) -> None: - curr_port = self._get_current_port() - new_port = get_client_port_selection( - self.client, - self.settings, - reconfigure=True, - ) - - cmd_sysctl_service("nginx", "stop") - set_listen_port(self.client, curr_port, new_port) - - Logger.print_status("Saving new port configuration ...") - self.client_settings.port = new_port - self.settings.save() - Logger.print_ok("Port configuration saved!") - - cmd_sysctl_service("nginx", "start") - - # noinspection HttpUrlsUsage - message = Message( - title="Port reconfiguration complete!", - text=[ - f"Open {self.client.display_name} now on: " - f"http://{get_ipv4_addr()}:{new_port}", - ], - color=Color.GREEN, - ) - self.message_service.set_message(message) - - def _get_current_port(self) -> int: - curr_port = 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 - return int(self.client_settings.port) - return curr_port diff --git a/kiauh/components/webui_client/menus/client_remove_menu.py b/kiauh/components/webui_client/menus/client_remove_menu.py deleted file mode 100644 index 82ce69d..0000000 --- a/kiauh/components/webui_client/menus/client_remove_menu.py +++ /dev/null @@ -1,114 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.webui_client import client_remove -from components.webui_client.base_data import BaseWebClient -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color - - -# noinspection PyUnusedLocal -class ClientRemoveMenu(BaseMenu): - def __init__( - self, client: BaseWebClient, previous_menu: Type[BaseMenu] | None = None - ): - super().__init__() - self.title = f"Remove {client.display_name}" - self.title_color = Color.RED - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.client: BaseWebClient = client - self.remove_client: bool = False - self.remove_client_cfg: bool = False - self.backup_config_json: bool = False - self.select_state: bool = False - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.remove_menu import RemoveMenu - - self.previous_menu = previous_menu if previous_menu is not None else RemoveMenu - - def set_options(self) -> None: - self.options = { - "a": Option(method=self.toggle_all), - "1": Option(method=self.toggle_rm_client), - "2": Option(method=self.toggle_rm_client_config), - "3": Option(method=self.toggle_backup_config_json), - "c": Option(method=self.run_removal_process), - } - - def print_menu(self) -> None: - client_name = self.client.display_name - client_config = self.client.client_config - client_config_name = client_config.display_name - - checked = f"[{Color.apply('x', Color.CYAN)}]" - unchecked = "[ ]" - o1 = checked if self.remove_client else unchecked - o2 = checked if self.remove_client_cfg else unchecked - o3 = checked if self.backup_config_json else unchecked - sel_state = f"{'Select' if not self.select_state else 'Deselect'} everything" - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ Enter a number and hit enter to select / deselect ║ - ║ the specific option for removal. ║ - ╟───────────────────────────────────────────────────────╢ - ║ a) {sel_state:49} ║ - ╟───────────────────────────────────────────────────────╢ - ║ 1) {o1} Remove {client_name:16} ║ - ║ 2) {o2} Remove {client_config_name:24} ║ - ║ 3) {o3} Backup config.json ║ - ╟───────────────────────────────────────────────────────╢ - ║ C) Continue ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def toggle_all(self, **kwargs) -> None: - self.select_state = not self.select_state - self.remove_client = self.select_state - self.remove_client_cfg = self.select_state - self.backup_config_json = self.select_state - - def toggle_rm_client(self, **kwargs) -> None: - self.remove_client = not self.remove_client - - def toggle_rm_client_config(self, **kwargs) -> None: - self.remove_client_cfg = not self.remove_client_cfg - - def toggle_backup_config_json(self, **kwargs) -> None: - self.backup_config_json = not self.backup_config_json - - def run_removal_process(self, **kwargs) -> None: - if ( - not self.remove_client - and not self.remove_client_cfg - and not self.backup_config_json - ): - print(Color.apply("Nothing selected ...", Color.RED)) - return - - completion_msg = client_remove.run_client_removal( - client=self.client, - remove_client=self.remove_client, - remove_client_cfg=self.remove_client_cfg, - backup_config=self.backup_config_json, - ) - self.message_service.set_message(completion_msg) - - self.remove_client = False - self.remove_client_cfg = False - self.backup_config_json = False - self.select_state = False diff --git a/kiauh/core/__init__.py b/kiauh/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/constants.py b/kiauh/core/constants.py deleted file mode 100644 index 9a9224a..0000000 --- a/kiauh/core/constants.py +++ /dev/null @@ -1,27 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -import os -import pwd -from pathlib import Path - -# global dependencies -GLOBAL_DEPS = ["git", "wget", "curl", "unzip", "dfu-util", "python3-virtualenv"] - -# strings -INVALID_CHOICE = "Invalid choice. Please select a valid value." - -# current user -CURRENT_USER = pwd.getpwuid(os.getuid())[0] - -# dirs -SYSTEMD = Path("/etc/systemd/system") -NGINX_SITES_AVAILABLE = Path("/etc/nginx/sites-available") -NGINX_SITES_ENABLED = Path("/etc/nginx/sites-enabled") -NGINX_CONFD = Path("/etc/nginx/conf.d") diff --git a/kiauh/core/decorators.py b/kiauh/core/decorators.py deleted file mode 100644 index 46dc2e6..0000000 --- a/kiauh/core/decorators.py +++ /dev/null @@ -1,24 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import warnings -from typing import Callable - - -def deprecated(info: str = "", replaced_by: Callable | None = None) -> Callable: - def decorator(func) -> Callable: - def wrapper(*args, **kwargs): - msg = f"{info}{replaced_by.__name__ if replaced_by else ''}" - warnings.warn(msg, category=DeprecationWarning, stacklevel=2) - return func(*args, **kwargs) - - return wrapper - - return decorator diff --git a/kiauh/core/instance_manager/__init__.py b/kiauh/core/instance_manager/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/instance_manager/base_instance.py b/kiauh/core/instance_manager/base_instance.py deleted file mode 100644 index 18af206..0000000 --- a/kiauh/core/instance_manager/base_instance.py +++ /dev/null @@ -1,59 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# 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 pathlib import Path -from typing import List - -from utils.fs_utils import get_data_dir - -# suffixes that are not allowed to be used for instances -# because they would cause conflicts with other components or are reserved -SUFFIX_BLACKLIST: List[str] = ["None", "mcu", "obico", "bambu", "companion", "hmi"] - -@dataclass(repr=True) -class BaseInstance: - instance_type: type - suffix: str - log_file_name: str | None = None - data_dir: Path = field(init=False) - base_folders: List[Path] = field(init=False) - cfg_dir: Path = field(init=False) - log_dir: Path = field(init=False) - gcodes_dir: Path = field(init=False) - comms_dir: Path = field(init=False) - sysd_dir: Path = field(init=False) - is_legacy_instance: bool = field(init=False) - - def __post_init__(self): - self.data_dir = get_data_dir(self.instance_type, self.suffix) - # the following attributes require the data_dir to be set - self.cfg_dir = self.data_dir.joinpath("config") - self.log_dir = self.data_dir.joinpath("logs") - self.gcodes_dir = self.data_dir.joinpath("gcodes") - self.comms_dir = self.data_dir.joinpath("comms") - self.sysd_dir = self.data_dir.joinpath("systemd") - self.is_legacy_instance = self._set_is_legacy_instance() - self.base_folders = [ - self.data_dir, - self.cfg_dir, - self.log_dir, - self.gcodes_dir, - self.comms_dir, - self.sysd_dir, - ] - - def _set_is_legacy_instance(self) -> bool: - legacy_pattern = r"^(?!printer)(.+)_data" - match = re.search(legacy_pattern, self.data_dir.name) - - return True if (match and self.suffix != "") else False diff --git a/kiauh/core/instance_manager/instance_manager.py b/kiauh/core/instance_manager/instance_manager.py deleted file mode 100644 index d41b66c..0000000 --- a/kiauh/core/instance_manager/instance_manager.py +++ /dev/null @@ -1,108 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from pathlib import Path -from subprocess import CalledProcessError -from typing import List - -from core.logger import Logger -from utils.instance_type import InstanceType -from utils.sys_utils import cmd_sysctl_service - - -class InstanceManager: - @staticmethod - def enable(instance: InstanceType) -> None: - service_name: str = instance.service_file_path.name - try: - cmd_sysctl_service(service_name, "enable") - except CalledProcessError as e: - Logger.print_error(f"Error enabling service {service_name}:") - Logger.print_error(f"{e}") - - @staticmethod - def disable(instance: InstanceType) -> None: - service_name: str = instance.service_file_path.name - try: - cmd_sysctl_service(service_name, "disable") - except CalledProcessError as e: - Logger.print_error(f"Error disabling {service_name}: {e}") - raise - - @staticmethod - def start(instance: InstanceType) -> None: - service_name: str = instance.service_file_path.name - try: - cmd_sysctl_service(service_name, "start") - except CalledProcessError as e: - Logger.print_error(f"Error starting {service_name}: {e}") - raise - - @staticmethod - def stop(instance: InstanceType) -> None: - name: str = instance.service_file_path.name - try: - cmd_sysctl_service(name, "stop") - except CalledProcessError as e: - Logger.print_error(f"Error stopping {name}: {e}") - raise - - @staticmethod - def restart(instance: InstanceType) -> None: - name: str = instance.service_file_path.name - try: - cmd_sysctl_service(name, "restart") - except CalledProcessError as e: - Logger.print_error(f"Error restarting {name}: {e}") - raise - - @staticmethod - def start_all(instances: List[InstanceType]) -> None: - for instance in instances: - InstanceManager.start(instance) - - @staticmethod - def stop_all(instances: List[InstanceType]) -> None: - for instance in instances: - InstanceManager.stop(instance) - - @staticmethod - def restart_all(instances: List[InstanceType]) -> None: - for instance in instances: - InstanceManager.restart(instance) - - @staticmethod - def remove(instance: InstanceType) -> None: - from utils.fs_utils import run_remove_routines - from utils.sys_utils import remove_system_service - - try: - # remove the service file - service_file_path: Path = instance.service_file_path - if service_file_path is not None: - remove_system_service(service_file_path.name) - - # then remove all the log files - if ( - not instance.log_file_name - or not instance.base.log_dir - or not instance.base.log_dir.exists() - ): - return - - files = instance.base.log_dir.iterdir() - logs = [f for f in files if f.name.startswith(instance.log_file_name)] - for log in logs: - Logger.print_status(f"Remove '{log}'") - run_remove_routines(log) - - except Exception as e: - Logger.print_error(f"Error removing service: {e}") - raise diff --git a/kiauh/core/logger.py b/kiauh/core/logger.py deleted file mode 100644 index cae4873..0000000 --- a/kiauh/core/logger.py +++ /dev/null @@ -1,169 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from enum import Enum -from typing import List - -from core.types.color import Color - - -class DialogType(Enum): - INFO = ("INFO", Color.WHITE) - SUCCESS = ("SUCCESS", Color.GREEN) - ATTENTION = ("ATTENTION", Color.YELLOW) - WARNING = ("WARNING", Color.YELLOW) - ERROR = ("ERROR", Color.RED) - CUSTOM = (None, None) - - -LINE_WIDTH = 53 - - -BORDER_TOP: str = "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" -BORDER_BOTTOM: str = "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛" -BORDER_TITLE: str = "┠───────────────────────────────────────────────────────┨" -BORDER_LEFT: str = "┃" -BORDER_RIGHT: str = "┃" - - -class Logger: - @staticmethod - def print_info(msg, prefix=True, start="", end="\n") -> None: - message = f"[INFO] {msg}" if prefix else msg - Logger.__print(Color.WHITE, start, message, end) - - @staticmethod - def print_ok(msg: str = "Success!", prefix=True, start="", end="\n") -> None: - message = f"[OK] {msg}" if prefix else msg - Logger.__print(Color.GREEN, start, message, end) - - @staticmethod - def print_warn(msg, prefix=True, start="", end="\n") -> None: - message = f"[WARN] {msg}" if prefix else msg - Logger.__print(Color.YELLOW, start, message, end) - - @staticmethod - def print_error(msg, prefix=True, start="", end="\n") -> None: - message = f"[ERROR] {msg}" if prefix else msg - Logger.__print(Color.RED, start, message, end) - - @staticmethod - def print_status(msg, prefix=True, start="", end="\n") -> None: - message = f"\n###### {msg}" if prefix else msg - Logger.__print(Color.MAGENTA, start, message, end) - - @staticmethod - def __print(color: Color, start: str, message: str, end: str) -> None: - print(Color.apply(f"{start}{message}", color), end=end) - - @staticmethod - def print_dialog( - title: DialogType, - content: List[str], - center_content: bool = False, - custom_title: str | None = None, - custom_color: Color | None = None, - margin_top: int = 0, - margin_bottom: int = 0, - ) -> None: - """ - Prints a dialog with the given title and content. - Those dialogs should be used to display verbose messages to the user which - require simple interaction like confirmation or input. Do not use this for - navigating through the application. - - :param title: The type of the dialog. - :param content: The content of the dialog. - :param center_content: Whether to center the content or not. - :param custom_title: A custom title for the dialog. - :param custom_color: A custom color for the dialog. - :param margin_top: The number of empty lines to print before the dialog. - :param margin_bottom: The number of empty lines to print after the dialog. - """ - color = Logger._get_dialog_color(title, custom_color) - dialog_title = Logger._get_dialog_title(title, custom_title) - - if margin_top > 0: - print("\n" * margin_top, end="") - - print(Color.apply(BORDER_TOP, color)) - - if dialog_title: - print(Color.apply(f"┃ {dialog_title:^{LINE_WIDTH}} ┃", color)) - print(Color.apply(BORDER_TITLE, color)) - - if content: - print( - Logger.format_content( - content, - LINE_WIDTH, - color, - center_content, - ) - ) - - print(Color.apply(BORDER_BOTTOM, color)) - - if margin_bottom > 0: - print("\n" * margin_bottom, end="") - - @staticmethod - def _get_dialog_title( - title: DialogType, custom_title: str | None = None - ) -> str | None: - if title == DialogType.CUSTOM and custom_title: - return f"[ {custom_title} ]" - return f"[ {title.value[0]} ]" if title.value[0] else None - - @staticmethod - def _get_dialog_color( - title: DialogType, custom_color: Color | None = None - ) -> Color: - if title == DialogType.CUSTOM and custom_color: - return custom_color - - color: Color = title.value[1] if title.value[1] else Color.WHITE - - return color - - @staticmethod - def format_content( - content: List[str], - line_width: int, - color: Color = Color.WHITE, - center_content: bool = False, - border_left: str = "┃", - border_right: str = "┃", - ) -> str: - wrapper = textwrap.TextWrapper(line_width) - - lines = [] - for i, c in enumerate(content): - paragraph = wrapper.wrap(c) - lines.extend(paragraph) - - # add a full blank line if we have a double newline - # character unless we are at the end of the list - if c == "\n\n" and i < len(content) - 1: - lines.append(" " * line_width) - - if not center_content: - formatted_lines = [ - Color.apply(f"{border_left} {line:<{line_width}} {border_right}", color) - for line in lines - ] - else: - formatted_lines = [ - Color.apply(f"{border_left} {line:^{line_width}} {border_right}", color) - for line in lines - ] - - return "\n".join(formatted_lines) diff --git a/kiauh/core/menus/__init__.py b/kiauh/core/menus/__init__.py deleted file mode 100644 index 568fcd3..0000000 --- a/kiauh/core/menus/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass -from enum import Enum -from typing import Any, Callable, Type - - -@dataclass -class Option: - """ - Represents a menu option. - :param method: Method that will be used to call the menu option - :param opt_index: Can be used to pass the user input to the menu option - :param opt_data: Can be used to pass any additional data to the menu option - """ - - def __repr__(self): - return f"Option(method={self.method.__name__}, opt_index={self.opt_index}, opt_data={self.opt_data})" - - method: Type[Callable] - opt_index: str = "" - opt_data: Any = None - - -class FooterType(Enum): - QUIT = "QUIT" - BACK = "BACK" - BACK_HELP = "BACK_HELP" - BLANK = "BLANK" diff --git a/kiauh/core/menus/advanced_menu.py b/kiauh/core/menus/advanced_menu.py deleted file mode 100644 index 7311d39..0000000 --- a/kiauh/core/menus/advanced_menu.py +++ /dev/null @@ -1,106 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.klipper import KLIPPER_DIR -from components.klipper.klipper import Klipper -from components.klipper.klipper_utils import install_input_shaper_deps -from components.klipper_firmware.menus.klipper_build_menu import ( - KlipperBuildFirmwareMenu, - KlipperKConfigMenu, -) -from components.klipper_firmware.menus.klipper_flash_menu import ( - KlipperFlashMethodMenu, - KlipperSelectMcuConnectionMenu, -) -from components.moonraker import MOONRAKER_DIR -from components.moonraker.moonraker import Moonraker -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from procedures.system import change_system_hostname -from utils.git_utils import rollback_repository - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class AdvancedMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.title = "Advanced Menu" - self.title_color = Color.YELLOW - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.build), - "2": Option(method=self.flash), - "3": Option(method=self.build_flash), - "4": Option(method=self.get_id), - "5": Option(method=self.input_shaper), - "6": Option(method=self.klipper_rollback), - "7": Option(method=self.moonraker_rollback), - "8": Option(method=self.change_hostname), - } - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - ╟───────────────────────────┬───────────────────────────╢ - ║ Klipper Firmware: │ Repository Rollback: ║ - ║ 1) [Build] │ 6) [Klipper] ║ - ║ 2) [Flash] │ 7) [Moonraker] ║ - ║ 3) [Build + Flash] │ ║ - ║ 4) [Get MCU ID] │ System: ║ - ║ │ 8) [Change hostname] ║ - ║ Extra Dependencies: │ ║ - ║ 5) [Input Shaper] │ ║ - ╟───────────────────────────┴───────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def klipper_rollback(self, **kwargs) -> None: - rollback_repository(KLIPPER_DIR, Klipper) - - def moonraker_rollback(self, **kwargs) -> None: - rollback_repository(MOONRAKER_DIR, Moonraker) - - def build(self, **kwargs) -> None: - KlipperKConfigMenu().run() - KlipperBuildFirmwareMenu(previous_menu=self.__class__).run() - - def flash(self, **kwargs) -> None: - KlipperKConfigMenu().run() - KlipperFlashMethodMenu(previous_menu=self.__class__).run() - - def build_flash(self, **kwargs) -> None: - KlipperKConfigMenu().run() - KlipperBuildFirmwareMenu(previous_menu=KlipperFlashMethodMenu).run() - KlipperFlashMethodMenu(previous_menu=self.__class__).run() - - def get_id(self, **kwargs) -> None: - KlipperSelectMcuConnectionMenu( - previous_menu=self.__class__, - standalone=True, - ).run() - - def change_hostname(self, **kwargs) -> None: - change_system_hostname() - - def input_shaper(self, **kwargs) -> None: - install_input_shaper_deps() diff --git a/kiauh/core/menus/backup_menu.py b/kiauh/core/menus/backup_menu.py deleted file mode 100644 index 7cd5a03..0000000 --- a/kiauh/core/menus/backup_menu.py +++ /dev/null @@ -1,107 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.klipper.klipper_utils import backup_klipper_dir -from components.klipperscreen.klipperscreen import backup_klipperscreen_dir -from components.moonraker.utils.utils import ( - backup_moonraker_db_dir, - backup_moonraker_dir, -) -from components.webui_client.client_utils import ( - backup_client_config_data, - backup_client_data, -) -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.services.backup_service import BackupService -from core.types.color import Color - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class BackupMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.title = "Backup Menu" - self.title_color = Color.GREEN - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.backup_klipper), - "2": Option(method=self.backup_moonraker), - "3": Option(method=self.backup_printer_config), - "4": Option(method=self.backup_moonraker_db), - "5": Option(method=self.backup_mainsail), - "6": Option(method=self.backup_fluidd), - "7": Option(method=self.backup_mainsail_config), - "8": Option(method=self.backup_fluidd_config), - "9": Option(method=self.backup_klipperscreen), - } - - def print_menu(self) -> None: - line1 = Color.apply( - "INFO: Backups are located in '~/kiauh-backups'", Color.YELLOW - ) - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ {line1:^62} ║ - ╟───────────────────────────┬───────────────────────────╢ - ║ Klipper & Moonraker API: │ Client-Config: ║ - ║ 1) [Klipper] │ 7) [Mainsail-Config] ║ - ║ 2) [Moonraker] │ 8) [Fluidd-Config] ║ - ║ 3) [Config Folder] │ ║ - ║ 4) [Moonraker Database] │ Touchscreen GUI: ║ - ║ │ 9) [KlipperScreen] ║ - ║ Webinterface: │ ║ - ║ 5) [Mainsail] │ ║ - ║ 6) [Fluidd] │ ║ - ╟───────────────────────────┴───────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def backup_klipper(self, **kwargs) -> None: - backup_klipper_dir() - - def backup_moonraker(self, **kwargs) -> None: - backup_moonraker_dir() - - def backup_printer_config(self, **kwargs) -> None: - BackupService().backup_printer_config_dir() - - def backup_moonraker_db(self, **kwargs) -> None: - backup_moonraker_db_dir() - - def backup_mainsail(self, **kwargs) -> None: - backup_client_data(MainsailData()) - - def backup_fluidd(self, **kwargs) -> None: - backup_client_data(FluiddData()) - - def backup_mainsail_config(self, **kwargs) -> None: - backup_client_config_data(MainsailData()) - - def backup_fluidd_config(self, **kwargs) -> None: - backup_client_config_data(FluiddData()) - - def backup_klipperscreen(self, **kwargs) -> None: - backup_klipperscreen_dir() diff --git a/kiauh/core/menus/base_menu.py b/kiauh/core/menus/base_menu.py deleted file mode 100644 index 1e8796c..0000000 --- a/kiauh/core/menus/base_menu.py +++ /dev/null @@ -1,239 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -import subprocess -import sys -import textwrap -import traceback -from abc import abstractmethod -from enum import Enum -from typing import Dict, Type - -from core.logger import Logger -from core.menus import FooterType, Option -from core.services.message_service import MessageService -from core.spinner import Spinner -from core.types.color import Color -from utils.input_utils import get_selection_input - - -def clear() -> None: - subprocess.call("clear -x", shell=True) - - -def print_header() -> None: - line1 = " [ KIAUH ] " - line2 = "Klipper Installation And Update Helper" - line3 = "" - color = Color.CYAN - count = 62 - len(str(color)) - len(str(Color.RST)) - header = textwrap.dedent( - f""" - ╔═══════════════════════════════════════════════════════╗ - ║ {Color.apply(f"{line1:~^{count}}", color)} ║ - ║ {Color.apply(f"{line2:^{count}}", color)} ║ - ║ {Color.apply(f"{line3:~^{count}}", color)} ║ - ╚═══════════════════════════════════════════════════════╝ - """ - )[1:] - print(header, end="") - - -def print_quit_footer() -> None: - text = "Q) Quit" - color = Color.RED - count = 62 - len(str(color)) - len(str(Color.RST)) - footer = textwrap.dedent( - f""" - ║ {color}{text:^{count}}{Color.RST} ║ - ╚═══════════════════════════════════════════════════════╝ - """ - )[1:] - print(footer, end="") - - -def print_back_footer() -> None: - text = "B) « Back" - color = Color.GREEN - count = 62 - len(str(color)) - len(str(Color.RST)) - footer = textwrap.dedent( - f""" - ║ {color}{text:^{count}}{Color.RST} ║ - ╚═══════════════════════════════════════════════════════╝ - """ - )[1:] - print(footer, end="") - - -def print_back_help_footer() -> None: - text1 = "B) « Back" - text2 = "H) Help [?]" - color1 = Color.GREEN - color2 = Color.YELLOW - count = 34 - len(str(color1)) - len(str(Color.RST)) - footer = textwrap.dedent( - f""" - ║ {color1}{text1:^{count}}{Color.RST} │ {color2}{text2:^{count}}{Color.RST} ║ - ╚═══════════════════════════╧═══════════════════════════╝ - """ - )[1:] - print(footer, end="") - - -def print_blank_footer() -> None: - print("╚═══════════════════════════════════════════════════════╝") - - -class MenuTitleStyle(Enum): - PLAIN = "plain" - STYLED = "styled" - - -class PostInitCaller(type): - def __call__(cls, *args, **kwargs): - obj = type.__call__(cls, *args, **kwargs) - obj.__post_init__() - return obj - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class BaseMenu(metaclass=PostInitCaller): - options: Dict[str, Option] = {} - options_offset: int = 0 - default_option: Option = None - input_label_txt: str = "Perform action" - header: bool = False - - loading_msg: str = "" - spinner: Spinner | None = None - - title: str = "" - title_style: MenuTitleStyle = MenuTitleStyle.STYLED - title_color: Color = Color.WHITE - - previous_menu: Type[BaseMenu] | None = None - help_menu: Type[BaseMenu] | None = None - footer_type: FooterType = FooterType.BACK - - message_service = MessageService() - - def __init__(self, **kwargs) -> None: - if type(self) is BaseMenu: - raise NotImplementedError("BaseMenu cannot be instantiated directly.") - - def __post_init__(self) -> None: - self.set_previous_menu(self.previous_menu) - self.set_options() - - # conditionally add options based on footer type - if self.footer_type is FooterType.QUIT: - self.options["q"] = Option(method=self.__exit) - if self.footer_type is FooterType.BACK: - self.options["b"] = Option(method=self.__go_back) - if self.footer_type is FooterType.BACK_HELP: - self.options["b"] = Option(method=self.__go_back) - self.options["h"] = Option(method=self.__go_to_help) - # if defined, add the default option to the options dict - if self.default_option is not None: - self.options[""] = self.default_option - - def __go_back(self, **kwargs) -> None: - if self.previous_menu is None: - return - self.previous_menu().run() - - def __go_to_help(self, **kwargs) -> None: - if self.help_menu is None: - return - self.help_menu(previous_menu=self.__class__).run() - - def __exit(self, **kwargs) -> None: - Logger.print_ok("###### Happy printing!", False) - sys.exit(0) - - @abstractmethod - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - raise NotImplementedError - - @abstractmethod - def set_options(self) -> None: - raise NotImplementedError - - @abstractmethod - def print_menu(self) -> None: - raise NotImplementedError - - def is_loading(self, state: bool) -> None: - if not self.spinner and state: - self.spinner = Spinner(self.loading_msg) - self.spinner.start() - else: - self.spinner.stop() - self.spinner = None - - def __print_menu_title(self) -> None: - count = 62 - len(str(self.title_color)) - len(str(Color.RST)) - menu_title = "╔═══════════════════════════════════════════════════════╗\n" - if self.title: - title = ( - f" [ {self.title} ] " - if self.title_style == MenuTitleStyle.STYLED - else self.title - ) - line = ( - f"{title:~^{count}}" - if self.title_style == MenuTitleStyle.STYLED - else f"{title:^{count}}" - ) - menu_title += f"║ {Color.apply(line, self.title_color)} ║\n" - print(menu_title, end="") - - def __print_footer(self) -> None: - if self.footer_type is FooterType.QUIT: - print_quit_footer() - elif self.footer_type is FooterType.BACK: - print_back_footer() - elif self.footer_type is FooterType.BACK_HELP: - print_back_help_footer() - elif self.footer_type is FooterType.BLANK: - print_blank_footer() - else: - raise NotImplementedError("FooterType not correctly implemented!") - - def __display_menu(self) -> None: - self.message_service.display_message() - - if self.header: - print_header() - - self.__print_menu_title() - self.print_menu() - self.__print_footer() - - def run(self) -> None: - """Start the menu lifecycle. When this function returns, the lifecycle of the menu ends.""" - try: - self.__display_menu() - option = get_selection_input(self.input_label_txt, self.options) - selected_option: Option = self.options.get(option) - - selected_option.method( - opt_index=selected_option.opt_index, - opt_data=selected_option.opt_data, - ) - - self.run() - - except Exception as e: - Logger.print_error( - f"An unexpected error occured:\n{e}\n{traceback.format_exc()}" - ) diff --git a/kiauh/core/menus/install_menu.py b/kiauh/core/menus/install_menu.py deleted file mode 100644 index 4fccfb9..0000000 --- a/kiauh/core/menus/install_menu.py +++ /dev/null @@ -1,109 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.crowsnest.crowsnest import install_crowsnest -from components.klipper.services.klipper_setup_service import KlipperSetupService -from components.klipperscreen.klipperscreen import install_klipperscreen -from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService -from components.webui_client.client_config.client_config_setup import ( - install_client_config, -) -from components.webui_client.client_setup import install_client -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from components.webui_client.menus.client_install_menu import ClientInstallMenu -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class InstallMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.title = "Installation Menu" - self.title_color = Color.GREEN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.klsvc = KlipperSetupService() - self.mrsvc = MoonrakerSetupService() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.install_klipper), - "2": Option(method=self.install_moonraker), - "3": Option(method=self.install_mainsail), - "4": Option(method=self.install_fluidd), - "5": Option(method=self.install_mainsail_config), - "6": Option(method=self.install_fluidd_config), - "7": Option(method=self.install_klipperscreen), - "8": Option(method=self.install_crowsnest), - } - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - ╟───────────────────────────┬───────────────────────────╢ - ║ Firmware & API: │ Touchscreen GUI: ║ - ║ 1) [Klipper] │ 7) [KlipperScreen] ║ - ║ 2) [Moonraker] │ ║ - ║ │ Webcam Streamer: ║ - ║ Webinterface: │ 8) [Crowsnest] ║ - ║ 3) [Mainsail] │ ║ - ║ 4) [Fluidd] │ ║ - ║ │ ║ - ║ Client-Config: │ ║ - ║ 5) [Mainsail-Config] │ ║ - ║ 6) [Fluidd-Config] │ ║ - ╟───────────────────────────┴───────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def install_klipper(self, **kwargs) -> None: - self.klsvc.install() - - def install_moonraker(self, **kwargs) -> None: - self.mrsvc.install() - - def install_mainsail(self, **kwargs) -> None: - client: MainsailData = MainsailData() - if client.client_dir.exists(): - ClientInstallMenu(client, self.__class__).run() - else: - install_client(client, settings=KiauhSettings()) - - def install_mainsail_config(self, **kwargs) -> None: - install_client_config(MainsailData()) - - def install_fluidd(self, **kwargs) -> None: - client: FluiddData = FluiddData() - if client.client_dir.exists(): - ClientInstallMenu(client, self.__class__).run() - else: - install_client(client, settings=KiauhSettings()) - - def install_fluidd_config(self, **kwargs) -> None: - install_client_config(FluiddData()) - - def install_klipperscreen(self, **kwargs) -> None: - install_klipperscreen() - - def install_crowsnest(self, **kwargs) -> None: - install_crowsnest() diff --git a/kiauh/core/menus/main_menu.py b/kiauh/core/menus/main_menu.py deleted file mode 100644 index 7fb55ff..0000000 --- a/kiauh/core/menus/main_menu.py +++ /dev/null @@ -1,179 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import sys -import textwrap -from typing import Callable, Type - -from components.crowsnest.crowsnest import get_crowsnest_status -from components.klipper.klipper_utils import get_klipper_status -from components.klipperscreen.klipperscreen import get_klipperscreen_status -from components.log_uploads.menus.log_upload_menu import LogUploadMenu -from components.moonraker.utils.utils import get_moonraker_status -from components.webui_client.client_utils import ( - get_client_status, - get_current_client_config, -) -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from core.logger import Logger -from core.menus import FooterType -from core.menus.advanced_menu import AdvancedMenu -from core.menus.backup_menu import BackupMenu -from core.menus.base_menu import BaseMenu, Option -from core.menus.install_menu import InstallMenu -from core.menus.remove_menu import RemoveMenu -from core.menus.settings_menu import SettingsMenu -from core.menus.update_menu import UpdateMenu -from core.types.color import Color -from core.types.component_status import ComponentStatus, StatusMap, StatusText -from extensions.extensions_menu import ExtensionsMenu -from utils.common import get_kiauh_version, trunc_string - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class MainMenu(BaseMenu): - def __init__(self) -> None: - super().__init__() - - self.header: bool = True - self.title = "Main Menu" - self.title_color = Color.CYAN - self.footer_type: FooterType = FooterType.QUIT - - self.version = "" - self.kl_status, self.kl_owner, self.kl_repo = "", "", "" - self.mr_status, self.mr_owner, self.mr_repo = "", "", "" - self.ms_status, self.fl_status, self.ks_status = "", "", "" - self.cn_status, self.cc_status = "", "" - self._init_status() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - """MainMenu does not have a previous menu""" - pass - - def set_options(self) -> None: - self.options = { - "0": Option(method=self.log_upload_menu), - "1": Option(method=self.install_menu), - "2": Option(method=self.update_menu), - "3": Option(method=self.remove_menu), - "4": Option(method=self.advanced_menu), - "5": Option(method=self.backup_menu), - "e": Option(method=self.extension_menu), - "s": Option(method=self.settings_menu), - } - - def _init_status(self) -> None: - status_vars = ["kl", "mr", "ms", "fl", "ks", "cn"] - for var in status_vars: - setattr( - self, - f"{var}_status", - Color.apply("Not installed", Color.RED), - ) - - def _fetch_status(self) -> None: - self.version = get_kiauh_version() - self._get_component_status("kl", get_klipper_status) - self._get_component_status("mr", get_moonraker_status) - self._get_component_status("ms", get_client_status, MainsailData()) - self._get_component_status("fl", get_client_status, FluiddData()) - self._get_component_status("ks", get_klipperscreen_status) - self._get_component_status("cn", get_crowsnest_status) - self.cc_status = get_current_client_config() - - def _get_component_status(self, name: str, status_fn: Callable, *args) -> None: - 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) - instance_count: int = status_data.instances - - count_txt: str = "" - if instance_count > 0 and code == 2: - count_txt = f": {instance_count}" - - setattr(self, f"{name}_status", self._format_by_code(code, status, count_txt)) - setattr(self, f"{name}_owner", Color.apply(owner, Color.CYAN)) - setattr(self, f"{name}_repo", Color.apply(repo, Color.CYAN)) - - def _format_by_code(self, code: int, status: str, count: str) -> str: - color = Color.RED - if code == 0: - color = Color.RED - elif code == 1: - color = Color.YELLOW - elif code == 2: - color = Color.GREEN - - return Color.apply(f"{status}{count}", color) - - def print_menu(self) -> None: - self._fetch_status() - - footer1 = Color.apply(self.version, Color.CYAN) - link = Color.apply("https://git.io/JnmlX", Color.MAGENTA) - footer2 = f"Changelog: {link}" - pad1 = 32 - pad2 = 26 - menu = textwrap.dedent( - f""" - ╟──────────────────┬────────────────────────────────────╢ - ║ 0) [Log-Upload] │ Klipper: {self.kl_status:<{pad1}} ║ - ║ │ Owner: {self.kl_owner:<{pad1}} ║ - ║ 1) [Install] │ Repo: {self.kl_repo:<{pad1}} ║ - ║ 2) [Update] ├────────────────────────────────────╢ - ║ 3) [Remove] │ Moonraker: {self.mr_status:<{pad1}} ║ - ║ 4) [Advanced] │ Owner: {self.mr_owner:<{pad1}} ║ - ║ 5) [Backup] │ Repo: {self.mr_repo:<{pad1}} ║ - ║ ├────────────────────────────────────╢ - ║ S) [Settings] │ Mainsail: {self.ms_status:<{pad2}} ║ - ║ │ Fluidd: {self.fl_status:<{pad2}} ║ - ║ Community: │ Client-Config: {self.cc_status:<{pad2}} ║ - ║ E) [Extensions] │ ║ - ║ │ KlipperScreen: {self.ks_status:<{pad2}} ║ - ║ │ Crowsnest: {self.cn_status:<{pad2}} ║ - ╟──────────────────┼────────────────────────────────────╢ - ║ {footer1:^25} │ {footer2:^43} ║ - ╟──────────────────┴────────────────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def exit(self, **kwargs) -> None: - Logger.print_ok("###### Happy printing!", False) - sys.exit(0) - - def log_upload_menu(self, **kwargs) -> None: - LogUploadMenu().run() - - def install_menu(self, **kwargs) -> None: - InstallMenu(previous_menu=self.__class__).run() - - def update_menu(self, **kwargs) -> None: - UpdateMenu(previous_menu=self.__class__).run() - - def remove_menu(self, **kwargs) -> None: - RemoveMenu(previous_menu=self.__class__).run() - - def advanced_menu(self, **kwargs) -> None: - AdvancedMenu(previous_menu=self.__class__).run() - - def backup_menu(self, **kwargs) -> None: - BackupMenu(previous_menu=self.__class__).run() - - def settings_menu(self, **kwargs) -> None: - SettingsMenu(previous_menu=self.__class__).run() - - def extension_menu(self, **kwargs) -> None: - ExtensionsMenu(previous_menu=self.__class__).run() diff --git a/kiauh/core/menus/remove_menu.py b/kiauh/core/menus/remove_menu.py deleted file mode 100644 index 710f359..0000000 --- a/kiauh/core/menus/remove_menu.py +++ /dev/null @@ -1,86 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.crowsnest.crowsnest import remove_crowsnest -from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu -from components.klipperscreen.klipperscreen import remove_klipperscreen -from components.moonraker.menus.moonraker_remove_menu import ( - MoonrakerRemoveMenu, -) -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from components.webui_client.menus.client_remove_menu import ClientRemoveMenu -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class RemoveMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.title = "Remove Menu" - self.title_color = Color.RED - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.remove_klipper), - "2": Option(method=self.remove_moonraker), - "3": Option(method=self.remove_mainsail), - "4": Option(method=self.remove_fluidd), - "5": Option(method=self.remove_klipperscreen), - "6": Option(method=self.remove_crowsnest), - } - - def print_menu(self) -> None: - menu = textwrap.dedent( - """ - ╟───────────────────────────────────────────────────────╢ - ║ INFO: Configurations and/or any backups will be kept! ║ - ╟───────────────────────────┬───────────────────────────╢ - ║ Firmware & API: │ Touchscreen GUI: ║ - ║ 1) [Klipper] │ 5) [KlipperScreen] ║ - ║ 2) [Moonraker] │ ║ - ║ │ Webcam Streamer: ║ - ║ Klipper Webinterface: │ 6) [Crowsnest] ║ - ║ 3) [Mainsail] │ ║ - ║ 4) [Fluidd] │ ║ - ╟───────────────────────────┴───────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def remove_klipper(self, **kwargs) -> None: - KlipperRemoveMenu(previous_menu=self.__class__).run() - - def remove_moonraker(self, **kwargs) -> None: - MoonrakerRemoveMenu(previous_menu=self.__class__).run() - - def remove_mainsail(self, **kwargs) -> None: - ClientRemoveMenu(previous_menu=self.__class__, client=MainsailData()).run() - - def remove_fluidd(self, **kwargs) -> None: - ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run() - - def remove_klipperscreen(self, **kwargs) -> None: - remove_klipperscreen() - - def remove_crowsnest(self, **kwargs) -> None: - remove_crowsnest() diff --git a/kiauh/core/menus/repo_select_menu.py b/kiauh/core/menus/repo_select_menu.py deleted file mode 100644 index 25772f7..0000000 --- a/kiauh/core/menus/repo_select_menu.py +++ /dev/null @@ -1,162 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from typing import List, Literal, Type - -from core.logger import Logger, DialogType -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.settings.kiauh_settings import KiauhSettings, Repository -from core.types.color import Color -from procedures.switch_repo import run_switch_repo_routine -from utils.input_utils import get_string_input, get_number_input, get_confirm - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class RepoSelectMenu(BaseMenu): - def __init__( - self, - name: Literal["klipper", "moonraker"], - repos: List[Repository], - previous_menu: Type[BaseMenu] | None = None, - ) -> None: - super().__init__() - self.title_color = Color.CYAN - self.previous_menu = previous_menu - self.settings = KiauhSettings() - self.input_label_txt = "Select repository" - self.name = name - self.repos = repos - - if self.name == "klipper": - self.title = "Klipper Repository Selection Menu" - - elif self.name == "moonraker": - self.title = "Moonraker Repository Selection Menu" - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.settings_menu import SettingsMenu - - self.previous_menu = ( - previous_menu if previous_menu is not None else SettingsMenu - ) - - def set_options(self) -> None: - self.options = {} - if self.repos: - for idx, repo in enumerate(self.repos, start=1): - self.options[str(idx)] = Option( - method=self.select_repository, opt_data=repo - ) - self.options["a"] = Option(method=self.add_repository) - self.options["r"] = Option(method=self.remove_repository) - self.options["b"] = Option(method=self.go_back) - - def print_menu(self) -> None: - menu = "╟───────────────────────────────────────────────────────╢\n" - menu += "║ Available Repositories: ║\n" - menu += "╟───────────────────────────────────────────────────────╢\n" - for idx, repo in enumerate(self.repos, start=1): - url = f"● Repo: {repo.url.replace('.git', '')}" - branch = f"└► Branch: {repo.branch}" - menu += f"║ {idx}) {Color.apply(url, Color.CYAN):<59} ║\n" - menu += f"║ {Color.apply(branch, Color.CYAN):<59} ║\n" - menu += "╟───────────────────────────────────────────────────────╢\n" - menu += "║ A) Add repository ║\n" - menu += "║ R) Remove repository ║\n" - menu += "╟───────────────────────────────────────────────────────╢\n" - print(menu, end="") - - def select_repository(self, **kwargs) -> None: - repo: Repository = kwargs.get("opt_data") - Logger.print_status( - f"Switching to {self.name.capitalize()}'s new source repository ..." - ) - run_switch_repo_routine(self.name, repo.url, repo.branch) - - def add_repository(self, **kwargs) -> None: - while True: - Logger.print_dialog( - DialogType.CUSTOM, - custom_title="Enter the repository URL", - content=[ - "NOTE: There is no input validation in place, " - "please check your input for correctness", - ], - ) - url = get_string_input("Repository URL", allow_special_chars=True).strip() - - Logger.print_dialog( - DialogType.CUSTOM, - custom_title="Enter the branch name", - content=[ "Press Enter to use the default branch (master)." ], - center_content=False, - ) - branch = get_string_input("Branch", allow_special_chars=True, default="master").strip() - Logger.print_dialog( - DialogType.CUSTOM, - custom_title="Summary", - content=[ - f"● URL: {url}", - f"● Branch: {branch}", - ], - ) - confirm = get_confirm("Save repository") - if confirm: - repo = Repository(url, branch) - if self.name == "klipper": - self.settings.klipper.repositories.append(repo) - self.settings.save() - self.repos = self.settings.klipper.repositories - else: - self.settings.moonraker.repositories.append(repo) - self.settings.save() - self.repos = self.settings.moonraker.repositories - Logger.print_ok("Repository added and saved.") - - # Refresh menu to show new repo immediately and update options - self.set_options() - self.run() - break - else: - Logger.print_info("Operation cancelled by user.") - break - - def remove_repository(self, **kwargs) -> None: - repos = self.repos - if not repos: - Logger.print_info("No repositories configured.") - return - repo_lines = [f"{idx}) {repo.url} [{repo.branch}]" for idx, repo in enumerate(repos, start=1)] - Logger.print_dialog( - DialogType.CUSTOM, - custom_title="Available Repositories", - content=[*repo_lines], - ) - idx = get_number_input("Select the repository to remove", 1, len(repos)) - removed = repos.pop(idx - 1) - if self.name == "klipper": - self.settings.klipper.repositories = repos - self.settings.save() - self.repos = self.settings.klipper.repositories - else: - self.settings.moonraker.repositories = repos - self.settings.save() - self.repos = self.settings.moonraker.repositories - Logger.print_ok(f"Removed repository: {removed.url} [{removed.branch}]") - - # Refresh menu to show updated repo list and options - self.set_options() - self.run() - - def go_back(self, **kwargs) -> None: - from core.menus.settings_menu import SettingsMenu - SettingsMenu().run() diff --git a/kiauh/core/menus/settings_menu.py b/kiauh/core/menus/settings_menu.py deleted file mode 100644 index a486315..0000000 --- a/kiauh/core/menus/settings_menu.py +++ /dev/null @@ -1,140 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Type - -from components.klipper.klipper_utils import get_klipper_status -from components.moonraker.utils.utils import get_moonraker_status -from core.logger import DialogType, Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.menus.repo_select_menu import RepoSelectMenu -from core.settings.kiauh_settings import KiauhSettings -from core.types.color import Color -from core.types.component_status import ComponentStatus - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class SettingsMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.title = "Settings Menu" - self.title_color = Color.CYAN - self.previous_menu: Type[BaseMenu] | None = previous_menu - - self.mainsail_unstable: bool | None = None - self.fluidd_unstable: bool | None = None - self.auto_backups_enabled: bool | None = None - - na: str = "Not available!" - self.kl_repo_url: str = Color.apply(na, Color.RED) - self.kl_branch: str = Color.apply(na, Color.RED) - self.mr_repo_url: str = Color.apply(na, Color.RED) - self.mr_branch: str = Color.apply(na, Color.RED) - - self._load_settings() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "1": Option(method=self.switch_klipper_repo), - "2": Option(method=self.switch_moonraker_repo), - "3": Option(method=self.toggle_mainsail_release), - "4": Option(method=self.toggle_fluidd_release), - "5": Option(method=self.toggle_backup_before_update), - } - - def print_menu(self) -> None: - checked = f"[{Color.apply('x', Color.GREEN)}]" - unchecked = "[ ]" - - o1 = checked if self.mainsail_unstable else unchecked - o2 = checked if self.fluidd_unstable else unchecked - o3 = checked if self.auto_backups_enabled else unchecked - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ 1) Switch Klipper source repository ║ - ║ ● Current repository: ║ - ║ └► Repo: {self.kl_repo_url:50} ║ - ║ └► Branch: {self.kl_branch:48} ║ - ╟───────────────────────────────────────────────────────╢ - ║ 2) Switch Moonraker source repository ║ - ║ ● Current repository: ║ - ║ └► Repo: {self.mr_repo_url:50} ║ - ║ └► Branch: {self.mr_branch:48} ║ - ╟───────────────────────────────────────────────────────╢ - ║ Install unstable releases: ║ - ║ 3) {o1} Mainsail ║ - ║ 4) {o2} Fluidd ║ - ╟───────────────────────────────────────────────────────╢ - ║ Auto-Backup: ║ - ║ 5) {o3} Backup before update ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def _load_settings(self) -> None: - self.settings = KiauhSettings() - self.auto_backups_enabled = self.settings.kiauh.backup_before_update - self.mainsail_unstable = self.settings.mainsail.unstable_releases - self.fluidd_unstable = self.settings.fluidd.unstable_releases - - klipper_status: ComponentStatus = get_klipper_status() - moonraker_status: ComponentStatus = get_moonraker_status() - - def trim_repo_url(repo: str) -> str: - return repo.replace(".git", "").replace("https://", "").replace("git@", "") - - if not 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 == "-": - 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) - - def _warn_no_repos(self, name: str) -> None: - Logger.print_dialog( - DialogType.WARNING, - [f"No {name} repositories configured in kiauh.cfg!"], - center_content=True, - ) - - def switch_klipper_repo(self, **kwargs) -> None: - repos = self.settings.klipper.repositories - RepoSelectMenu("klipper", repos=repos, previous_menu=self.__class__).run() - - def switch_moonraker_repo(self, **kwargs) -> None: - repos = self.settings.moonraker.repositories - RepoSelectMenu("moonraker", repos=repos, previous_menu=self.__class__).run() - - def toggle_mainsail_release(self, **kwargs) -> None: - self.mainsail_unstable = not self.mainsail_unstable - self.settings.mainsail.unstable_releases = self.mainsail_unstable - self.settings.save() - - def toggle_fluidd_release(self, **kwargs) -> None: - self.fluidd_unstable = not self.fluidd_unstable - self.settings.fluidd.unstable_releases = self.fluidd_unstable - self.settings.save() - - def toggle_backup_before_update(self, **kwargs) -> None: - self.auto_backups_enabled = not self.auto_backups_enabled - self.settings.kiauh.backup_before_update = self.auto_backups_enabled - self.settings.save() diff --git a/kiauh/core/menus/update_menu.py b/kiauh/core/menus/update_menu.py deleted file mode 100644 index d1050f3..0000000 --- a/kiauh/core/menus/update_menu.py +++ /dev/null @@ -1,327 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import textwrap -from typing import Callable, List, Type - -from components.crowsnest.crowsnest import get_crowsnest_status, update_crowsnest -from components.klipper.klipper_utils import ( - get_klipper_status, -) -from components.klipper.services.klipper_setup_service import KlipperSetupService -from components.klipperscreen.klipperscreen import ( - get_klipperscreen_status, - update_klipperscreen, -) -from components.moonraker.services.moonraker_setup_service import MoonrakerSetupService -from components.moonraker.utils.utils import get_moonraker_status -from components.webui_client.client_config.client_config_setup import ( - update_client_config, -) -from components.webui_client.client_setup import update_client -from components.webui_client.client_utils import ( - get_client_config_status, - get_client_status, -) -from components.webui_client.fluidd_data import FluiddData -from components.webui_client.mainsail_data import MainsailData -from core.logger import DialogType, Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from core.types.component_status import ComponentStatus -from utils.input_utils import get_confirm -from utils.sys_utils import ( - get_upgradable_packages, - update_system_package_lists, - upgrade_system_packages, -) - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class UpdateMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None) -> None: - super().__init__() - self.loading_msg = "Loading update menu, please wait" - self.is_loading(True) - - self.title = "Update Menu" - self.title_color = Color.GREEN - self.previous_menu: Type[BaseMenu] | None = previous_menu - - self.packages: List[str] = [] - self.package_count: int = 0 - - self.klipper_local = self.klipper_remote = "" - self.moonraker_local = self.moonraker_remote = "" - self.mainsail_local = self.mainsail_remote = "" - self.mainsail_config_local = self.mainsail_config_remote = "" - self.fluidd_local = self.fluidd_remote = "" - self.fluidd_config_local = self.fluidd_config_remote = "" - self.klipperscreen_local = self.klipperscreen_remote = "" - self.crowsnest_local = self.crowsnest_remote = "" - - self.mainsail_data = MainsailData() - self.fluidd_data = FluiddData() - self.status_data = { - "klipper": { - "display_name": "Klipper", - "installed": False, - "local": None, - "remote": None, - }, - "moonraker": { - "display_name": "Moonraker", - "installed": False, - "local": None, - "remote": None, - }, - "mainsail": { - "display_name": "Mainsail", - "installed": False, - "local": None, - "remote": None, - }, - "mainsail_config": { - "display_name": "Mainsail-Config", - "installed": False, - "local": None, - "remote": None, - }, - "fluidd": { - "display_name": "Fluidd", - "installed": False, - "local": None, - "remote": None, - }, - "fluidd_config": { - "display_name": "Fluidd-Config", - "installed": False, - "local": None, - "remote": None, - }, - "klipperscreen": { - "display_name": "KlipperScreen", - "installed": False, - "local": None, - "remote": None, - }, - "crowsnest": { - "display_name": "Crowsnest", - "installed": False, - "local": None, - "remote": None, - }, - } - - self._fetch_update_status() - self.is_loading(False) - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - "a": Option(self.update_all), - "1": Option(self.update_klipper), - "2": Option(self.update_moonraker), - "3": Option(self.update_mainsail), - "4": Option(self.update_fluidd), - "5": Option(self.update_mainsail_config), - "6": Option(self.update_fluidd_config), - "7": Option(self.update_klipperscreen), - "8": Option(self.update_crowsnest), - "9": Option(self.upgrade_system_packages), - } - - def print_menu(self) -> None: - sysupgrades: str = "No upgrades available." - padding = 29 - if self.package_count > 0: - sysupgrades = Color.apply( - f"{self.package_count} upgrades available!", Color.GREEN - ) - padding = 38 - - menu = textwrap.dedent( - f""" - ╟───────────────────────┬───────────────┬───────────────╢ - ║ a) Update all │ │ ║ - ║ │ Current: │ Latest: ║ - ║ Klipper & API: ├───────────────┼───────────────╢ - ║ 1) Klipper │ {self.klipper_local:<22} │ {self.klipper_remote:<22} ║ - ║ 2) Moonraker │ {self.moonraker_local:<22} │ {self.moonraker_remote:<22} ║ - ║ │ │ ║ - ║ Webinterface: ├───────────────┼───────────────╢ - ║ 3) Mainsail │ {self.mainsail_local:<22} │ {self.mainsail_remote:<22} ║ - ║ 4) Fluidd │ {self.fluidd_local:<22} │ {self.fluidd_remote:<22} ║ - ║ │ │ ║ - ║ Client-Config: ├───────────────┼───────────────╢ - ║ 5) Mainsail-Config │ {self.mainsail_config_local:<22} │ {self.mainsail_config_remote:<22} ║ - ║ 6) Fluidd-Config │ {self.fluidd_config_local:<22} │ {self.fluidd_config_remote:<22} ║ - ║ │ │ ║ - ║ Other: ├───────────────┼───────────────╢ - ║ 7) KlipperScreen │ {self.klipperscreen_local:<22} │ {self.klipperscreen_remote:<22} ║ - ║ 8) Crowsnest │ {self.crowsnest_local:<22} │ {self.crowsnest_remote:<22} ║ - ║ ├───────────────┴───────────────╢ - ║ 9) System │ {sysupgrades:^{padding}} ║ - ╟───────────────────────┴───────────────────────────────╢ - """ - )[1:] - print(menu, end="") - - def update_all(self, **kwargs) -> None: - Logger.print_status("Updating all components ...") - self.update_klipper() - self.update_moonraker() - self.update_mainsail() - self.update_mainsail_config() - self.update_fluidd() - self.update_fluidd_config() - self.update_klipperscreen() - self.update_crowsnest() - self.upgrade_system_packages() - - def update_klipper(self, **kwargs) -> None: - klsvc = KlipperSetupService() - self._run_update_routine("klipper", klsvc.update) - - def update_moonraker(self, **kwargs) -> None: - mrsvc = MoonrakerSetupService() - self._run_update_routine("moonraker", mrsvc.update) - - def update_mainsail(self, **kwargs) -> None: - self._run_update_routine( - "mainsail", - update_client, - self.mainsail_data, - ) - - def update_mainsail_config(self, **kwargs) -> None: - self._run_update_routine( - "mainsail_config", - update_client_config, - self.mainsail_data, - ) - - def update_fluidd(self, **kwargs) -> None: - self._run_update_routine( - "fluidd", - update_client, - self.fluidd_data, - ) - - def update_fluidd_config(self, **kwargs) -> None: - self._run_update_routine( - "fluidd_config", - update_client_config, - self.fluidd_data, - ) - - def update_klipperscreen(self, **kwargs) -> None: - self._run_update_routine("klipperscreen", update_klipperscreen) - - def update_crowsnest(self, **kwargs) -> None: - self._run_update_routine("crowsnest", update_crowsnest) - - def upgrade_system_packages(self, **kwargs) -> None: - self._run_system_updates() - - def _fetch_update_status(self) -> None: - self._set_status_data("klipper", get_klipper_status) - self._set_status_data("moonraker", get_moonraker_status) - self._set_status_data("mainsail", get_client_status, self.mainsail_data, True) - self._set_status_data( - "mainsail_config", get_client_config_status, self.mainsail_data - ) - self._set_status_data("fluidd", get_client_status, self.fluidd_data, True) - self._set_status_data( - "fluidd_config", get_client_config_status, self.fluidd_data - ) - self._set_status_data("klipperscreen", get_klipperscreen_status) - self._set_status_data("crowsnest", get_crowsnest_status) - - 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: - 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) - - def _set_status_data(self, name: str, status_fn: Callable, *args) -> None: - comp_status: ComponentStatus = status_fn(*args) - - self.status_data[name]["installed"] = True if comp_status.status == 2 else False - self.status_data[name]["local"] = comp_status.local - self.status_data[name]["remote"] = comp_status.remote - - self._set_status_string(name) - - def _set_status_string(self, name: str) -> None: - local_status = self.status_data[name].get("local", None) - remote_status = self.status_data[name].get("remote", None) - - color = Color.GREEN if remote_status else Color.RED - local_txt = self._format_local_status(local_status, remote_status) - remote_txt = Color.apply(remote_status or "-", color) - - setattr(self, f"{name}_local", local_txt) - setattr(self, f"{name}_remote", remote_txt) - - def _check_is_installed(self, name: str) -> bool: - 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"] - - def _run_update_routine(self, name: str, update_fn: Callable, *args) -> None: - display_name = self.status_data[name]["display_name"] - is_installed = self._check_is_installed(name) - is_update_available = self._is_update_available(name) - - if not is_installed: - Logger.print_info(f"{display_name} is not installed! Skipped ...") - return - elif not is_update_available: - Logger.print_info(f"{display_name} is already up to date! Skipped ...") - return - - update_fn(*args) - - def _run_system_updates(self) -> None: - if not self.packages: - Logger.print_info("No system upgrades available!") - return - - 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?"): - return - Logger.print_status("Upgrading system packages ...") - upgrade_system_packages(self.packages) - except Exception as e: - Logger.print_error(f"Error upgrading system packages:\n{e}") - raise diff --git a/kiauh/core/services/__init__.py b/kiauh/core/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/services/backup_service.py b/kiauh/core/services/backup_service.py deleted file mode 100644 index c48e401..0000000 --- a/kiauh/core/services/backup_service.py +++ /dev/null @@ -1,189 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import shutil -from datetime import datetime -from pathlib import Path -from typing import List, Optional - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.logger import Logger -from utils.instance_utils import get_instances - - -class BackupService: - def __init__(self): - self._backup_root = Path.home().joinpath("kiauh_backups") - - @property - def backup_root(self) -> Path: - return self._backup_root - - @property - def timestamp(self) -> str: - return datetime.now().strftime("%Y%m%d-%H%M%S") - - ################################################ - # GENERIC BACKUP METHODS - ################################################ - - def backup_file( - self, - source_path: Path, - target_path: Optional[Path | str] = None, - target_name: Optional[str] = None, - ) -> bool: - source_path = Path(source_path) - - Logger.print_status(f"Creating backup of {source_path} ...") - - if not source_path.exists(): - Logger.print_info( - f"File '{source_path}' does not exist! Skipping backup..." - ) - return False - - if not source_path.is_file(): - Logger.print_info(f"'{source_path}' is not a file! Skipping backup...") - return False - - try: - self._backup_root.mkdir(parents=True, exist_ok=True) - - filename = ( - 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) - - Logger.print_ok( - f"Successfully backed up '{source_path}' to '{backup_path}'" - ) - return True - - except Exception as e: - Logger.print_error(f"Failed to backup '{source_path}': {e}") - return False - - def backup_directory( - self, - source_path: Path, - backup_name: str, - target_path: Optional[Path | str] = None, - ) -> Optional[Path]: - source_path = Path(source_path) - - Logger.print_status(f"Creating backup of {source_path} ...") - - if not source_path.exists(): - Logger.print_info( - f"Directory '{source_path}' does not exist! Skipping backup..." - ) - return None - - if not source_path.is_dir(): - Logger.print_info(f"'{source_path}' is not a directory! Skipping backup...") - return None - - try: - self._backup_root.mkdir(parents=True, exist_ok=True) - - backup_dir_name = f"{backup_name}_{self.timestamp}" - - if target_path is not None: - backup_path = self._backup_root.joinpath(target_path, backup_dir_name) - else: - backup_path = self._backup_root.joinpath(backup_dir_name) - - shutil.copytree(source_path, backup_path) - - Logger.print_ok( - f"Successfully backed up '{source_path}' to '{backup_path}'" - ) - return backup_path - - except Exception as e: - Logger.print_error(f"Failed to backup directory '{source_path}': {e}") - return None - - ################################################ - # SPECIFIC BACKUP METHODS - ################################################ - - def backup_printer_cfg(self): - 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}" - ) - self.backup_file( - source_path=instance.cfg_file, - target_path=target_path, - target_name=instance.cfg_file.name, - ) - - def backup_moonraker_conf(self): - 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}" - ) - 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: - instances: List[Klipper] = get_instances(Klipper) - if not instances: - # fallback: search for printer data directories in the user's home directory - Logger.print_info("No Klipper instances found via systemd services.") - Logger.print_info( - "Attempting to find printer data directories in home directory..." - ) - - home_dir = Path.home() - printer_data_dirs = [] - - for pattern in ["printer_data", "printer_*_data"]: - for data_dir in home_dir.glob(pattern): - if data_dir.is_dir(): - printer_data_dirs.append(data_dir) - - if not printer_data_dirs: - Logger.print_info("Unable to find directory to backup!") - Logger.print_info( - "No printer data directories found in home directory." - ) - return - - for data_dir in printer_data_dirs: - self.backup_directory( - source_path=data_dir.joinpath("config"), - target_path=data_dir.name, - backup_name="config", - ) - - return - - for instance in instances: - self.backup_directory( - source_path=instance.base.cfg_dir, - target_path=f"{instance.data_dir.name}", - backup_name="config", - ) diff --git a/kiauh/core/services/message_service.py b/kiauh/core/services/message_service.py deleted file mode 100644 index bf7edda..0000000 --- a/kiauh/core/services/message_service.py +++ /dev/null @@ -1,61 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import List - -from core.logger import DialogType, Logger -from core.types.color import Color - - -@dataclass() -class Message: - title: str = field(default="") - text: List[str] = field(default_factory=list) - color: Color = field(default=Color.WHITE) - centered: bool = field(default=False) - - -class MessageService: - __cls_instance = None - __message: Message | None - - def __new__(cls) -> "MessageService": - if cls.__cls_instance is None: - cls.__cls_instance = super(MessageService, cls).__new__(cls) - return cls.__cls_instance - - def __init__(self) -> None: - if not hasattr(self, "__initialized"): - self.__initialized = False - if self.__initialized: - return - self.__initialized = True - self.__message = None - - def set_message(self, message: Message) -> None: - self.__message = message - - def display_message(self) -> None: - if self.__message is None: - return - - Logger.print_dialog( - title=DialogType.CUSTOM, - content=self.__message.text, - custom_title=self.__message.title, - custom_color=self.__message.color, - center_content=self.__message.centered, - ) - - self.__clear_message() - - def __clear_message(self) -> None: - self.__message = None diff --git a/kiauh/core/settings/__init__.py b/kiauh/core/settings/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/settings/kiauh_settings.py b/kiauh/core/settings/kiauh_settings.py deleted file mode 100644 index e002e56..0000000 --- a/kiauh/core/settings/kiauh_settings.py +++ /dev/null @@ -1,414 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import shutil -from dataclasses import dataclass, field -from typing import Any, Callable, List, TypeVar - -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 ( - SimpleConfigParser, -) -from utils.input_utils import get_confirm -from utils.sys_utils import kill - -from kiauh import PROJECT_ROOT - -DEFAULT_CFG = PROJECT_ROOT.joinpath("default.kiauh.cfg") -CUSTOM_CFG = PROJECT_ROOT.joinpath("kiauh.cfg") - -T = TypeVar("T") - - -class InvalidValueError(Exception): - """Raised when a value is invalid for an option""" - - def __init__(self, section: str, option: str, value: str): - msg = f"Invalid value '{value}' for option '{option}' in section '{section}'" - super().__init__(msg) - - -@dataclass -class AppSettings: - backup_before_update: bool | None = field(default=None) - - -@dataclass -class Repository: - url: str - branch: str - - -@dataclass -class KlipperSettings: - repositories: List[Repository] | None = field(default=None) - use_python_binary: str | None = field(default=None) - - -@dataclass -class MoonrakerSettings: - optional_speedups: bool | None = field(default=None) - repositories: List[Repository] | None = field(default=None) - use_python_binary: str | None = field(default=None) - - -@dataclass -class WebUiSettings: - port: int | None = field(default=None) - unstable_releases: bool | None = field(default=None) - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class KiauhSettings: - __instance = None - __initialized = False - - def __new__(cls, *args, **kwargs) -> "KiauhSettings": - if cls.__instance is None: - cls.__instance = super(KiauhSettings, cls).__new__(cls, *args, **kwargs) - return cls.__instance - - def __repr__(self) -> str: - return ( - f"KiauhSettings(kiauh={self.kiauh}, klipper={self.klipper}," - f" moonraker={self.moonraker}, mainsail={self.mainsail}," - f" fluidd={self.fluidd})" - ) - - def __getitem__(self, item: str) -> Any: - return getattr(self, item) - - def __init__(self) -> None: - if self.__initialized: - return - self.__initialized = True - - self.config = SimpleConfigParser() - self.kiauh = AppSettings() - self.klipper = KlipperSettings() - self.moonraker = MoonrakerSettings() - self.mainsail = WebUiSettings() - self.fluidd = WebUiSettings() - - self.__read_config_set_internal_state() - - # todo: refactor this, at least rename to something else! - def get(self, section: str, option: str) -> str | int | bool: - """ - Get a value from the settings state by providing the section and option name as - strings. Prefer direct access to the properties, as it is usually safer! - :param section: The section name as string. - :param option: The option name as string. - :return: The value of the option as string, int or bool. - """ - - try: - section = getattr(self, section) - value = getattr(section, option) - return value # type: ignore - except AttributeError: - raise - - def save(self) -> None: - self.__write_internal_state_to_cfg() - self.__read_config_set_internal_state() - - def __read_config_set_internal_state(self) -> None: - if not CUSTOM_CFG.exists() and not DEFAULT_CFG.exists(): - Logger.print_dialog( - DialogType.ERROR, - [ - "No KIAUH configuration file found! Please make sure you have at least " - "one of the following configuration files in KIAUH's root directory:", - "● default.kiauh.cfg", - "● kiauh.cfg", - ], - ) - kill() - - # copy default config to custom config if it does not exist - if not CUSTOM_CFG.exists(): - shutil.copyfile(DEFAULT_CFG, CUSTOM_CFG) - - self.config.read_file(CUSTOM_CFG) - - # check if there are deprecated repo_url and branch options in the kiauh.cfg - if self._check_deprecated_repo_config(): - self._prompt_migration_dialog() - - self.__set_internal_state() - - def __set_internal_state(self) -> None: - # parse Kiauh options - self.kiauh.backup_before_update = self.__read_from_cfg( - "kiauh", - "backup_before_update", - self.config.getboolean, - False, - ) - - # parse Klipper options - self.klipper.use_python_binary = self.__read_from_cfg( - "klipper", - "use_python_binary", - self.config.getval, - None, - True, - ) - kl_repos: List[str] = self.__read_from_cfg( - "klipper", - "repositories", - self.config.getvals, - [KLIPPER_REPO_URL], - ) - self.klipper.repositories = self.__set_repo_state("klipper", kl_repos) - - # parse Moonraker options - self.moonraker.use_python_binary = self.__read_from_cfg( - "moonraker", - "use_python_binary", - self.config.getval, - None, - True, - ) - self.moonraker.optional_speedups = self.__read_from_cfg( - "moonraker", - "optional_speedups", - self.config.getboolean, - True, - ) - mr_repos: List[str] = self.__read_from_cfg( - "moonraker", - "repositories", - self.config.getvals, - [MOONRAKER_REPO_URL], - ) - self.moonraker.repositories = self.__set_repo_state("moonraker", mr_repos) - - # parse Mainsail options - self.mainsail.port = self.__read_from_cfg( - "mainsail", - "port", - self.config.getint, - 80, - ) - self.mainsail.unstable_releases = self.__read_from_cfg( - "mainsail", - "unstable_releases", - self.config.getboolean, - False, - ) - - # parse Fluidd options - self.fluidd.port = self.__read_from_cfg( - "fluidd", - "port", - self.config.getint, - 80, - ) - self.fluidd.unstable_releases = self.__read_from_cfg( - "fluidd", - "unstable_releases", - self.config.getboolean, - False, - ) - - def __check_option_exists( - self, section: str, option: str, fallback: Any, silent: bool = False - ) -> bool: - has_section = self.config.has_section(section) - has_option = self.config.has_option(section, option) - - if not (has_section and has_option): - if not silent: - Logger.print_warn( - f"Option '{option}' in section '{section}' not defined. Falling back to '{fallback}'." - ) - return False - return True - - def __read_bool_from_cfg( - self, - section: str, - option: str, - fallback: bool | None = None, - silent: bool = False, - ) -> bool | None: - if not self.__check_option_exists(section, option, fallback, silent): - return fallback - return self.config.getboolean(section, option, fallback) - - def __read_from_cfg( - self, - section: str, - option: str, - getter: Callable[[str, str, T | None], T], - fallback: T = None, - silent: bool = False, - ) -> T: - if not self.__check_option_exists(section, option, fallback, silent): - return fallback - return getter(section, option, fallback) - - def __set_repo_state(self, section: str, repos: List[str]) -> List[Repository]: - _repos: List[Repository] = [] - for repo in repos: - try: - if repo.strip().startswith("#") or repo.strip().startswith(";"): - continue - if "," in repo: - url, branch = repo.strip().split(",") - - if not branch: - branch = "master" - else: - url = repo.strip() - branch = "master" - - # url must not be empty otherwise it's considered - # as an unrecoverable, invalid configuration - if not url: - raise InvalidValueError(section, "repositories", repo) - - _repos.append(Repository(url.strip(), branch.strip())) - - except InvalidValueError as e: - Logger.print_error(f"Error parsing kiauh.cfg: {e}") - kill() - - return _repos - - def __write_internal_state_to_cfg(self) -> None: - """Updates the config with current settings, preserving values that haven't been modified""" - if self.kiauh.backup_before_update is not None: - self.config.set_option( - "kiauh", - "backup_before_update", - str(self.kiauh.backup_before_update), - ) - - # Handle repositories - if self.klipper.repositories is not None: - repos = [f"{repo.url}, {repo.branch}" for repo in self.klipper.repositories] - self.config.set_option("klipper", "repositories", repos) - - if self.moonraker.repositories is not None: - repos = [ - f"{repo.url}, {repo.branch}" for repo in self.moonraker.repositories - ] - self.config.set_option("moonraker", "repositories", repos) - - # Handle Mainsail settings - if self.mainsail.port is not None: - self.config.set_option("mainsail", "port", str(self.mainsail.port)) - if self.mainsail.unstable_releases is not None: - self.config.set_option( - "mainsail", - "unstable_releases", - str(self.mainsail.unstable_releases), - ) - - # Handle Fluidd settings - if self.fluidd.port is not None: - self.config.set_option("fluidd", "port", str(self.fluidd.port)) - if self.fluidd.unstable_releases is not None: - self.config.set_option( - "fluidd", "unstable_releases", str(self.fluidd.unstable_releases) - ) - - self.config.write_file(CUSTOM_CFG) - - def _check_deprecated_repo_config(self) -> bool: - # repo_url and branch are deprecated - 2025.03.23 - for section in ["klipper", "moonraker"]: - if self.config.has_option(section, "repo_url") or self.config.has_option( - section, "branch" - ): - return True - return False - - def _prompt_migration_dialog(self) -> None: - migration_1: List[str] = [ - "Options 'repo_url' and 'branch' are now combined into a 'repositories' option.", - "\n\n", - "● Old format:", - " [klipper]", - " repo_url: https://github.com/Klipper3d/klipper", - " branch: master", - "\n\n", - "● New format:", - " [klipper]", - " repositories:", - " https://github.com/Klipper3d/klipper, master", - ] - Logger.print_dialog( - DialogType.ATTENTION, - [ - "Deprecated kiauh.cfg configuration found!", - "KAIUH can now attempt to automatically migrate the configuration.", - "\n\n", - *migration_1, - ], - ) - if get_confirm("Migrate to the new format?"): - self._migrate_repo_config() - else: - Logger.print_dialog( - DialogType.ERROR, - [ - "Please update the configuration file manually.", - ], - center_content=True, - ) - kill() - - def _migrate_repo_config(self) -> None: - svc = BackupService() - if not svc.backup_file(CUSTOM_CFG): - Logger.print_dialog( - DialogType.ERROR, - [ - "Failed to create backup of kiauh.cfg. Aborting migration. Please migrate manually." - ], - ) - kill() - - # run migrations - try: - # migrate deprecated repo_url and branch options - 2025.03.23 - for section in ["klipper", "moonraker"]: - if not self.config.has_section(section): - continue - - repo_url = self.config.getval(section, "repo_url", fallback="") - branch = self.config.getval(section, "branch", fallback="master") - - if repo_url: - # create repositories option with the old values - repositories = [f"{repo_url}, {branch}\n"] - self.config.set_option(section, "repositories", repositories) - - # remove deprecated options - self.config.remove_option(section, "repo_url") - self.config.remove_option(section, "branch") - - Logger.print_ok(f"Successfully migrated {section} configuration") - - self.config.write_file(CUSTOM_CFG) - self.config.read_file(CUSTOM_CFG) # reload config - - except Exception as e: - Logger.print_error(f"Error migrating configuration: {e}") - Logger.print_error("Please migrate manually.") - kill() diff --git a/kiauh/core/spinner.py b/kiauh/core/spinner.py deleted file mode 100644 index db603ac..0000000 --- a/kiauh/core/spinner.py +++ /dev/null @@ -1,42 +0,0 @@ -import sys -import threading -import time -from typing import List, Literal - -from core.types.color import Color - -SpinnerColor = Literal["white", "red", "green", "yellow"] - - -class Spinner: - def __init__( - self, - message: str = "Loading", - interval: float = 0.2, - ) -> None: - self.message = f"{message} ..." - self.interval = interval - self._stop_event = threading.Event() - self._thread = threading.Thread(target=self._animate) - - def _animate(self) -> None: - animation: List[str] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] - while not self._stop_event.is_set(): - for char in animation: - sys.stdout.write(f"\r{Color.GREEN}{char}{Color.RST} {self.message}") - sys.stdout.flush() - time.sleep(self.interval) - if self._stop_event.is_set(): - break - sys.stdout.write("\r" + " " * (len(self.message) + 1) + "\r") - sys.stdout.flush() - - def start(self) -> None: - self._stop_event.clear() - if not self._thread.is_alive(): - self._thread = threading.Thread(target=self._animate) - self._thread.start() - - def stop(self) -> None: - self._stop_event.set() - self._thread.join() diff --git a/kiauh/core/submodules/__init__.py b/kiauh/core/submodules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/.editorconfig b/kiauh/core/submodules/simple_config_parser/.editorconfig deleted file mode 100644 index 2546a60..0000000 --- a/kiauh/core/submodules/simple_config_parser/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# see https://editorconfig.org/ -root = true - -[*] -end_of_line = lf -trim_trailing_whitespace = true -indent_style = space -insert_final_newline = true -indent_size = 4 -charset = utf-8 - -[*.py] -max_line_length = 88 diff --git a/kiauh/core/submodules/simple_config_parser/.gitignore b/kiauh/core/submodules/simple_config_parser/.gitignore deleted file mode 100644 index a5d5089..0000000 --- a/kiauh/core/submodules/simple_config_parser/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -*.py[cod] -*.pyc -__pycache__ -.pytest_cache/ - -.idea/ -.vscode/ - -.venv*/ -venv*/ - -.coverage -htmlcov/ diff --git a/kiauh/core/submodules/simple_config_parser/LICENSE b/kiauh/core/submodules/simple_config_parser/LICENSE deleted file mode 100644 index f288702..0000000 --- a/kiauh/core/submodules/simple_config_parser/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/kiauh/core/submodules/simple_config_parser/README.md b/kiauh/core/submodules/simple_config_parser/README.md deleted file mode 100644 index b84b6c7..0000000 --- a/kiauh/core/submodules/simple_config_parser/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# 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 -- Comment: A line starting with a `#` or `;` -- Blank: A line containing only whitespace characters -- SaveConfig: Klippers auto-generated SAVE_CONFIG section that can be found at the very end of the config file - ---- - -### Internally, the config is stored as a dictionary of sections, each containing a header and a list of elements: -```python -config = { - "section_name": { - "header": "[section_name]\n", - "elements": [ - { - "type": "comment", - "content": "# This is a comment\n" - }, - { - "type": "option", - "name": "option1", - "value": "value1", - "raw": "option1: value1\n" - }, - { - "type": "blank", - "content": "\n" - }, - { - "type": "option_block", - "name": "option2", - "value": [ - "value2", - "value3" - ], - "raw": "option2:" - } - ] - } - } -``` diff --git a/kiauh/core/submodules/simple_config_parser/pyproject.toml b/kiauh/core/submodules/simple_config_parser/pyproject.toml deleted file mode 100644 index 8b7b9ed..0000000 --- a/kiauh/core/submodules/simple_config_parser/pyproject.toml +++ /dev/null @@ -1,66 +0,0 @@ -[project] -name = "simple-config-parser" -version = "0.0.1" -description = "A simple config parser for Python" -authors = [ - {name = "Dominik Willner", email = "th33xitus@gmail.com"}, -] -readme = "README.md" -license = {text = "GPL-3.0-only"} -requires-python = ">=3.8" - -[project.urls] -homepage = "https://github.com/dw-0/simple-config-parser" -repository = "https://github.com/dw-0/simple-config-parser" -documentation = "https://github.com/dw-0/simple-config-parser" - -[project.optional-dependencies] -dev=["ruff"] - -[tool.ruff] -required-version = ">=0.3.4" -respect-gitignore = true -exclude = [".git",".github", "./docs"] -line-length = 88 -indent-width = 4 -output-format = "full" - -[tool.ruff.format] -indent-style = "space" -line-ending = "lf" -quote-style = "double" - -[tool.ruff.lint] -extend-select = ["I"] - -[tool.pytest.ini_options] -minversion = "8.2.1" -testpaths = ["tests/**/*.py"] -addopts = "-svvv --cov --cov-config=pyproject.toml --cov-report=html" - -[tool.coverage.run] -branch = true -source = ["src.simple_config_parser"] - -[tool.coverage.report] -# Regexes for lines to exclude from consideration -exclude_also = [ - # Don't complain about missing debug-only code: - "def __repr__", - "if self\\.debug", - - # Don't complain if tests don't hit defensive assertion code: - "raise AssertionError", - "raise NotImplementedError", - - # Don't complain if non-runnable code isn't run: - "if 0:", - "if __name__ == .__main__.:", - - # Don't complain about abstract methods, they aren't run: - "@(abc\\.)?abstractmethod", - ] - -[tool.coverage.html] -title = "SimpleConfigParser Coverage Report" -directory = "htmlcov" diff --git a/kiauh/core/submodules/simple_config_parser/requirements-dev.txt b/kiauh/core/submodules/simple_config_parser/requirements-dev.txt deleted file mode 100644 index 7e73e5f..0000000 --- a/kiauh/core/submodules/simple_config_parser/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -ruff >= 0.3.4 -pytest >= 8.2.1 -pytest-cov >= 5.0.0 diff --git a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/__init__.py b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py deleted file mode 100644 index db9ecb3..0000000 --- a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/constants.py +++ /dev/null @@ -1,74 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# https://github.com/dw-0/simple-config-parser # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import re -from enum import Enum - -# definition of section line: -# - then 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 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]+)\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, -} - -HEADER_IDENT = "#_header" - -INDENT = " " * 4 - -class LineType(Enum): - OPTION = "option" - OPTION_BLOCK = "option_block" - COMMENT = "comment" - BLANK = "blank" diff --git a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py b/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py deleted file mode 100644 index 4084779..0000000 --- a/kiauh/core/submodules/simple_config_parser/src/simple_config_parser/simple_config_parser.py +++ /dev/null @@ -1,426 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 - -from pathlib import Path -from typing import Callable, Dict, List - -from ..simple_config_parser.constants import ( - BOOLEAN_STATES, - EMPTY_LINE_RE, - HEADER_IDENT, - LINE_COMMENT_RE, - OPTION_RE, - OPTIONS_BLOCK_START_RE, - SECTION_RE, LineType, INDENT, SAVE_CONFIG_START_RE, SAVE_CONFIG_CONTENT_RE, -) - -_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) - - -# noinspection PyMethodMayBeStatic -class SimpleConfigParser: - """A customized config parser targeted at handling Klipper style config files""" - - def __init__(self) -> None: - self.header: List[str] = [] - self.save_config_block: List[str] = [] - self.config: Dict = {} - self.current_section: str | None = None - self.current_opt_block: str | None = None - self.in_option_block: bool = False - - 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_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._match_section(line): - self.current_opt_block = None - self.current_section = SECTION_RE.match(line).group(1) - self.config[self.current_section] = { - "header": line, - "elements": [] - } - - elif self._match_option(line): - self.current_opt_block = None - option = OPTION_RE.match(line).group(1) - value = OPTION_RE.match(line).group(2) - self.config[self.current_section]["elements"].append({ - "type": LineType.OPTION.value, - "name": option, - "value": value, - "raw": line - }) - - elif self._match_options_block_start(line): - option = OPTIONS_BLOCK_START_RE.match(line).group(1) - self.current_opt_block = option - self.config[self.current_section]["elements"].append({ - "type": LineType.OPTION_BLOCK.value, - "name": option, - "value": [], - "raw": line - }) - - elif self.current_opt_block is not None: - # we are in an option block, so we add the line to the option's value - for element in reversed(self.config[self.current_section]["elements"]): - if element["type"] == LineType.OPTION_BLOCK.value and element["name"] == self.current_opt_block: - element["value"].append(line.strip()) # indentation is removed - break - - elif self._match_save_config_start(line): - self.current_opt_block = None - self.save_config_block.append(line) - - elif self._match_save_config_content(line): - self.current_opt_block = None - self.save_config_block.append(line) - - elif self._match_empty_line(line) or self._match_line_comment(line): - self.current_opt_block = None - - # if current_section is None, we are at the beginning of the file, - # so we consider the part up to the first section as the file header - if not self.current_section: - self.config.setdefault(HEADER_IDENT, []).append(line) - else: - element_type = LineType.BLANK.value if self._match_empty_line(line) else LineType.COMMENT.value - self.config[self.current_section]["elements"].append({ - "type": element_type, - "content": line - }) - - def read_file(self, file: Path) -> None: - """Read and parse a config file""" - with open(file, "r") 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") - - with open(path, "w", encoding="utf-8") as f: - if HEADER_IDENT in self.config: - for line in self.config[HEADER_IDENT]: - f.write(line) - - sections = self.get_sections() - for i, section in enumerate(sections): - f.write(self.config[section]["header"]) - - for element in self.config[section]["elements"]: - if element["type"] == LineType.OPTION.value: - f.write(element["raw"]) - elif element["type"] == LineType.OPTION_BLOCK.value: - f.write(element["raw"]) - for line in element["value"]: - f.write(INDENT + line.strip() + "\n") - elif element["type"] in [LineType.COMMENT.value, LineType.BLANK.value]: - f.write(element["content"]) - else: - raise UnknownLineError(element["raw"]) - - # Ensure file ends with a single newline - if sections: # Only if we have any sections - last_section = sections[-1] - last_elements = self.config[last_section]["elements"] - - if last_elements: - last_element = last_elements[-1] - if "raw" in last_element: - last_line = last_element["raw"] - else: # comment or blank line - last_line = last_element["content"] - - if not last_line.endswith("\n"): - f.write("\n") - - if self.save_config_block: - for line in self.save_config_block: - f.write(line) - f.write("\n") - - def get_sections(self) -> List[str]: - """Return a list of all section names, but exclude any section starting with '#_'""" - return list( - filter( - lambda section: not section.startswith("#_"), - self.config.keys(), - ) - ) - - def has_section(self, section: str) -> bool: - """Check if a section exists""" - return section in self.get_sections() - - def add_section(self, section: str) -> None: - """Add a new section to the config""" - if section in self.get_sections(): - raise DuplicateSectionError(section) - - if len(self.get_sections()) >= 1: - self._check_set_section_spacing() - - self.config[section] = { - "header": f"[{section}]\n", - "elements": [] - } - - def _check_set_section_spacing(self): - """Check if there is a blank line between the last section and the new section""" - prev_section_name: str = self.get_sections()[-1] - prev_section = self.config[prev_section_name] - prev_elements = prev_section["elements"] - - if prev_elements: - last_element = prev_elements[-1] - - # If the last element is a comment or blank line - if last_element["type"] in [LineType.COMMENT.value, LineType.BLANK.value]: - last_content = last_element["content"] - - # If the last element doesn't end with a newline, add one - if not last_content.endswith("\n"): - last_element["content"] += "\n" - - # If the last element is not a blank line, add a blank line - if last_content.strip() != "": - prev_elements.append({ - "type": "blank", - "content": "\n" - }) - else: - # If the last element is an option, add a blank line - prev_elements.append({ - "type": LineType.BLANK.value, - "content": "\n" - }) - - def remove_section(self, section: str) -> None: - """Remove a section from the config""" - self.config.pop(section, None) - - def get_options(self, section: str) -> List[str]: - """Return a list of all option names for a given section""" - options = [] - if self.has_section(section): - for element in self.config[section]["elements"]: - if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value]: - options.append(element["name"]) - return options - - 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. - """ - if not self.has_section(section): - self.add_section(section) - - # Check if option already exists - for element in self.config[section]["elements"]: - if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value] and element["name"] == option: - # Update existing option - if isinstance(value, list): - element["type"] = LineType.OPTION_BLOCK.value - element["value"] = value - element["raw"] = f"{option}:\n" - else: - element["type"] = LineType.OPTION.value - element["value"] = value - element["raw"] = f"{option}: {value}\n" - return - - # Option doesn't exist, create new one - if isinstance(value, list): - new_element = { - "type": LineType.OPTION_BLOCK.value, - "name": option, - "value": value, - "raw": f"{option}:\n" - } - else: - new_element = { - "type": LineType.OPTION.value, - "name": option, - "value": value, - "raw": f"{option}: {value}\n" - } - - # scan through elements to find the last option, after which we insert the new option - insert_pos = 0 - elements = self.config[section]["elements"] - for i, element in enumerate(elements): - if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value]: - insert_pos = i + 1 - - elements.insert(insert_pos, new_element) - - def remove_option(self, section: str, option: str) -> None: - """Remove an option from a section""" - if self.has_section(section): - elements = self.config[section]["elements"] - for i, element in enumerate(elements): - if element["type"] in [LineType.OPTION.value, LineType.OPTION_BLOCK.value] and element["name"] == option: - elements.pop(i) - break - - 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: - if section not in self.get_sections(): - raise NoSectionError(section) - if option not in self.get_options(section): - raise NoOptionError(option, section) - - for element in self.config[section]["elements"]: - if element["type"] is LineType.OPTION.value and element["name"] == option: - return str(element["value"].strip().replace("\n", "")) - return "" - - except (NoSectionError, NoOptionError): - if fallback is _UNSET: - raise - return fallback - - def getvals(self, section: str, option: str, fallback: List[str] | _UNSET = _UNSET) -> List[str]: - """ - Return the values of the given multi-line option in the given section - - If the key is not found and 'fallback' is provided, it is used as - a fallback value. - """ - try: - if section not in self.get_sections(): - raise NoSectionError(section) - if option not in self.get_options(section): - raise NoOptionError(option, section) - - for element in self.config[section]["elements"]: - if element["type"] is LineType.OPTION_BLOCK.value and element["name"] == option: - return [val.strip() for val in element["value"] if val.strip()] - return [] - - 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: _UNSET = _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 diff --git a/kiauh/core/submodules/simple_config_parser/tests/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/klipper_config.txt b/kiauh/core/submodules/simple_config_parser/tests/assets/klipper_config.txt deleted file mode 100644 index 383c625..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/klipper_config.txt +++ /dev/null @@ -1,1337 +0,0 @@ -[mcu] -serial: -baud: 250000 -canbus_uuid: -canbus_interface: -restart_method: -[printer] -kinematics: -max_velocity: -max_accel: -minimum_cruise_ratio: 0.5 -square_corner_velocity: 5.0 -max_accel_to_decel: -[stepper_x] -step_pin: -dir_pin: -enable_pin: -rotation_distance: -microsteps: -full_steps_per_rotation: 200 -gear_ratio: -step_pulse_duration: -endstop_pin: -position_min: 0 -position_endstop: -position_max: -homing_speed: 5.0 -homing_retract_dist: 5.0 -homing_retract_speed: -second_homing_speed: -homing_positive_dir: -[printer] -kinematics: cartesian -max_z_velocity: -max_z_accel: -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -kinematics: delta -max_z_velocity: -max_z_accel: -minimum_z_position: 0 -delta_radius: -print_radius: -[stepper_a] -position_endstop: -arm_length: -angle: -[stepper_b] -[stepper_c] -[delta_calibrate] -radius: -speed: 50 -horizontal_move_z: 5 -[printer] -kinematics: deltesian -max_z_velocity: -max_z_accel: -minimum_z_position: 0 -min_angle: 5 -print_width: -slow_ratio: 3 -[stepper_left] -position_endstop: -arm_length: -arm_x_length: -[stepper_right] -[stepper_y] -[printer] -kinematics: corexy -max_z_velocity: -max_z_accel: -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -kinematics: corexz -max_z_velocity: -max_z_accel: -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -kinematics: hybrid_corexy -max_z_velocity: -max_z_accel: -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -kinematics: hybrid_corexz -max_z_velocity: -max_z_accel: -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -kinematics: polar -max_z_velocity: -max_z_accel: -[stepper_bed] -gear_ratio: -[stepper_arm] -[stepper_z] -[printer] -kinematics: rotary_delta -max_z_velocity: -minimum_z_position: 0 -shoulder_radius: -shoulder_height: -[stepper_a] -gear_ratio: -position_endstop: -upper_arm_length: -lower_arm_length: -angle: -[stepper_b] -[stepper_c] -[delta_calibrate] -radius: -speed: 50 -horizontal_move_z: 5 -[printer] -kinematics: winch -[stepper_a] -rotation_distance: -anchor_x: -anchor_y: -anchor_z: -[printer] -kinematics: none -max_velocity: 1 -max_accel: 1 -[extruder] -step_pin: -dir_pin: -enable_pin: -microsteps: -rotation_distance: -full_steps_per_rotation: -gear_ratio: -nozzle_diameter: -filament_diameter: -max_extrude_cross_section: -instantaneous_corner_velocity: 1.000 -max_extrude_only_distance: 50.0 -max_extrude_only_velocity: -max_extrude_only_accel: -pressure_advance: 0.0 -pressure_advance_smooth_time: 0.040 -heater_pin: -max_power: 1.0 -sensor_type: -sensor_pin: -pullup_resistor: 4700 -smooth_time: 1.0 -control: -pid_Kp: -pid_Ki: -pid_Kd: -max_delta: 2.0 -pwm_cycle_time: 0.100 -min_extrude_temp: 170 -min_temp: -max_temp: -[heater_bed] -heater_pin: -sensor_type: -sensor_pin: -control: -min_temp: -max_temp: -[bed_mesh] -speed: 50 -horizontal_move_z: 5 -mesh_radius: -mesh_origin: -mesh_min: -mesh_max: -probe_count: 3, 3 -round_probe_count: 5 -fade_start: 1.0 -fade_end: 0.0 -fade_target: -split_delta_z: .025 -move_check_distance: 5.0 -mesh_pps: 2, 2 -algorithm: lagrange -bicubic_tension: .2 -zero_reference_position: -faulty_region_1_min: -faulty_region_1_max: -adaptive_margin: -scan_overshoot: -[bed_tilt] -x_adjust: 0 -y_adjust: 0 -z_adjust: 0 -points: -speed: 50 -horizontal_move_z: 5 -[bed_screws] -screw1: -screw1_name: -screw1_fine_adjust: -screw2: -screw2_name: -screw2_fine_adjust: -horizontal_move_z: 5 -probe_height: 0 -speed: 50 -probe_speed: 5 -[screws_tilt_adjust] -screw1: -screw1_name: -screw2: -screw2_name: -speed: 50 -horizontal_move_z: 5 -screw_thread: CW-M3 -[z_tilt] -z_positions: -points: -speed: 50 -horizontal_move_z: 5 -retries: 0 -retry_tolerance: 0 -[quad_gantry_level] -gantry_corners: -points: -speed: 50 -horizontal_move_z: 5 -max_adjust: 4 -retries: 0 -retry_tolerance: 0 -[skew_correction] -[z_thermal_adjust] -temp_coeff: -smooth_time: -z_adjust_off_above: -max_z_adjustment: -sensor_type: -sensor_pin: -min_temp: -max_temp: -gcode_id: -[safe_z_home] -home_xy_position: -speed: 50.0 -z_hop: -z_hop_speed: 15.0 -move_to_previous: False -[homing_override] -gcode: -axes: xyz -set_position_x: -set_position_y: -set_position_z: -[endstop_phase stepper_z] -endstop_accuracy: -trigger_phase: -endstop_align_zero: False -[gcode_macro my_cmd] -gcode: -variable_: -rename_existing: -description: G-Code macro -[delayed_gcode my_delayed_gcode] -gcode: -initial_duration: 0.0 -[save_variables] -filename: -[idle_timeout] -gcode: -timeout: 600 -[virtual_sdcard] -path: -on_error_gcode: -[sdcard_loop] -[force_move] -enable_force_move: False -[pause_resume] -recover_velocity: 50. -[firmware_retraction] -retract_length: 0 -retract_speed: 20 -unretract_extra_length: 0 -unretract_speed: 10 -[gcode_arcs] -resolution: 1.0 -[respond] -default_type: echo -default_prefix: echo: -[exclude_object] -[input_shaper] -shaper_freq_x: 0 -shaper_freq_y: 0 -shaper_type: mzv -shaper_type_x: -shaper_type_y: -damping_ratio_x: 0.1 -damping_ratio_y: 0.1 -[adxl345] -cs_pin: -spi_speed: 5000000 -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -axes_map: x, y, z -rate: 3200 -[lis2dw] -cs_pin: -spi_speed: 5000000 -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -axes_map: x, y, z -[mpu9250 my_accelerometer] -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: 400000 -axes_map: x, y, z -[resonance_tester] -probe_points: -accel_chip: -accel_chip_x: -accel_chip_y: -max_smoothing: -min_freq: 5 -max_freq: 133.33 -accel_per_hz: 75 -hz_per_sec: 1 -[board_pins my_aliases] -mcu: mcu -aliases: -aliases_: -[duplicate_pin_override] -pins: -[probe] -pin: -deactivate_on_each_sample: True -x_offset: 0.0 -y_offset: 0.0 -z_offset: -speed: 5.0 -samples: 1 -sampleretract_dist: 2.0 -lift_speed: -samples_result: average -samples_tolerance: 0.100 -samples_toleranceretries: 0 -activate_gcode: -deactivate_gcode: -[bltouch] -sensor_pin: -control_pin: -pin_move_time: 0.680 -stow_on_each_sample: True -probe_with_touch_mode: False -pin_up_reports_not_triggered: True -pin_up_touch_modereports_triggered: True -set_output_mode: -x_offset: -y_offset: -z_offset: -speed: -lift_speed: -samples: -sampleretract_dist: -samples_result: -samples_tolerance: -samples_toleranceretries: -[smart_effector] -pin: -control_pin: -probe_accel: -recovery_time: 0.4 -x_offset: -y_offset: -z_offset: -speed: -samples: -sampleretract_dist: -samples_result: -samples_tolerance: -samples_toleranceretries: -activate_gcode: -deactivate_gcode: -deactivate_on_each_sample: -[probe_eddy_current my_eddy_probe] -sensor_type: ldc1612 -intb_pin: -z_offset: -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -x_offset: -y_offset: -speed: -lift_speed: -samples: -sampleretract_dist: -samples_result: -samples_tolerance: -samples_toleranceretries: -[axis_twist_compensation] -speed: 50 -horizontal_move_z: 5 -calibrate_start_x: 20 -calibrate_end_x: 200 -calibrate_y: 112.5 -[stepper_z1] -step_pin: -dir_pin: -enable_pin: -microsteps: -rotation_distance: -endstop_pin: -[extruder1] -step_pin: -dir_pin: -shared_heater: -[dual_carriage] -axis: -safe_distance: -step_pin: -dir_pin: -enable_pin: -microsteps: -rotation_distance: -endstop_pin: -position_endstop: -position_min: -position_max: -[extruder_stepper my_extra_stepper] -extruder: -step_pin: -dir_pin: -enable_pin: -microsteps: -rotation_distance: -[manual_stepper my_stepper] -step_pin: -dir_pin: -enable_pin: -microsteps: -rotation_distance: -velocity: -accel: -endstop_pin: -[verify_heater heater_config_name] -max_error: 120 -check_gain_time: -hysteresis: 5 -heating_gain: 2 -[homing_heaters] -steppers: -heaters: -[thermistor my_thermistor] -temperature1: -resistance1: -temperature2: -resistance2: -temperature3: -resistance3: -beta: -[adc_temperature my_sensor] -temperature1: -voltage1: -temperature2: -voltage2: -temperature1: -resistance1: -temperature2: -resistance2: -[heater_generic my_generic_heater] -gcode_id: -heater_pin: -max_power: -sensor_type: -sensor_pin: -smooth_time: -control: -pid_Kp: -pid_Ki: -pid_Kd: -pwm_cycle_time: -min_temp: -max_temp: -[temperature_sensor my_sensor] -sensor_type: -sensor_pin: -min_temp: -max_temp: -gcode_id: -[temperature_probe my_probe] -sensor_type: -sensor_pin: -min_temp: -max_temp: -smooth_time: -gcode_id: -speed: -horizontal_move_z: -resting_z: -calibration_position: -calibration_bed_temp: -calibration_extruder_temp: -extruder_heating_z: 50. -max_validation_temp: 60. -sensor_type: -sensor_pin: -pullup_resistor: 4700 -inlineresistor: 0 -sensor_type: -sensor_pin: -adc_voltage: 5.0 -voltage_offset: 0 -sensor_type: PT1000 -sensor_pin: -pullup_resistor: 4700 -sensor_type: -sensor_pin: -spi_speed: 4000000 -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -tc_type: K -tc_use_50Hz_filter: False -tc_averaging_count: 1 -rtd_nominal_r: 100 -rtd_referencer: 430 -rtd_num_of_wires: 2 -rtd_use_50Hz_filter: False -sensor_type: BME280 -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -sensor_type: AHT10 -i2c_address: -i2c_mcu: -i2c_bus: -i2c_speed: -aht10_report_time: -sensor_type: -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -htu21d_hold_master: -htu21d_resolution: -htu21d_report_time: -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -lm75_report_time: -sensor_type: temperature_mcu -sensor_mcu: mcu -sensor_temperature1: -sensor_adc1: -sensor_temperature2: -sensor_adc2: -sensor_type: temperature_host -sensor_path: -sensor_type: DS18B20 -serial_no: -ds18_report_time: -sensor_mcu: -sensor_type: temperature_combined -sensor_list: -combination_method: -maximum_deviation: -[fan] -pin: -max_power: 1.0 -shutdown_speed: 0 -cycle_time: 0.010 -hardware_pwm: False -kick_start_time: 0.100 -off_below: 0.0 -tachometer_pin: -tachometer_ppr: 2 -tachometer_poll_interval: 0.0015 -enable_pin: -[heater_fan heatbreak_cooling_fan] -pin: -max_power: -shutdown_speed: -cycle_time: -hardware_pwm: -kick_start_time: -off_below: -tachometer_pin: -tachometer_ppr: -tachometer_poll_interval: -enable_pin: -heater: extruder -heater_temp: 50.0 -fan_speed: 1.0 -[controller_fan my_controller_fan] -pin: -max_power: -shutdown_speed: -cycle_time: -hardware_pwm: -kick_start_time: -off_below: -tachometer_pin: -tachometer_ppr: -tachometer_poll_interval: -enable_pin: -fan_speed: 1.0 -idle_timeout: -idle_speed: -heater: -stepper: -[temperature_fan my_temp_fan] -pin: -max_power: -shutdown_speed: -cycle_time: -hardware_pwm: -kick_start_time: -off_below: -tachometer_pin: -tachometer_ppr: -tachometer_poll_interval: -enable_pin: -sensor_type: -sensor_pin: -control: -max_delta: -min_temp: -max_temp: -pid_Kp: -pid_Ki: -pid_Kd: -pid_deriv_time: 2.0 -target_temp: 40.0 -max_speed: 1.0 -min_speed: 0.3 -gcode_id: -[fan_generic extruder_partfan] -pin: -max_power: -shutdown_speed: -cycle_time: -hardware_pwm: -kick_start_time: -off_below: -tachometer_pin: -tachometer_ppr: -tachometer_poll_interval: -enable_pin: -[led my_led] -red_pin: -green_pin: -blue_pin: -white_pin: -cycle_time: 0.010 -hardware_pwm: False -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -[neopixel my_neopixel] -pin: -chain_count: -color_order: GRB -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -[dotstar my_dotstar] -data_pin: -clock_pin: -chain_count: -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -[pca9533 my_pca9533] -i2c_address: 98 -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -[pca9632 my_pca9632] -i2c_address: 98 -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -scl_pin: -sda_pin: -color_order: RGBW -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -[servo my_servo] -pin: -maximum_servo_angle: 180 -minimum_pulse_width: 0.001 -maximum_pulse_width: 0.002 -initial_angle: -initial_pulse_width: -[gcode_button my_gcode_button] -pin: -analog_range: -analog_pullup_resistor: -press_gcode: -release_gcode: -[output_pin my_pin] -pin: -pwm: False -value: -shutdown_value: -cycle_time: 0.100 -hardware_pwm: False -scale: -maximum_mcu_duration: -static_value: -[pwm_tool my_tool] -pin: -maximum_mcu_duration: -value: -shutdown_value: -cycle_time: 0.100 -hardware_pwm: False -scale: -[pwm_cycle_time my_pin] -pin: -value: -shutdown_value: -cycle_time: 0.100 -scale: -[static_digital_output my_output_pins] -pins: -[multi_pin my_multi_pin] -pins: -[tmc2130 stepper_x] -cs_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -chain_position: -chain_length: -interpolate: True -run_current: -hold_current: -senseresistor: 0.110 -stealthchop_threshold: 0 -coolstep_threshold: -high_velocity_threshold: -driver_MSLUT0: 2863314260 -driver_MSLUT1: 1251300522 -driver_MSLUT2: 608774441 -driver_MSLUT3: 269500962 -driver_MSLUT4: 4227858431 -driver_MSLUT5: 3048961917 -driver_MSLUT6: 1227445590 -driver_MSLUT7: 4211234 -driver_W0: 2 -driver_W1: 1 -driver_W2: 1 -driver_W3: 1 -driver_X1: 128 -driver_X2: 255 -driver_X3: 255 -driver_START_SIN: 0 -driver_START_SIN90: 247 -driver_IHOLDDELAY: 8 -driver_TPOWERDOWN: 0 -driver_TBL: 1 -driver_TOFF: 4 -driver_HEND: 7 -driver_HSTRT: 0 -driver_VHIGHFS: 0 -driver_VHIGHCHM: 0 -driver_PWM_AUTOSCALE: True -driver_PWM_FREQ: 1 -driver_PWM_GRAD: 4 -driver_PWM_AMPL: 128 -driver_SGT: 0 -driver_SEMIN: 0 -driver_SEUP: 0 -driver_SEMAX: 0 -driver_SEDN: 0 -driver_SEIMIN: 0 -driver_SFILT: 0 -diag0_pin: -diag1_pin: -[tmc2208 stepper_x] -uart_pin: -tx_pin: -select_pins: -interpolate: True -run_current: -hold_current: -sense_resistor: 0.110 -stealthchop_threshold: 0 -driver_MULTISTEP_FILT: True -driver_IHOLDDELAY: 8 -driver_TPOWERDOWN: 20 -driver_TBL: 2 -driver_TOFF: 3 -driver_HEND: 0 -driver_HSTRT: 5 -driver_PWM_AUTOGRAD: True -driver_PWM_AUTOSCALE: True -driver_PWM_LIM: 12 -driver_PWM_REG: 8 -driver_PWM_FREQ: 1 -driver_PWM_GRAD: 14 -driver_PWM_OFS: 36 -[tmc2209 stepper_x] -uart_pin: -tx_pin: -select_pins: -interpolate: True -run_current: -hold_current: -sense_resistor: 0.110 -stealthchop_threshold: 0 -coolstep_threshold: -uart_address: -driver_MULTISTEP_FILT: True -driver_IHOLDDELAY: 8 -driver_TPOWERDOWN: 20 -driver_TBL: 2 -driver_TOFF: 3 -driver_HEND: 0 -driver_HSTRT: 5 -driver_PWM_AUTOGRAD: True -driver_PWM_AUTOSCALE: True -driver_PWM_LIM: 12 -driver_PWM_REG: 8 -driver_PWM_FREQ: 1 -driver_PWM_GRAD: 14 -driver_PWM_OFS: 36 -driver_SGTHRS: 0 -driver_SEMIN: 0 -driver_SEUP: 0 -driver_SEMAX: 0 -driver_SEDN: 0 -driver_SEIMIN: 0 -diag_pin: -[tmc2660 stepper_x] -cs_pin: -spi_speed: 4000000 -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -interpolate: True -run_current: -sense_resistor: -idle_current_percent: 100 -driver_TBL: 2 -driver_RNDTF: 0 -driver_HDEC: 0 -driver_CHM: 0 -driver_HEND: 3 -driver_HSTRT: 3 -driver_TOFF: 4 -driver_SEIMIN: 0 -driver_SEDN: 0 -driver_SEMAX: 0 -driver_SEUP: 0 -driver_SEMIN: 0 -driver_SFILT: 0 -driver_SGT: 0 -driver_SLPH: 0 -driver_SLPL: 0 -driver_DISS2G: 0 -driver_TS2G: 3 -[tmc2240 stepper_x] -cs_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -uart_pin: -chain_position: -chain_length: -interpolate: True -run_current: -hold_current: -rref: 12000 -stealthchop_threshold: 0 -coolstep_threshold: -high_velocity_threshold: -driver_MSLUT0: 2863314260 -driver_MSLUT1: 1251300522 -driver_MSLUT2: 608774441 -driver_MSLUT3: 269500962 -driver_MSLUT4: 4227858431 -driver_MSLUT5: 3048961917 -driver_MSLUT6: 1227445590 -driver_MSLUT7: 4211234 -driver_W0: 2 -driver_W1: 1 -driver_W2: 1 -driver_W3: 1 -driver_X1: 128 -driver_X2: 255 -driver_X3: 255 -driver_START_SIN: 0 -driver_START_SIN90: 247 -driver_OFFSET_SIN90: 0 -driver_MULTISTEP_FILT: True -driver_IHOLDDELAY: 6 -driver_IRUNDELAY: 4 -driver_TPOWERDOWN: 10 -driver_TBL: 2 -driver_TOFF: 3 -driver_HEND: 2 -driver_HSTRT: 5 -driver_FD3: 0 -driver_TPFD: 4 -driver_CHM: 0 -driver_VHIGHFS: 0 -driver_VHIGHCHM: 0 -driver_DISS2G: 0 -driver_DISS2VS: 0 -driver_PWM_AUTOSCALE: True -driver_PWM_AUTOGRAD: True -driver_PWM_FREQ: 0 -driver_FREEWHEEL: 0 -driver_PWM_GRAD: 0 -driver_PWM_OFS: 29 -driver_PWM_REG: 4 -driver_PWM_LIM: 12 -driver_SGT: 0 -driver_SEMIN: 0 -driver_SEUP: 0 -driver_SEMAX: 0 -driver_SEDN: 0 -driver_SEIMIN: 0 -driver_SFILT: 0 -driver_SG4_ANGLE_OFFSET: 1 -diag0_pin: -diag1_pin: -[tmc5160 stepper_x] -cs_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -chain_position: -chain_length: -interpolate: True -run_current: -hold_current: -sense_resistor: 0.075 -stealthchop_threshold: 0 -coolstep_threshold: -high_velocity_threshold: -driver_MSLUT0: 2863314260 -driver_MSLUT1: 1251300522 -driver_MSLUT2: 608774441 -driver_MSLUT3: 269500962 -driver_MSLUT4: 4227858431 -driver_MSLUT5: 3048961917 -driver_MSLUT6: 1227445590 -driver_MSLUT7: 4211234 -driver_W0: 2 -driver_W1: 1 -driver_W2: 1 -driver_W3: 1 -driver_X1: 128 -driver_X2: 255 -driver_X3: 255 -driver_START_SIN: 0 -driver_START_SIN90: 247 -driver_MULTISTEP_FILT: True -driver_IHOLDDELAY: 6 -driver_TPOWERDOWN: 10 -driver_TBL: 2 -driver_TOFF: 3 -driver_HEND: 2 -driver_HSTRT: 5 -driver_FD3: 0 -driver_TPFD: 4 -driver_CHM: 0 -driver_VHIGHFS: 0 -driver_VHIGHCHM: 0 -driver_DISS2G: 0 -driver_DISS2VS: 0 -driver_PWM_AUTOSCALE: True -driver_PWM_AUTOGRAD: True -driver_PWM_FREQ: 0 -driver_FREEWHEEL: 0 -driver_PWM_GRAD: 0 -driver_PWM_OFS: 30 -driver_PWM_REG: 4 -driver_PWM_LIM: 12 -driver_SGT: 0 -driver_SEMIN: 0 -driver_SEUP: 0 -driver_SEMAX: 0 -driver_SEDN: 0 -driver_SEIMIN: 0 -driver_SFILT: 0 -driver_DRVSTRENGTH: 0 -driver_BBMCLKS: 4 -driver_BBMTIME: 0 -driver_FILT_ISENSE: 0 -diag0_pin: -diag1_pin: -[ad5206 my_digipot] -enable_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -channel_1: -channel_2: -channel_3: -channel_4: -channel_5: -channel_6: -scale: -[mcp4451 my_digipot] -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -wiper_0: -wiper_1: -wiper_2: -wiper_3: -scale: -[mcp4728 my_dac] -i2c_address: 96 -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -channel_a: -channel_b: -channel_c: -channel_d: -scale: -[mcp4018 my_digipot] -scl_pin: -sda_pin: -wiper: -scale: -[display] -lcd_type: -display_group: -menu_timeout: -menu_root: -menu_reverse_navigation: -encoder_pins: -encoder_steps_per_detent: -click_pin: -back_pin: -up_pin: -down_pin: -kill_pin: -analog_pullup_resistor: 4700 -analog_range_click_pin: -analog_range_back_pin: -analog_range_up_pin: -analog_range_down_pin: -analog_range_kill_pin: -[display] -lcd_type: hd44780 -rs_pin: -e_pin: -d4_pin: -d5_pin: -d6_pin: -d7_pin: -hd44780_protocol_init: True -line_length: -[display] -lcd_type: hd44780_spi -latch_pin: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -hd44780_protocol_init: True -line_length: -[display] -lcd_type: st7920 -cs_pin: -sclk_pin: -sid_pin: -[display] -lcd_type: emulated_st7920 -en_pin: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -[display] -lcd_type: uc1701 -cs_pin: -a0_pin: -rst_pin: -contrast: -[display] -lcd_type: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -cs_pin: -dc_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -reset_pin: -contrast: -vcomh: 0 -invert: False -x_offset: 0 -[display_data my_group_name my_data_name] -position: -text: -[display_template my_template_name] -param_: -text: -[display_glyph my_display_glyph] -data: -hd44780_data: -hd44780_slot: -[menu __some_list __some_name] -type: disabled -[menu some_name] -type: -name: -enable: -index: -[menu some_list] -type: list -name: -enable: -[menu some_list some_command] -type: command -name: -enable: -gcode: -[menu some_list some_input] -type: input -name: -enable: -input: -input_min: -input_max: -input_step: -realtime: -gcode: -[filament_switch_sensor my_sensor] -pause_on_runout: True -runout_gcode: -insert_gcode: -event_delay: 3.0 -pause_delay: 0.5 -switch_pin: -[filament_motion_sensor my_sensor] -detection_length: 7.0 -extruder: -switch_pin: -pause_on_runout: -runout_gcode: -insert_gcode: -event_delay: -pause_delay: -[tsl1401cl_filament_width_sensor] -pin: -default_nominal_filament_diameter: 1.75 -max_difference: 0.2 -measurement_delay: 100 -[hall_filament_width_sensor] -adc1: -adc2: -cal_dia1: 1.50 -cal_dia2: 2.00 -raw_dia1: 9500 -raw_dia2: 10500 -default_nominal_filament_diameter: 1.75 -max_difference: 0.200 -measurement_delay: 70 -enable: False -measurement_interval: 10 -logging: False -min_diameter: 1.0 -max_diameter: -use_current_dia_while_delay: False -pause_on_runout: -runout_gcode: -insert_gcode: -event_delay: -pause_delay: -[load_cell] -sensor_type: -[load_cell] -sensor_type: hx711 -sclk_pin: -dout_pin: -gain: A-128 -sample_rate: 80 -[load_cell] -sensor_type: hx717 -sclk_pin: -dout_pin: -gain: A-128 -sample_rate: 320 -[load_cell] -sensor_type: ads1220 -cs_pin: -spi_speed: 512000 -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -data_ready_pin: -gain: 128 -sample_rate: 660 -[sx1509 my_sx1509] -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: -[samd_sercom my_sercom] -sercom: -tx_pin: -rx_pin: -clk_pin: -[adc_scaled my_name] -vref_pin: -vssa_pin: -smooth_time: 2.0 -[replicape] -revision: -enable_pin: !gpio0_20 -host_mcu: -standstill_power_down: False -stepper_x_microstep_mode: -stepper_y_microstep_mode: -stepper_z_microstep_mode: -stepper_e_microstep_mode: -stepper_h_microstep_mode: -stepper_x_current: -stepper_y_current: -stepper_z_current: -stepper_e_current: -stepper_h_current: -stepper_x_chopper_off_time_high: -stepper_y_chopper_off_time_high: -stepper_z_chopper_off_time_high: -stepper_e_chopper_off_time_high: -stepper_h_chopper_off_time_high: -stepper_x_chopper_hysteresis_high: -stepper_y_chopper_hysteresis_high: -stepper_z_chopper_hysteresis_high: -stepper_e_chopper_hysteresis_high: -stepper_h_chopper_hysteresis_high: -stepper_x_chopper_blank_time_high: -stepper_y_chopper_blank_time_high: -stepper_z_chopper_blank_time_high: -stepper_e_chopper_blank_time_high: -stepper_h_chopper_blank_time_high: -[palette2] -serial: -baud: 115200 -feedrate_splice: 0.8 -feedrate_normal: 1.0 -auto_load_speed: 2 -auto_cancel_variation: 0.1 -[angle my_angle_sensor] -sensor_type: -sample_period: 0.000400 -stepper: -cs_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -spi_speed: -spi_bus: -spi_software_sclk_pin: -spi_software_mosi_pin: -spi_software_miso_pin: -i2c_address: -i2c_mcu: -i2c_bus: -i2c_software_scl_pin: -i2c_software_sda_pin: -i2c_speed: diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_1.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_1.cfg deleted file mode 100644 index 0da8889..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_1.cfg +++ /dev/null @@ -1,32 +0,0 @@ -# a comment at the very top -# should be treated as the file header - -# up to the first section, including all blank lines - -[section_1] -option_1: value_1 -option_1_1: True # this is a boolean -option_1_2: 5 ; this is an integer -option_1_3: 1.123 #;this is a float - -[section_2] ; comment -option_2: value_2 - -; comment - -[section_3] -option_3: value_3 # comment - -[section_4] -# comment -option_4: value_4 - -[section number 5] -#option_5: value_5 -option_5 = this.is.value-5 -multi_option: - # these are multi-line values - value_5_1 - value_5_2 ; here is a comment - value_5_3 -option_5_1: value_5_1 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_2.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_2.cfg deleted file mode 100644 index 3224ca6..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_2.cfg +++ /dev/null @@ -1,33 +0,0 @@ -# a comment at the very top -# should be treated as the file header - -# up to the first section, including all blank lines - -[section_1] -option_1: value_1 -option_1_1: True # this is a boolean -option_1_2: 5 ; this is an integer -option_1_3: 1.123 #;this is a float - -[section_2] ; comment -option_2: value_2 - -; comment - -[section_3] -option_3: value_3 # comment - -[section_4] -# comment -option_4: value_4 - -[section number 5] -#option_5: value_5 -option_5 = this.is.value-5 -multi_option: - # these are multi-line values - value_5_1 - value_5_2 ; here is a comment - value_5_3 -option_5_1: value_5_1 -# config ending with a comment diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_3.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_3.cfg deleted file mode 100644 index 00a0a20..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_3.cfg +++ /dev/null @@ -1,94 +0,0 @@ -# a comment at the very top -# should be treated as the file header - -# up to the first section, including all blank lines - -[section_1] -option_1: value_1 -option_1_1: True # this is a boolean -option_1_2: 5 ; this is an integer -option_1_3: 1.123 #;this is a float - -[section_2] ; comment -option_2: value_2 - -; comment - -[section_3] -option_3: value_3 # comment - -[section_4] -# comment -option_4: value_4 - -[section number 5] -#option_5: value_5 -option_5 = this.is.value-5 -multi_option: - # these are multi-line values - value_5_1 - value_5_2 ; here is a comment - value_5_3 -option_5_1: value_5_1 - -[gcode_macro M117] -rename_existing: M117.1 -gcode: - {% if rawparams %} - {% set escaped_msg = rawparams.split(';', 1)[0].split('\x23', 1)[0]|replace('"', '\\"') %} - SET_DISPLAY_TEXT MSG="{escaped_msg}" - RESPOND TYPE=command MSG="{escaped_msg}" - {% else %} - SET_DISPLAY_TEXT - {% endif %} - -# SDCard 'looping' (aka Marlin M808 commands) support -# -# Support SDCard looping -[sdcard_loop] -[gcode_macro M486] -gcode: - # Parameters known to M486 are as follows: - # [C] Cancel the current object - # [P] Cancel the object with the given index - # [S] Set the index of the current object. - # If the object with the given index has been canceled, this will cause - # the firmware to skip to the next object. The value -1 is used to - # indicate something that isn’t an object and shouldn’t be skipped. - # [T] Reset the state and set the number of objects - # [U] Un-cancel the object with the given index. This command will be - # ignored if the object has already been skipped - - {% if 'exclude_object' not in printer %} - {action_raise_error("[exclude_object] is not enabled")} - {% endif %} - - {% if 'T' in params %} - EXCLUDE_OBJECT RESET=1 - - {% for i in range(params.T | int) %} - EXCLUDE_OBJECT_DEFINE NAME={i} - {% endfor %} - {% endif %} - - {% if 'C' in params %} - EXCLUDE_OBJECT CURRENT=1 - {% endif %} - - {% if 'P' in params %} - EXCLUDE_OBJECT NAME={params.P} - {% endif %} - - {% if 'S' in params %} - {% if params.S == '-1' %} - {% if printer.exclude_object.current_object %} - EXCLUDE_OBJECT_END NAME={printer.exclude_object.current_object} - {% endif %} - {% else %} - EXCLUDE_OBJECT_START NAME={params.S} - {% endif %} - {% endif %} - - {% if 'U' in params %} - EXCLUDE_OBJECT RESET=1 NAME={params.U} - {% endif %} diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_4.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_4.cfg deleted file mode 100644 index 04cf0c7..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/test_config_4.cfg +++ /dev/null @@ -1,116 +0,0 @@ -# a comment at the very top -# should be treated as the file header - -# up to the first section, including all blank lines - -[section_1] -option_1: value_1 -option_1_1: True # this is a boolean -option_1_2: 5 ; this is an integer -option_1_3: 1.123 #;this is a float - -[section_2] ; comment -option_2: value_2 - -; comment - -[section_3] -option_3: value_3 # comment - -[section_4] -# comment -option_4: value_4 - -[section number 5] -#option_5: value_5 -option_5 = this.is.value-5 -multi_option: - # these are multi-line values - value_5_1 - value_5_2 ; here is a comment - value_5_3 -option_5_1: value_5_1 - -[gcode_macro M117] -rename_existing: M117.1 -gcode: - {% if rawparams %} - {% set escaped_msg = rawparams.split(';', 1)[0].split('\x23', 1)[0]|replace('"', '\\"') %} - SET_DISPLAY_TEXT MSG="{escaped_msg}" - RESPOND TYPE=command MSG="{escaped_msg}" - {% else %} - SET_DISPLAY_TEXT - {% endif %} - -# SDCard 'looping' (aka Marlin M808 commands) support -# -# Support SDCard looping -[sdcard_loop] -[gcode_macro M486] -gcode: - # Parameters known to M486 are as follows: - # [C] Cancel the current object - # [P] Cancel the object with the given index - # [S] Set the index of the current object. - # If the object with the given index has been canceled, this will cause - # the firmware to skip to the next object. The value -1 is used to - # indicate something that isn’t an object and shouldn’t be skipped. - # [T] Reset the state and set the number of objects - # [U] Un-cancel the object with the given index. This command will be - # ignored if the object has already been skipped - - {% if 'exclude_object' not in printer %} - {action_raise_error("[exclude_object] is not enabled")} - {% endif %} - - {% if 'T' in params %} - EXCLUDE_OBJECT RESET=1 - - {% for i in range(params.T | int) %} - EXCLUDE_OBJECT_DEFINE NAME={i} - {% endfor %} - {% endif %} - - {% if 'C' in params %} - EXCLUDE_OBJECT CURRENT=1 - {% endif %} - - {% if 'P' in params %} - EXCLUDE_OBJECT NAME={params.P} - {% endif %} - - {% if 'S' in params %} - {% if params.S == '-1' %} - {% if printer.exclude_object.current_object %} - EXCLUDE_OBJECT_END NAME={printer.exclude_object.current_object} - {% endif %} - {% else %} - EXCLUDE_OBJECT_START NAME={params.S} - {% endif %} - {% endif %} - - {% if 'U' in params %} - EXCLUDE_OBJECT RESET=1 NAME={params.U} - {% endif %} - -#*# <---------------------- SAVE_CONFIG ----------------------> -#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated. -#*# -#*# [bed_mesh default] -#*# version = 1 -#*# points = -#*# -0.152500, -0.133125, -0.113125, -0.159375, -0.232500 -#*# -0.095000, -0.078750, -0.068125, -0.133125, -0.235000 -#*# -0.092500, -0.040625, 0.004375, -0.077500, -0.193125 -#*# -0.073750, 0.023750, 0.085625, 0.026875, -0.085000 -#*# -0.140625, 0.038125, 0.126250, 0.097500, 0.003750 -#*# tension = 0.2 -#*# min_x = 26.0 -#*# algo = bicubic -#*# y_count = 5 -#*# mesh_y_pps = 2 -#*# min_y = 5.0 -#*# x_count = 5 -#*# max_y = 174.0 -#*# mesh_x_pps = 2 -#*# max_x = 194.0 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/expected.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/expected.cfg deleted file mode 100644 index f53531b..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/expected.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[section_1] -# comment -option_1: value_1 -option_2: value_2 ; comment -new_option: new_value - -[section_2] -option_3: value_3 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/input.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/input.cfg deleted file mode 100644 index 0d7aa1f..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/add_option/input.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[section_1] -# comment -option_1: value_1 -option_2: value_2 ; comment - -[section_2] -option_3: value_3 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/expected.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/expected.cfg deleted file mode 100644 index 0d7aa1f..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/expected.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[section_1] -# comment -option_1: value_1 -option_2: value_2 ; comment - -[section_2] -option_3: value_3 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/input.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/input.cfg deleted file mode 100644 index 949f7e7..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_option/input.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[section_1] -# comment -option_1: value_1 -option_to_remove: value_to_remove -option_2: value_2 ; comment - -[section_2] -option_3: value_3 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/expected.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/expected.cfg deleted file mode 100644 index d0fdca3..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/expected.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[section_1] -option_1: value_1 -option_2: value_2 - -# comment -[section_2] -option_5: value_5 diff --git a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/input.cfg b/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/input.cfg deleted file mode 100644 index cb7575e..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/assets/write_tests/remove_section/input.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[section_1] -option_1: value_1 -option_2: value_2 - -# comment -[section_to_remove] -option_3: value_3 -option_4: value_4 - -[section_2] -option_5: value_5 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/matching_data.txt deleted file mode 100644 index 6fb66a5..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/matching_data.txt +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/non_matching_data.txt deleted file mode 100644 index ceadadf..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_data/non_matching_data.txt +++ /dev/null @@ -1,7 +0,0 @@ -not_empty -[also_not_empty] -# -; - ; - # -option: value diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_match_empty_line.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_match_empty_line.py deleted file mode 100644 index ff5f3ba..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_empty_line/test_match_empty_line.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 src.simple_config_parser.simple_config_parser import SimpleConfigParser -from 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_line_comment(parser, line): - """Test that a line matches the definition of a line comment""" - assert ( - parser._match_empty_line(line) is True - ), f"Expected line '{line}' to match line comment definition!" - - -@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH)) -def test_non_matching_line_comment(parser, line): - """Test that a line does not match the definition of a line comment""" - assert ( - parser._match_empty_line(line) is False - ), f"Expected line '{line}' to not match line comment definition!" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/matching_data.txt deleted file mode 100644 index e9c232d..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/matching_data.txt +++ /dev/null @@ -1,28 +0,0 @@ -;[example_section] -#[example_section] -# [example_section] -; [example_section] -;[gcode_macro CANCEL_PRINT] -#[gcode_macro CANCEL_PRINT] -# [gcode_macro CANCEL_PRINT] -; [gcode_macro CANCEL_PRINT] -;[gcode_macro SET_PAUSE_NEXT_LAYER] -#[gcode_macro SET_PAUSE_NEXT_LAYER] -# [gcode_macro SET_PAUSE_NEXT_LAYER] -; [gcode_macro SET_PAUSE_NEXT_LAYER] -;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] -#[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] -# [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] -; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - ;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - #[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - # [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - ; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - ;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - #[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - # [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - ; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - ;[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - #[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - # [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] - ; [gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/non_matching_data.txt deleted file mode 100644 index 0a585e8..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_data/non_matching_data.txt +++ /dev/null @@ -1,5 +0,0 @@ -not_a_comment: nono - -[also not a comment] -not_a_comment: ; comment -not_a_comment: # comment diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_match_line_comment.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_match_line_comment.py deleted file mode 100644 index 2e1f9df..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_line_comment/test_match_line_comment.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 src.simple_config_parser.simple_config_parser import SimpleConfigParser -from 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_line_comment(parser, line): - """Test that a line matches the definition of a line comment""" - assert ( - parser._match_line_comment(line) is True - ), f"Expected line '{line}' to match line comment definition!" - - -@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH)) -def test_non_matching_line_comment(parser, line): - """Test that a line does not match the definition of a line comment""" - assert ( - parser._match_line_comment(line) is False - ), f"Expected line '{line}' to not match line comment definition!" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/matching_data.txt deleted file mode 100644 index ef6286d..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/matching_data.txt +++ /dev/null @@ -1,461 +0,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 -homing_retract_dist: 5.0 -kinematics: cartesian -kinematics: delta -minimum_z_position: 0 -speed: 50 -horizontal_move_z: 5 -kinematics: deltesian -minimum_z_position: 0 -min_angle: 5 -slow_ratio: 3 -kinematics: corexy -kinematics: corexz -kinematics: hybrid_corexy -kinematics: hybrid_corexz -kinematics: polar -kinematics: rotary_delta -minimum_z_position: 0 -speed: 50 -horizontal_move_z: 5 -kinematics: winch -kinematics: none -max_velocity: 1 -max_accel: 1 -instantaneous_corner_velocity: 1.000 -max_extrude_only_distance: 50.0 -pressure_advance: 0.0 -pressure_advance_smooth_time: 0.040 -max_power: 1.0 -pullup_resistor: 4700 -smooth_time: 1.0 -max_delta: 2.0 -pwm_cycle_time: 0.100 -min_extrude_temp: 170 -speed: 50 -horizontal_move_z: 5 -probe_count: 3, 3 -round_probe_count: 5 -fade_start: 1.0 -fade_end: 0.0 -split_delta_z: .025 -move_check_distance: 5.0 -mesh_pps: 2, 2 -algorithm: lagrange -bicubic_tension: .2 -x_adjust: 0 -y_adjust: 0 -z_adjust: 0 -speed: 50 -horizontal_move_z: 5 -horizontal_move_z: 5 -probe_height: 0 -speed: 50 -probe_speed: 5 -speed: 50 -horizontal_move_z: 5 -screw_thread: CW-M3 -speed: 50 -horizontal_move_z: 5 -retries: 0 -retry_tolerance: 0 -speed: 50 -horizontal_move_z: 5 -max_adjust: 4 -retries: 0 -retry_tolerance: 0 -speed: 50.0 -z_hop_speed: 15.0 -move_to_previous: False -axes: xyz -endstop_align_zero: False -description: G-Code macro -initial_duration: 0.0 -timeout: 600 -enable_force_move: False -recover_velocity: 50. -retract_length: 0 -retract_speed: 20 -unretract_extra_length: 0 -unretract_speed: 10 -resolution: 1.0 -default_type: echo -default_prefix: echo: -shaper_freq_x: 0 -shaper_freq_y: 0 -shaper_type: mzv -damping_ratio_x: 0.1 -damping_ratio_y: 0.1 -spi_speed: 5000000 -axes_map: x, y, z -rate: 3200 -spi_speed: 5000000 -axes_map: x, y, z -i2c_speed: 400000 -axes_map: x, y, z -min_freq: 5 -max_freq: 133.33 -accel_per_hz: 75 -hz_per_sec: 1 -mcu: mcu -deactivate_on_each_sample: True -x_offset: 0.0 -y_offset: 0.0 -speed: 5.0 -samples: 1 -sampleretract_dist: 2.0 -samples_result: average -samples_tolerance: 0.100 -samples_toleranceretries: 0 -pin_move_time: 0.680 -stow_on_each_sample: True -probe_with_touch_mode: False -pin_up_reports_not_triggered: True -pin_up_touch_modereports_triggered: True -recovery_time: 0.4 -sensor_type: ldc1612 -speed: 50 -horizontal_move_z: 5 -calibrate_start_x: 20 -calibrate_end_x: 200 -calibrate_y: 112.5 -max_error: 120 -hysteresis: 5 -heating_gain: 2 -extruder_heating_z: 50. -max_validation_temp: 60. -pullup_resistor: 4700 -inlineresistor: 0 -adc_voltage: 5.0 -voltage_offset: 0 -sensor_type: PT1000 -pullup_resistor: 4700 -spi_speed: 4000000 -tc_type: K -tc_use_50Hz_filter: False -tc_averaging_count: 1 -rtd_nominal_r: 100 -rtd_referencer: 430 -rtd_num_of_wires: 2 -rtd_use_50Hz_filter: False -sensor_type: BME280 -sensor_type: AHT10 -sensor_type: temperature_mcu -sensor_mcu: mcu -sensor_type: temperature_host -sensor_type: DS18B20 -sensor_type: temperature_combined -max_power: 1.0 -shutdown_speed: 0 -cycle_time: 0.010 -hardware_pwm: False -kick_start_time: 0.100 -off_below: 0.0 -tachometer_ppr: 2 -tachometer_poll_interval: 0.0015 -heater: extruder -heater_temp: 50.0 -fan_speed: 1.0 -fan_speed: 1.0 -pid_deriv_time: 2.0 -target_temp: 40.0 -max_speed: 1.0 -min_speed: 0.3 -cycle_time: 0.010 -hardware_pwm: False -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -color_order: GRB -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -i2c_address: 98 -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -i2c_address: 98 -color_order: RGBW -initial_RED: 0.0 -initial_GREEN: 0.0 -initial_BLUE: 0.0 -initial_WHITE: 0.0 -maximum_servo_angle: 180 -minimum_pulse_width: 0.001 -maximum_pulse_width: 0.002 -pwm: False -cycle_time: 0.100 -hardware_pwm: False -cycle_time: 0.100 -hardware_pwm: False -cycle_time: 0.100 -interpolate: True -senseresistor: 0.110 -stealthchop_threshold: 0 -driver_MSLUT0: 2863314260 -driver_MSLUT1: 1251300522 -driver_MSLUT2: 608774441 -driver_MSLUT3: 269500962 -driver_MSLUT4: 4227858431 -driver_MSLUT5: 3048961917 -driver_MSLUT6: 1227445590 -driver_MSLUT7: 4211234 -driver_W0: 2 -driver_W1: 1 -driver_W2: 1 -driver_W3: 1 -driver_X1: 128 -driver_X2: 255 -driver_X3: 255 -driver_START_SIN: 0 -driver_START_SIN90: 247 -driver_IHOLDDELAY: 8 -driver_TPOWERDOWN: 0 -driver_TBL: 1 -driver_TOFF: 4 -driver_HEND: 7 -driver_HSTRT: 0 -driver_VHIGHFS: 0 -driver_VHIGHCHM: 0 -driver_PWM_AUTOSCALE: True -driver_PWM_FREQ: 1 -driver_PWM_GRAD: 4 -driver_PWM_AMPL: 128 -driver_SGT: 0 -driver_SEMIN: 0 -driver_SEUP: 0 -driver_SEMAX: 0 -driver_SEDN: 0 -driver_SEIMIN: 0 -driver_SFILT: 0 -interpolate: True -sense_resistor: 0.110 -stealthchop_threshold: 0 -driver_MULTISTEP_FILT: True -driver_IHOLDDELAY: 8 -driver_TPOWERDOWN: 20 -driver_TBL: 2 -driver_TOFF: 3 -driver_HEND: 0 -driver_HSTRT: 5 -driver_PWM_AUTOGRAD: True -driver_PWM_AUTOSCALE: True -driver_PWM_LIM: 12 -driver_PWM_REG: 8 -driver_PWM_FREQ: 1 -driver_PWM_GRAD: 14 -driver_PWM_OFS: 36 -interpolate: True -sense_resistor: 0.110 -stealthchop_threshold: 0 -driver_MULTISTEP_FILT: True -driver_IHOLDDELAY: 8 -driver_TPOWERDOWN: 20 -driver_TBL: 2 -driver_TOFF: 3 -driver_HEND: 0 -driver_HSTRT: 5 -driver_PWM_AUTOGRAD: True -driver_PWM_AUTOSCALE: True -driver_PWM_LIM: 12 -driver_PWM_REG: 8 -driver_PWM_FREQ: 1 -driver_PWM_GRAD: 14 -driver_PWM_OFS: 36 -driver_SGTHRS: 0 -driver_SEMIN: 0 -driver_SEUP: 0 -driver_SEMAX: 0 -driver_SEDN: 0 -driver_SEIMIN: 0 -spi_speed: 4000000 -interpolate: True -idle_current_percent: 100 -driver_TBL: 2 -driver_RNDTF: 0 -driver_HDEC: 0 -driver_CHM: 0 -driver_HEND: 3 -driver_HSTRT: 3 -driver_TOFF: 4 -driver_SEIMIN: 0 -driver_SEDN: 0 -driver_SEMAX: 0 -driver_SEUP: 0 -driver_SEMIN: 0 -driver_SFILT: 0 -driver_SGT: 0 -driver_SLPH: 0 -driver_SLPL: 0 -driver_DISS2G: 0 -driver_TS2G: 3 -interpolate: True -rref: 12000 -stealthchop_threshold: 0 -driver_MSLUT0: 2863314260 -driver_MSLUT1: 1251300522 -driver_MSLUT2: 608774441 -driver_MSLUT3: 269500962 -driver_MSLUT4: 4227858431 -driver_MSLUT5: 3048961917 -driver_MSLUT6: 1227445590 -driver_MSLUT7: 4211234 -driver_W0: 2 -driver_W1: 1 -driver_W2: 1 -driver_W3: 1 -driver_X1: 128 -driver_X2: 255 -driver_X3: 255 -driver_START_SIN: 0 -driver_START_SIN90: 247 -driver_OFFSET_SIN90: 0 -driver_MULTISTEP_FILT: True -driver_IHOLDDELAY: 6 -driver_IRUNDELAY: 4 -driver_TPOWERDOWN: 10 -driver_TBL: 2 -driver_TOFF: 3 -driver_HEND: 2 -driver_HSTRT: 5 -driver_FD3: 0 -driver_TPFD: 4 -driver_CHM: 0 -driver_VHIGHFS: 0 -driver_VHIGHCHM: 0 -driver_DISS2G: 0 -driver_DISS2VS: 0 -driver_PWM_AUTOSCALE: True -driver_PWM_AUTOGRAD: True -driver_PWM_FREQ: 0 -driver_FREEWHEEL: 0 -driver_PWM_GRAD: 0 -driver_PWM_OFS: 29 -driver_PWM_REG: 4 -driver_PWM_LIM: 12 -driver_SGT: 0 -driver_SEMIN: 0 -driver_SEUP: 0 -driver_SEMAX: 0 -driver_SEDN: 0 -driver_SEIMIN: 0 -driver_SFILT: 0 -driver_SG4_ANGLE_OFFSET: 1 -interpolate: True -sense_resistor: 0.075 -stealthchop_threshold: 0 -driver_MSLUT0: 2863314260 -driver_MSLUT1: 1251300522 -driver_MSLUT2: 608774441 -driver_MSLUT3: 269500962 -driver_MSLUT4: 4227858431 -driver_MSLUT5: 3048961917 -driver_MSLUT6: 1227445590 -driver_MSLUT7: 4211234 -driver_W0: 2 -driver_W1: 1 -driver_W2: 1 -driver_W3: 1 -driver_X1: 128 -driver_X2: 255 -driver_X3: 255 -driver_START_SIN: 0 -driver_START_SIN90: 247 -driver_MULTISTEP_FILT: True -driver_IHOLDDELAY: 6 -driver_TPOWERDOWN: 10 -driver_TBL: 2 -driver_TOFF: 3 -driver_HEND: 2 -driver_HSTRT: 5 -driver_FD3: 0 -driver_TPFD: 4 -driver_CHM: 0 -driver_VHIGHFS: 0 -driver_VHIGHCHM: 0 -driver_DISS2G: 0 -driver_DISS2VS: 0 -driver_PWM_AUTOSCALE: True -driver_PWM_AUTOGRAD: True -driver_PWM_FREQ: 0 -driver_FREEWHEEL: 0 -driver_PWM_GRAD: 0 -driver_PWM_OFS: 30 -driver_PWM_REG: 4 -driver_PWM_LIM: 12 -driver_SGT: 0 -driver_SEMIN: 0 -driver_SEUP: 0 -driver_SEMAX: 0 -driver_SEDN: 0 -driver_SEIMIN: 0 -driver_SFILT: 0 -driver_DRVSTRENGTH: 0 -driver_BBMCLKS: 4 -driver_BBMTIME: 0 -driver_FILT_ISENSE: 0 -i2c_address: 96 -analog_pullup_resistor: 4700 -lcd_type: hd44780 -hd44780_protocol_init: True -lcd_type: hd44780_spi -hd44780_protocol_init: True -lcd_type: st7920 -lcd_type: emulated_st7920 -lcd_type: uc1701 -vcomh: 0 -invert: False -x_offset: 0 -type: disabled -type: list -type: command -type: input -pause_on_runout: True -event_delay: 3.0 -pause_delay: 0.5 -detection_length: 7.0 -default_nominal_filament_diameter: 1.75 -max_difference: 0.2 -measurement_delay: 100 -cal_dia1: 1.50 -cal_dia2: 2.00 -raw_dia1: 9500 -raw_dia2: 10500 -default_nominal_filament_diameter: 1.75 -max_difference: 0.200 -measurement_delay: 70 -enable: False -measurement_interval: 10 -logging: False -min_diameter: 1.0 -use_current_dia_while_delay: False -sensor_type: hx711 -gain: A-128 -sample_rate: 80 -sensor_type: hx717 -gain: A-128 -sample_rate: 320 -sensor_type: ads1220 -spi_speed: 512000 -gain: 128 -sample_rate: 660 -smooth_time: 2.0 -enable_pin: !gpio0_20 -standstill_power_down: False -baud: 115200 -feedrate_splice: 0.8 -feedrate_normal: 1.0 -auto_load_speed: 2 -auto_cancel_variation: 0.1 -sample_period: 0.000400 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/non_matching_data.txt deleted file mode 100644 index 582572b..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_data/non_matching_data.txt +++ /dev/null @@ -1,37 +0,0 @@ -[section] -[section with spaces] -[section with spaces and comments] ; comment 1 -[section with spaces and comments] # comment 2 - indented_option: value -option_with_no_value: -another_option_with_no_value: - indented_option_with_no_value: -# position_min: 0 -# homing_speed: 5.0 - -### this is a comment -; this is also a comment -# [section] -# [section with spaces] -# [section with spaces and comments] ; comment 1 -;[section] -;[section with spaces] -;[section with spaces and comments] ; comment 1 -# commented_option: value -#commented_option: value -;commented_option: value -; commented_option: value -# -; -option_1 :: value -option_1:: value -option_1 ::value -option_2 == value -option_2== value -option_2 ==value -option_1 := value -option_1:= value -option_1 :=value -option_2 := value -option_2:= value -option_2 :=value diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_match_option.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_match_option.py deleted file mode 100644 index 5dba4a0..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option/test_match_option.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 src.simple_config_parser.simple_config_parser import SimpleConfigParser -from 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_option(parser, line): - """Test that a line matches the definition of an option""" - assert ( - parser._match_option(line) is True - ), f"Expected line '{line}' to match option definition!" - - -@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH)) -def test_non_matching_option(parser, line): - """Test that a line does not match the definition of an option""" - assert ( - parser._match_option(line) is False - ), f"Expected line '{line}' to not match option definition!" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/matching_data.txt deleted file mode 100644 index 89d43f2..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/matching_data.txt +++ /dev/null @@ -1,15 +0,0 @@ -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 -options_block_start_with_comment:;this is a comment -options_block_start_with_comment :;this is a comment -options_block_start_with_comment:#this is a comment -options_block_start_with_comment :#this is a comment -parameter_temperature_(°C): -parameter_temperature_(°C)= -parameter_humidity_(%_RH): -parameter_humidity_(%_RH) : -parameter_spool_weight_(%): -parameter_spool_weight_(%) = diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/non_matching_data.txt deleted file mode 100644 index 02da2de..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_data/non_matching_data.txt +++ /dev/null @@ -1,31 +0,0 @@ -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 - -### 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 =: diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_match_options_block_start.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_match_options_block_start.py deleted file mode 100644 index 904b2f7..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_option_block_start/test_match_options_block_start.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 src.simple_config_parser.simple_config_parser import SimpleConfigParser -from 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_options_block_start(parser, line): - """Test that a line matches the definition of an options block start""" - assert ( - parser._match_options_block_start(line) is True - ), f"Expected line '{line}' to match options block start definition!" - - -@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH)) -def test_non_matching_options_block_start(parser, line): - """Test that a line does not match the definition of an options block start""" - assert ( - parser._match_options_block_start(line) is False - ), f"Expected line '{line}' to not match options block start definition!" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/matching_data.txt deleted file mode 100644 index 0839d60..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/matching_data.txt +++ /dev/null @@ -1,22 +0,0 @@ -#*# any content -#*# -#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated. -#*# -#*# [bed_mesh default] -#*# version = 1 -#*# points = -#*# -0.152500, -0.133125, -0.113125, -0.159375, -0.232500 -#*# -0.095000, -0.078750, -0.068125, -0.133125, -0.235000 -#*# -0.092500, -0.040625, 0.004375, -0.077500, -0.193125 -#*# -0.073750, 0.023750, 0.085625, 0.026875, -0.085000 -#*# -0.140625, 0.038125, 0.126250, 0.097500, 0.003750 -#*# tension = 0.2 -#*# min_x = 26.0 -#*# algo = bicubic -#*# y_count = 5 -#*# mesh_y_pps = 2 -#*# min_y = 5.0 -#*# x_count = 5 -#*# max_y = 174.0 -#*# mesh_x_pps = 2 -#*# max_x = 194.0 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/non_matching_data.txt deleted file mode 100644 index a97d893..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_data/non_matching_data.txt +++ /dev/null @@ -1,6 +0,0 @@ - #*# leading space prevents match -random -*# not starting with hash-star-hash -# *# spaced out -<- SAVE_CONFIG -> -;#*# semicolon first diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_match_save_config_content.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_match_save_config_content.py deleted file mode 100644 index 7677266..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_content/test_match_save_config_content.py +++ /dev/null @@ -1,37 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 src.simple_config_parser.simple_config_parser import SimpleConfigParser -from 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() - - -def test_matching_lines(parser): - """Alle Zeilen in matching_data.txt sollen als Save-Config-Content erkannt werden.""" - matching_lines = load_testdata_from_file(MATCHING_TEST_DATA_PATH) - for line in matching_lines: - assert parser._match_save_config_content(line) is True, f"Line should be a save config content: {line!r}" - - -def test_non_matching_lines(parser): - """Alle Zeilen in non_matching_data.txt sollen NICHT als Save-Config-Content erkannt werden.""" - non_matching_lines = load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH) - for line in non_matching_lines: - assert parser._match_save_config_content(line) is False, f"Line should not be a save config content: {line!r}" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/matching_data.txt deleted file mode 100644 index b2516e3..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/matching_data.txt +++ /dev/null @@ -1,6 +0,0 @@ -#*# <- SAVE_CONFIG -> -#*# <---- SAVE_CONFIG ----> -#*# <------------------- SAVE_CONFIG -------------------> -#*# <---------------------- SAVE_CONFIG ----------------------> -#*# <----- SAVE_CONFIG -> -#*# <- SAVE_CONFIG -----------------> diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/non_matching_data.txt deleted file mode 100644 index 59b2841..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_data/non_matching_data.txt +++ /dev/null @@ -1,13 +0,0 @@ -#*#<- SAVE_CONFIG -> -#*# <-SAVE_CONFIG -> -#*# <- SAVE_CONFIG-> -#*# <- SAVE_CONFIG -> extra -#*# SAVE_CONFIG ----> -#*# < SAVE_CONFIG > -# *# <- SAVE_CONFIG -> -<- SAVE_CONFIG -> -random text - #*# <---------------------- SAVE_CONFIG ----------------------> -#*# <---------------------- SAVE_CONFIG ----------------------> #*# -#*# <--------------------------------------------> -#*# SAVE_CONFIG diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_match_save_config_start.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_match_save_config_start.py deleted file mode 100644 index 3a85237..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_save_config_start/test_match_save_config_start.py +++ /dev/null @@ -1,37 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 src.simple_config_parser.simple_config_parser import SimpleConfigParser -from 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() - - -def test_matching_lines(parser): - """Test that all lines in the matching data file are correctly identified as save config start lines.""" - matching_lines = load_testdata_from_file(MATCHING_TEST_DATA_PATH) - for line in matching_lines: - assert parser._match_save_config_start(line) is True, f"Line should be a save config start: {line!r}" - - -def test_non_matching_lines(parser): - """Test that all lines in the non-matching data file are correctly identified as not save config start lines.""" - non_matching_lines = load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH) - for line in non_matching_lines: - assert parser._match_save_config_start(line) is False, f"Line should not be a save config start: {line!r}" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/__init__,py.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/__init__,py.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/matching_data.txt deleted file mode 100644 index a3e335a..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/matching_data.txt +++ /dev/null @@ -1,127 +0,0 @@ -[example_section] -[gcode_macro CANCEL_PRINT] -[gcode_macro SET_PAUSE_NEXT_LAYER] -[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL] -[update_manager moonraker-obico] -[include moonraker_obico_macros.cfg] -[include moonraker-obico-update.cfg] -[example_section two] -[valid_content] -[valid content] -[content123] -[a] -[valid_content] # comment -[something];comment -[mcu] -[printer] -[printer] -[stepper_x] -[stepper_y] -[stepper_z] -[printer] -[stepper_a] -[stepper_b] -[stepper_c] -[delta_calibrate] -[printer] -[stepper_left] -[stepper_right] -[stepper_bed] -[stepper_arm] -[delta_calibrate] -[extruder] -[heater_bed] -[bed_mesh] -[bed_tilt] -[bed_screws] -[screws_tilt_adjust] -[z_tilt] -[quad_gantry_level] -[skew_correction] -[z_thermal_adjust] -[safe_z_home] -[homing_override] -[endstop_phase stepper_z] -[gcode_macro my_cmd] -[delayed_gcode my_delayed_gcode] -[save_variables] -[idle_timeout] -[virtual_sdcard] -[sdcard_loop] -[force_move] -[pause_resume] -[firmware_retraction] -[gcode_arcs] -[respond] -[exclude_object] -[input_shaper] -[adxl345] -[lis2dw] -[mpu9250 my_accelerometer] -[resonance_tester] -[board_pins my_aliases] -[duplicate_pin_override] -[probe] -[bltouch] -[smart_effector] -[probe_eddy_current my_eddy_probe] -[axis_twist_compensation] -[stepper_z1] -[extruder1] -[dual_carriage] -[extruder_stepper my_extra_stepper] -[manual_stepper my_stepper] -[verify_heater heater_config_name] -[homing_heaters] -[thermistor my_thermistor] -[adc_temperature my_sensor] -[heater_generic my_generic_heater] -[temperature_sensor my_sensor] -[temperature_probe my_probe] -[fan] -[heater_fan heatbreak_cooling_fan] -[controller_fan my_controller_fan] -[temperature_fan my_temp_fan] -[fan_generic extruder_partfan] -[led my_led] -[neopixel my_neopixel] -[dotstar my_dotstar] -[pca9533 my_pca9533] -[pca9632 my_pca9632] -[servo my_servo] -[gcode_button my_gcode_button] -[output_pin my_pin] -[pwm_tool my_tool] -[pwm_cycle_time my_pin] -[static_digital_output my_output_pins] -[multi_pin my_multi_pin] -[tmc2130 stepper_x] -[tmc2208 stepper_x] -[tmc2209 stepper_x] -[tmc2660 stepper_x] -[tmc2240 stepper_x] -[tmc5160 stepper_x] -[ad5206 my_digipot] -[mcp4451 my_digipot] -[mcp4728 my_dac] -[mcp4018 my_digipot] -[display] -[display_data my_group_name my_data_name] -[display_template my_template_name] -[display_glyph my_display_glyph] -[menu __some_list __some_name] -[menu some_name] -[menu some_list] -[menu some_list some_command] -[menu some_list some_input] -[filament_switch_sensor my_sensor] -[filament_motion_sensor my_sensor] -[tsl1401cl_filament_width_sensor] -[hall_filament_width_sensor] -[load_cell] -[sx1509 my_sx1509] -[samd_sercom my_sercom] -[adc_scaled my_name] -[replicape] -[palette2] -[angle my_angle_sensor] diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/non_matching_data.txt b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/non_matching_data.txt deleted file mode 100644 index 42371ab..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_data/non_matching_data.txt +++ /dev/null @@ -1,19 +0,0 @@ -section: invalid -not_a_valid_section -[missing_square_bracket -missing_square_bracket] -[] -[ ] - [indented_section] - [indented_section] # comment - [indented_section] ; comment -;[commented_section] -#[another_commented_section] -; [commented_section] -# [another_commented_section] -this_is_an_option: 123 - this_is_an_indented_option: 123 -this_is_an_option_block_start: - -# -; diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_match_section.py b/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_match_section.py deleted file mode 100644 index e950dd2..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_matching/match_section/test_match_section.py +++ /dev/null @@ -1,39 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 src.simple_config_parser.simple_config_parser import SimpleConfigParser -from 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_section(parser, line): - """Test that a line matches the definition of a section""" - assert ( - parser._match_section(line) is True - ), f"Expected line '{line}' to match section definition!" - - -@pytest.mark.parametrize("line", load_testdata_from_file(NON_MATCHING_TEST_DATA_PATH)) -def test_non_matching_section(parser, line): - """Test that a line does not match the definition of a section""" - assert ( - parser._match_section(line) is False - ), f"Expected line '{line}' to not match section definition!" diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_parsing/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/line_parsing/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/line_parsing/test_line_parsing.py b/kiauh/core/submodules/simple_config_parser/tests/line_parsing/test_line_parsing.py deleted file mode 100644 index 7656420..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/line_parsing/test_line_parsing.py +++ /dev/null @@ -1,79 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# https://github.com/dw-0/simple-config-parser # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import json -from pathlib import Path - -import pytest - -from src.simple_config_parser.constants import HEADER_IDENT, LineType -from src.simple_config_parser.simple_config_parser import SimpleConfigParser -from tests.utils import load_testdata_from_file - -BASE_DIR = Path(__file__).parent.parent.joinpath("assets") -TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg") - - -@pytest.fixture -def parser(): - parser = SimpleConfigParser() - for line in load_testdata_from_file(TEST_DATA_PATH): - parser._parse_line(line) # noqa - - return parser - - -def test_section_parsing(parser): - expected_keys = {"section_1", "section_2", "section_3", "section_4"} - assert expected_keys.issubset( - parser.config.keys() - ), f"Expected keys: {expected_keys}, got: {parser.config.keys()}" - assert parser.in_option_block is False - assert parser.current_section == parser.get_sections()[-1] - assert parser.config["section_2"] is not None - assert parser.config["section_2"]["header"] == "[section_2] ; comment" - assert parser.config["section_2"]["elements"] is not None - assert len(parser.config["section_2"]["elements"]) > 0 - - -def test_option_parsing(parser): - assert parser.config["section_1"]["elements"][0]["type"] == LineType.OPTION.value - assert parser.config["section_1"]["elements"][0]["name"] == "option_1" - assert parser.config["section_1"]["elements"][0]["value"] == "value_1" - assert parser.config["section_1"]["elements"][0]["raw"] == "option_1: value_1" - - -def test_header_parsing(parser): - header = parser.config[HEADER_IDENT] - assert isinstance(header, list) - assert len(header) > 0 - - -def test_option_block_parsing(parser): - section = "section number 5" - option_block = None - for element in parser.config[section]["elements"]: - if (element["type"] == LineType.OPTION_BLOCK.value and - element["name"] == "multi_option"): - option_block = element - break - - assert option_block is not None, "multi_option block not found" - assert option_block["type"] == LineType.OPTION_BLOCK.value - assert option_block["name"] == "multi_option" - assert option_block["raw"] == "multi_option:" - - expected_values = [ - "# these are multi-line values", - "value_5_1", - "value_5_2 ; here is a comment", - "value_5_3" - ] - assert option_block["value"] == expected_values, ( - f"Expected values: {expected_values}, " - f"got: {option_block['value']}" - ) diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py deleted file mode 100644 index fdd77fa..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/conftest.py +++ /dev/null @@ -1,26 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 src.simple_config_parser.simple_config_parser import SimpleConfigParser -from tests.utils import load_testdata_from_file - -BASE_DIR = Path(__file__).parent.parent.joinpath("assets") -CONFIG_FILES = ["test_config_1.cfg", "test_config_2.cfg", "test_config_3.cfg", "test_config_4.cfg"] - - -@pytest.fixture(params=CONFIG_FILES) -def parser(request): - parser = SimpleConfigParser() - file_path = BASE_DIR.joinpath(request.param) - for line in load_testdata_from_file(file_path): - parser._parse_line(line) # noqa - - return parser diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_options_api.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/test_options_api.py deleted file mode 100644 index 1e6f5fe..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_options_api.py +++ /dev/null @@ -1,186 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# https://github.com/dw-0/simple-config-parser # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -import pytest - -from src.simple_config_parser.constants import LineType -from src.simple_config_parser.simple_config_parser import ( - NoOptionError, - NoSectionError, -) - - -def test_get_options(parser): - expected_options = { - "section_1": {"option_1"}, - "section_2": {"option_2"}, - "section_3": {"option_3"}, - "section_4": {"option_4"}, - "section number 5": {"option_5", "multi_option", "option_5_1"}, - } - - for section, options in expected_options.items(): - assert options.issubset( - parser.get_options(section) - ), f"Expected options: {options} in section: {section}, got: {parser.get_options(section)}" - assert "_raw" not in parser.get_options(section) - assert all( - not option.startswith("#_") for option in parser.get_options(section) - ) - - -def test_has_option(parser): - assert parser.has_option("section_1", "option_1") is True - assert parser.has_option("section_1", "option_128") is False - # section does not exist: - assert parser.has_option("section_128", "option_1") is False - - -def test_getval(parser): - # test regular option values - assert parser.getval("section_1", "option_1") == "value_1" - assert parser.getval("section_3", "option_3") == "value_3" - assert parser.getval("section_4", "option_4") == "value_4" - assert parser.getval("section number 5", "option_5") == "this.is.value-5" - assert parser.getval("section number 5", "option_5_1") == "value_5_1" - assert parser.getval("section_2", "option_2") == "value_2" - - # test multiline option values - ml_val = parser.getvals("section number 5", "multi_option") - assert isinstance(ml_val, list) - assert len(ml_val) > 0 - - -def test_getval_fallback(parser): - assert parser.getval("section_1", "option_128", "fallback") == "fallback" - assert parser.getval("section_1", "option_128", None) is None - - -def test_getval_exceptions(parser): - with pytest.raises(NoSectionError): - parser.getval("section_128", "option_1") - - with pytest.raises(NoOptionError): - parser.getval("section_1", "option_128") - - -def test_getint(parser): - value = parser.getint("section_1", "option_1_2") - assert isinstance(value, int) - - -def test_getint_from_val(parser): - with pytest.raises(ValueError): - parser.getint("section_1", "option_1") - - -def test_getint_from_float(parser): - with pytest.raises(ValueError): - parser.getint("section_1", "option_1_3") - - -def test_getint_from_boolean(parser): - with pytest.raises(ValueError): - parser.getint("section_1", "option_1_1") - - -def test_getint_fallback(parser): - assert parser.getint("section_1", "option_128", 128) == 128 - assert parser.getint("section_1", "option_128", None) is None - - -def test_getboolean(parser): - value = parser.getboolean("section_1", "option_1_1") - assert isinstance(value, bool) - assert value is True or value is False - - -def test_getboolean_from_val(parser): - with pytest.raises(ValueError): - parser.getboolean("section_1", "option_1") - - -def test_getboolean_from_int(parser): - with pytest.raises(ValueError): - parser.getboolean("section_1", "option_1_2") - - -def test_getboolean_from_float(parser): - with pytest.raises(ValueError): - parser.getboolean("section_1", "option_1_3") - - -def test_getboolean_fallback(parser): - assert parser.getboolean("section_1", "option_128", True) is True - assert parser.getboolean("section_1", "option_128", False) is False - assert parser.getboolean("section_1", "option_128", None) is None - - -def test_getfloat(parser): - value = parser.getfloat("section_1", "option_1_3") - assert isinstance(value, float) - - -def test_getfloat_from_val(parser): - with pytest.raises(ValueError): - parser.getfloat("section_1", "option_1") - - -def test_getfloat_from_int(parser): - value = parser.getfloat("section_1", "option_1_2") - assert isinstance(value, float) - - -def test_getfloat_from_boolean(parser): - with pytest.raises(ValueError): - parser.getfloat("section_1", "option_1_1") - - -def test_getfloat_fallback(parser): - assert parser.getfloat("section_1", "option_128", 1.234) == 1.234 - assert parser.getfloat("section_1", "option_128", None) is None - - -def test_set_existing_option(parser): - parser.set_option("section_1", "new_option", "new_value") - assert parser.getval("section_1", "new_option") == "new_value" - assert parser.config["section_1"]["elements"][4] is not None - assert parser.config["section_1"]["elements"][4]["type"] == LineType.OPTION.value - assert parser.config["section_1"]["elements"][4]["name"] == "new_option" - assert parser.config["section_1"]["elements"][4]["value"] == "new_value" - assert parser.config["section_1"]["elements"][4]["raw"] == "new_option: new_value\n" - - -def test_set_new_option(parser): - parser.set_option("new_section", "very_new_option", "very_new_value") - assert ( - parser.has_section("new_section") is True - ), f"Expected 'new_section' in {parser.get_sections()}" - assert parser.getval("new_section", "very_new_option") == "very_new_value" - - parser.set_option("section_2", "array_option", ["value_1", "value_2", "value_3"]) - assert parser.getvals("section_2", "array_option") == [ - "value_1", - "value_2", - "value_3", - ] - - assert parser.config["section_2"]["elements"][1] is not None - assert parser.config["section_2"]["elements"][1]["type"] == LineType.OPTION_BLOCK.value - assert parser.config["section_2"]["elements"][1]["name"] == "array_option" - assert parser.config["section_2"]["elements"][1]["value"] == [ - "value_1", - "value_2", - "value_3", - ] - assert parser.config["section_2"]["elements"][1]["raw"] == "array_option:\n" - - -def test_remove_option(parser): - parser.remove_option("section_1", "option_1") - assert parser.has_option("section_1", "option_1") is False diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_read_file.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/test_read_file.py deleted file mode 100644 index f9272df..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_read_file.py +++ /dev/null @@ -1,22 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 - -from src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) - -BASE_DIR = Path(__file__).parent.parent.joinpath("assets") -TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg") - - -def test_read_file(): - parser = SimpleConfigParser() - parser.read_file(TEST_DATA_PATH) - assert parser.config is not None - assert parser.config.keys() is not None diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_sections_api.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/test_sections_api.py deleted file mode 100644 index 0b731ce..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_sections_api.py +++ /dev/null @@ -1,65 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# https://github.com/dw-0/simple-config-parser # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -import pytest - -from src.simple_config_parser.simple_config_parser import ( - DuplicateSectionError, -) - - -def test_get_sections(parser): - expected_keys = { - "section_1", - "section_2", - "section_3", - "section_4", - "section number 5", - } - assert expected_keys.issubset( - parser.get_sections() - ), f"Expected keys: {expected_keys}, got: {parser.get_sections()}" - - -def test_has_section(parser): - assert parser.has_section("section_1") is True - assert parser.has_section("not_available") is False - - -def test_add_section(parser): - pre_add_count = len(parser.get_sections()) - parser.add_section("new_section") - parser.add_section("new_section2") - assert parser.has_section("new_section") is True - assert parser.has_section("new_section2") is True - assert len(parser.get_sections()) == pre_add_count + 2 - - new_section = parser.config["new_section"] - assert isinstance(new_section, dict) - assert new_section["header"] == "[new_section]\n" - assert new_section["elements"] is not None - assert new_section["elements"] == [] - - new_section2 = parser.config["new_section2"] - assert isinstance(new_section2, dict) - assert new_section2["header"] == "[new_section2]\n" - assert new_section2["elements"] is not None - assert new_section2["elements"] == [] - - -def test_add_section_duplicate(parser): - with pytest.raises(DuplicateSectionError): - parser.add_section("section_1") - - -def test_remove_section(parser): - pre_remove_count = len(parser.get_sections()) - parser.remove_section("section_1") - assert parser.has_section("section_1") is False - assert len(parser.get_sections()) == pre_remove_count - 1 - assert "section_1" not in parser.config diff --git a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_write_file.py b/kiauh/core/submodules/simple_config_parser/tests/public_api/test_write_file.py deleted file mode 100644 index 67b205b..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/public_api/test_write_file.py +++ /dev/null @@ -1,119 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) - -BASE_DIR = Path(__file__).parent.parent.joinpath("assets") -TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg") -# TEST_DATA_PATH_2 = BASE_DIR.joinpath("test_config_1_write.cfg") - - -def test_write_file_exception(): - parser = SimpleConfigParser() - with pytest.raises(ValueError): - parser.write_file(None) # noqa - - -def test_write_to_file(tmp_path): - tmp_file = Path(tmp_path).joinpath("tmp_config.cfg") - parser1 = SimpleConfigParser() - parser1.read_file(TEST_DATA_PATH) - # parser1.write_file(TEST_DATA_PATH_2) - parser1.write_file(tmp_file) - - parser2 = SimpleConfigParser() - parser2.read_file(tmp_file) - - assert tmp_file.exists() - assert parser2.config is not None - - with open(TEST_DATA_PATH, "r") as original, open(tmp_file, "r") as written: - assert original.read() == written.read() - -def test_remove_option_and_write(tmp_path): - # Setup paths - test_dir = BASE_DIR.joinpath("write_tests/remove_option") - input_file = test_dir.joinpath("input.cfg") - expected_file = test_dir.joinpath("expected.cfg") - output_file = Path(tmp_path).joinpath("output.cfg") - - # Read input file and remove option - parser = SimpleConfigParser() - parser.read_file(input_file) - parser.remove_option("section_1", "option_to_remove") - - # Write modified config - parser.write_file(output_file) - # parser.write_file(test_dir.joinpath("output.cfg")) - - # Compare with expected output - with open(expected_file, "r") as expected, open(output_file, "r") as actual: - assert expected.read() == actual.read() - - # Additional verification - parser2 = SimpleConfigParser() - parser2.read_file(output_file) - assert not parser2.has_option("section_1", "option_to_remove") - -def test_remove_section_and_write(tmp_path): - # Setup paths - test_dir = BASE_DIR.joinpath("write_tests/remove_section") - input_file = test_dir.joinpath("input.cfg") - expected_file = test_dir.joinpath("expected.cfg") - output_file = Path(tmp_path).joinpath("output.cfg") - - # Read input file and remove section - parser = SimpleConfigParser() - parser.read_file(input_file) - parser.remove_section("section_to_remove") - - # Write modified config - parser.write_file(output_file) - # parser.write_file(test_dir.joinpath("output.cfg")) - - # Compare with expected output - with open(expected_file, "r") as expected, open(output_file, "r") as actual: - assert expected.read() == actual.read() - - # Additional verification - parser2 = SimpleConfigParser() - parser2.read_file(output_file) - assert not parser2.has_section("section_to_remove") - assert "section_1" in parser2.get_sections() - assert "section_2" in parser2.get_sections() - -def test_add_option_and_write(tmp_path): - # Setup paths - test_dir = BASE_DIR.joinpath("write_tests/add_option") - input_file = test_dir.joinpath("input.cfg") - expected_file = test_dir.joinpath("expected.cfg") - output_file = Path(tmp_path).joinpath("output.cfg") - - # Read input file and add option - parser = SimpleConfigParser() - parser.read_file(input_file) - parser.set_option("section_1", "new_option", "new_value") - - # Write modified config - parser.write_file(output_file) - # parser.write_file(test_dir.joinpath("output.cfg")) - - # Compare with expected output - with open(expected_file, "r") as expected, open(output_file, "r") as actual: - assert expected.read() == actual.read() - - # Additional verification - parser2 = SimpleConfigParser() - parser2.read_file(output_file) - assert parser2.has_option("section_1", "new_option") - assert parser2.getval("section_1", "new_option") == "new_value" diff --git a/kiauh/core/submodules/simple_config_parser/tests/utils.py b/kiauh/core/submodules/simple_config_parser/tests/utils.py deleted file mode 100644 index 91299cf..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/utils.py +++ /dev/null @@ -1,15 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 - - -def load_testdata_from_file(file_path: Path): - """Helper function to load test data from a text file""" - - with open(file_path, "r") as f: - return [line.replace("\n", "") for line in f] diff --git a/kiauh/core/submodules/simple_config_parser/tests/value_conversion/__init__.py b/kiauh/core/submodules/simple_config_parser/tests/value_conversion/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/submodules/simple_config_parser/tests/value_conversion/test_get_conv.py b/kiauh/core/submodules/simple_config_parser/tests/value_conversion/test_get_conv.py deleted file mode 100644 index 893ecc1..0000000 --- a/kiauh/core/submodules/simple_config_parser/tests/value_conversion/test_get_conv.py +++ /dev/null @@ -1,89 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2024 Dominik Willner # -# # -# 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 src.simple_config_parser.simple_config_parser import SimpleConfigParser -from tests.utils import load_testdata_from_file - -BASE_DIR = Path(__file__).parent.parent.joinpath("assets") -TEST_DATA_PATH = BASE_DIR.joinpath("test_config_1.cfg") - - -@pytest.fixture -def parser(): - parser = SimpleConfigParser() - for line in load_testdata_from_file(TEST_DATA_PATH): - parser._parse_line(line) # noqa - - return parser - - -def test_get_int_conv(parser): - should_be_int = parser._get_conv("section_1", "option_1_2", int) - assert isinstance(should_be_int, int) - - -def test_get_float_conv(parser): - should_be_float = parser._get_conv("section_1", "option_1_3", float) - assert isinstance(should_be_float, float) - - -def test_get_bool_conv(parser): - should_be_bool = parser._get_conv( - "section_1", "option_1_1", parser._convert_to_boolean - ) - assert isinstance(should_be_bool, bool) - - -def test_get_int_conv_fallback(parser): - should_be_fallback_int = parser._get_conv( - "section_1", "option_128", int, fallback=128 - ) - assert isinstance(should_be_fallback_int, int) - assert should_be_fallback_int == 128 - assert parser._get_conv("section_1", "option_128", int, None) is None - - -def test_get_float_conv_fallback(parser): - should_be_fallback_float = parser._get_conv( - "section_1", "option_128", float, fallback=1.234 - ) - assert isinstance(should_be_fallback_float, float) - assert should_be_fallback_float == 1.234 - - assert parser._get_conv("section_1", "option_128", float, None) is None - - -def test_get_bool_conv_fallback(parser): - should_be_fallback_bool = parser._get_conv( - "section_1", "option_128", parser._convert_to_boolean, fallback=True - ) - assert isinstance(should_be_fallback_bool, bool) - assert should_be_fallback_bool is True - - assert ( - parser._get_conv("section_1", "option_128", parser._convert_to_boolean, None) - is None - ) - - -def test_get_int_conv_exception(parser): - with pytest.raises(ValueError): - parser._get_conv("section_1", "option_1", int) - - -def test_get_float_conv_exception(parser): - with pytest.raises(ValueError): - parser._get_conv("section_1", "option_1", float) - - -def test_get_bool_conv_exception(parser): - with pytest.raises(ValueError): - parser._get_conv("section_1", "option_1", parser._convert_to_boolean) diff --git a/kiauh/core/types/__init__.py b/kiauh/core/types/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/core/types/color.py b/kiauh/core/types/color.py deleted file mode 100644 index 11a317a..0000000 --- a/kiauh/core/types/color.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from enum import Enum - - -class Color(Enum): - WHITE = "\033[37m" # white - MAGENTA = "\033[35m" # magenta - GREEN = "\033[92m" # bright green - YELLOW = "\033[93m" # bright yellow - RED = "\033[91m" # bright red - CYAN = "\033[96m" # bright cyan - RST = "\033[0m" # reset format - - def __str__(self): - return self.value - - @staticmethod - def apply(text: str | int, color: "Color") -> str: - """Apply a given color to a given text string.""" - return f"{color}{text}{Color.RST}" diff --git a/kiauh/core/types/component_status.py b/kiauh/core/types/component_status.py deleted file mode 100644 index 09a02ce..0000000 --- a/kiauh/core/types/component_status.py +++ /dev/null @@ -1,32 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass -from typing import Dict, Literal - -StatusText = Literal["Installed", "Not installed", "Incomplete"] -StatusCode = Literal[0, 1, 2] -StatusMap: Dict[StatusCode, StatusText] = { - 0: "Not installed", - 1: "Incomplete", - 2: "Installed", -} - - -@dataclass -class ComponentStatus: - status: StatusCode - owner: str | None = None - repo: str | None = None - repo_url: str | None = None - branch: str = "" - local: str | None = None - remote: str | None = None - instances: int | None = None diff --git a/kiauh/extensions/__init__.py b/kiauh/extensions/__init__.py deleted file mode 100644 index b287ee9..0000000 --- a/kiauh/extensions/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -EXTENSION_ROOT = Path(__file__).resolve().parents[1].joinpath("extensions") diff --git a/kiauh/extensions/base_extension.py b/kiauh/extensions/base_extension.py deleted file mode 100644 index b0c34e4..0000000 --- a/kiauh/extensions/base_extension.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from abc import ABC, abstractmethod -from typing import Dict - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class BaseExtension(ABC): - def __init__(self, metadata: Dict[str, str]): - self.metadata = metadata - - @abstractmethod - def install_extension(self, **kwargs) -> None: - raise NotImplementedError - - def update_extension(self, **kwargs) -> None: - raise NotImplementedError - - @abstractmethod - def remove_extension(self, **kwargs) -> None: - raise NotImplementedError diff --git a/kiauh/extensions/extensions_menu.py b/kiauh/extensions/extensions_menu.py deleted file mode 100644 index cfc539f..0000000 --- a/kiauh/extensions/extensions_menu.py +++ /dev/null @@ -1,185 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import importlib -import inspect -import json -import textwrap -from pathlib import Path -from typing import Dict, List, Type - -from core.logger import Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from extensions import EXTENSION_ROOT -from extensions.base_extension import BaseExtension - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class ExtensionsMenu(BaseMenu): - def __init__(self, previous_menu: Type[BaseMenu] | None = None): - super().__init__() - self.title = "Extensions Menu" - self.title_color = Color.CYAN - self.previous_menu: Type[BaseMenu] | None = previous_menu - self.extensions: Dict[str, BaseExtension] = self.discover_extensions() - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from core.menus.main_menu import MainMenu - - self.previous_menu = previous_menu if previous_menu is not None else MainMenu - - def set_options(self) -> None: - self.options = { - i: Option(self.extension_submenu, opt_data=self.extensions.get(i)) - for i in self.extensions - } - - def discover_extensions(self) -> Dict[str, BaseExtension]: - ext_dict = {} - - for ext in EXTENSION_ROOT.iterdir(): - metadata_json = Path(ext).joinpath("metadata.json") - if not metadata_json.exists(): - continue - - try: - with open(metadata_json, "r") as m: - # read extension metadata from json - metadata = json.load(m).get("metadata") - module_name = metadata.get("module") - module_path = f"kiauh.extensions.{ext.name}.{module_name}" - - # get the class name of the extension - module = importlib.import_module(module_path) - - def predicate(o): - return ( - inspect.isclass(o) - and issubclass(o, BaseExtension) - and o != BaseExtension - ) - - ext_class: type = inspect.getmembers(module, predicate)[0][1] - - # instantiate the extension with its metadata and add to dict - ext_instance: BaseExtension = ext_class(metadata) - ext_dict[f"{metadata.get('index')}"] = ext_instance - - except (IOError, json.JSONDecodeError, ImportError) as e: - print(f"Failed loading extension {ext}: {e}") - - return dict(sorted(ext_dict.items(), key=lambda x: int(x[0]))) - - def extension_submenu(self, **kwargs): - ExtensionSubmenu(kwargs.get("opt_data"), self.__class__).run() - - def print_menu(self) -> None: - line1 = Color.apply("Available Extensions:", Color.YELLOW) - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ {line1:<62} ║ - ║ ║ - """ - )[1:] - print(menu, end="") - - for extension in self.extensions.values(): - index = extension.metadata.get("index") - name = extension.metadata.get("display_name") - row = f"{index}) {name}" - print(f"║ {row:<53} ║") - print("╟───────────────────────────────────────────────────────╢") - - -# noinspection PyUnusedLocal -# noinspection PyMethodMayBeStatic -class ExtensionSubmenu(BaseMenu): - def __init__( - self, extension: BaseExtension, previous_menu: Type[BaseMenu] | None = None - ): - super().__init__() - self.title = extension.metadata.get("display_name") - self.title_color = Color.YELLOW - self.extension = extension - self.previous_menu: Type[BaseMenu] | None = previous_menu - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - self.previous_menu = ( - previous_menu if previous_menu is not None else ExtensionsMenu - ) - - def set_options(self) -> None: - self.options["1"] = Option(self.extension.install_extension) - if self.extension.metadata.get("updates"): - self.options["2"] = Option(self.extension.update_extension) - self.options["3"] = Option(self.extension.remove_extension) - else: - self.options["2"] = Option(self.extension.remove_extension) - - def print_menu(self) -> None: - line_width = 53 - description: List[str] = self.extension.metadata.get("description", []) - description_text = Logger.format_content( - description, - line_width, - border_left="║", - border_right="║", - ) - - menu = textwrap.dedent( - """ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - menu += f"{description_text}\n" - - # add links if available - website: str = (self.extension.metadata.get("website") or "").strip() - repo: str = (self.extension.metadata.get("repo") or "").strip() - if website or repo: - links_lines: List[str] = ["Links:"] - if website: - links_lines.append(f"● {website}") - if repo: - links_lines.append(f"● {repo}") - - links_text = Logger.format_content( - links_lines, - line_width, - border_left="║", - border_right="║", - ) - - menu += textwrap.dedent( - """ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - menu += f"{links_text}\n" - - menu += textwrap.dedent( - """ - ╟───────────────────────────────────────────────────────╢ - ║ 1) Install ║ - """ - )[1:] - - if self.extension.metadata.get("updates"): - menu += "║ 2) Update ║\n" - menu += "║ 3) Remove ║\n" - else: - menu += "║ 2) Remove ║\n" - menu += "╟───────────────────────────────────────────────────────╢\n" - - print(menu, end="") diff --git a/kiauh/extensions/gcode_shell_cmd/__init__.py b/kiauh/extensions/gcode_shell_cmd/__init__.py deleted file mode 100644 index 6714ef1..0000000 --- a/kiauh/extensions/gcode_shell_cmd/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -EXT_MODULE_NAME = "gcode_shell_command.py" -MODULE_PATH = Path(__file__).resolve().parent -MODULE_ASSETS = MODULE_PATH.joinpath("assets") -KLIPPER_DIR = Path.home().joinpath("klipper") -KLIPPER_EXTRAS = KLIPPER_DIR.joinpath("klippy/extras") -EXTENSION_SRC = MODULE_ASSETS.joinpath(EXT_MODULE_NAME) -EXTENSION_TARGET_PATH = KLIPPER_EXTRAS.joinpath(EXT_MODULE_NAME) -EXAMPLE_CFG_SRC = MODULE_ASSETS.joinpath("shell_command.cfg") diff --git a/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py b/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py deleted file mode 100644 index 85b664b..0000000 --- a/kiauh/extensions/gcode_shell_cmd/assets/gcode_shell_command.py +++ /dev/null @@ -1,94 +0,0 @@ -# Run a shell command via gcode -# -# Copyright (C) 2019 Eric Callahan -# -# This file may be distributed under the terms of the GNU GPLv3 license. -import logging -import os -import shlex -import subprocess - - -class ShellCommand: - def __init__(self, config): - self.name = config.get_name().split()[-1] - self.printer = config.get_printer() - self.gcode = self.printer.lookup_object("gcode") - cmd = config.get("command") - cmd = os.path.expanduser(cmd) - self.command = shlex.split(cmd) - self.timeout = config.getfloat("timeout", 2.0, above=0.0) - self.verbose = config.getboolean("verbose", True) - self.proc_fd = None - self.partial_output = "" - self.gcode.register_mux_command( - "RUN_SHELL_COMMAND", - "CMD", - self.name, - self.cmd_RUN_SHELL_COMMAND, - desc=self.cmd_RUN_SHELL_COMMAND_help, - ) - - def _process_output(self, eventime): - if self.proc_fd is None: - return - try: - data = os.read(self.proc_fd, 4096) - except Exception: - pass - data = self.partial_output + data.decode() - if "\n" not in data: - self.partial_output = data - return - elif data[-1] != "\n": - split = data.rfind("\n") + 1 - self.partial_output = data[split:] - data = data[:split] - else: - self.partial_output = "" - self.gcode.respond_info(data) - - cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command" - - def cmd_RUN_SHELL_COMMAND(self, params): - gcode_params = params.get("PARAMS", "") - gcode_params = shlex.split(gcode_params) - reactor = self.printer.get_reactor() - try: - proc = subprocess.Popen( - self.command + gcode_params, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - except Exception: - logging.exception("shell_command: Command {%s} failed" % (self.name)) - raise self.gcode.error("Error running command {%s}" % (self.name)) - if self.verbose: - self.proc_fd = proc.stdout.fileno() - self.gcode.respond_info("Running Command {%s}...:" % (self.name)) - hdl = reactor.register_fd(self.proc_fd, self._process_output) - eventtime = reactor.monotonic() - endtime = eventtime + self.timeout - complete = False - while eventtime < endtime: - eventtime = reactor.pause(eventtime + 0.05) - if proc.poll() is not None: - complete = True - break - if not complete: - proc.terminate() - if self.verbose: - if self.partial_output: - self.gcode.respond_info(self.partial_output) - self.partial_output = "" - if complete: - msg = "Command {%s} finished\n" % (self.name) - else: - msg = "Command {%s} timed out" % (self.name) - self.gcode.respond_info(msg) - reactor.unregister_fd(hdl) - self.proc_fd = None - - -def load_config_prefix(config): - return ShellCommand(config) diff --git a/kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg b/kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg deleted file mode 100644 index 34e7581..0000000 --- a/kiauh/extensions/gcode_shell_cmd/assets/shell_command.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[gcode_shell_command hello_world] -command: echo hello world -timeout: 2. -verbose: True -[gcode_macro HELLO_WORLD] -gcode: - RUN_SHELL_COMMAND CMD=hello_world \ No newline at end of file diff --git a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py b/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py deleted file mode 100644 index 2f57762..0000000 --- a/kiauh/extensions/gcode_shell_cmd/gcode_shell_cmd_extension.py +++ /dev/null @@ -1,134 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -import os -import shutil -from datetime import datetime -from typing import List - -from components.klipper.klipper import Klipper -from core.instance_manager.instance_manager import InstanceManager -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 ( - SimpleConfigParser, -) -from extensions.base_extension import BaseExtension -from extensions.gcode_shell_cmd import ( - EXAMPLE_CFG_SRC, - EXTENSION_SRC, - EXTENSION_TARGET_PATH, - KLIPPER_DIR, - KLIPPER_EXTRAS, -) -from utils.fs_utils import check_file_exist -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances - - -# noinspection PyMethodMayBeStatic -class GcodeShellCmdExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - install_example = get_confirm("Create an example shell command?", False, False) - - klipper_dir_exists = check_file_exist(KLIPPER_DIR) - if not klipper_dir_exists: - Logger.print_warn( - "No Klipper directory found! Unable to install extension." - ) - return - - extension_installed = check_file_exist(EXTENSION_TARGET_PATH) - overwrite = True - if extension_installed: - overwrite = get_confirm( - "Extension seems to be installed already. Overwrite?", - True, - False, - ) - - if not overwrite: - Logger.print_warn("Installation aborted due to user request.") - return - - instances = get_instances(Klipper) - InstanceManager.stop_all(instances) - - try: - Logger.print_status(f"Copy extension to '{KLIPPER_EXTRAS}' ...") - shutil.copy(EXTENSION_SRC, EXTENSION_TARGET_PATH) - except OSError as e: - Logger.print_error(f"Unable to install extension: {e}") - return - - if install_example: - self.install_example_cfg(instances) - - InstanceManager.start_all(instances) - - Logger.print_ok("Installing G-Code Shell Command extension successful!") - - def remove_extension(self, **kwargs) -> None: - extension_installed = check_file_exist(EXTENSION_TARGET_PATH) - if not extension_installed: - Logger.print_info("Extension does not seem to be installed! Skipping ...") - return - - question = "Do you really want to remove the extension?" - if get_confirm(question, True, False): - try: - Logger.print_status(f"Removing '{EXTENSION_TARGET_PATH}' ...") - os.remove(EXTENSION_TARGET_PATH) - Logger.print_ok("Extension successfully removed!") - except OSError as e: - Logger.print_error(f"Unable to remove extension: {e}") - - Logger.print_warn("PLEASE NOTE:") - Logger.print_warn( - "Remaining gcode shell command will cause Klipper to throw an error." - ) - Logger.print_warn("Make sure to remove them from the printer.cfg!") - - def install_example_cfg(self, instances: List[Klipper]): - cfg_dirs = [instance.base.cfg_dir for instance in instances] - # copy extension to klippy/extras - for cfg_dir in cfg_dirs: - Logger.print_status(f"Create shell_command.cfg in '{cfg_dir}' ...") - if check_file_exist(cfg_dir.joinpath("shell_command.cfg")): - Logger.print_info("File already exists! Skipping ...") - continue - try: - shutil.copy(EXAMPLE_CFG_SRC, cfg_dir) - Logger.print_ok("Done!") - except OSError as e: - Logger.warn(f"Unable to create example config: {e}") - - # backup each printer.cfg before modification - svc = BackupService() - timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") - for instance in instances: - svc.backup_file( - source_path=instance.cfg_file, - target_path=f"{instance.data_dir.name}/config_{timestamp}", - target_name=instance.cfg_file.name, - ) - - # add section to printer.cfg if not already defined - section = "include shell_command.cfg" - cfg_files = [instance.cfg_file for instance in instances] - for cfg_file in cfg_files: - Logger.print_status(f"Include shell_command.cfg in '{cfg_file}' ...") - scp = SimpleConfigParser() - scp.read_file(cfg_file) - if scp.has_section(section): - Logger.print_info("Section already defined! Skipping ...") - continue - scp.add_section(section) - scp.write_file(cfg_file) - Logger.print_ok("Done!") diff --git a/kiauh/extensions/gcode_shell_cmd/metadata.json b/kiauh/extensions/gcode_shell_cmd/metadata.json deleted file mode 100644 index efdc2cc..0000000 --- a/kiauh/extensions/gcode_shell_cmd/metadata.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "metadata": { - "index": 1, - "module": "gcode_shell_cmd_extension", - "maintained_by": "dw-0", - "display_name": "G-Code Shell Command", - "description": ["Run a shell commands from gcode."], - "updates": false - } -} diff --git a/kiauh/extensions/klipper_backup/__init__.py b/kiauh/extensions/klipper_backup/__init__.py deleted file mode 100644 index e65e0f5..0000000 --- a/kiauh/extensions/klipper_backup/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet # -# https://github.com/Staubgeborener/klipper-backup # -# https://klipperbackup.xyz # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -EXT_MODULE_NAME = "klipper_backup_extension.py" -MODULE_PATH = Path(__file__).resolve().parent -MOONRAKER_CONF = Path.home().joinpath("printer_data", "config", "moonraker.conf") -KLIPPERBACKUP_DIR = Path.home().joinpath("klipper-backup") -KLIPPERBACKUP_CONFIG_DIR = Path.home().joinpath("config_backup") -KLIPPERBACKUP_REPO_URL = "https://github.com/staubgeborener/klipper-backup" diff --git a/kiauh/extensions/klipper_backup/klipper_backup_extension.py b/kiauh/extensions/klipper_backup/klipper_backup_extension.py deleted file mode 100644 index a3e1242..0000000 --- a/kiauh/extensions/klipper_backup/klipper_backup_extension.py +++ /dev/null @@ -1,160 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2023 - 2024 Staubgeborener and Tylerjet # -# https://github.com/Staubgeborener/klipper-backup # -# https://klipperbackup.xyz # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -import os -import subprocess -from pathlib import Path - -from core.constants import SYSTEMD -from core.logger import Logger -from extensions.base_extension import BaseExtension -from extensions.klipper_backup import ( - KLIPPERBACKUP_CONFIG_DIR, - KLIPPERBACKUP_DIR, - KLIPPERBACKUP_REPO_URL, - MOONRAKER_CONF, -) -from utils.fs_utils import check_file_exist, remove_with_sudo -from utils.git_utils import git_cmd_clone -from utils.input_utils import get_confirm -from utils.sys_utils import cmd_sysctl_manage, remove_system_service, unit_file_exists - - -class KlipperbackupExtension(BaseExtension): - def remove_extension(self, **kwargs) -> None: - if not check_file_exist(KLIPPERBACKUP_DIR): - Logger.print_info("Extension does not seem to be installed! Skipping ...") - return - - def uninstall_service(service_name: str, unit_type: str) -> bool: - try: - full_service_name = f"{service_name}.{unit_type}" - if unit_type == "service": - remove_system_service(full_service_name) - elif unit_type == "timer": - full_service_path: Path = SYSTEMD.joinpath(full_service_name) - Logger.print_status(f"Removing {full_service_name} ...") - remove_with_sudo(full_service_path) - Logger.print_ok(f"{service_name}.{unit_type} successfully removed!") - cmd_sysctl_manage("daemon-reload") - cmd_sysctl_manage("reset-failed") - else: - Logger.print_error( - f"Unknown unit type {unit_type} of {full_service_name}" - ) - except: - Logger.print_error(f"Failed to remove {full_service_name}: {str(e)}") - - def check_crontab_entry(entry) -> bool: - try: - crontab_content = subprocess.check_output( - ["crontab", "-l"], stderr=subprocess.DEVNULL, text=True - ) - except subprocess.CalledProcessError: - return False - return any(entry in line for line in crontab_content.splitlines()) - - def remove_moonraker_entry(): - original_file_path = MOONRAKER_CONF - comparison_file_path = os.path.join( - str(KLIPPERBACKUP_DIR), "install-files", "moonraker.conf" - ) - if not ( - os.path.exists(original_file_path) - and os.path.exists(comparison_file_path) - ): - return False - with open(original_file_path, "r") as original_file, open( - comparison_file_path, "r" - ) as comparison_file: - original_content = original_file.read() - comparison_content = comparison_file.read() - if comparison_content in original_content: - Logger.print_status("Removing Klipper-Backup moonraker entry ...") - modified_content = original_content.replace( - comparison_content, "" - ).strip() - modified_content = "\n".join( - line for line in modified_content.split("\n") if line.strip() - ) - with open(original_file_path, "w") as original_file: - original_file.write(modified_content) - Logger.print_ok("Klipper-Backup moonraker entry successfully removed!") - return True - return False - - if get_confirm("Do you really want to remove the extension?", True, False): - # Remove systemd timer and services - service_names = [ - "klipper-backup-on-boot", - "klipper-backup-filewatch", - "klipper-backup", - ] - unit_types = ["timer", "service"] - - for service_name in service_names: - for unit_type in unit_types: - if unit_file_exists(service_name, unit_type): - uninstall_service(service_name, unit_type) - - # Remnove crontab entry - try: - if check_crontab_entry("/klipper-backup/script.sh"): - Logger.print_status("Removing Klipper-Backup crontab entry ...") - crontab_content = subprocess.check_output( - ["crontab", "-l"], text=True - ) - modified_content = "\n".join( - line - for line in crontab_content.splitlines() - if "/klipper-backup/script.sh" not in line - ) - subprocess.run( - ["crontab", "-"], - input=modified_content + "\n", - text=True, - check=True, - ) - Logger.print_ok( - "Klipper-Backup crontab entry successfully removed!" - ) - except subprocess.CalledProcessError: - Logger.print_error("Unable to remove the Klipper-Backup cron entry") - - # Remove moonraker entry - try: - remove_moonraker_entry() - except: - Logger.print_error( - "Unable to remove the Klipper-Backup moonraker entry" - ) - - # Remove Klipper-backup extension - Logger.print_status("Removing Klipper-Backup extension ...") - try: - remove_with_sudo(KLIPPERBACKUP_DIR) - if check_file_exist(KLIPPERBACKUP_CONFIG_DIR): - remove_with_sudo(KLIPPERBACKUP_CONFIG_DIR) - Logger.print_ok("Extension Klipper-Backup successfully removed!") - except: - Logger.print_error("Unable to remove Klipper-Backup extension") - - def install_extension(self, **kwargs) -> None: - if not KLIPPERBACKUP_DIR.exists(): - git_cmd_clone(KLIPPERBACKUP_REPO_URL, KLIPPERBACKUP_DIR) - subprocess.run(["chmod", "+x", str(KLIPPERBACKUP_DIR / "install.sh")]) - subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh")]) - - def update_extension(self, **kwargs) -> None: - if not check_file_exist(KLIPPERBACKUP_DIR): - Logger.print_info("Extension does not seem to be installed! Skipping ...") - return - subprocess.run([str(KLIPPERBACKUP_DIR / "install.sh"), "check_updates"]) diff --git a/kiauh/extensions/klipper_backup/metadata.json b/kiauh/extensions/klipper_backup/metadata.json deleted file mode 100644 index 85f6926..0000000 --- a/kiauh/extensions/klipper_backup/metadata.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "metadata": { - "index": 4, - "module": "klipper_backup_extension", - "maintained_by": "Staubgeborener", - "display_name": "Klipper-Backup", - "description": ["Backup all your Klipper files to GitHub"], - "website": "https://klipperbackup.xyz", - "repo": "https://github.com/Staubgeborener/klipper-backup", - "updates": true - } -} diff --git a/kiauh/extensions/mainsail_theme_installer/__init__.py b/kiauh/extensions/mainsail_theme_installer/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py deleted file mode 100644 index 82c50e0..0000000 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ /dev/null @@ -1,188 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import csv -import shutil -import textwrap -import urllib.request -from dataclasses import dataclass -from typing import Any, Dict, List, Type - -from components.klipper.klipper import Klipper -from components.klipper.klipper_dialogs import ( - DisplayType, - print_instance_overview, -) -from core.logger import Logger -from core.menus import Option -from core.menus.base_menu import BaseMenu -from core.types.color import Color -from extensions.base_extension import BaseExtension -from utils.git_utils import git_clone_wrapper -from utils.input_utils import get_selection_input -from utils.instance_type import InstanceType -from utils.instance_utils import get_instances - - -@dataclass -class ThemeData: - name: str - short_note: str - author: str - repo: str - - -# noinspection PyMethodMayBeStatic -class MainsailThemeInstallerExtension(BaseExtension): - instances: List[Klipper] = get_instances(Klipper) - - def install_extension(self, **kwargs) -> None: - MainsailThemeInstallMenu(self.instances).run() - - def remove_extension(self, **kwargs) -> None: - print_instance_overview( - self.instances, - display_type=DisplayType.PRINTER_NAME, - show_headline=True, - show_index=True, - show_select_all=True, - ) - printer_list = get_printer_selection(self.instances, True) - if printer_list is None: - return - - for printer in printer_list: - Logger.print_status(f"Uninstalling theme from {printer.base.cfg_dir} ...") - theme_dir = printer.base.cfg_dir.joinpath(".theme") - if not theme_dir.exists(): - Logger.print_info(f"{theme_dir} not found. Skipping ...") - continue - try: - shutil.rmtree(theme_dir) - Logger.print_ok("Theme successfully uninstalled!") - except OSError as e: - Logger.print_error("Unable to uninstall theme") - Logger.print_error(e) - - -# noinspection PyMethodMayBeStatic -class MainsailThemeInstallMenu(BaseMenu): - THEMES_URL: str = ( - "https://raw.githubusercontent.com/mainsail-crew/gb-docs/main/_data/themes.csv" - ) - - def __init__(self, instances: List[Klipper]): - super().__init__() - self.title = "Mainsail Theme Installer" - self.title_color = Color.YELLOW - self.themes: List[ThemeData] = self.load_themes() - self.instances = instances - - def set_previous_menu(self, previous_menu: Type[BaseMenu] | None) -> None: - from extensions.extensions_menu import ExtensionsMenu - - self.previous_menu = ( - previous_menu if previous_menu is not None else ExtensionsMenu - ) - - def set_options(self) -> None: - self.options = { - f"{index}": Option(self.install_theme, opt_index=f"{index}") - for index in range(len(self.themes)) - } - - def print_menu(self) -> None: - line1 = Color.apply( - "A preview of each Mainsail theme can be found here:", Color.YELLOW - ) - menu = textwrap.dedent( - f""" - ╟───────────────────────────────────────────────────────╢ - ║ {line1:<62} ║ - ║ https://docs.mainsail.xyz/theming/themes ║ - ╟───────────────────────────────────────────────────────╢ - """ - )[1:] - for i, theme in enumerate(self.themes): - j: str = f" {i}" if i < 10 else f"{i}" - row: str = f"{j}) [{theme.name}]" - menu += f"║ {row:<53} ║\n" - menu += "╟───────────────────────────────────────────────────────╢\n" - print(menu, end="") - - def load_themes(self) -> List[ThemeData]: - with urllib.request.urlopen(self.THEMES_URL) as response: - themes: List[ThemeData] = [] - content: str = response.read().decode() - csv_data: List[str] = content.splitlines() - fieldnames = ["name", "short_note", "author", "repo"] - csv_reader = csv.DictReader(csv_data, fieldnames=fieldnames, delimiter=",") - next(csv_reader) # skip the header of the csv file - for row in csv_reader: - row: Dict[str, str] # type: ignore - theme: ThemeData = ThemeData(**row) - themes.append(theme) - - return themes - - def install_theme(self, **kwargs: Any): - opt_index: str | None = kwargs.get("opt_index", None) - - if not opt_index: - raise ValueError("No option index provided") - - index: int = int(opt_index) - theme_data: ThemeData = self.themes[index] - theme_author: str = theme_data.author - theme_repo: str = theme_data.repo - theme_repo_url: str = f"https://github.com/{theme_author}/{theme_repo}" - - print_instance_overview( - self.instances, - display_type=DisplayType.PRINTER_NAME, - show_headline=True, - show_index=True, - show_select_all=True, - ) - - printer_list = get_printer_selection(self.instances, True) - if printer_list is None: - return - - for printer in printer_list: - git_clone_wrapper(theme_repo_url, printer.base.cfg_dir.joinpath(".theme")) - - if len(theme_data.short_note) > 1: - Logger.print_warn("Info from the creator:", prefix=False, start="\n") - Logger.print_info(theme_data.short_note, prefix=False, end="\n\n") - - -def get_printer_selection( - instances: List[InstanceType], is_install: bool -) -> List[InstanceType] | None: - options = [str(i) for i in range(len(instances))] - options.extend(["a", "b"]) - - if is_install: - q = "Select the printer to install the theme for" - else: - q = "Select the printer to remove the theme from" - selection = get_selection_input(q, options) - - install_for = [] - if selection == "b": - return None - elif selection == "a": - install_for.extend(instances) - else: - instance = instances[int(selection)] - install_for.append(instance) - - return install_for diff --git a/kiauh/extensions/mainsail_theme_installer/metadata.json b/kiauh/extensions/mainsail_theme_installer/metadata.json deleted file mode 100644 index fc01274..0000000 --- a/kiauh/extensions/mainsail_theme_installer/metadata.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "metadata": { - "index": 2, - "module": "mainsail_theme_installer_extension", - "maintained_by": "dw-0", - "display_name": "Mainsail Theme Installer", - "description": ["Install Mainsail Themes maintained by the Mainsail community."], - "website": "https://docs.mainsail.xyz/theming/themes", - "updates": false - } -} diff --git a/kiauh/extensions/mobileraker/__init__.py b/kiauh/extensions/mobileraker/__init__.py deleted file mode 100644 index 2169512..0000000 --- a/kiauh/extensions/mobileraker/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -from core.constants import SYSTEMD - -# repo -MOBILERAKER_REPO = "https://github.com/Clon1998/mobileraker_companion.git" - -# names -MOBILERAKER_SERVICE_NAME = "mobileraker.service" -MOBILERAKER_UPDATER_SECTION_NAME = "update_manager mobileraker" -MOBILERAKER_LOG_NAME = "mobileraker.log" - -# directories -MOBILERAKER_DIR = Path.home().joinpath("mobileraker_companion") -MOBILERAKER_ENV_DIR = Path.home().joinpath("mobileraker-env") - -# files -MOBILERAKER_INSTALL_SCRIPT = MOBILERAKER_DIR.joinpath("scripts/install.sh") -MOBILERAKER_REQ_FILE = MOBILERAKER_DIR.joinpath("scripts/mobileraker-requirements.txt") -MOBILERAKER_SERVICE_FILE = SYSTEMD.joinpath(MOBILERAKER_SERVICE_NAME) diff --git a/kiauh/extensions/mobileraker/metadata.json b/kiauh/extensions/mobileraker/metadata.json deleted file mode 100644 index 0a4969a..0000000 --- a/kiauh/extensions/mobileraker/metadata.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "metadata": { - "index": 3, - "module": "mobileraker_extension", - "maintained_by": "Clon1998", - "display_name": "Mobileraker", - "description": [ - "Companion for Mobileraker, enabling push notification for Klipper using Moonraker." - ], - "repo": "https://github.com/Clon1998/mobileraker_companion", - "updates": true - } -} diff --git a/kiauh/extensions/mobileraker/mobileraker_extension.py b/kiauh/extensions/mobileraker/mobileraker_extension.py deleted file mode 100644 index 2615d57..0000000 --- a/kiauh/extensions/mobileraker/mobileraker_extension.py +++ /dev/null @@ -1,193 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import shutil -from pathlib import Path -from subprocess import CalledProcessError, run -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.backup_service import BackupService -from core.settings.kiauh_settings import KiauhSettings -from extensions.base_extension import BaseExtension -from extensions.mobileraker import ( - MOBILERAKER_DIR, - MOBILERAKER_ENV_DIR, - MOBILERAKER_INSTALL_SCRIPT, - MOBILERAKER_LOG_NAME, - MOBILERAKER_REPO, - MOBILERAKER_REQ_FILE, - MOBILERAKER_SERVICE_FILE, - MOBILERAKER_SERVICE_NAME, - MOBILERAKER_UPDATER_SECTION_NAME, -) -from utils.common import check_install_dependencies -from utils.config_utils import add_config_section, remove_config_section -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - check_python_version, - cmd_sysctl_service, - install_python_requirements, - remove_system_service, -) - - -# noinspection PyMethodMayBeStatic -class MobilerakerExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing Mobileraker's companion ...") - - if not check_python_version(3, 7): - return - - mr_instances = get_instances(Moonraker) - if not mr_instances: - Logger.print_dialog( - DialogType.WARNING, - [ - "Moonraker not found! Mobileraker's companion will not properly " - "work without a working Moonraker installation.", - "Mobileraker's companion's update manager configuration for " - "Moonraker will not be added to any moonraker.conf.", - ], - ) - if not get_confirm( - "Continue Mobileraker's companion installation?", - default_choice=False, - allow_go_back=True, - ): - return - - check_install_dependencies() - - git_clone_wrapper(MOBILERAKER_REPO, MOBILERAKER_DIR) - - try: - run(MOBILERAKER_INSTALL_SCRIPT.as_posix(), shell=True, check=True) - if mr_instances: - self._patch_mobileraker_update_manager(mr_instances) - InstanceManager.restart_all(mr_instances) - else: - Logger.print_info( - "Moonraker is not installed! Cannot add Mobileraker's " - "companion to update manager!" - ) - Logger.print_ok("Mobileraker's companion successfully installed!") - except CalledProcessError as e: - Logger.print_error(f"Error installing Mobileraker's companion:\n{e}") - return - - def update_extension(self, **kwargs) -> None: - try: - if not MOBILERAKER_DIR.exists(): - Logger.print_info( - "Mobileraker's companion doesn't seem to be installed! Skipping ..." - ) - return - - Logger.print_status("Updating Mobileraker's companion ...") - - cmd_sysctl_service(MOBILERAKER_SERVICE_NAME, "stop") - - settings = KiauhSettings() - if settings.kiauh.backup_before_update: - self._backup_mobileraker_dir() - - git_pull_wrapper(MOBILERAKER_DIR) - - install_python_requirements(MOBILERAKER_ENV_DIR, MOBILERAKER_REQ_FILE) - - cmd_sysctl_service(MOBILERAKER_SERVICE_NAME, "start") - - Logger.print_ok("Mobileraker's companion updated successfully.", end="\n\n") - except CalledProcessError as e: - Logger.print_error(f"Error updating Mobileraker's companion:\n{e}") - return - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing Mobileraker's companion ...") - try: - if MOBILERAKER_DIR.exists(): - Logger.print_status("Removing Mobileraker's companion directory ...") - shutil.rmtree(MOBILERAKER_DIR) - Logger.print_ok( - "Mobileraker's companion directory successfully removed!" - ) - else: - Logger.print_warn("Mobileraker's companion directory not found!") - - if MOBILERAKER_ENV_DIR.exists(): - Logger.print_status("Removing Mobileraker's companion environment ...") - shutil.rmtree(MOBILERAKER_ENV_DIR) - Logger.print_ok( - "Mobileraker's companion environment successfully removed!" - ) - else: - Logger.print_warn("Mobileraker's companion environment not found!") - - if MOBILERAKER_SERVICE_FILE.exists(): - remove_system_service(MOBILERAKER_SERVICE_NAME) - - kl_instances: List[Klipper] = get_instances(Klipper) - for instance in kl_instances: - logfile = instance.base.log_dir.joinpath(MOBILERAKER_LOG_NAME) - if logfile.exists(): - Logger.print_status(f"Removing {logfile} ...") - Path(logfile).unlink() - Logger.print_ok(f"{logfile} successfully removed!") - - mr_instances: List[Moonraker] = get_instances(Moonraker) - if mr_instances: - Logger.print_status( - "Removing Mobileraker's companion from update manager ..." - ) - BackupService().backup_moonraker_conf() - remove_config_section(MOBILERAKER_UPDATER_SECTION_NAME, mr_instances) - Logger.print_ok( - "Mobileraker's companion successfully removed from update manager!" - ) - - Logger.print_ok("Mobileraker's companion successfully removed!") - - except Exception as e: - Logger.print_error(f"Error removing Mobileraker's companion:\n{e}") - - def _patch_mobileraker_update_manager(self, instances: List[Moonraker]) -> None: - BackupService().backup_moonraker_conf() - add_config_section( - section=MOBILERAKER_UPDATER_SECTION_NAME, - instances=instances, - options=[ - ("type", "git_repo"), - ("path", MOBILERAKER_DIR.as_posix()), - ("origin", MOBILERAKER_REPO), - ("primary_branch", "main"), - ("managed_services", "mobileraker"), - ("env", f"{MOBILERAKER_ENV_DIR}/bin/python"), - ("requirements", MOBILERAKER_REQ_FILE.as_posix()), - ("install_script", MOBILERAKER_INSTALL_SCRIPT.as_posix()), - ], - ) - - def _backup_mobileraker_dir(self) -> None: - svc = BackupService() - svc.backup_directory( - source_path=MOBILERAKER_DIR, - backup_name="mobileraker", - target_path="mobileraker", - ) - svc.backup_directory( - source_path=MOBILERAKER_ENV_DIR, - backup_name="mobileraker-env", - target_path="mobileraker", - ) diff --git a/kiauh/extensions/obico/__init__.py b/kiauh/extensions/obico/__init__.py deleted file mode 100644 index 6780bb1..0000000 --- a/kiauh/extensions/obico/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent - -# repo -OBICO_REPO = "https://github.com/TheSpaghettiDetective/moonraker-obico.git" - -# names -OBICO_SERVICE_NAME = "moonraker-obico.service" -OBICO_ENV_FILE_NAME = "moonraker-obico.env" -OBICO_CFG_NAME = "moonraker-obico.cfg" -OBICO_CFG_SAMPLE_NAME = "moonraker-obico.cfg.sample" -OBICO_LOG_NAME = "moonraker-obico.log" -OBICO_UPDATE_CFG_NAME = "moonraker-obico-update.cfg" -OBICO_UPDATE_CFG_SAMPLE_NAME = "moonraker-obico-update.cfg.sample" -OBICO_MACROS_CFG_NAME = "moonraker_obico_macros.cfg" - -# directories -OBICO_DIR = Path.home().joinpath("moonraker-obico") -OBICO_ENV_DIR = Path.home().joinpath("moonraker-obico-env") - -# files -OBICO_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{OBICO_SERVICE_NAME}") -OBICO_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{OBICO_ENV_FILE_NAME}") -OBICO_LINK_SCRIPT = OBICO_DIR.joinpath("scripts/link.sh") -OBICO_REQ_FILE = OBICO_DIR.joinpath("requirements.txt") diff --git a/kiauh/extensions/obico/assets/moonraker-obico.env b/kiauh/extensions/obico/assets/moonraker-obico.env deleted file mode 100644 index 3c3d32b..0000000 --- a/kiauh/extensions/obico/assets/moonraker-obico.env +++ /dev/null @@ -1 +0,0 @@ -OBICO_ARGS="-m moonraker_obico.app -c %CFG%" diff --git a/kiauh/extensions/obico/assets/moonraker-obico.service b/kiauh/extensions/obico/assets/moonraker-obico.service deleted file mode 100644 index e6bed45..0000000 --- a/kiauh/extensions/obico/assets/moonraker-obico.service +++ /dev/null @@ -1,16 +0,0 @@ -#Systemd service file for moonraker-obico -[Unit] -Description=Moonraker-Obico -After=network-online.target moonraker.service - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -WorkingDirectory=%OBICO_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python3 $OBICO_ARGS -Restart=always -RestartSec=5 diff --git a/kiauh/extensions/obico/metadata.json b/kiauh/extensions/obico/metadata.json deleted file mode 100644 index 0a26fdb..0000000 --- a/kiauh/extensions/obico/metadata.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "metadata": { - "index": 6, - "module": "moonraker_obico_extension", - "maintained_by": "Obico", - "display_name": "Obico for Klipper", - "description": [ - "Open source 3D Printing cloud and AI", - "- AI-Powered Failure Detection", - "- Free Remote Monitoring and Access", - "- 25FPS High-Def Webcam Streaming", - "- Free 4.9-Star Mobile App" - ], - "website": "https://obico.io", - "repo": "github.com/TheSpaghettiDetective/moonraker-obico", - "updates": true - } -} diff --git a/kiauh/extensions/obico/moonraker_obico.py b/kiauh/extensions/obico/moonraker_obico.py deleted file mode 100644 index ddf442c..0000000 --- a/kiauh/extensions/obico/moonraker_obico.py +++ /dev/null @@ -1,145 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError, run - -from components.moonraker.moonraker import Moonraker -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 ( - SimpleConfigParser, -) -from extensions.obico import ( - OBICO_CFG_NAME, - OBICO_DIR, - OBICO_ENV_DIR, - OBICO_ENV_FILE_NAME, - OBICO_ENV_FILE_TEMPLATE, - OBICO_LINK_SCRIPT, - OBICO_LOG_NAME, - OBICO_SERVICE_TEMPLATE, -) -from utils.fs_utils import create_folders -from utils.sys_utils import get_service_file_path - - -# noinspection PyMethodMayBeStatic -@dataclass(repr=True) -class MoonrakerObico: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name: str = OBICO_LOG_NAME - dir: Path = OBICO_DIR - env_dir: Path = OBICO_ENV_DIR - data_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - is_linked: bool = False - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path( - MoonrakerObico, self.suffix - ) - self.data_dir: Path = self.base.data_dir - self.cfg_file = self.base.cfg_dir.joinpath(OBICO_CFG_NAME) - self.is_linked: bool = self._check_link_status() - - def create(self) -> None: - from utils.sys_utils import create_env_file, create_service_file - - Logger.print_status("Creating new Obico for Klipper Instance ...") - - try: - create_folders(self.base.base_folders) - create_service_file( - name=self.service_file_path.name, - content=self._prep_service_file_content(), - ) - create_env_file( - path=self.base.sysd_dir.joinpath(OBICO_ENV_FILE_NAME), - content=self._prep_env_file_content(), - ) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - except OSError as e: - Logger.print_error(f"Error creating env file: {e}") - raise - - def link(self) -> None: - Logger.print_status( - f"Linking instance for printer {self.data_dir.name} to the Obico server ..." - ) - try: - cmd = [f"{OBICO_LINK_SCRIPT} -q -c {self.cfg_file}"] - if self.suffix: - cmd.append(f"-n {self.suffix}") - run(cmd, check=True, shell=True) - except CalledProcessError as e: - Logger.print_error(f"Error during Obico linking: {e}") - raise - - def _prep_service_file_content(self) -> str: - template = OBICO_SERVICE_TEMPLATE - - try: - with open(template, "r") as template_file: - template_content = template_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - service_content = template_content.replace( - "%USER%", - CURRENT_USER, - ) - service_content = service_content.replace( - "%OBICO_DIR%", - self.dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV%", - self.env_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV_FILE%", - self.base.sysd_dir.joinpath(OBICO_ENV_FILE_NAME).as_posix(), - ) - return service_content - - def _prep_env_file_content(self) -> str: - template = OBICO_ENV_FILE_TEMPLATE - - try: - with open(template, "r") as env_file: - env_template_file_content = env_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - env_file_content = env_template_file_content.replace( - "%CFG%", - f"{self.cfg_file}", - ) - return env_file_content - - def _check_link_status(self) -> bool: - if not self.cfg_file or not self.cfg_file.exists(): - return False - - scp = SimpleConfigParser() - scp.read_file(self.cfg_file) - return scp.getval("server", "auth_token", None) is not None diff --git a/kiauh/extensions/obico/moonraker_obico_extension.py b/kiauh/extensions/obico/moonraker_obico_extension.py deleted file mode 100644 index bf2b20d..0000000 --- a/kiauh/extensions/obico/moonraker_obico_extension.py +++ /dev/null @@ -1,380 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import shutil -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.instance_manager.base_instance import SUFFIX_BLACKLIST -from core.instance_manager.instance_manager import InstanceManager -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 ( - SimpleConfigParser, -) -from extensions.base_extension import BaseExtension -from extensions.obico import ( - OBICO_CFG_SAMPLE_NAME, - OBICO_DIR, - OBICO_ENV_DIR, - OBICO_MACROS_CFG_NAME, - OBICO_REPO, - OBICO_REQ_FILE, - OBICO_UPDATE_CFG_NAME, - OBICO_UPDATE_CFG_SAMPLE_NAME, -) -from extensions.obico.moonraker_obico import ( - MoonrakerObico, -) -from utils.common import ( - check_install_dependencies, - moonraker_exists, -) -from utils.config_utils import ( - add_config_section, - remove_config_section, -) -from utils.fs_utils import run_remove_routines -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import get_confirm, get_selection_input, get_string_input -from utils.instance_utils import get_instances -from utils.sys_utils import ( - cmd_sysctl_manage, - cmd_sysctl_service, - create_python_venv, - install_python_requirements, - parse_packages_from_file, -) - - -# noinspection PyMethodMayBeStatic -class ObicoExtension(BaseExtension): - server_url: str - - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing Obico for Klipper ...") - - # check if moonraker is installed. if not, notify the user and exit - if not moonraker_exists(): - return - - # if obico is already installed, ask if the user wants to repair an - # incomplete installation or link to the obico server - force_clone = False - obico_instances: List[MoonrakerObico] = get_instances(MoonrakerObico) - if obico_instances: - self._print_is_already_installed() - options = ["l", "r", "b"] - action = get_selection_input("Perform action", option_list=options) - if action.lower() == "b": - Logger.print_info("Exiting Obico for Klipper installation ...") - return - elif action.lower() == "l": - unlinked_instances: List[MoonrakerObico] = [ - obico for obico in obico_instances if not obico.is_linked - ] - self._link_obico_instances(unlinked_instances) - return - else: - Logger.print_status("Re-Installing Obico for Klipper ...") - force_clone = True - - # let the user confirm installation - kl_instances: List[Klipper] = get_instances(Klipper) - mr_instances: List[Moonraker] = get_instances(Moonraker) - self._print_moonraker_instances(mr_instances) - if not get_confirm( - "Continue Obico for Klipper installation?", - default_choice=True, - allow_go_back=True, - ): - return - - try: - git_clone_wrapper(OBICO_REPO, OBICO_DIR, force=force_clone) - self._install_dependencies() - - # ask the user for the obico server url - self._get_server_url() - - # create obico instances - for moonraker in mr_instances: - instance = MoonrakerObico(suffix=moonraker.suffix) - instance.create() - - cmd_sysctl_service(instance.service_file_path.name, "enable") - - # create obico config - self._create_obico_cfg(instance, moonraker) - - # create obico macros - self._create_obico_macros_cfg(moonraker) - - # create obico update manager - self._create_obico_update_manager_cfg(moonraker) - - cmd_sysctl_service(instance.service_file_path.name, "start") - - cmd_sysctl_manage("daemon-reload") - - BackupService().backup_printer_config_dir() - - # add to klippers config - self._patch_printer_cfg(kl_instances) - InstanceManager.restart_all(kl_instances) - - # add to moonraker update manager - self._patch_moonraker_conf(mr_instances) - InstanceManager.restart_all(mr_instances) - - # check linking of / ask for linking instances - self._check_and_opt_link_instances() - - Logger.print_dialog( - DialogType.SUCCESS, - ["Obico for Klipper successfully installed!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during Obico for Klipper installation:\n{e}") - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating Obico for Klipper ...") - try: - instances = get_instances(MoonrakerObico) - InstanceManager.stop_all(instances) - - git_pull_wrapper(OBICO_DIR) - self._install_dependencies() - - InstanceManager.start_all(instances) - Logger.print_ok("Obico for Klipper successfully updated!") - - except Exception as e: - Logger.print_error(f"Error during Obico for Klipper update:\n{e}") - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing Obico for Klipper ...") - - kl_instances: List[Klipper] = get_instances(Klipper) - mr_instances: List[Moonraker] = get_instances(Moonraker) - ob_instances: List[MoonrakerObico] = get_instances(MoonrakerObico) - - try: - self._remove_obico_instances(ob_instances) - self._remove_obico_dir() - self._remove_obico_env() - BackupService().backup_printer_config_dir() - remove_config_section(f"include {OBICO_MACROS_CFG_NAME}", kl_instances) - remove_config_section(f"include {OBICO_UPDATE_CFG_NAME}", mr_instances) - Logger.print_dialog( - DialogType.SUCCESS, - ["Obico for Klipper successfully removed!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during Obico for Klipper removal:\n{e}") - - def _obico_server_url_prompt(self) -> None: - Logger.print_dialog( - DialogType.CUSTOM, - custom_title="Obico Server URL", - content=[ - "You can use a self-hosted Obico Server or the Obico Cloud. " - "For more information, please visit:", - "https://obico.io.", - "\n\n", - "For the Obico Cloud, leave it as the default:", - "https://app.obico.io.", - "\n\n", - "For self-hosted server, specify:", - "http://server_ip:port", - "For instance, 'http://192.168.0.5:3334'.", - ], - ) - - def _print_moonraker_instances(self, mr_instances: List[Moonraker]) -> None: - mr_names = [f"● {moonraker.data_dir.name}" for moonraker in mr_instances] - if len(mr_names) > 1: - Logger.print_dialog( - DialogType.INFO, - [ - "The following Moonraker instances were found:", - *mr_names, - "\n\n", - "The setup will apply the same names to Obico!", - ], - ) - - def _print_is_already_installed(self) -> None: - Logger.print_dialog( - DialogType.INFO, - [ - "Obico is already installed!", - "It is safe to run the installer again to link your " - "printer or repair any issues.", - "\n\n", - "You can perform the following actions:", - "L) Link printer to the Obico server", - "R) Repair installation", - ], - ) - - def _get_server_url(self) -> None: - self._obico_server_url_prompt() - pattern = r"^(http|https)://[a-zA-Z0-9./?=_%:-]*$" - self.server_url = get_string_input( - "Obico Server URL", - regex=pattern, - default="https://app.obico.io", - ) - - def _install_dependencies(self) -> None: - # install dependencies - script = OBICO_DIR.joinpath("install.sh") - package_list = parse_packages_from_file(script) - check_install_dependencies({*package_list}) - - # create virtualenv - if create_python_venv(OBICO_ENV_DIR): - install_python_requirements(OBICO_ENV_DIR, OBICO_REQ_FILE) - - def _create_obico_macros_cfg(self, moonraker: Moonraker) -> None: - macros_cfg = OBICO_DIR.joinpath(f"include_cfgs/{OBICO_MACROS_CFG_NAME}") - macros_target = moonraker.base.cfg_dir.joinpath(OBICO_MACROS_CFG_NAME) - if not macros_target.exists(): - shutil.copy(macros_cfg, macros_target) - else: - Logger.print_info( - f"Obico's '{OBICO_MACROS_CFG_NAME}' in {moonraker.base.cfg_dir} already exists! Skipped ..." - ) - - def _create_obico_update_manager_cfg(self, moonraker: Moonraker) -> None: - update_cfg = OBICO_DIR.joinpath(OBICO_UPDATE_CFG_SAMPLE_NAME) - update_cfg_target = moonraker.base.cfg_dir.joinpath(OBICO_UPDATE_CFG_NAME) - if not update_cfg_target.exists(): - shutil.copy(update_cfg, update_cfg_target) - else: - Logger.print_info( - f"Obico's '{OBICO_UPDATE_CFG_NAME}' in {moonraker.base.cfg_dir} already exists! Skipped ..." - ) - - def _create_obico_cfg( - self, current_instance: MoonrakerObico, moonraker: Moonraker - ) -> None: - cfg_template = OBICO_DIR.joinpath(OBICO_CFG_SAMPLE_NAME) - cfg_target_file = current_instance.cfg_file - - if not cfg_template.exists(): - Logger.print_error( - f"Obico config template file {cfg_target_file} does not exist!" - ) - return - - if not cfg_target_file.exists(): - shutil.copy(cfg_template, cfg_target_file) - self._patch_obico_cfg(moonraker, current_instance) - else: - Logger.print_info( - f"Obico config in {current_instance.base.cfg_dir} already exists! Skipped ..." - ) - - def _patch_obico_cfg(self, moonraker: Moonraker, obico: MoonrakerObico) -> None: - scp = SimpleConfigParser() - scp.read_file(obico.cfg_file) - scp.set_option("server", "url", self.server_url) - scp.set_option("moonraker", "port", str(moonraker.port)) - scp.set_option( - "logging", - "path", - obico.base.log_dir.joinpath(obico.log_file_name).as_posix(), - ) - scp.write_file(obico.cfg_file) - - def _patch_printer_cfg(self, klipper: List[Klipper]) -> None: - add_config_section( - section=f"include {OBICO_MACROS_CFG_NAME}", instances=klipper - ) - - def _patch_moonraker_conf(self, instances: List[Moonraker]) -> None: - add_config_section( - section=f"include {OBICO_UPDATE_CFG_NAME}", instances=instances - ) - - def _link_obico_instances(self, unlinked_instances) -> None: - for obico in unlinked_instances: - obico.link() - - def _check_and_opt_link_instances(self) -> None: - Logger.print_status("Checking link status of Obico instances ...") - - suffix_blacklist: List[str] = [ - suffix for suffix in SUFFIX_BLACKLIST if suffix != "obico" - ] - ob_instances: List[MoonrakerObico] = get_instances( - MoonrakerObico, suffix_blacklist=suffix_blacklist - ) - unlinked_instances: List[MoonrakerObico] = [ - obico for obico in ob_instances if not obico.is_linked - ] - if unlinked_instances: - Logger.print_dialog( - DialogType.INFO, - [ - "The Obico instances for the following printers are not " - "linked to the server:", - *[f"● {obico.data_dir.name}" for obico in unlinked_instances], - "\n\n", - "It will take only 10 seconds to link the printer to the Obico server.", - "For more information visit:", - "https://www.obico.io/docs/user-guides/klipper-setup/", - "\n\n", - "If you don't want to link the printer now, you can restart the " - "linking process later by running this installer again.", - ], - ) - if not get_confirm("Do you want to link the printers now?"): - Logger.print_info("Linking to Obico server skipped ...") - return - - self._link_obico_instances(unlinked_instances) - - def _remove_obico_instances( - self, - instance_list: List[MoonrakerObico], - ) -> None: - if not instance_list: - Logger.print_info("No Obico instances found. Skipped ...") - return - - for instance in instance_list: - Logger.print_status( - f"Removing instance {instance.service_file_path.stem} ..." - ) - InstanceManager.remove(instance) - - def _remove_obico_dir(self) -> None: - Logger.print_status("Removing Obico for Klipper directory ...") - - if not OBICO_DIR.exists(): - Logger.print_info(f"'{OBICO_DIR}' does not exist. Skipped ...") - return - - run_remove_routines(OBICO_DIR) - - def _remove_obico_env(self) -> None: - Logger.print_status("Removing Obico for Klipper environment ...") - - if not OBICO_ENV_DIR.exists(): - Logger.print_info(f"'{OBICO_ENV_DIR}' does not exist. Skipped ...") - return - - run_remove_routines(OBICO_ENV_DIR) diff --git a/kiauh/extensions/octoapp/__init__.py b/kiauh/extensions/octoapp/__init__.py deleted file mode 100644 index 9ce8939..0000000 --- a/kiauh/extensions/octoapp/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -# repo -OA_REPO = "https://github.com/crysxd/OctoApp-Plugin.git" - -# directories -OA_DIR = Path.home().joinpath("octoapp") -OA_ENV_DIR = Path.home().joinpath("octoapp-env") - -# files -OA_REQ_FILE = OA_DIR.joinpath("requirements.txt") -OA_DEPS_JSON_FILE = OA_DIR.joinpath("moonraker-system-dependencies.json") -OA_INSTALL_SCRIPT = OA_DIR.joinpath("install.sh") -OA_UPDATE_SCRIPT = OA_DIR.joinpath("update.sh") -OA_INSTALLER_LOG_FILE = Path.home().joinpath("octoapp-installer.log") - -# filenames -OA_CFG_NAME = "octoapp.conf" -OA_LOG_NAME = "octoapp.log" -OA_SYS_CFG_NAME = "octoapp-system.cfg" diff --git a/kiauh/extensions/octoapp/metadata.json b/kiauh/extensions/octoapp/metadata.json deleted file mode 100644 index 6f1b9ba..0000000 --- a/kiauh/extensions/octoapp/metadata.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "metadata": { - "index": 9, - "module": "octoapp_extension", - "maintained_by": "crysxd", - "display_name": "OctoApp for Klipper", - "description": [ - "Your favorite 3D printing app for iOS & Android", - "- Print notifications on your phone & watch", - "- Control and start prints from your phone", - "- Live webcam view", - "- Live Gcode preview", - "- And much much more!" - ], - "repo": "https://github.com/crysxd/OctoApp-Plugin", - "updates": true - } -} diff --git a/kiauh/extensions/octoapp/octoapp.py b/kiauh/extensions/octoapp/octoapp.py deleted file mode 100644 index 8480d70..0000000 --- a/kiauh/extensions/octoapp/octoapp.py +++ /dev/null @@ -1,73 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError, run - -from components.moonraker import MOONRAKER_CFG_NAME -from components.moonraker.moonraker import Moonraker -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from extensions.octoapp import ( - OA_CFG_NAME, - OA_DIR, - OA_ENV_DIR, - OA_INSTALL_SCRIPT, - OA_LOG_NAME, - OA_SYS_CFG_NAME, - OA_UPDATE_SCRIPT, -) -from utils.sys_utils import get_service_file_path - - -@dataclass -class Octoapp: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name = OA_LOG_NAME - dir: Path = OA_DIR - env_dir: Path = OA_ENV_DIR - data_dir: Path = field(init=False) - store_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - sys_cfg_file: Path = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path(Octoapp, self.suffix) - self.store_dir = self.base.data_dir.joinpath("store") - self.cfg_file = self.base.cfg_dir.joinpath(OA_CFG_NAME) - self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME) - self.data_dir = self.base.data_dir - self.sys_cfg_file = self.base.cfg_dir.joinpath(OA_SYS_CFG_NAME) - - def create(self) -> None: - Logger.print_status("Creating OctoApp for Klipper Instance ...") - - try: - cmd = f"{OA_INSTALL_SCRIPT} {self.base.cfg_dir}/{MOONRAKER_CFG_NAME}" - run(cmd, check=True, shell=True) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - - @staticmethod - def update() -> None: - try: - run(OA_UPDATE_SCRIPT.as_posix(), check=True, shell=True, cwd=OA_DIR) - - except CalledProcessError as e: - Logger.print_error(f"Error updating OctoApp for Klipper: {e}") - raise diff --git a/kiauh/extensions/octoapp/octoapp_extension.py b/kiauh/extensions/octoapp/octoapp_extension.py deleted file mode 100644 index 642b9d7..0000000 --- a/kiauh/extensions/octoapp/octoapp_extension.py +++ /dev/null @@ -1,206 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import json -from typing import List - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.backup_service import BackupService -from extensions.base_extension import BaseExtension -from extensions.octoapp import ( - OA_DEPS_JSON_FILE, - OA_DIR, - OA_ENV_DIR, - OA_INSTALL_SCRIPT, - OA_INSTALLER_LOG_FILE, - OA_REPO, - OA_REQ_FILE, - OA_SYS_CFG_NAME, -) -from extensions.octoapp.octoapp import Octoapp -from utils.common import ( - check_install_dependencies, - moonraker_exists, -) -from utils.config_utils import ( - remove_config_section, -) -from utils.fs_utils import run_remove_routines -from utils.git_utils import git_clone_wrapper -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - install_python_requirements, - parse_packages_from_file, -) - - -# noinspection PyMethodMayBeStatic -class OctoappExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing OctoApp for Klipper ...") - - # check if moonraker is installed. if not, notify the user and exit - if not moonraker_exists(): - return - - force_clone = False - OA_instances: List[Octoapp] = get_instances(Octoapp) - if OA_instances: - Logger.print_dialog( - DialogType.INFO, - [ - "OctoApp is already installed!", - "It is safe to run the installer again to link your " - "printer or repair any issues.", - ], - ) - if not get_confirm("Re-run OctoApp installation?"): - Logger.print_info("Exiting OctoApp for Klipper installation ...") - return - else: - Logger.print_status("Re-Installing OctoApp for Klipper ...") - force_clone = True - - mr_instances: List[Moonraker] = get_instances(Moonraker) - - mr_names = [f"● {moonraker.data_dir.name}" for moonraker in mr_instances] - if len(mr_names) > 1: - Logger.print_dialog( - DialogType.INFO, - [ - "The following Moonraker instances were found:", - *mr_names, - "\n\n", - "The setup will apply the same names to OctoApp!", - ], - ) - - if not get_confirm( - "Continue OctoApp for Klipper installation?", - default_choice=True, - allow_go_back=True, - ): - Logger.print_info("Exiting OctoApp for Klipper installation ...") - return - - try: - git_clone_wrapper(OA_REPO, OA_DIR, force=force_clone) - - for moonraker in mr_instances: - instance = Octoapp(suffix=moonraker.suffix) - instance.create() - - InstanceManager.restart_all(mr_instances) - - Logger.print_dialog( - DialogType.SUCCESS, - ["OctoApp for Klipper successfully installed!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during OctoApp for Klipper installation:\n{e}") - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating OctoApp for Klipper ...") - try: - Octoapp.update() - Logger.print_dialog( - DialogType.SUCCESS, - ["OctoApp for Klipper successfully updated!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during OctoApp for Klipper update:\n{e}") - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing OctoApp for Klipper ...") - - mr_instances: List[Moonraker] = get_instances(Moonraker) - ob_instances: List[Octoapp] = get_instances(Octoapp) - - try: - self._remove_OA_instances(ob_instances) - self._remove_OA_store_dirs() - self._remove_OA_dir() - self._remove_OA_env() - BackupService().backup_moonraker_conf() - remove_config_section(f"include {OA_SYS_CFG_NAME}", mr_instances) - run_remove_routines(OA_INSTALLER_LOG_FILE) - Logger.print_dialog( - DialogType.SUCCESS, - ["OctoApp for Klipper successfully removed!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during OctoApp for Klipper removal:\n{e}") - - def _install_OA_dependencies(self) -> None: - OA_deps = [] - if OA_DEPS_JSON_FILE.exists(): - with open(OA_DEPS_JSON_FILE, "r") as deps: - OA_deps = json.load(deps).get("debian", []) - elif OA_INSTALL_SCRIPT.exists(): - OA_deps = parse_packages_from_file(OA_INSTALL_SCRIPT) - - if not OA_deps: - raise ValueError("Error reading OctoApp dependencies!") - - check_install_dependencies({*OA_deps}) - install_python_requirements(OA_ENV_DIR, OA_REQ_FILE) - - def _remove_OA_instances( - self, - instance_list: List[Octoapp], - ) -> None: - if not instance_list: - Logger.print_info("No OctoApp instances found. Skipped ...") - return - - for instance in instance_list: - Logger.print_status( - f"Removing instance {instance.service_file_path.stem} ..." - ) - InstanceManager.remove(instance) - - def _remove_OA_dir(self) -> None: - Logger.print_status("Removing OctoApp for Klipper directory ...") - - if not OA_DIR.exists(): - Logger.print_info(f"'{OA_DIR}' does not exist. Skipped ...") - return - - run_remove_routines(OA_DIR) - - def _remove_OA_store_dirs(self) -> None: - Logger.print_status("Removing OctoApp for Klipper store directory ...") - - klipper_instances: List[Moonraker] = get_instances(Klipper) - - for instance in klipper_instances: - store_dir = instance.data_dir.joinpath("octoapp-store") - if not store_dir.exists(): - Logger.print_info(f"'{store_dir}' does not exist. Skipped ...") - return - - run_remove_routines(store_dir) - - def _remove_OA_env(self) -> None: - Logger.print_status("Removing OctoApp for Klipper environment ...") - - if not OA_ENV_DIR.exists(): - Logger.print_info(f"'{OA_ENV_DIR}' does not exist. Skipped ...") - return - - run_remove_routines(OA_ENV_DIR) diff --git a/kiauh/extensions/octoeverywhere/__init__.py b/kiauh/extensions/octoeverywhere/__init__.py deleted file mode 100644 index e05c859..0000000 --- a/kiauh/extensions/octoeverywhere/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -# repo -OE_REPO = "https://github.com/QuinnDamerell/OctoPrint-OctoEverywhere.git" - -# directories -OE_DIR = Path.home().joinpath("octoeverywhere") -OE_ENV_DIR = Path.home().joinpath("octoeverywhere-env") -OE_STORE_DIR = OE_DIR.joinpath("octoeverywhere-store") - -# files -OE_REQ_FILE = OE_DIR.joinpath("requirements.txt") -OE_DEPS_JSON_FILE = OE_DIR.joinpath("moonraker-system-dependencies.json") -OE_INSTALL_SCRIPT = OE_DIR.joinpath("install.sh") -OE_UPDATE_SCRIPT = OE_DIR.joinpath("update.sh") -OE_INSTALLER_LOG_FILE = Path.home().joinpath("octoeverywhere-installer.log") - -# filenames -OE_CFG_NAME = "octoeverywhere.conf" -OE_LOG_NAME = "octoeverywhere.log" -OE_SYS_CFG_NAME = "octoeverywhere-system.cfg" diff --git a/kiauh/extensions/octoeverywhere/metadata.json b/kiauh/extensions/octoeverywhere/metadata.json deleted file mode 100644 index 42fecd7..0000000 --- a/kiauh/extensions/octoeverywhere/metadata.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "metadata": { - "index": 7, - "module": "octoeverywhere_extension", - "maintained_by": "QuinnDamerell", - "display_name": "OctoEverywhere for Klipper", - "description": [ - "Cloud Empower Your Klipper 3D Printers With:", - "- Free, Private, And Secure Remote Access", - "- AI Print Failure Detection", - "- Real-time Notifications", - "- Live Streaming, and More!" - ], - "website": "https://octoeverywhere.com", - "repo": "github.com/QuinnDamerell/OctoPrint-OctoEverywhere", - "updates": true - } -} diff --git a/kiauh/extensions/octoeverywhere/octoeverywhere.py b/kiauh/extensions/octoeverywhere/octoeverywhere.py deleted file mode 100644 index 7de2122..0000000 --- a/kiauh/extensions/octoeverywhere/octoeverywhere.py +++ /dev/null @@ -1,75 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError, run - -from components.moonraker import MOONRAKER_CFG_NAME -from components.moonraker.moonraker import Moonraker -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from extensions.octoeverywhere import ( - OE_CFG_NAME, - OE_DIR, - OE_ENV_DIR, - OE_INSTALL_SCRIPT, - OE_LOG_NAME, - OE_SYS_CFG_NAME, - OE_UPDATE_SCRIPT, -) -from utils.sys_utils import get_service_file_path - - -@dataclass -class Octoeverywhere: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name = OE_LOG_NAME - dir: Path = OE_DIR - env_dir: Path = OE_ENV_DIR - data_dir: Path = field(init=False) - store_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - sys_cfg_file: Path = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path( - Octoeverywhere, self.suffix - ) - self.store_dir = self.base.data_dir.joinpath("store") - self.cfg_file = self.base.cfg_dir.joinpath(OE_CFG_NAME) - self.sys_cfg_file = self.base.cfg_dir.joinpath(OE_SYS_CFG_NAME) - self.data_dir = self.base.data_dir - self.sys_cfg_file = self.base.cfg_dir.joinpath(OE_SYS_CFG_NAME) - - def create(self) -> None: - Logger.print_status("Creating OctoEverywhere for Klipper Instance ...") - - try: - cmd = f"{OE_INSTALL_SCRIPT} {self.base.cfg_dir}/{MOONRAKER_CFG_NAME}" - run(cmd, check=True, shell=True) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - - @staticmethod - def update() -> None: - try: - run(OE_UPDATE_SCRIPT.as_posix(), check=True, shell=True, cwd=OE_DIR) - - except CalledProcessError as e: - Logger.print_error(f"Error updating OctoEverywhere for Klipper: {e}") - raise diff --git a/kiauh/extensions/octoeverywhere/octoeverywhere_extension.py b/kiauh/extensions/octoeverywhere/octoeverywhere_extension.py deleted file mode 100644 index ae1433f..0000000 --- a/kiauh/extensions/octoeverywhere/octoeverywhere_extension.py +++ /dev/null @@ -1,193 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import json -from typing import List - -from components.moonraker.moonraker import Moonraker -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.backup_service import BackupService -from extensions.base_extension import BaseExtension -from extensions.octoeverywhere import ( - OE_DEPS_JSON_FILE, - OE_DIR, - OE_ENV_DIR, - OE_INSTALL_SCRIPT, - OE_INSTALLER_LOG_FILE, - OE_REPO, - OE_REQ_FILE, - OE_SYS_CFG_NAME, -) -from extensions.octoeverywhere.octoeverywhere import Octoeverywhere -from utils.common import ( - check_install_dependencies, - moonraker_exists, -) -from utils.config_utils import ( - remove_config_section, -) -from utils.fs_utils import run_remove_routines -from utils.git_utils import git_clone_wrapper -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - install_python_requirements, - parse_packages_from_file, -) - - -# noinspection PyMethodMayBeStatic -class OctoeverywhereExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing OctoEverywhere for Klipper ...") - - # check if moonraker is installed. if not, notify the user and exit - if not moonraker_exists(): - return - - force_clone = False - oe_instances: List[Octoeverywhere] = get_instances(Octoeverywhere) - if oe_instances: - Logger.print_dialog( - DialogType.INFO, - [ - "OctoEverywhere is already installed!", - "It is safe to run the installer again to link your " - "printer or repair any issues.", - ], - ) - if not get_confirm("Re-run OctoEverywhere installation?"): - Logger.print_info("Exiting OctoEverywhere for Klipper installation ...") - return - else: - Logger.print_status("Re-Installing OctoEverywhere for Klipper ...") - force_clone = True - - mr_instances: List[Moonraker] = get_instances(Moonraker) - - mr_names = [f"● {moonraker.data_dir.name}" for moonraker in mr_instances] - if len(mr_names) > 1: - Logger.print_dialog( - DialogType.INFO, - [ - "The following Moonraker instances were found:", - *mr_names, - "\n\n", - "The setup will apply the same names to OctoEverywhere!", - ], - ) - - if not get_confirm( - "Continue OctoEverywhere for Klipper installation?", - default_choice=True, - allow_go_back=True, - ): - Logger.print_info("Exiting OctoEverywhere for Klipper installation ...") - return - - try: - git_clone_wrapper(OE_REPO, OE_DIR, force=force_clone) - - for moonraker in mr_instances: - instance = Octoeverywhere(suffix=moonraker.suffix) - instance.create() - - InstanceManager.restart_all(mr_instances) - - Logger.print_dialog( - DialogType.SUCCESS, - ["OctoEverywhere for Klipper successfully installed!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error( - f"Error during OctoEverywhere for Klipper installation:\n{e}" - ) - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating OctoEverywhere for Klipper ...") - try: - Octoeverywhere.update() - Logger.print_dialog( - DialogType.SUCCESS, - ["OctoEverywhere for Klipper successfully updated!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during OctoEverywhere for Klipper update:\n{e}") - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing OctoEverywhere for Klipper ...") - - mr_instances: List[Moonraker] = get_instances(Moonraker) - ob_instances: List[Octoeverywhere] = get_instances(Octoeverywhere) - - try: - self._remove_oe_instances(ob_instances) - self._remove_oe_dir() - self._remove_oe_env() - BackupService().backup_moonraker_conf() - remove_config_section(f"include {OE_SYS_CFG_NAME}", mr_instances) - run_remove_routines(OE_INSTALLER_LOG_FILE) - Logger.print_dialog( - DialogType.SUCCESS, - ["OctoEverywhere for Klipper successfully removed!"], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during OctoEverywhere for Klipper removal:\n{e}") - - def _install_oe_dependencies(self) -> None: - oe_deps = [] - if OE_DEPS_JSON_FILE.exists(): - with open(OE_DEPS_JSON_FILE, "r") as deps: - oe_deps = json.load(deps).get("debian", []) - elif OE_INSTALL_SCRIPT.exists(): - oe_deps = parse_packages_from_file(OE_INSTALL_SCRIPT) - - if not oe_deps: - raise ValueError("Error reading OctoEverywhere dependencies!") - - check_install_dependencies({*oe_deps}) - install_python_requirements(OE_ENV_DIR, OE_REQ_FILE) - - def _remove_oe_instances( - self, - instance_list: List[Octoeverywhere], - ) -> None: - if not instance_list: - Logger.print_info("No OctoEverywhere instances found. Skipped ...") - return - - for instance in instance_list: - Logger.print_status( - f"Removing instance {instance.service_file_path.stem} ..." - ) - InstanceManager.remove(instance) - - def _remove_oe_dir(self) -> None: - Logger.print_status("Removing OctoEverywhere for Klipper directory ...") - - if not OE_DIR.exists(): - Logger.print_info(f"'{OE_DIR}' does not exist. Skipped ...") - return - - run_remove_routines(OE_DIR) - - def _remove_oe_env(self) -> None: - Logger.print_status("Removing OctoEverywhere for Klipper environment ...") - - if not OE_ENV_DIR.exists(): - Logger.print_info(f"'{OE_ENV_DIR}' does not exist. Skipped ...") - return - - run_remove_routines(OE_ENV_DIR) diff --git a/kiauh/extensions/octoprint/__init__.py b/kiauh/extensions/octoprint/__init__.py deleted file mode 100644 index 17e0c96..0000000 --- a/kiauh/extensions/octoprint/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -# Constants -OP_DEFAULT_PORT = 5000 - -# OctoPrint instance naming/prefixes -OP_ENV_PREFIX = "OctoPrint" -OP_BASEDIR_PREFIX = ".octoprint" - -# Service/log filenames -OP_LOG_NAME = "octoprint.log" - -# Files/paths (computed per-instance where applicable) -OP_SUDOERS_FILE = Path("/etc/sudoers.d/octoprint-shutdown") diff --git a/kiauh/extensions/octoprint/metadata.json b/kiauh/extensions/octoprint/metadata.json deleted file mode 100644 index 4049ef5..0000000 --- a/kiauh/extensions/octoprint/metadata.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "metadata": { - "index": 12, - "module": "octoprint_extension", - "maintained_by": "dw-0", - "display_name": "OctoPrint", - "description": [ - "Open-source web interface to control and monitor your 3D printer", - "- Upload and manage G-code, start/pause/cancel prints", - "- Live webcam view and timelapse support", - "- Real-time temperature graphs and printer status", - "- Powerful plugin ecosystem" - ], - "website": "https://octoprint.org", - "repo": "https://github.com/OctoPrint/OctoPrint", - "updates": false - } -} diff --git a/kiauh/extensions/octoprint/octoprint.py b/kiauh/extensions/octoprint/octoprint.py deleted file mode 100644 index 829dd1b..0000000 --- a/kiauh/extensions/octoprint/octoprint.py +++ /dev/null @@ -1,116 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from textwrap import dedent - -from components.klipper.klipper import Klipper -from core.constants import CURRENT_USER -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from extensions.octoprint import ( - OP_BASEDIR_PREFIX, - OP_ENV_PREFIX, - OP_LOG_NAME, -) -from utils.fs_utils import create_folders -from utils.sys_utils import create_service_file, get_service_file_path - - -@dataclass -class Octoprint: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name = OP_LOG_NAME - env_dir: Path = field(init=False) - basedir: Path = field(init=False) - cfg_file: Path = field(init=False) - - def __post_init__(self): - self.base = BaseInstance(Klipper, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path = get_service_file_path(Octoprint, self.suffix) - - # OctoPrint stores its data under ~/.octoprint[_SUFFIX] - self.basedir = ( - Path.home().joinpath(OP_BASEDIR_PREFIX) - if self.suffix == "" - else Path.home().joinpath(f"{OP_BASEDIR_PREFIX}_{self.suffix}") - ) - self.cfg_file = self.basedir.joinpath("config.yaml") - - # OctoPrint virtualenv lives under ~/OctoPrint[_SUFFIX] - self.env_dir = ( - Path.home().joinpath(OP_ENV_PREFIX) - if self.suffix == "" - else Path.home().joinpath(f"{OP_ENV_PREFIX}_{self.suffix}") - ) - - def create(self, port: int) -> None: - Logger.print_status( - f"Creating OctoPrint instance '{self.service_file_path.stem}' ..." - ) - - # Ensure basedir exists and config.yaml is present - create_folders([self.basedir]) - if not self.cfg_file.exists(): - Logger.print_status("Creating config.yaml ...") - self.cfg_file.write_text(self._prep_config_yaml()) - Logger.print_ok("config.yaml created!") - else: - Logger.print_info("config.yaml already exists. Skipped ...") - - create_service_file(self.service_file_path.name, self._prep_service_content(port)) - - def _prep_service_content(self, port: int) -> str: - basedir = self.basedir.as_posix() - cfg = self.cfg_file.as_posix() - octo_exec = self.env_dir.joinpath("bin/octoprint").as_posix() - - return dedent( - f"""\ - [Unit] - Description=Starts OctoPrint on startup - After=network-online.target - Wants=network-online.target - - [Service] - Environment="LC_ALL=C.UTF-8" - Environment="LANG=C.UTF-8" - Type=simple - User={CURRENT_USER} - ExecStart={octo_exec} --basedir {basedir} --config {cfg} --port={port} serve - - [Install] - WantedBy=multi-user.target - """ - ) - - def _prep_config_yaml(self) -> str: - printer = self.base.comms_dir.joinpath("klippy.serial").as_posix() - restart_service = self.service_file_path.stem - - return dedent( - f"""\ - serial: - additionalPorts: - - {printer} - disconnectOnErrors: false - port: {printer} - server: - commands: - serverRestartCommand: sudo service {restart_service} restart - systemRestartCommand: sudo shutdown -r now - systemShutdownCommand: sudo shutdown -h now - """ - ) diff --git a/kiauh/extensions/octoprint/octoprint_extension.py b/kiauh/extensions/octoprint/octoprint_extension.py deleted file mode 100644 index e6ec720..0000000 --- a/kiauh/extensions/octoprint/octoprint_extension.py +++ /dev/null @@ -1,286 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import re -from typing import Dict, List, Optional, Set - -from components.klipper.klipper import Klipper -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.types.color import Color -from core.menus.base_menu import print_back_footer -from extensions.base_extension import BaseExtension -from extensions.octoprint import ( - OP_SUDOERS_FILE, OP_DEFAULT_PORT, -) -from extensions.octoprint.octoprint import Octoprint -from utils.common import check_install_dependencies -from utils.fs_utils import run_remove_routines, remove_with_sudo -from utils.input_utils import get_selection_input, get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - create_python_venv, - get_ipv4_addr, - install_python_packages, -) - - -# noinspection PyMethodMayBeStatic -class OctoprintExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing OctoPrint ...") - - klipper_instances: List[Klipper] = get_instances(Klipper) - if not klipper_instances: - Logger.print_dialog( - DialogType.WARNING, - [ - "Klipper not found! Please install Klipper first.", - ], - ) - return - - existing_ops: List[Octoprint] = get_instances(Octoprint) - existing_by_suffix: Dict[str, Octoprint] = {op.suffix: op for op in existing_ops} - candidates: List[Klipper] = [k for k in klipper_instances if k.suffix not in existing_by_suffix] - - chosen: List[Klipper] = [] - - if len(klipper_instances) == 1: - k = klipper_instances[0] - if k.suffix in existing_by_suffix: - if not get_confirm( - f"OctoPrint already exists for '{k.service_file_path.stem}'. Reinstall?", - default_choice=True, - allow_go_back=True, - ): - Logger.print_info("Aborted OctoPrint installation.") - return - chosen = [k] - else: - while True: - dialog = "╔═══════════════════════════════════════════════════════╗\n" - headline = Color.apply( - "The following Klipper instances were found:", Color.GREEN - ) - dialog += f"║{headline:^64}║\n" - dialog += "╟───────────────────────────────────────────────────────╢\n" - - if candidates: - line_all = Color.apply("a) Select all (install for all missing)", Color.YELLOW) - dialog += f"║ {line_all:<63}║\n" - dialog += "║ ║\n" - - index_map: Dict[str, Klipper] = {} - for i, k in enumerate(klipper_instances, start=1): - mapping = existing_by_suffix.get(k.suffix) - suffix = f" <-> {mapping.service_file_path.stem}" if mapping else "" - line = Color.apply(f"{i}) {k.service_file_path.stem}{suffix}", Color.CYAN) - dialog += f"║ {line:<63}║\n" - index_map[str(i)] = k - - dialog += "╟───────────────────────────────────────────────────────╢\n" - print(dialog, end="") - print_back_footer() - - allowed = list(index_map.keys()) + ["b"] + (["a"] if candidates else []) - choice = get_selection_input("Choose instance to install OctoPrint for", allowed) - - if choice == "b": - Logger.print_info("Aborted OctoPrint installation.") - return - if choice == "a": - chosen = candidates - break - - selected = index_map[choice] - if selected.suffix in existing_by_suffix: - confirm = get_confirm( - f"OctoPrint already exists for '{selected.service_file_path.stem}'. Reinstall?", - default_choice=True, - allow_go_back=True, - ) - if not confirm: - # back to menu - continue - chosen = [selected] - break - - deps = { - "git", - "wget", - "python3-pip", - "python3-dev", - "libyaml-dev", - "build-essential", - "python3-setuptools", - "python3-virtualenv", - } - check_install_dependencies(deps) - - # Determine used ports from existing OctoPrint services and prepare regex - used_ports: Set[int] = set() - port_re = re.compile(r"--port=(\d+)") - for op in existing_ops: - try: - content = op.service_file_path.read_text() - m = port_re.search(content) - if m: - used_ports.add(int(m.group(1))) - except OSError: - pass - - # noinspection PyShadowingNames - def read_existing_port(suffix: str) -> Optional[int]: - op = existing_by_suffix.get(suffix) - if not op: - return None - try: - content = op.service_file_path.read_text() - m = port_re.search(content) - return int(m.group(1)) if m else None - except OSError: - return None - - def next_free_port(start: int, used: Set[int]) -> int: - p = start - while p in used: - p += 1 - used.add(p) - return p - - created_ops: List[Octoprint] = [] - for k in chosen: - # Keep existing port on reinstall, otherwise assign next free one - existing_port = read_existing_port(k.suffix) - port = existing_port if existing_port is not None else next_free_port(OP_DEFAULT_PORT, used_ports) - - instance = Octoprint(suffix=k.suffix) - - if create_python_venv(instance.env_dir, force=False): - Logger.print_ok( - f"Virtualenv created: {instance.env_dir}", prefix=False - ) - else: - Logger.print_info( - f"Virtualenv exists: {instance.env_dir}. Skipping creation ..." - ) - - install_python_packages(instance.env_dir, ["octoprint"]) - - instance.create(port=port) - created_ops.append(instance) - - for inst in created_ops: - try: - InstanceManager.enable(inst) - InstanceManager.start(inst) - except Exception as e: - Logger.print_error( - f"Failed to enable/start {inst.service_file_path.name}: {e}" - ) - - ip = get_ipv4_addr() - lines = ["Access your new OctoPrint instance(s) at:"] - for inst in created_ops: - try: - content = inst.service_file_path.read_text() - m = port_re.search(content) - if m: - # noinspection HttpUrlsUsage - lines.append(f"● {inst.service_file_path.stem}: http://{ip}:{m.group(1)}") - except OSError: - pass - - Logger.print_dialog(DialogType.SUCCESS, lines, center_content=False) - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing OctoPrint ...") - - try: - op_instances: List[Octoprint] = get_instances(Octoprint) - if not op_instances: - Logger.print_info("No OctoPrint instances found. Skipped ...") - return - - remove_all = False - if len(op_instances) == 1: - to_remove = op_instances - else: - dialog = "╔═══════════════════════════════════════════════════════╗\n" - headline = Color.apply( - "The following OctoPrint instances were found:", Color.GREEN - ) - dialog += f"║{headline:^64}║\n" - dialog += "╟───────────────────────────────────────────────────────╢\n" - select_all = Color.apply("a) Select all", Color.YELLOW) - dialog += f"║ {select_all:<63}║\n" - dialog += "║ ║\n" - - for i, inst in enumerate(op_instances, start=1): - line = Color.apply( - f"{i}) {inst.service_file_path.stem}", Color.CYAN - ) - dialog += f"║ {line:<63}║\n" - dialog += "╟───────────────────────────────────────────────────────╢\n" - print(dialog, end="") - print_back_footer() - - allowed = [str(i) for i in range(1, len(op_instances) + 1)] - allowed.extend(["a", "b"]) - choice = get_selection_input("Choose instance to remove", allowed) - - if choice == "a": - remove_all = True - to_remove = op_instances - elif choice == "b": - Logger.print_info("Aborted OctoPrint removal.") - return - else: - idx = int(choice) - 1 - to_remove = [op_instances[idx]] - - for inst in to_remove: - Logger.print_status( - f"Removing instance {inst.service_file_path.stem} ..." - ) - try: - InstanceManager.remove(inst) - except Exception as e: - Logger.print_error( - f"Failed to remove service {inst.service_file_path.name}: {e}" - ) - - # Remove only this instance's env and basedir - if inst.env_dir.exists(): - Logger.print_status(f"Removing {inst.env_dir} ...") - run_remove_routines(inst.env_dir) - if inst.basedir.exists(): - Logger.print_status(f"Removing {inst.basedir} ...") - run_remove_routines(inst.basedir) - - # Remove sudoers file only if no instances remain - remaining = get_instances(Octoprint) - if not remaining and OP_SUDOERS_FILE.exists(): - Logger.print_status(f"Removing {OP_SUDOERS_FILE} ...") - remove_with_sudo(OP_SUDOERS_FILE) - - Logger.print_dialog( - DialogType.SUCCESS, - [ - "Selected OctoPrint instance(s) successfully removed!" - if not remove_all - else "All OctoPrint instances successfully removed!", - ], - center_content=True, - ) - - except Exception as e: - Logger.print_error(f"Error during OctoPrint removal: {e}") diff --git a/kiauh/extensions/pretty_gcode/__init__.py b/kiauh/extensions/pretty_gcode/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/extensions/pretty_gcode/assets/pgcode.local.conf b/kiauh/extensions/pretty_gcode/assets/pgcode.local.conf deleted file mode 100644 index eab6162..0000000 --- a/kiauh/extensions/pretty_gcode/assets/pgcode.local.conf +++ /dev/null @@ -1,19 +0,0 @@ -# PrettyGCode website configuration -# copy this file to /etc/nginx/sites-available/pgcode.local.conf -# then to enable: -# sudo ln -s /etc/nginx/sites-available/pgcode.local.conf /etc/nginx/sites-enabled/pgcode.local.conf -# then restart ngninx: -# sudo systemctl reload nginx -server { - listen %PORT%; - listen [::]:%PORT%; - server_name pgcode.local; - - root %ROOT_DIR%; - - index pgcode.html; - - location / { - try_files $uri $uri/ =404; - } -} diff --git a/kiauh/extensions/pretty_gcode/metadata.json b/kiauh/extensions/pretty_gcode/metadata.json deleted file mode 100644 index 6959558..0000000 --- a/kiauh/extensions/pretty_gcode/metadata.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "metadata": { - "index": 8, - "module": "pretty_gcode_extension", - "maintained_by": "Kragrathea", - "display_name": "PrettyGCode for Klipper", - "description": ["3D G-Code viewer for Klipper"], - "repo": "https://github.com/Kragrathea/pgcode", - "updates": true - } -} diff --git a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py b/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py deleted file mode 100644 index 0fe60ce..0000000 --- a/kiauh/extensions/pretty_gcode/pretty_gcode_extension.py +++ /dev/null @@ -1,101 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import shutil -from pathlib import Path - -from components.webui_client.client_utils import create_nginx_cfg -from core.constants import NGINX_SITES_AVAILABLE, NGINX_SITES_ENABLED -from core.logger import DialogType, Logger -from extensions.base_extension import BaseExtension -from utils.common import check_install_dependencies -from utils.fs_utils import ( - remove_file, -) -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import get_number_input -from utils.sys_utils import cmd_sysctl_service, get_ipv4_addr - -MODULE_PATH = Path(__file__).resolve().parent -PGC_DIR = Path.home().joinpath("pgcode") -PGC_REPO = "https://github.com/Kragrathea/pgcode" -PGC_CONF = "pgcode.local.conf" - - -# noinspection PyMethodMayBeStatic -class PrettyGcodeExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing PrettyGCode for Klipper ...") - Logger.print_dialog( - DialogType.ATTENTION, - [ - "Make sure you don't select a port which is already in use by " - "another application. Your input will not be validated! Choosing a port " - "which is already in use by another application may cause issues!", - "The default port is 7136.", - ], - ) - - port = get_number_input( - "On which port should PrettyGCode run", - min_value=0, - default=7136, - allow_go_back=True, - ) - - check_install_dependencies({"nginx"}) - - try: - if PGC_DIR.exists(): - shutil.rmtree(PGC_DIR) - - git_clone_wrapper(PGC_REPO, PGC_DIR) - - create_nginx_cfg( - "PrettyGCode for Klipper", - cfg_name=PGC_CONF, - template_src=MODULE_PATH.joinpath(f"assets/{PGC_CONF}"), - ROOT_DIR=PGC_DIR, - PORT=port, - ) - - cmd_sysctl_service("nginx", "restart") - - log = f"Open PrettyGCode now on: http://{get_ipv4_addr()}:{port}" - Logger.print_ok("PrettyGCode installation complete!", start="\n") - Logger.print_ok(log, prefix=False, end="\n\n") - - except Exception as e: - Logger.print_error( - f"Error during PrettyGCode for Klipper installation: {e}" - ) - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating PrettyGCode for Klipper ...") - try: - git_pull_wrapper(PGC_DIR) - - except Exception as e: - Logger.print_error(f"Error during PrettyGCode for Klipper update: {e}") - - def remove_extension(self, **kwargs) -> None: - try: - Logger.print_status("Removing PrettyGCode for Klipper ...") - - # remove pgc dir - shutil.rmtree(PGC_DIR) - # remove nginx config - remove_file(NGINX_SITES_AVAILABLE.joinpath(PGC_CONF), True) - remove_file(NGINX_SITES_ENABLED.joinpath(PGC_CONF), True) - # restart nginx - cmd_sysctl_service("nginx", "restart") - - Logger.print_ok("PrettyGCode for Klipper removed!") - - except Exception as e: - Logger.print_error(f"Error during PrettyGCode for Klipper removal: {e}") diff --git a/kiauh/extensions/simply_print/__init__.py b/kiauh/extensions/simply_print/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/extensions/simply_print/metadata.json b/kiauh/extensions/simply_print/metadata.json deleted file mode 100644 index 2091b5d..0000000 --- a/kiauh/extensions/simply_print/metadata.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "metadata": { - "index": 10, - "module": "simply_print_extension", - "maintained_by": "dw-0", - "display_name": "SimplyPrint", - "description": [ - "3D Printer Cloud Management Software.", - "\n\n", - "3D printing doesn't have to be a complicated, analog, SD card-filled experience; step into the future of modern 3D printing" - ], - "website": "https://simplyprint.io", - "repo": "https://github.com/SimplyPrint", - "updates": false - } -} diff --git a/kiauh/extensions/simply_print/simply_print_extension.py b/kiauh/extensions/simply_print/simply_print_extension.py deleted file mode 100644 index aa83a76..0000000 --- a/kiauh/extensions/simply_print/simply_print_extension.py +++ /dev/null @@ -1,132 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from typing import List - -from components.moonraker.moonraker import Moonraker -from core.instance_manager.instance_manager import InstanceManager -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 ( - SimpleConfigParser, -) -from extensions.base_extension import BaseExtension -from utils.common import moonraker_exists -from utils.input_utils import get_confirm - - -# noinspection PyMethodMayBeStatic -class SimplyPrintExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing SimplyPrint ...") - - if not (mr_instances := moonraker_exists("SimplyPrint Installer")): - return - - Logger.print_dialog( - DialogType.INFO, - self._construct_dialog(mr_instances, True), - ) - - if not get_confirm( - "Continue SimplyPrint installation?", - default_choice=True, - allow_go_back=True, - ): - Logger.print_info("Exiting SimplyPrint installation ...") - return - - try: - self._patch_moonraker_confs(mr_instances, True) - - except Exception as e: - Logger.print_error(f"Error during SimplyPrint installation:\n{e}") - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing SimplyPrint ...") - - if not (mr_instances := moonraker_exists("SimplyPrint Uninstaller")): - return - - Logger.print_dialog( - DialogType.INFO, - self._construct_dialog(mr_instances, False), - ) - - if not get_confirm( - "Do you really want to uninstall SimplyPrint?", - default_choice=True, - allow_go_back=True, - ): - Logger.print_info("Exiting SimplyPrint uninstallation ...") - return - - try: - self._patch_moonraker_confs(mr_instances, False) - - except Exception as e: - Logger.print_error(f"Error during SimplyPrint installation:\n{e}") - - def _construct_dialog( - self, mr_instances: List[Moonraker], is_install: bool - ) -> List[str]: - mr_names = [f"● {m.service_file_path.name}" for m in mr_instances] - _type = "install" if is_install else "uninstall" - - return [ - "The following Moonraker instances were found:", - *mr_names, - "\n\n", - f"The setup will {_type} SimplyPrint for all Moonraker instances. " - f"After {_type}ation, all Moonraker services will be restarted!", - ] - - def _patch_moonraker_confs( - self, mr_instances: List[Moonraker], is_install: bool - ) -> None: - section = "simplyprint" - _type, _ft = ("Adding", "to") if is_install else ("Removing", "from") - - patched_files = [] - for moonraker in mr_instances: - Logger.print_status( - f"{_type} section 'simplyprint' {_ft} {moonraker.cfg_file} ..." - ) - scp = SimpleConfigParser() - scp.read_file(moonraker.cfg_file) - - install_and_has_section = is_install and scp.has_section(section) - uninstall_and_has_no_section = not is_install and not scp.has_section( - section - ) - - if install_and_has_section or uninstall_and_has_no_section: - status = "already" if is_install else "does not" - Logger.print_info( - f"Section 'simplyprint' {status} exists! Skipping ..." - ) - continue - - if is_install and not scp.has_section("simplyprint"): - BackupService().backup_printer_config_dir() - scp.add_section(section) - elif not is_install and scp.has_section("simplyprint"): - BackupService().backup_printer_config_dir() - scp.remove_section(section) - scp.write_file(moonraker.cfg_file) - patched_files.append(moonraker.cfg_file) - - if patched_files: - InstanceManager.restart_all(mr_instances) - - install_state = "successfully" if patched_files else "was already" - Logger.print_dialog( - DialogType.SUCCESS, - [f"SimplyPrint {install_state} {'' if is_install else 'un'}installed!"], - center_content=True, - ) diff --git a/kiauh/extensions/spoolman/__init__.py b/kiauh/extensions/spoolman/__init__.py deleted file mode 100644 index d1cff1b..0000000 --- a/kiauh/extensions/spoolman/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent -SPOOLMAN_DOCKER_IMAGE = "ghcr.io/donkie/spoolman:latest" -SPOOLMAN_DIR = Path.home().joinpath("spoolman") -SPOOLMAN_DATA_DIR = SPOOLMAN_DIR.joinpath("data") -SPOOLMAN_COMPOSE_FILE = SPOOLMAN_DIR.joinpath("docker-compose.yml") -SPOOLMAN_DEFAULT_PORT = 7912 diff --git a/kiauh/extensions/spoolman/assets/docker-compose.yml b/kiauh/extensions/spoolman/assets/docker-compose.yml deleted file mode 100644 index 686c6a7..0000000 --- a/kiauh/extensions/spoolman/assets/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -services: - spoolman: - image: ghcr.io/donkie/spoolman:latest - restart: unless-stopped - volumes: - # Mount the host machine's ./data directory into the container's /home/app/.local/share/spoolman directory - - type: bind - source: ./data # This is where the data will be stored locally. Could also be set to for example `source: /home/pi/printer_data/spoolman`. - target: /home/app/.local/share/spoolman # Do NOT modify this line - ports: - # Map the host machine's port 7912 to the container's port 8000 - - "7912:8000" - environment: - - TZ=Europe/Stockholm # Optional, defaults to UTC diff --git a/kiauh/extensions/spoolman/metadata.json b/kiauh/extensions/spoolman/metadata.json deleted file mode 100644 index c569835..0000000 --- a/kiauh/extensions/spoolman/metadata.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "metadata": { - "index": 11, - "module": "spoolman_extension", - "maintained_by": "dw-0", - "display_name": "Spoolman (Docker)", - "description": [ - "Filament manager for 3D printing", - "- Track your filament inventory", - "- Monitor filament usage", - "- Manage vendors, materials, and spools", - "- Integrates with Moonraker", - "\n\n", - "Note: This extension installs Spoolman using Docker. Docker must be installed on your system before installing Spoolman." - ], - "repo": "https://github.com/Donkie/Spoolman", - "updates": true - } -} diff --git a/kiauh/extensions/spoolman/spoolman.py b/kiauh/extensions/spoolman/spoolman.py deleted file mode 100644 index bedd0d6..0000000 --- a/kiauh/extensions/spoolman/spoolman.py +++ /dev/null @@ -1,190 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import shutil -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError, run - -from components.moonraker.moonraker import Moonraker -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from extensions.spoolman import ( - MODULE_PATH, - SPOOLMAN_COMPOSE_FILE, - SPOOLMAN_DIR, - SPOOLMAN_DOCKER_IMAGE, -) -from utils.sys_utils import get_system_timezone - - -@dataclass -class Spoolman: - suffix: str - base: BaseInstance = field(init=False, repr=False) - dir: Path = SPOOLMAN_DIR - data_dir: Path = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) - self.data_dir = self.base.data_dir - - @staticmethod - def is_container_running() -> bool: - """Check if the Spoolman container is running""" - try: - result = run( - ["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "ps", "-q"], - capture_output=True, - text=True, - check=True, - ) - return bool(result.stdout.strip()) - except CalledProcessError: - return False - - @staticmethod - def is_docker_available() -> bool: - """Check if Docker is installed and available""" - try: - run(["docker", "--version"], capture_output=True, check=True) - return True - except (CalledProcessError, FileNotFoundError): - return False - - @staticmethod - def is_docker_compose_available() -> bool: - """Check if Docker Compose is installed and available""" - try: - # Try modern docker compose command - run(["docker", "compose", "version"], capture_output=True, check=True) - return True - except (CalledProcessError, FileNotFoundError): - # Try legacy docker-compose command - try: - run(["docker-compose", "--version"], capture_output=True, check=True) - return True - except (CalledProcessError, FileNotFoundError): - return False - - @staticmethod - def create_docker_compose() -> bool: - """Copy the docker-compose.yml file for Spoolman and set system timezone""" - try: - shutil.copy( - MODULE_PATH.joinpath("assets/docker-compose.yml"), - SPOOLMAN_COMPOSE_FILE, - ) - - # get system timezone - timezone = get_system_timezone() - - with open(SPOOLMAN_COMPOSE_FILE, "r") as f: - content = f.read() - - content = content.replace("TZ=Europe/Stockholm", f"TZ={timezone}") - - with open(SPOOLMAN_COMPOSE_FILE, "w") as f: - f.write(content) - - return True - except Exception as e: - Logger.print_error(f"Error creating Docker Compose file: {e}") - return False - - @staticmethod - def start_container() -> bool: - """Start the Spoolman container""" - try: - run( - ["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "up", "-d"], - check=True, - ) - return True - except CalledProcessError as e: - Logger.print_error(f"Failed to start Spoolman container: {e}") - return False - - @staticmethod - def update_container() -> bool: - """Update the Spoolman container""" - - def __get_image_id() -> str: - """Get the image ID of the Spoolman Docker image""" - try: - result = run( - ["docker", "images", "-q", SPOOLMAN_DOCKER_IMAGE], - capture_output=True, - text=True, - check=True, - ) - return result.stdout.strip() - except CalledProcessError: - raise Exception("Failed to get Spoolman Docker image ID") - - try: - old_image_id = __get_image_id() - Logger.print_status("Pulling latest Spoolman image...") - Spoolman.pull_image() - new_image_id = __get_image_id() - Logger.print_status("Tearing down old Spoolman container...") - Spoolman.tear_down_container() - Logger.print_status("Spinning up new Spoolman container...") - Spoolman.start_container() - if old_image_id != new_image_id: - Logger.print_status("Removing old Spoolman image...") - run(["docker", "rmi", old_image_id], check=True) - return True - - except CalledProcessError as e: - Logger.print_error(f"Failed to update Spoolman container: {e}") - return False - - @staticmethod - def tear_down_container() -> bool: - """Stop and remove the Spoolman container""" - try: - run( - ["docker", "compose", "-f", str(SPOOLMAN_COMPOSE_FILE), "down"], - check=True, - ) - return True - except CalledProcessError as e: - Logger.print_error(f"Failed to tear down Spoolman container: {e}") - return False - - @staticmethod - def pull_image() -> bool: - """Pull the Spoolman Docker image""" - try: - run(["docker", "pull", SPOOLMAN_DOCKER_IMAGE], check=True) - return True - except CalledProcessError as e: - Logger.print_error(f"Failed to pull Spoolman Docker image: {e}") - return False - - @staticmethod - def remove_image() -> bool: - """Remove the Spoolman Docker image""" - try: - image_exists = run( - ["docker", "images", "-q", SPOOLMAN_DOCKER_IMAGE], - capture_output=True, - text=True, - ).stdout.strip() - if not image_exists: - Logger.print_info("Spoolman Docker image not found. Nothing to remove.") - return False - - run(["docker", "rmi", SPOOLMAN_DOCKER_IMAGE], check=True) - return True - except CalledProcessError as e: - Logger.print_error(f"Failed to remove Spoolman Docker image: {e}") - return False diff --git a/kiauh/extensions/spoolman/spoolman_extension.py b/kiauh/extensions/spoolman/spoolman_extension.py deleted file mode 100644 index 072e5bb..0000000 --- a/kiauh/extensions/spoolman/spoolman_extension.py +++ /dev/null @@ -1,345 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -import re -from subprocess import CalledProcessError, run -from typing import List, Tuple - -from components.moonraker.moonraker import Moonraker -from components.moonraker.services.moonraker_instance_service import ( - MoonrakerInstanceService, -) -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.backup_service import BackupService -from extensions.base_extension import BaseExtension -from extensions.spoolman import ( - SPOOLMAN_COMPOSE_FILE, - SPOOLMAN_DATA_DIR, - SPOOLMAN_DEFAULT_PORT, - SPOOLMAN_DIR, -) -from extensions.spoolman.spoolman import Spoolman -from utils.config_utils import ( - add_config_section, - remove_config_section, -) -from utils.fs_utils import run_remove_routines -from utils.input_utils import get_confirm, get_number_input -from utils.sys_utils import get_ipv4_addr - - -# noinspection PyMethodMayBeStatic -class SpoolmanExtension(BaseExtension): - ip: str = "" - port: int = SPOOLMAN_DEFAULT_PORT - - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing Spoolman using Docker...") - - docker_available, docker_compose_available = self.__check_docker_prereqs() - if not docker_available or not docker_compose_available: - return - - if not self.__handle_existing_installation(): - self.ip: str = get_ipv4_addr() - self.__run_setup() - - # noinspection HttpUrlsUsage - Logger.print_dialog( - DialogType.SUCCESS, - [ - "Spoolman successfully installed using Docker!", - "You can access Spoolman via the following URL:", - f"http://{self.ip}:{self.port}", - ], - center_content=True, - ) - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating Spoolman Docker container...") - - if not SPOOLMAN_DIR.exists() or not SPOOLMAN_COMPOSE_FILE.exists(): - Logger.print_error("Spoolman installation not found or incomplete.") - return - - docker_available, docker_compose_available = self.__check_docker_prereqs() - if not docker_available or not docker_compose_available: - return - - Logger.print_status("Updating Spoolman container...") - if not Spoolman.update_container(): - return - - Logger.print_dialog( - DialogType.SUCCESS, - ["Spoolman Docker container successfully updated!"], - center_content=True, - ) - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing Spoolman Docker container...") - - if not SPOOLMAN_DIR.exists(): - Logger.print_info("Spoolman is not installed. Nothing to remove.") - return - - docker_available, docker_compose_available = self.__check_docker_prereqs() - if not docker_available or not docker_compose_available: - return - - # remove moonraker integration - mrsvc = MoonrakerInstanceService() - mrsvc.load_instances() - mr_instances: List[Moonraker] = mrsvc.get_all_instances() - - Logger.print_status("Removing Spoolman configuration from moonraker.conf...") - BackupService().backup_moonraker_conf() - remove_config_section("spoolman", mr_instances) - - Logger.print_status("Removing Spoolman from moonraker.asvc...") - self.__remove_from_moonraker_asvc() - - # stop and remove the container if docker-compose exists - if SPOOLMAN_COMPOSE_FILE.exists(): - Logger.print_status("Stopping and removing Spoolman container...") - - if Spoolman.tear_down_container(): - Logger.print_ok("Spoolman container removed!") - else: - Logger.print_error( - "Failed to remove Spoolman container! Please remove it manually." - ) - - if Spoolman.remove_image(): - Logger.print_ok("Spoolman container and image removed!") - else: - Logger.print_error( - "Failed to remove Spoolman image! Please remove it manually." - ) - - try: - svc = BackupService() - success = svc.backup_directory( - source_path=SPOOLMAN_DIR, - backup_name="spoolman", - target_path="spoolman", - ) - if success: - Logger.print_ok(f"Spoolman data backed up to {success}") - Logger.print_status("Removing Spoolman directory...") - if run_remove_routines(SPOOLMAN_DIR): - Logger.print_ok("Spoolman directory removed!") - else: - Logger.print_error( - "Failed to remove Spoolman directory! Please remove it manually." - ) - except Exception as e: - Logger.print_error(f"Failed to backup Spoolman directory: {e}") - Logger.print_info("Skipping Spoolman directory removal...") - - Logger.print_dialog( - DialogType.SUCCESS, - ["Spoolman successfully removed!"], - center_content=True, - ) - - def __run_setup(self) -> None: - # Create Spoolman directory and data directory - Logger.print_status("Setting up Spoolman directories...") - SPOOLMAN_DIR.mkdir(parents=True) - Logger.print_ok(f"Directory {SPOOLMAN_DIR} created!") - SPOOLMAN_DATA_DIR.mkdir(parents=True) - Logger.print_ok(f"Directory {SPOOLMAN_DATA_DIR} created!") - - # Set correct permissions for data directory - try: - Logger.print_status("Setting permissions for Spoolman data directory...") - run(["chown", "1000:1000", str(SPOOLMAN_DATA_DIR)], check=True) - Logger.print_ok("Permissions set!") - except CalledProcessError: - Logger.print_warn( - "Could not set permissions on data directory. This might cause issues." - ) - - Logger.print_status("Creating Docker Compose file...") - if Spoolman.create_docker_compose(): - Logger.print_ok("Docker Compose file created!") - else: - Logger.print_error("Failed to create Docker Compose file!") - - self.__port_config_prompt() - - Logger.print_status("Spinning up Spoolman container...") - if Spoolman.start_container(): - Logger.print_ok("Spoolman container started!") - else: - Logger.print_error("Failed to start Spoolman container!") - - if self.__add_moonraker_integration(): - Logger.print_ok("Spoolman integration added to Moonraker!") - else: - Logger.print_info("Moonraker integration skipped.") - - def __check_docker_prereqs(self) -> Tuple[bool, bool]: - # check if Docker is available - is_docker_available = Spoolman.is_docker_available() - if not is_docker_available: - Logger.print_error("Docker is not installed or not available.") - Logger.print_info( - "Please install Docker first: https://docs.docker.com/engine/install/" - ) - - # check if Docker Compose is available - is_docker_compose_available = Spoolman.is_docker_compose_available() - if not is_docker_compose_available: - Logger.print_error("Docker Compose is not installed or not available.") - - return is_docker_available, is_docker_compose_available - - def __port_config_prompt(self) -> None: - """Prompt for advanced configuration options""" - Logger.print_dialog( - DialogType.INFO, - [ - "You can configure Spoolman to run on a different port than the default. " - "Make sure you don't select a port which is already in use by " - "another application. Your input will not be validated! " - "The default port is 7912.", - ], - ) - if not get_confirm("Continue with default port 7912?", default_choice=True): - self.__set_port() - - def __set_port(self) -> None: - """Configure advanced options for Spoolman Docker container""" - port = get_number_input( - "Which port should Spoolman run on?", - default=SPOOLMAN_DEFAULT_PORT, - min_value=1024, - max_value=65535, - ) - - if port != SPOOLMAN_DEFAULT_PORT: - self.port = port - - with open(SPOOLMAN_COMPOSE_FILE, "r") as f: - content = f.read() - - port_mapping_pattern = r'"(\d+):8000"' - content = re.sub(port_mapping_pattern, f'"{port}:8000"', content) - - with open(SPOOLMAN_COMPOSE_FILE, "w") as f: - f.write(content) - - Logger.print_ok(f"Port set to {port}...") - - def __handle_existing_installation(self) -> bool: - if not (SPOOLMAN_DIR.exists() and SPOOLMAN_DIR.is_dir()): - return False - - compose_file_exists = SPOOLMAN_COMPOSE_FILE.exists() - container_running = Spoolman.is_container_running() - - if container_running and compose_file_exists: - Logger.print_info("Spoolman is already installed!") - return True - elif container_running and not compose_file_exists: - Logger.print_status( - "Spoolman container is running but Docker Compose file is missing..." - ) - if get_confirm( - "Do you want to recreate the Docker Compose file?", - default_choice=True, - ): - Spoolman.create_docker_compose() - self.__port_config_prompt() - return True - elif not container_running and compose_file_exists: - Logger.print_status( - "Docker Compose file exists but container is not running..." - ) - Spoolman.start_container() - return True - return False - - def __add_moonraker_integration(self) -> bool: - """Enable Moonraker integration for Spoolman Docker container""" - if not get_confirm("Add Moonraker integration?", default_choice=True): - return False - - Logger.print_status("Adding Spoolman integration to Moonraker...") - - # read port from the docker-compose file - port = SPOOLMAN_DEFAULT_PORT - if SPOOLMAN_COMPOSE_FILE.exists(): - with open(SPOOLMAN_COMPOSE_FILE, "r") as f: - content = f.read() - # Extract port from the port mapping - port_match = re.search(r'"(\d+):8000"', content) - if port_match: - port = port_match.group(1) - - mrsvc = MoonrakerInstanceService() - mrsvc.load_instances() - mr_instances = mrsvc.get_all_instances() - - BackupService().backup_moonraker_conf() - # noinspection HttpUrlsUsage - add_config_section( - section="spoolman", - instances=mr_instances, - options=[("server", f"http://{self.ip}:{port}")], - ) - - Logger.print_status("Adding Spoolman to moonraker.asvc...") - self.__add_to_moonraker_asvc() - - InstanceManager.restart_all(mr_instances) - - return True - - def __add_to_moonraker_asvc(self) -> None: - """Add Spoolman to moonraker.asvc""" - mrsvc = MoonrakerInstanceService() - mrsvc.load_instances() - mr_instances = mrsvc.get_all_instances() - for instance in mr_instances: - asvc_path = instance.data_dir.joinpath("moonraker.asvc") - if asvc_path.exists(): - if "Spoolman" in open(asvc_path).read(): - Logger.print_info(f"Spoolman already in {asvc_path}. Skipping...") - continue - - with open(asvc_path, "a") as f: - f.write("Spoolman\n") - - Logger.print_ok(f"Spoolman added to {asvc_path}!") - - def __remove_from_moonraker_asvc(self) -> None: - """Remove Spoolman from moonraker.asvc""" - mrsvc = MoonrakerInstanceService() - mrsvc.load_instances() - mr_instances = mrsvc.get_all_instances() - for instance in mr_instances: - asvc_path = instance.data_dir.joinpath("moonraker.asvc") - if asvc_path.exists(): - if "Spoolman" not in open(asvc_path).read(): - Logger.print_info(f"Spoolman not in {asvc_path}. Skipping...") - continue - - with open(asvc_path, "r") as f: - lines = f.readlines() - - new_lines = [line for line in lines if "Spoolman" not in line] - - with open(asvc_path, "w") as f: - f.writelines(new_lines) - - Logger.print_ok(f"Spoolman removed from {asvc_path}!") diff --git a/kiauh/extensions/telegram_bot/__init__.py b/kiauh/extensions/telegram_bot/__init__.py deleted file mode 100644 index c37a838..0000000 --- a/kiauh/extensions/telegram_bot/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent - -# repo -TG_BOT_REPO = "https://github.com/nlef/moonraker-telegram-bot.git" - -# names -TG_BOT_CFG_NAME = "telegram.conf" -TG_BOT_LOG_NAME = "telegram.log" -TG_BOT_SERVICE_NAME = "moonraker-telegram-bot.service" -TG_BOT_ENV_FILE_NAME = "moonraker-telegram-bot.env" - -# directories -TG_BOT_DIR = Path.home().joinpath("moonraker-telegram-bot") -TG_BOT_ENV = Path.home().joinpath("moonraker-telegram-bot-env") - -# files -TG_BOT_SERVICE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{TG_BOT_SERVICE_NAME}") -TG_BOT_ENV_FILE_TEMPLATE = MODULE_PATH.joinpath(f"assets/{TG_BOT_ENV_FILE_NAME}") -TG_BOT_REQ_FILE = TG_BOT_DIR.joinpath("scripts/requirements.txt") diff --git a/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env b/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env deleted file mode 100644 index 280f165..0000000 --- a/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.env +++ /dev/null @@ -1 +0,0 @@ -TELEGRAM_BOT_ARGS="%TELEGRAM_BOT_DIR%/bot/main.py -c %CFG% -l %LOG%" \ No newline at end of file diff --git a/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service b/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service deleted file mode 100644 index ded7475..0000000 --- a/kiauh/extensions/telegram_bot/assets/moonraker-telegram-bot.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Moonraker Telegram Bot SV1 -Documentation=https://github.com/nlef/moonraker-telegram-bot/wiki -After=network-online.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=simple -User=%USER% -WorkingDirectory=%TELEGRAM_BOT_DIR% -EnvironmentFile=%ENV_FILE% -ExecStart=%ENV%/bin/python $TELEGRAM_BOT_ARGS -Restart=always -RestartSec=10 diff --git a/kiauh/extensions/telegram_bot/metadata.json b/kiauh/extensions/telegram_bot/metadata.json deleted file mode 100644 index 342d3fb..0000000 --- a/kiauh/extensions/telegram_bot/metadata.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "metadata": { - "index": 5, - "module": "moonraker_telegram_bot_extension", - "maintained_by": "nlef", - "display_name": "Moonraker Telegram Bot", - "description": ["Control your printer with the Telegram messenger app."], - "repo": "https://github.com/nlef/moonraker-telegram-bot", - "updates": true - } -} diff --git a/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py b/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py deleted file mode 100644 index e3d4191..0000000 --- a/kiauh/extensions/telegram_bot/moonraker_telegram_bot.py +++ /dev/null @@ -1,124 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from subprocess import CalledProcessError - -from components.moonraker.moonraker import Moonraker -from core.constants import CURRENT_USER -from core.instance_manager.base_instance import BaseInstance -from core.logger import Logger -from extensions.telegram_bot import ( - TG_BOT_CFG_NAME, - TG_BOT_DIR, - TG_BOT_ENV, - TG_BOT_ENV_FILE_NAME, - TG_BOT_ENV_FILE_TEMPLATE, - TG_BOT_LOG_NAME, - TG_BOT_SERVICE_TEMPLATE, -) -from utils.fs_utils import create_folders -from utils.sys_utils import get_service_file_path - - -# noinspection PyMethodMayBeStatic -@dataclass(repr=True) -class MoonrakerTelegramBot: - suffix: str - base: BaseInstance = field(init=False, repr=False) - service_file_path: Path = field(init=False) - log_file_name: str = TG_BOT_LOG_NAME - bot_dir: Path = TG_BOT_DIR - env_dir: Path = TG_BOT_ENV - data_dir: Path = field(init=False) - cfg_file: Path = field(init=False) - - def __post_init__(self): - self.base: BaseInstance = BaseInstance(Moonraker, self.suffix) - self.base.log_file_name = self.log_file_name - - self.service_file_path: Path = get_service_file_path( - MoonrakerTelegramBot, self.suffix - ) - self.data_dir: Path = self.base.data_dir - self.cfg_file = self.base.cfg_dir.joinpath(TG_BOT_CFG_NAME) - - def create(self) -> None: - from utils.sys_utils import create_env_file, create_service_file - - Logger.print_status("Creating new Moonraker Telegram Bot Instance ...") - - try: - create_folders(self.base.base_folders) - create_service_file( - name=self.service_file_path.name, - content=self._prep_service_file_content(), - ) - create_env_file( - path=self.base.sysd_dir.joinpath(TG_BOT_ENV_FILE_NAME), - content=self._prep_env_file_content(), - ) - - except CalledProcessError as e: - Logger.print_error(f"Error creating instance: {e}") - raise - except OSError as e: - Logger.print_error(f"Error creating env file: {e}") - raise - - def _prep_service_file_content(self) -> str: - template = TG_BOT_SERVICE_TEMPLATE - - try: - with open(template, "r") as template_file: - template_content = template_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - service_content = template_content.replace( - "%USER%", - CURRENT_USER, - ) - service_content = service_content.replace( - "%TELEGRAM_BOT_DIR%", - self.bot_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV%", - self.env_dir.as_posix(), - ) - service_content = service_content.replace( - "%ENV_FILE%", - self.base.sysd_dir.joinpath(TG_BOT_ENV_FILE_NAME).as_posix(), - ) - return service_content - - def _prep_env_file_content(self) -> str: - template = TG_BOT_ENV_FILE_TEMPLATE - - try: - with open(template, "r") as env_file: - env_template_file_content = env_file.read() - except FileNotFoundError: - Logger.print_error(f"Unable to open {template} - File not found") - raise - - env_file_content = env_template_file_content.replace( - "%TELEGRAM_BOT_DIR%", - self.bot_dir.as_posix(), - ) - env_file_content = env_file_content.replace("%CFG%", self.cfg_file.as_posix()) - env_file_content = env_file_content.replace( - "%LOG%", - self.base.log_dir.joinpath(self.log_file_name).as_posix(), - ) - return env_file_content diff --git a/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py b/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py deleted file mode 100644 index ed45be5..0000000 --- a/kiauh/extensions/telegram_bot/moonraker_telegram_bot_extension.py +++ /dev/null @@ -1,229 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import shutil -from subprocess import run -from typing import List - -from components.moonraker.moonraker import Moonraker -from core.instance_manager.instance_manager import InstanceManager -from core.logger import DialogType, Logger -from core.services.backup_service import BackupService -from extensions.base_extension import BaseExtension -from extensions.telegram_bot import TG_BOT_REPO, TG_BOT_REQ_FILE -from extensions.telegram_bot.moonraker_telegram_bot import ( - TG_BOT_DIR, - TG_BOT_ENV, - MoonrakerTelegramBot, -) -from utils.common import check_install_dependencies -from utils.config_utils import add_config_section, remove_config_section -from utils.fs_utils import remove_file -from utils.git_utils import git_clone_wrapper, git_pull_wrapper -from utils.input_utils import get_confirm -from utils.instance_utils import get_instances -from utils.sys_utils import ( - cmd_sysctl_manage, - cmd_sysctl_service, - create_python_venv, - install_python_requirements, - parse_packages_from_file, -) - - -# noinspection PyMethodMayBeStatic -class TelegramBotExtension(BaseExtension): - def install_extension(self, **kwargs) -> None: - Logger.print_status("Installing Moonraker Telegram Bot ...") - - mr_instances: List[Moonraker] = get_instances(Moonraker) - if not mr_instances: - Logger.print_dialog( - DialogType.WARNING, - [ - "No Moonraker instances found!", - "Moonraker Telegram Bot requires Moonraker to be installed. " - "Please install Moonraker first!", - ], - ) - return - - instance_names = [ - f"● {instance.service_file_path.name}" for instance in mr_instances - ] - Logger.print_dialog( - DialogType.INFO, - [ - "The following Moonraker instances were found:", - *instance_names, - "\n\n", - "The setup will apply the same names to Telegram Bot!", - ], - ) - if not get_confirm( - "Continue Moonraker Telegram Bot installation?", - default_choice=True, - allow_go_back=True, - ): - return - - create_example_cfg = get_confirm("Create example telegram.conf?") - - try: - git_clone_wrapper(TG_BOT_REPO, TG_BOT_DIR) - self._install_dependencies() - - # create and start services / create bot configs - show_config_dialog = False - tb_names = [mr_i.suffix for mr_i in mr_instances] - for name in tb_names: - instance = MoonrakerTelegramBot(suffix=name) - instance.create() - - cmd_sysctl_service(instance.service_file_path.name, "enable") - - if create_example_cfg: - Logger.print_status( - f"Creating Telegram Bot config in {instance.base.cfg_dir} ..." - ) - template = TG_BOT_DIR.joinpath("scripts/base_install_template") - target_file = instance.cfg_file - if not target_file.exists(): - show_config_dialog = True - run(["cp", template, target_file], check=True) - else: - Logger.print_info( - f"Telegram Bot config in {instance.base.cfg_dir} already exists! Skipped ..." - ) - - cmd_sysctl_service(instance.service_file_path.name, "start") - - cmd_sysctl_manage("daemon-reload") - - # add to moonraker update manager - BackupService().backup_moonraker_conf() - self._patch_bot_update_manager(mr_instances) - - # restart moonraker - InstanceManager.restart_all(mr_instances) - - if show_config_dialog: - Logger.print_dialog( - DialogType.ATTENTION, - [ - "During the installation of the Moonraker Telegram Bot, " - "a basic config was created per instance. You need to edit the " - "config file to set up your Telegram Bot. Please refer to the " - "following wiki page for further information:", - "https://github.com/nlef/moonraker-telegram-bot/wiki", - ], - margin_bottom=1, - ) - - Logger.print_ok("Telegram Bot installation complete!") - except Exception as e: - Logger.print_error( - f"Error during installation of Moonraker Telegram Bot:\n{e}" - ) - - def update_extension(self, **kwargs) -> None: - Logger.print_status("Updating Moonraker Telegram Bot ...") - - instances = get_instances(MoonrakerTelegramBot) - InstanceManager.stop_all(instances) - - git_pull_wrapper(TG_BOT_DIR) - self._install_dependencies() - - InstanceManager.start_all(instances) - - def remove_extension(self, **kwargs) -> None: - Logger.print_status("Removing Moonraker Telegram Bot ...") - - mr_instances: List[Moonraker] = get_instances(Moonraker) - tb_instances: List[MoonrakerTelegramBot] = get_instances(MoonrakerTelegramBot) - - try: - self._remove_bot_instances(tb_instances) - self._remove_bot_dir() - self._remove_bot_env() - BackupService().backup_moonraker_conf() - remove_config_section("update_manager moonraker-telegram-bot", mr_instances) - self._delete_bot_logs(tb_instances) - except Exception as e: - Logger.print_error(f"Error during removal of Moonraker Telegram Bot:\n{e}") - - Logger.print_ok("Moonraker Telegram Bot removed!") - - def _install_dependencies(self) -> None: - # install dependencies - script = TG_BOT_DIR.joinpath("scripts/install.sh") - package_list = parse_packages_from_file(script) - - check_install_dependencies({*package_list}) - - # create virtualenv - if create_python_venv(TG_BOT_ENV, allow_access_to_system_site_packages=True): - install_python_requirements(TG_BOT_ENV, TG_BOT_REQ_FILE) - - def _patch_bot_update_manager(self, instances: List[Moonraker]) -> None: - env_py = f"{TG_BOT_ENV}/bin/python" - add_config_section( - section="update_manager moonraker-telegram-bot", - instances=instances, - options=[ - ("type", "git_repo"), - ("path", str(TG_BOT_DIR)), - ("origin", TG_BOT_REPO), - ("env", env_py), - ("requirements", "scripts/requirements.txt"), - ("install_script", "scripts/install.sh"), - ], - ) - - def _remove_bot_instances( - self, - instance_list: List[MoonrakerTelegramBot], - ) -> None: - for instance in instance_list: - Logger.print_status( - f"Removing instance {instance.service_file_path.stem} ..." - ) - InstanceManager.remove(instance) - - def _remove_bot_dir(self) -> None: - if not TG_BOT_DIR.exists(): - Logger.print_info(f"'{TG_BOT_DIR}' does not exist. Skipped ...") - return - - try: - shutil.rmtree(TG_BOT_DIR) - except OSError as e: - Logger.print_error(f"Unable to delete '{TG_BOT_DIR}':\n{e}") - - def _remove_bot_env(self) -> None: - if not TG_BOT_ENV.exists(): - Logger.print_info(f"'{TG_BOT_ENV}' does not exist. Skipped ...") - return - - try: - shutil.rmtree(TG_BOT_ENV) - except OSError as e: - Logger.print_error(f"Unable to delete '{TG_BOT_ENV}':\n{e}") - - def _delete_bot_logs(self, instances: List[MoonrakerTelegramBot]) -> None: - all_logfiles = [] - for instance in instances: - all_logfiles = list(instance.base.log_dir.glob("telegram_bot.log*")) - if not all_logfiles: - Logger.print_info("No Moonraker Telegram Bot logs found. Skipped ...") - return - - for log in all_logfiles: - Logger.print_status(f"Remove '{log}'") - remove_file(log) diff --git a/kiauh/main.py b/kiauh/main.py deleted file mode 100644 index 06512ea..0000000 --- a/kiauh/main.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -import io -import sys - -from core.logger import Logger -from core.menus.main_menu import MainMenu -from core.settings.kiauh_settings import KiauhSettings - - -def ensure_encoding() -> None: - if sys.stdout.encoding == "UTF-8" or not isinstance(sys.stdout, io.TextIOWrapper): - return - sys.stdout.reconfigure(encoding="utf-8") - - -def main() -> None: - try: - KiauhSettings() - ensure_encoding() - MainMenu().run() - except KeyboardInterrupt: - Logger.print_ok("\nHappy printing!\n", prefix=False) diff --git a/kiauh/procedures/__init__.py b/kiauh/procedures/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kiauh/procedures/switch_repo.py b/kiauh/procedures/switch_repo.py deleted file mode 100644 index e7fbdfd..0000000 --- a/kiauh/procedures/switch_repo.py +++ /dev/null @@ -1,145 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import shutil -from pathlib import Path -from typing import Literal - -from components.klipper import ( - KLIPPER_DIR, - KLIPPER_ENV_DIR, - KLIPPER_REQ_FILE, -) -from components.klipper.klipper import Klipper -from components.klipper.klipper_utils import install_klipper_packages -from components.moonraker import ( - MOONRAKER_DIR, - MOONRAKER_ENV_DIR, - MOONRAKER_REQ_FILE, -) -from components.moonraker.moonraker import Moonraker -from components.moonraker.services.moonraker_setup_service import ( - install_moonraker_packages, -) -from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from core.services.backup_service import BackupService -from utils.git_utils import GitException, git_clone_wrapper -from utils.instance_utils import get_instances -from utils.sys_utils import ( - VenvCreationFailedException, - create_python_venv, - install_python_requirements, -) - - -class RepoSwitchFailedException(Exception): - pass - - -def run_switch_repo_routine( - name: Literal["klipper", "moonraker"], repo_url: str, branch: str -) -> None: - repo_dir: Path = KLIPPER_DIR if name == "klipper" else MOONRAKER_DIR - env_dir: Path = KLIPPER_ENV_DIR if name == "klipper" else MOONRAKER_ENV_DIR - req_file = KLIPPER_REQ_FILE if name == "klipper" else MOONRAKER_REQ_FILE - _type = Klipper if name == "klipper" else Moonraker - - # step 1: stop all instances - Logger.print_status(f"Stopping all {_type.__name__} instances ...") - instances = get_instances(_type) - InstanceManager.stop_all(instances) - - repo_dir_backup_path: Path | None = None - env_dir_backup_path: Path | None = None - - try: - svc = BackupService() - svc.backup_directory( - source_path=repo_dir, - backup_name=name, - target_path=name, - ) - env_backup_name: str = f"{name if name == 'moonraker' else 'klippy'}-env" - svc.backup_directory( - source_path=env_dir, - backup_name=env_backup_name, - target_path=name, - ) - - if not (repo_url or branch): - error = f"Invalid repository URL ({repo_url}) or branch ({branch})!" - raise ValueError(error) - - # step 4: clone new repo - git_clone_wrapper(repo_url, repo_dir, branch, force=True) - - # step 5: install os dependencies - if name == "klipper": - install_klipper_packages() - elif name == "moonraker": - install_moonraker_packages() - - # step 6: recreate python virtualenv - Logger.print_status(f"Recreating {_type.__name__} virtualenv ...") - if not create_python_venv(env_dir, force=True): - raise GitException(f"Failed to recreate virtualenv for {_type.__name__}") - else: - install_python_requirements(env_dir, req_file) - - Logger.print_ok(f"Switched to {repo_url} at branch {branch}!") - - except (GitException, VenvCreationFailedException) as e: - # if something goes wrong during cloning or recreating the virtualenv, - # we restore the backup of the repo and env - Logger.print_error(f"Error during repository switch: {e}", start="\n") - Logger.print_status(f"Restoring last backup of {_type.__name__} ...") - _restore_repo_backup( - _type.__name__, - env_dir, - env_dir_backup_path, - repo_dir, - repo_dir_backup_path, - ) - - except RepoSwitchFailedException as e: - Logger.print_error(f"Something went wrong: {e}") - return - - except Exception as e: - raise RepoSwitchFailedException(e) - - Logger.print_status(f"Restarting all {_type.__name__} instances ...") - InstanceManager.start_all(instances) - - -def _restore_repo_backup( - name: str, - env_dir: Path, - env_dir_backup_path: Path | None, - repo_dir: Path, - repo_dir_backup_path: Path | None, -) -> None: - # if repo_dir_backup_path is not None and env_dir_backup_path is not None: - if not repo_dir_backup_path or not env_dir_backup_path: - raise RepoSwitchFailedException( - f"Unable to restore backup of {name}! Path of backups directory is None!" - ) - - try: - if repo_dir.exists(): - shutil.rmtree(repo_dir) - shutil.copytree(repo_dir_backup_path, repo_dir) - if env_dir.exists(): - shutil.rmtree(env_dir) - shutil.copytree(env_dir_backup_path, env_dir) - Logger.print_warn(f"Restored backup of {name} successfully!") - except Exception as e: - raise RepoSwitchFailedException(f"Error restoring backup: {e}") diff --git a/kiauh/procedures/system.py b/kiauh/procedures/system.py deleted file mode 100644 index dc937c2..0000000 --- a/kiauh/procedures/system.py +++ /dev/null @@ -1,103 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path -from subprocess import PIPE, CalledProcessError, run - -from core.logger import DialogType, Logger -from utils.common import check_install_dependencies, get_current_date -from utils.fs_utils import check_file_exist -from utils.input_utils import get_confirm, get_string_input - - -def change_system_hostname() -> None: - """ - Procedure to change the system hostname. - :return: - """ - - Logger.print_dialog( - DialogType.CUSTOM, - [ - "Changing the hostname of this system allows you to access an installed " - "webinterface by simply typing the hostname like this in the browser:", - "\n\n", - "http://.local", - "\n\n", - "Example: If you set your hostname to 'my-printer', you can access an " - "installed webinterface by typing 'http://my-printer.local' in the " - "browser.", - ], - custom_title="CHANGE SYSTEM HOSTNAME", - ) - if not get_confirm("Do you want to change the hostname?", default_choice=False): - return - - Logger.print_dialog( - DialogType.CUSTOM, - [ - "Allowed characters: a-z, 0-9 and '-'", - "The name must not contain the following:", - "\n\n", - "● Any special characters", - "● No leading or trailing '-'", - ], - ) - hostname = get_string_input( - "Enter the new hostname", - regex=r"^[a-z0-9]+([a-z0-9-]*[a-z0-9])?$", - ) - if not get_confirm(f"Change the hostname to '{hostname}'?", default_choice=False): - Logger.print_info("Aborting hostname change ...") - return - - try: - Logger.print_status("Changing hostname ...") - - Logger.print_status("Checking for dependencies ...") - check_install_dependencies({"avahi-daemon"}, include_global=False) - - # create or backup hosts file - Logger.print_status("Creating backup of hosts file ...") - hosts_file = Path("/etc/hosts") - if not check_file_exist(hosts_file, True): - cmd = ["sudo", "touch", hosts_file.as_posix()] - run(cmd, stderr=PIPE, check=True) - else: - date_time = get_current_date() - name = f"hosts.{date_time.get('date')}-{date_time.get('time')}.bak" - hosts_file_backup = Path(f"/etc/{name}") - cmd = [ - "sudo", - "cp", - hosts_file.as_posix(), - hosts_file_backup.as_posix(), - ] - run(cmd, stderr=PIPE, check=True) - Logger.print_ok() - - # call hostnamectl set-hostname - Logger.print_status(f"Setting hostname to '{hostname}' ...") - cmd = ["sudo", "hostnamectl", "set-hostname", hostname] - run(cmd, stderr=PIPE, check=True) - Logger.print_ok() - - # add hostname to hosts file at the end of the file - Logger.print_status("Writing new hostname to /etc/hosts ...") - stdin = f"127.0.0.1 {hostname}\n" - cmd = ["sudo", "tee", "-a", hosts_file.as_posix()] - run(cmd, input=stdin.encode(), stderr=PIPE, stdout=PIPE, check=True) - Logger.print_ok() - - Logger.print_ok("New hostname successfully configured!") - Logger.print_ok("Remember to reboot for the changes to take effect!\n") - - except CalledProcessError as e: - Logger.print_error(f"Error during change hostname procedure: {e}") - return diff --git a/kiauh/utils/__init__.py b/kiauh/utils/__init__.py deleted file mode 100644 index 8bfcd7a..0000000 --- a/kiauh/utils/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from pathlib import Path - -MODULE_PATH = Path(__file__).resolve().parent diff --git a/kiauh/utils/common.py b/kiauh/utils/common.py deleted file mode 100644 index 99db882..0000000 --- a/kiauh/utils/common.py +++ /dev/null @@ -1,181 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from __future__ import annotations - -import re -from datetime import datetime -from pathlib import Path -from typing import Dict, List, Literal, Set - -from components.moonraker.moonraker import Moonraker -from core.constants import ( - GLOBAL_DEPS, -) -from core.logger import DialogType, Logger -from core.types.color import Color -from core.types.component_status import ComponentStatus, StatusCode -from utils.git_utils import ( - get_current_branch, - get_local_commit, - get_local_tags, - get_remote_commit, - get_repo_name, - get_repo_url, -) -from utils.instance_utils import get_instances -from utils.sys_utils import ( - check_package_install, - install_system_packages, - update_system_package_lists, -) - - -def get_kiauh_version() -> str: - """ - Helper method to get the current KIAUH version by reading the latest tag - :return: string of the latest tag or a default value if no tags exist - """ - tags = get_local_tags(Path(__file__).parent.parent) - if tags: - return tags[-1] - else: - return "v?.?.?" - - -def convert_camelcase_to_kebabcase(name: str) -> str: - return re.sub(r"(? Dict[Literal["date", "time"], str]: - """ - Get the current date | - :return: Dict holding a date and time key:value pair - """ - now: datetime = datetime.today() - date: str = now.strftime("%Y%m%d") - time: str = now.strftime("%H%M%S") - - return {"date": date, "time": time} - - -def check_install_dependencies( - deps: Set[str] | None = None, include_global: bool = True -) -> None: - """ - Common helper method to check if dependencies are installed - and if not, install them automatically | - :param include_global: Wether to include the global dependencies or not - :param deps: List of strings of package names to check if installed - :return: None - """ - if deps is None: - deps = set() - - if include_global: - deps.update(GLOBAL_DEPS) - - requirements = check_package_install(deps) - if requirements: - Logger.print_status("Installing dependencies ...") - Logger.print_info("The following packages need installation:") - for r in requirements: - print(Color.apply(f"● {r}", Color.CYAN)) - update_system_package_lists(silent=False) - install_system_packages(requirements) - - -def get_install_status( - repo_dir: Path, - env_dir: Path | None = None, - instance_type: type | None = None, - files: List[Path] | None = None, -) -> ComponentStatus: - """ - Helper method to get the installation status of software components - :param repo_dir: the repository directory - :param env_dir: the python environment directory - :param instance_type: The component type - :param files: List of optional files to check for existence - :return: Dictionary with status string, statuscode and instance count - """ - from utils.instance_utils import get_instances - - checks = [] - branch: str = "" - - if repo_dir.exists(): - checks.append(True) - branch = get_current_branch(repo_dir) - - if env_dir is not None: - checks.append(env_dir.exists()) - - instances = 0 - if instance_type is not None: - instances = len(get_instances(instance_type)) - checks.append(instances > 0) - - if files is not None: - for f in files: - checks.append(f.exists()) - - status: StatusCode - if checks and all(checks): - status = 2 # installed - elif not any(checks): - status = 0 # not installed - else: - status = 1 # incomplete - - org, repo = get_repo_name(repo_dir) - repo_url = get_repo_url(repo_dir) if repo_dir.exists() else None - - return ComponentStatus( - status=status, - instances=instances, - owner=org, - repo=repo, - repo_url=repo_url, - branch=branch, - local=get_local_commit(repo_dir), - remote=get_remote_commit(repo_dir), - ) - - -def moonraker_exists(name: str = "") -> List[Moonraker]: - """ - Helper method to check if a Moonraker instance exists - :param name: Optional name of an installer where the check is performed - :return: True if at least one Moonraker instance exists, False otherwise - """ - mr_instances: List[Moonraker] = get_instances(Moonraker) - - info = ( - f"{name} requires Moonraker to be installed" - if name - else "A Moonraker installation is required" - ) - - if not mr_instances: - Logger.print_dialog( - DialogType.WARNING, - [ - "No Moonraker instances found!", - f"{info}. Please install Moonraker first!", - ], - ) - return [] - return mr_instances - - -def trunc_string(input_str: str, length: int) -> str: - if len(input_str) > length: - return f"{input_str[: length - 3]}..." - return input_str diff --git a/kiauh/utils/config_utils.py b/kiauh/utils/config_utils.py deleted file mode 100644 index cddfa16..0000000 --- a/kiauh/utils/config_utils.py +++ /dev/null @@ -1,105 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import shutil -import tempfile -from pathlib import Path -from typing import List, Tuple, Union - -from core.logger import Logger -from core.submodules.simple_config_parser.src.simple_config_parser.simple_config_parser import ( - SimpleConfigParser, -) -from utils.instance_type import InstanceType - -ConfigOption = Tuple[str, Union[str, List[str]]] - - -def add_config_section( - section: str, - instances: List[InstanceType], - options: List[ConfigOption] | None = None, -) -> None: - if not instances: - return - - for instance in instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Add section '[{section}]' to '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - continue - - scp = SimpleConfigParser() - scp.read_file(cfg_file) - if scp.has_section(section): - Logger.print_info("Section already exist. Skipped ...") - continue - - scp.add_section(section) - - if options is not None: - for option in reversed(options): - scp.set_option(section, option[0], option[1]) - - scp.write_file(cfg_file) - - Logger.print_ok("OK!") - - -def add_config_section_at_top(section: str, instances: List[InstanceType]) -> None: - # TODO: this could be implemented natively in SimpleConfigParser - for instance in instances: - tmp_cfg = tempfile.NamedTemporaryFile(mode="w", delete=False) - tmp_cfg_path = Path(tmp_cfg.name) - scp = SimpleConfigParser() - scp.read_file(tmp_cfg_path) - scp.add_section(section) - scp.write_file(tmp_cfg_path) - tmp_cfg.close() - - cfg_file = instance.cfg_file - with open(cfg_file, "r") as org: - org_content = org.readlines() - with open(tmp_cfg_path, "a") as tmp: - tmp.writelines(org_content) - - cfg_file.unlink() - shutil.move(tmp_cfg_path.as_posix(), cfg_file) - - Logger.print_ok("OK!") - - -def remove_config_section( - section: str, instances: List[InstanceType] -) -> List[InstanceType]: - removed_from: List[InstanceType] = [] - for instance in instances: - cfg_file = instance.cfg_file - Logger.print_status(f"Remove section '[{section}]' from '{cfg_file}' ...") - - if not Path(cfg_file).exists(): - Logger.print_warn(f"'{cfg_file}' not found!") - continue - - scp = SimpleConfigParser() - scp.read_file(cfg_file) - if not scp.has_section(section): - Logger.print_info("Section does not exist. Skipped ...") - continue - - scp.remove_section(section) - scp.write_file(cfg_file) - - removed_from.append(instance) - Logger.print_ok("OK!") - - return removed_from diff --git a/kiauh/utils/fs_utils.py b/kiauh/utils/fs_utils.py deleted file mode 100644 index cabdaf8..0000000 --- a/kiauh/utils/fs_utils.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env python3 - -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import os -import re -import shutil -from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, call, check_output, run -from typing import List -from zipfile import ZipFile - -from core.decorators import deprecated -from core.logger import Logger - - -def check_file_exist(file_path: Path, sudo=False) -> bool: - """ - Helper function for checking the existence of a file | - :param file_path: the absolute path of the file to check - :param sudo: use sudo if required - :return: True, if file exists, otherwise False - """ - if sudo: - command = ["sudo", "find", file_path.as_posix()] - try: - check_output(command, stderr=DEVNULL) - return True - except CalledProcessError: - return False - else: - if os.access(file_path, os.F_OK): - return file_path.exists() - else: - return False - - -def create_symlink(source: Path, target: Path, sudo=False) -> None: - try: - cmd = ["ln", "-sf", source.as_posix(), target.as_posix()] - if sudo: - cmd.insert(0, "sudo") - run(cmd, stderr=PIPE, check=True) - except CalledProcessError as e: - Logger.print_error(f"Failed to create symlink: {e}") - raise - - -def remove_with_sudo(files: Path | List[Path]) -> bool: - _files = [] - _removed = [] - if isinstance(files, list): - _files = files - else: - _files.append(files) - - for f in _files: - try: - cmd = ["sudo", "find", f.as_posix()] - if call(cmd, stderr=DEVNULL, stdout=DEVNULL) == 1: - Logger.print_info(f"File '{f}' does not exist. Skipped ...") - continue - cmd = ["sudo", "rm", "-rf", f.as_posix()] - run(cmd, stderr=PIPE, check=True) - Logger.print_ok(f"File '{f}' was successfully removed!") - _removed.append(f) - except CalledProcessError as e: - Logger.print_error(f"Error removing file '{f}': {e}") - - return len(_removed) > 0 - - -@deprecated(info="Use remove_with_sudo instead", replaced_by=remove_with_sudo) -def remove_file(file_path: Path, sudo=False) -> None: - try: - cmd = f"{'sudo ' if sudo else ''}rm -f {file_path}" - run(cmd, stderr=PIPE, check=True, shell=True) - except CalledProcessError as e: - log = f"Cannot remove file {file_path}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def run_remove_routines(file: Path) -> bool: - try: - if not file.is_symlink() and not file.exists(): - Logger.print_info(f"File '{file}' does not exist. Skipped ...") - return False - - if file.is_dir(): - shutil.rmtree(file) - elif file.is_file() or file.is_symlink(): - file.unlink() - else: - Logger.print_error(f"File '{file}' is neither a file nor a directory!") - return False - Logger.print_ok(f"File '{file}' was successfully removed!") - return True - except OSError as e: - Logger.print_error(f"Unable to delete '{file}':\n{e}") - try: - Logger.print_info("Trying to remove with sudo ...") - if remove_with_sudo(file): - Logger.print_ok(f"File '{file}' was successfully removed!") - return True - except CalledProcessError as e: - Logger.print_error(f"Error deleting '{file}' with sudo:\n{e}") - Logger.print_error("Remove this directory manually!") - return False - - -def unzip(filepath: Path, target_dir: Path) -> None: - """ - Helper function to unzip a zip-archive into a target directory | - :param filepath: the path to the zip-file to unzip - :param target_dir: the target directory to extract the files into - :return: None - """ - with ZipFile(filepath, "r") as _zip: - _zip.extractall(target_dir) - - -def create_folders(dirs: List[Path]) -> None: - try: - for _dir in dirs: - if _dir.exists(): - continue - _dir.mkdir(exist_ok=True) - Logger.print_ok(f"Created directory '{_dir}'!") - except OSError as e: - Logger.print_error(f"Error creating directories: {e}") - raise - - -def get_data_dir(instance_type: type, suffix: str) -> Path: - from utils.sys_utils import get_service_file_path - - # if the service file exists, we read the data dir path from it - # this also ensures compatibility with pre v6.0.0 instances - service_file_path: Path = get_service_file_path(instance_type, suffix) - if service_file_path and service_file_path.exists(): - with open(service_file_path, "r") as service_file: - lines = service_file.readlines() - for line in lines: - pattern = r"^EnvironmentFile=(.+)(/systemd/.+\.env)" - match = re.search(pattern, line) - if match: - return Path(match.group(1)) - - if suffix != "": - # this is the new data dir naming scheme introduced in v6.0.0 - return Path.home().joinpath(f"printer_{suffix}_data") - - return Path.home().joinpath("printer_data") diff --git a/kiauh/utils/git_utils.py b/kiauh/utils/git_utils.py deleted file mode 100644 index 857c0ad..0000000 --- a/kiauh/utils/git_utils.py +++ /dev/null @@ -1,360 +0,0 @@ -from __future__ import annotations - -import json -import re -import shutil -import urllib.request -from http.client import HTTPResponse -from json import JSONDecodeError -from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, check_output, run -from typing import List, Tuple, Type - -from core.instance_manager.instance_manager import InstanceManager -from core.logger import Logger -from utils.input_utils import get_confirm, get_number_input -from utils.instance_type import InstanceType -from utils.instance_utils import get_instances - - -class GitException(Exception): - pass - - -def git_clone_wrapper( - repo: str, target_dir: Path, branch: str | None = None, force: bool = False -) -> None: - """ - Clones a repository from the given URL and checks out the specified branch if given. - The clone will be performed with the '--filter=blob:none' flag to perform a blobless clone. - - :param repo: The URL of the repository to clone. - :param branch: The branch to check out. If None, master or main, no checkout will be performed. - :param target_dir: The directory where the repository will be cloned. - :param force: Force the cloning of the repository even if it already exists. - :return: None - """ - log = f"Cloning repository from '{repo}'" - Logger.print_status(log) - try: - if Path(target_dir).exists(): - question = f"'{target_dir}' already exists. Overwrite?" - if not force and not get_confirm(question, default_choice=False): - Logger.print_info("Skip cloning of repository ...") - return - shutil.rmtree(target_dir) - - git_cmd_clone(repo, target_dir, blobless=True) - - if branch not in ("master", "main"): - git_cmd_checkout(branch, target_dir) - - except CalledProcessError: - log = "An unexpected error occured during cloning of the repository." - Logger.print_error(log) - raise GitException(log) - except OSError as e: - Logger.print_error(f"Error removing existing repository: {e.strerror}") - raise GitException(f"Error removing existing repository: {e.strerror}") - - -def git_pull_wrapper(target_dir: Path) -> None: - """ - A function that updates a repository using git pull. - - :param target_dir: The directory of the repository. - :return: None - """ - Logger.print_status("Updating repository ...") - try: - git_cmd_pull(target_dir) - except CalledProcessError: - log = "An unexpected error occured during updating the repository." - Logger.print_error(log) - return - - -def get_repo_name(repo: Path) -> Tuple[str, str]: - """ - Helper method to extract the organisation and name of a repository | - :param repo: repository to extract the values from - :return: String in form of "/" or None - """ - if not repo.exists() or not repo.joinpath(".git").exists(): - return "-", "-" - - try: - cmd = ["git", "-C", repo.as_posix(), "config", "--get", "remote.origin.url"] - result: str = check_output(cmd, stderr=DEVNULL).decode(encoding="utf-8") - substrings: List[str] = result.strip().split("/")[-2:] - - orga: str = substrings[0] if substrings[0] else "-" - name: str = substrings[1] if substrings[1] else "-" - - return orga, name.replace(".git", "") - - except CalledProcessError: - return "-", "-" - - -def get_current_branch(repo: Path) -> str: - """ - Get the current branch of a local Git repository - :param repo: Path to the local Git repository - :return: Current branch - """ - try: - cmd = ["git", "branch", "--show-current"] - result: str = check_output(cmd, stderr=DEVNULL, cwd=repo).decode( - encoding="utf-8" - ) - return result.strip() if result else "-" - - except CalledProcessError: - return "-" - - -def get_local_tags(repo_path: Path, _filter: str | None = None) -> List[str]: - """ - Get all tags of a local Git repository - :param repo_path: Path to the local Git repository - :param _filter: Optional filter to filter the tags by - :return: List of tags - """ - try: - cmd: List[str] = ["git", "tag", "-l"] - - if _filter is not None: - cmd.append(f"'${_filter}'") - - result: str = check_output( - cmd, - stderr=DEVNULL, - cwd=repo_path.as_posix(), - ).decode(encoding="utf-8") - - tags: List[str] = result.split("\n")[:-1] - - return sorted( - tags, - key=lambda x: [int(i) if i.isdigit() else i for i in re.split(r"(\d+)", x)], - ) - - except CalledProcessError: - return [] - - -def get_remote_tags(repo_path: str) -> List[str]: - """ - Gets the tags of a GitHub repostiory - :param repo_path: path of the GitHub repository - e.g. `/` - :return: List of tags - """ - try: - url = f"https://api.github.com/repos/{repo_path}/tags" - with urllib.request.urlopen(url) as r: - response: HTTPResponse = r - if response.getcode() != 200: - Logger.print_error( - f"Error retrieving tags: HTTP status code {response.getcode()}" - ) - return [] - - data = json.loads(response.read()) - return [item["name"] for item in data] - except (JSONDecodeError, TypeError) as e: - Logger.print_error(f"Error while processing the response: {e}") - raise - - -def get_latest_remote_tag(repo_path: str) -> str: - """ - Gets the latest stable tag of a GitHub repostiory - :param repo_path: path of the GitHub repository - e.g. `/` - :return: tag or empty string - """ - try: - if len(latest_tag := get_remote_tags(repo_path)) > 0: - return latest_tag[0] - else: - return "" - except Exception: - raise - - -def get_latest_unstable_tag(repo_path: str) -> str: - """ - Gets the latest unstable (alpha, beta, rc) tag of a GitHub repository - :param repo_path: path of the GitHub repository - e.g. `/` - :return: tag or empty string - """ - try: - if ( - len(unstable_tags := [t for t in get_remote_tags(repo_path) if "-" in t]) - > 0 - ): - return unstable_tags[0] - else: - return "" - except Exception: - Logger.print_error("Error while getting the latest unstable tag") - raise - - -def compare_semver_tags(tag1: str, tag2: str) -> bool: - """ - Compare two semver version strings. - Does not support comparing pre-release versions (e.g. 1.0.0-rc.1, 1.0.0-beta.1) - :param tag1: First version string - :param tag2: Second version string - :return: True if tag1 is greater than tag2, False otherwise - """ - if tag1 == tag2: - return False - - def parse_version(v) -> List[int]: - return list(map(int, v[1:].split("."))) - - tag1_parts = parse_version(tag1) - tag2_parts = parse_version(tag2) - - max_len = max(len(tag1_parts), len(tag2_parts)) - tag1_parts += [0] * (max_len - len(tag1_parts)) - tag2_parts += [0] * (max_len - len(tag2_parts)) - - for part1, part2 in zip(tag1_parts, tag2_parts): - if part1 != part2: - return part1 > part2 - - return False - - -def get_local_commit(repo: Path) -> str | None: - if not repo.exists() or not repo.joinpath(".git").exists(): - return None - - try: - cmd = "git describe HEAD --always --tags | cut -d '-' -f 1,2" - return check_output(cmd, shell=True, text=True, cwd=repo).strip() - except CalledProcessError: - return None - - -def get_remote_commit(repo: Path) -> str | None: - if not repo.exists() or not repo.joinpath(".git").exists(): - return None - - try: - branch = get_current_branch(repo) - cmd = f"git describe 'origin/{branch}' --always --tags | cut -d '-' -f 1,2" - return check_output( - cmd, - shell=True, - text=True, - cwd=repo, - stderr=DEVNULL, - ).strip() - except CalledProcessError: - return None - - -def git_cmd_clone(repo: str, target_dir: Path, blobless: bool = False) -> None: - """ - Clones a repository with optional blobless clone. - - :param repo: URL of the repository to clone. - :param target_dir: Path where the repository will be cloned. - :param blobless: If True, perform a blobless clone by adding the '--filter=blob:none' flag. - """ - try: - command = ["git", "clone"] - - if blobless: - command.append("--filter=blob:none") - - command += [repo, target_dir.as_posix()] - - run(command, check=True) - Logger.print_ok("Clone successful!") - except CalledProcessError as e: - error = e.stderr.decode() if e.stderr else "Unknown error" - log = f"Error cloning repository {repo}: {error}" - Logger.print_error(log) - raise - - -def git_cmd_checkout(branch: str | None, target_dir: Path) -> None: - if branch is None: - return - - try: - command = ["git", "checkout", f"{branch}"] - run(command, cwd=target_dir, check=True) - - Logger.print_ok("Checkout successful!") - except CalledProcessError as e: - log = f"Error checking out branch {branch}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def git_cmd_pull(target_dir: Path) -> None: - try: - command = ["git", "pull"] - run(command, cwd=target_dir, check=True) - except CalledProcessError as e: - log = f"Error on git pull: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def rollback_repository(repo_dir: Path, instance: Type[InstanceType]) -> None: - q1 = "How many commits do you want to roll back" - amount = get_number_input(q1, 1, allow_go_back=True) - - instances = get_instances(instance) - - Logger.print_warn("Do not continue if you have ongoing prints!", start="\n") - Logger.print_warn( - f"All currently running {instance.__name__} services will be stopped!" - ) - if not get_confirm( - f"Roll back {amount} commit{'s' if amount > 1 else ''}", - default_choice=False, - allow_go_back=True, - ): - Logger.print_info("Aborting roll back ...") - return - - InstanceManager.stop_all(instances) - - try: - cmd = ["git", "reset", "--hard", f"HEAD~{amount}"] - run(cmd, cwd=repo_dir, check=True, stdout=PIPE, stderr=PIPE) - Logger.print_ok(f"Rolled back {amount} commits!", start="\n") - except CalledProcessError as e: - Logger.print_error(f"An error occured during repo rollback:\n{e}") - - InstanceManager.start_all(instances) - - -def get_repo_url(repo_dir: Path) -> str | None: - """ - Get the remote repository URL for a git repository - :param repo_dir: Path to the git repository - :return: URL of the remote repository or None if not found - """ - if not repo_dir.exists(): - return None - - try: - result = run( - ["git", "config", "--get", "remote.origin.url"], - cwd=repo_dir, - capture_output=True, - text=True, - check=True, - ) - return result.stdout.strip() - except CalledProcessError: - return None diff --git a/kiauh/utils/input_utils.py b/kiauh/utils/input_utils.py deleted file mode 100644 index 6b77e15..0000000 --- a/kiauh/utils/input_utils.py +++ /dev/null @@ -1,177 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import re -from typing import Dict, List - -from core.constants import INVALID_CHOICE -from core.logger import Logger -from core.types.color import Color - - -def get_confirm(question: str, default_choice=True, allow_go_back=False) -> bool | None: - """ - Helper method for validating confirmation (yes/no) user input. | - :param question: The question to display - :param default_choice: A default if input was submitted without input - :param allow_go_back: Navigate back to a previous dialog - :return: Either True or False, or None on go_back - """ - options_confirm = ["y", "yes"] - options_decline = ["n", "no"] - options_go_back = ["b", "B"] - - if default_choice: - def_choice = "(Y/n)" - options_confirm.append("") - else: - def_choice = "(y/N)" - options_decline.append("") - - while True: - choice = ( - input(format_question(question + f" {def_choice}", None)).strip().lower() - ) - - if choice in options_confirm: - return True - elif choice in options_decline: - return False - elif allow_go_back and choice in options_go_back: - return None - else: - Logger.print_error(INVALID_CHOICE) - - -def get_number_input( - question: str, - min_value: int, - max_value: int | None = None, - default: int | None = None, - allow_go_back: bool = False, -) -> int | None: - """ - Helper method to get a number input from the user - :param question: The question to display - :param min_value: The lowest allowed value - :param max_value: The highest allowed value (or None) - :param default: Optional default value - :param allow_go_back: Navigate back to a previous dialog - :return: Either the validated number input, or None on go_back - """ - options_go_back = ["b", "B"] - _question = format_question(question, default) - while True: - _input = input(_question) - if allow_go_back and _input in options_go_back: - return None - - if _input == "" and default is not None: - return default - - try: - return validate_number_input(_input, min_value, max_value) - except ValueError: - Logger.print_error(INVALID_CHOICE) - - -def get_string_input( - question: str, - regex: str | None = None, - exclude: List[str] | None = None, - allow_empty: bool = False, - allow_special_chars: bool = False, - default: str | None = None, -) -> str: - """ - Helper method to get a string input from the user - :param question: The question to display - :param regex: An optional regex pattern to validate the input against - :param exclude: List of strings which are not allowed - :param allow_empty: Whether to allow empty input - :param allow_special_chars: Wheter to allow special characters in the input - :param default: Optional default value - :return: The validated string value - """ - _exclude = [] if exclude is None else exclude - _question = format_question(question, default) - _pattern = re.compile(regex) if regex is not None else None - while True: - _input = input(_question) - - if default is not None and _input == "": - return default - elif _input == "" and not allow_empty: - Logger.print_error("Input must not be empty!") - elif _pattern is not None and _pattern.match(_input): - return _input - elif _input.lower() in _exclude: - Logger.print_error("This value is already in use/reserved.") - elif allow_special_chars: - return _input - elif not allow_special_chars and _input.isalnum(): - return _input - else: - Logger.print_error(INVALID_CHOICE) - - -def get_selection_input(question: str, option_list: List | Dict, default=None) -> str: - """ - Helper method to get a selection from a list of options from the user - :param question: The question to display - :param option_list: The list of options the user can select from - :param default: Optional default value - :return: The option that was selected by the user - """ - while True: - _input = input(format_question(question, default)).strip().lower() - - if isinstance(option_list, list): - if _input in option_list: - return _input - elif isinstance(option_list, dict): - if _input in option_list.keys(): - return _input - else: - raise ValueError("Invalid option_list type") - - Logger.print_error("Invalid option! Please select a valid option.", False) - - -def format_question(question: str, default=None) -> str: - """ - Helper method to have a standardized formatting of questions | - :param question: The question to display - :param default: If defined, the default option will be displayed to the user - :return: The formatted question string - """ - formatted_q = question - if default is not None: - formatted_q += f" (default={default})" - - return Color.apply(f"###### {formatted_q}: ", Color.CYAN) - - -def validate_number_input(value: str, min_count: int, max_count: int | None) -> int: - """ - Helper method for a simple number input validation. | - :param value: The value to validate - :param min_count: The lowest allowed value - :param max_count: The highest allowed value (or None) - :return: The validated value as Integer - :raises: ValueError if value is invalid - """ - if max_count is not None: - if min_count <= int(value) <= max_count: - return int(value) - elif int(value) >= min_count: - return int(value) - - raise ValueError diff --git a/kiauh/utils/instance_type.py b/kiauh/utils/instance_type.py deleted file mode 100644 index cd7bf31..0000000 --- a/kiauh/utils/instance_type.py +++ /dev/null @@ -1,29 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # - -from typing import TypeVar - -from components.klipper.klipper import Klipper -from components.moonraker.moonraker import Moonraker -from extensions.obico.moonraker_obico import MoonrakerObico -from extensions.octoeverywhere.octoeverywhere import Octoeverywhere -from extensions.octoapp.octoapp import Octoapp -from extensions.telegram_bot.moonraker_telegram_bot import MoonrakerTelegramBot -from extensions.octoprint.octoprint import Octoprint - -InstanceType = TypeVar( - "InstanceType", - Klipper, - Moonraker, - MoonrakerTelegramBot, - MoonrakerObico, - Octoeverywhere, - Octoapp, - Octoprint, -) diff --git a/kiauh/utils/instance_utils.py b/kiauh/utils/instance_utils.py deleted file mode 100644 index c73f690..0000000 --- a/kiauh/utils/instance_utils.py +++ /dev/null @@ -1,58 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import re -from pathlib import Path -from typing import List - -from core.constants import SYSTEMD -from core.instance_manager.base_instance import SUFFIX_BLACKLIST -from utils.instance_type import InstanceType - - -def get_instances( - instance_type: type, suffix_blacklist: List[str] = SUFFIX_BLACKLIST -) -> List[InstanceType]: - from utils.common import convert_camelcase_to_kebabcase - - if not isinstance(instance_type, type): - raise ValueError("instance_type must be a class") - - name = convert_camelcase_to_kebabcase(instance_type.__name__) - pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.service$") - - service_list = [ - Path(SYSTEMD, service) - for service in SYSTEMD.iterdir() - if pattern.search(service.name) - and not any(s in service.name for s in suffix_blacklist) - ] - - instance_list = [ - instance_type(get_instance_suffix(name, service)) for service in service_list - ] - - def _sort_instance_list(suffix: int | str | None): - if suffix is None: - return - elif isinstance(suffix, str) and suffix.isdigit(): - return f"{int(suffix):04}" - else: - return suffix - - return sorted(instance_list, key=lambda x: _sort_instance_list(x.suffix)) - - -def get_instance_suffix(name: str, file_path: Path) -> str: - # to get the suffix of the instance, we remove the name of the instance from - # the file name, if the remaining part an empty string we return it - # otherwise there is and hyphen left, and we return the part after the hyphen - suffix = file_path.stem[len(name) :] - return suffix[1:] if suffix else "" diff --git a/kiauh/utils/sys_utils.py b/kiauh/utils/sys_utils.py deleted file mode 100644 index 626342e..0000000 --- a/kiauh/utils/sys_utils.py +++ /dev/null @@ -1,638 +0,0 @@ -# ======================================================================= # -# Copyright (C) 2020 - 2025 Dominik Willner # -# # -# This file is part of KIAUH - Klipper Installation And Update Helper # -# https://github.com/dw-0/kiauh # -# # -# This file may be distributed under the terms of the GNU GPLv3 license # -# ======================================================================= # -from __future__ import annotations - -import os -import re -import select -import shutil -import socket -import sys -import time -import urllib.error -import urllib.request -from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, Popen, check_output, run -from typing import List, Literal, Set, Tuple - -from core.constants import SYSTEMD -from core.logger import Logger -from utils.fs_utils import check_file_exist, remove_with_sudo -from utils.input_utils import get_confirm - -SysCtlServiceAction = Literal[ - "start", - "stop", - "restart", - "reload", - "enable", - "disable", - "mask", - "unmask", -] -SysCtlManageAction = Literal["daemon-reload", "reset-failed"] - - -class VenvCreationFailedException(Exception): - pass - - -def kill(opt_err_msg: str = "") -> None: - """ - Kills the application | - :param opt_err_msg: an optional, additional error message - :return: None - """ - - if opt_err_msg: - Logger.print_error(opt_err_msg) - Logger.print_error("A critical error has occured. KIAUH was terminated.") - sys.exit(1) - - -def check_python_version(major: int, minor: int) -> bool: - """ - Checks the python version and returns True if it's at least the given version - :param major: the major version to check - :param minor: the minor version to check - :return: bool - """ - if not (sys.version_info.major >= major and sys.version_info.minor >= minor): - Logger.print_error("Versioncheck failed!") - Logger.print_error(f"Python {major}.{minor} or newer required.") - return False - return True - - -def parse_packages_from_file(source_file: Path) -> List[str]: - """ - Read the package names from bash scripts, when defined like: - PKGLIST="package1 package2 package3" | - :param source_file: path of the sourcefile to read from - :return: A list of package names - """ - - packages = [] - with open(source_file, "r") as file: - for line in file: - line = line.strip() - if line.startswith("PKGLIST="): - line = line.replace('"', "") - line = line.replace("PKGLIST=", "") - line = line.replace("${PKGLIST}", "") - packages.extend(line.split()) - - return packages - - -def create_python_venv( - target: Path, - force: bool = False, - allow_access_to_system_site_packages: bool = False, - use_python_binary: str | None = None -) -> bool: - """ - Create a python 3 virtualenv at the provided target destination. - Returns True if the virtualenv was created successfully. - Returns False if the virtualenv already exists, recreation was declined or creation failed. - :param target: Path where to create the virtualenv at - :param force: Force recreation of the virtualenv - :param allow_access_to_system_site_packages: give the virtual environment access to the system site-packages dir - :param use_python_binary: allows to override default python binary - :return: bool - """ - Logger.print_status("Set up Python virtual environment ...") - # If binarry override is not set, we use default defined here - python_binary = use_python_binary if use_python_binary else "/usr/bin/python3" - cmd = ["virtualenv", "-p", python_binary, target.as_posix()] - cmd.append( - "--system-site-packages" - ) if allow_access_to_system_site_packages else None - - n = 2 - while(n > 0): - if not target.exists(): - try: - run(cmd, check=True) - Logger.print_ok("Setup of virtualenv successful!") - return True - except CalledProcessError as e: - Logger.print_error(f"Error setting up virtualenv:\n{e}") - return False - else: - if n == 1: - # This case should never happen, - # but the function should still behave correctly - Logger.print_error("Virtualenv still exists after deletion.") - return False - if not force and not get_confirm( - "Virtualenv already exists. Re-create?", default_choice=False - ): - Logger.print_info("Skipping re-creation of virtualenv ...") - return False - - try: - shutil.rmtree(target) - n -= 1 - except OSError as e: - log = f"Error removing existing virtualenv: {e.strerror}" - Logger.print_error(log, False) - return False - - -def update_python_pip(target: Path) -> None: - """ - Updates pip in the provided target destination | - :param target: Path of the virtualenv - :return: None - """ - Logger.print_status("Updating pip ...") - try: - pip_location: Path = target.joinpath("bin/pip") - pip_exists: bool = check_file_exist(pip_location) - - if not pip_exists: - raise FileNotFoundError("Error updating pip! Not found.") - - command = [pip_location.as_posix(), "install", "-U", "pip"] - result = run(command, stderr=PIPE, text=True) - if result.returncode != 0 or result.stderr: - Logger.print_error(f"{result.stderr}", False) - Logger.print_error("Updating pip failed!") - return - - Logger.print_ok("Updating pip successful!") - except FileNotFoundError as e: - Logger.print_error(e) - raise - except CalledProcessError as e: - Logger.print_error(f"Error updating pip:\n{e.output.decode()}") - raise - - -def install_python_requirements(target: Path, requirements: Path) -> None: - """ - Installs the python packages based on a provided requirements.txt | - :param target: Path of the virtualenv - :param requirements: Path to the requirements.txt file - :return: None - """ - try: - Logger.print_status("Installing Python requirements ...") - command = [ - target.joinpath("bin/pip").as_posix(), - "install", - "-r", - f"{requirements}", - ] - result = run(command, stderr=PIPE, text=True) - - if result.returncode != 0: - Logger.print_error(f"{result.stderr}", False) - raise VenvCreationFailedException("Installing Python requirements failed!") - - Logger.print_ok("Installing Python requirements successful!") - - except Exception as e: - log = f"Error installing Python requirements: {e}" - Logger.print_error(log) - raise VenvCreationFailedException(log) - - -def install_python_packages(target: Path, packages: List[str]) -> None: - """ - Installs the python packages based on a provided packages list | - :param target: Path of the virtualenv - :param packages: str list of required packages - :return: None - """ - try: - Logger.print_status("Installing Python requirements ...") - command = [ - target.joinpath("bin/pip").as_posix(), - "install", - ] - for pkg in packages: - command.append(pkg) - result = run(command, stderr=PIPE, text=True) - - if result.returncode != 0: - Logger.print_error(f"{result.stderr}", False) - raise VenvCreationFailedException("Installing Python requirements failed!") - - Logger.print_ok("Installing Python requirements successful!") - - except Exception as e: - log = f"Error installing Python requirements: {e}" - Logger.print_error(log) - raise VenvCreationFailedException(log) - - -def update_system_package_lists(silent: bool, rls_info_change=False) -> None: - """ - Updates the systems package list | - :param silent: Log info to the console or not - :param rls_info_change: Flag for "--allow-releaseinfo-change" - :return: None - """ - cache_mtime: float = 0 - cache_files: List[Path] = [ - Path("/var/lib/apt/periodic/update-success-stamp"), - Path("/var/lib/apt/lists"), - ] - for cache_file in cache_files: - if cache_file.exists(): - cache_mtime = max(cache_mtime, os.path.getmtime(cache_file)) - - update_age = int(time.time() - cache_mtime) - update_interval = 6 * 3600 # 48hrs - - if update_age <= update_interval: - return - - if not silent: - Logger.print_status("Updating package list...") - - try: - command = ["sudo", "apt-get", "update"] - if rls_info_change: - command.append("--allow-releaseinfo-change") - - result = run(command, stderr=PIPE, text=True) - if result.returncode != 0 or result.stderr: - Logger.print_error(f"{result.stderr}", False) - Logger.print_error("Updating system package list failed!") - return - - Logger.print_ok("System package list update successful!") - except CalledProcessError as e: - Logger.print_error(f"Error updating system package list:\n{e.stderr.decode()}") - raise - - -def get_upgradable_packages() -> List[str]: - """ - Reads all system packages that can be upgraded. - :return: A list of package names available for upgrade - """ - try: - command = ["apt", "list", "--upgradable"] - output: str = check_output(command, stderr=DEVNULL, text=True, encoding="utf-8") - pkglist = [] - for line in output.split("\n"): - if "/" not in line: - continue - pkg = line.split("/")[0] - pkglist.append(pkg) - return pkglist - except CalledProcessError as e: - raise Exception(f"Error reading upgradable packages: {e}") - - -def check_package_install(packages: Set[str]) -> List[str]: - """ - Checks the system for installed packages | - :param packages: List of strings of package names - :return: A list containing the names of packages that are not installed - """ - not_installed = [] - for package in packages: - command = ["dpkg-query", "-f'${Status}'", "--show", package] - result = run( - command, - stdout=PIPE, - stderr=DEVNULL, - text=True, - ) - if "installed" not in result.stdout.strip("'").split(): - not_installed.append(package) - - return not_installed - - -def install_system_packages(packages: List[str]) -> None: - """ - Installs a list of system packages | - :param packages: List of system package names - :return: None - """ - try: - command = ["sudo", "apt-get", "install", "-y"] - for pkg in packages: - command.append(pkg) - run(command, stderr=PIPE, check=True) - - Logger.print_ok("Packages successfully installed.") - except CalledProcessError as e: - Logger.print_error(f"Error installing packages:\n{e.stderr.decode()}") - raise - - -def upgrade_system_packages(packages: List[str]) -> None: - """ - Updates a list of system packages | - :param packages: List of system package names - :return: None - """ - try: - command = ["sudo", "apt-get", "upgrade", "-y"] - for pkg in packages: - command.append(pkg) - run(command, stderr=PIPE, check=True) - - Logger.print_ok("Packages successfully upgraded.") - except CalledProcessError as e: - raise Exception(f"Error upgrading packages:\n{e.stderr.decode()}") - - -# this feels hacky and not quite right, but for now it works -# see: https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib -def get_ipv4_addr() -> str: - """ - Helper function that returns the IPv4 of the current machine - by opening a socket and sending a package to an arbitrary IP. | - :return: Local IPv4 of the current machine - """ - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.settimeout(0) - try: - # doesn't even have to be reachable - s.connect(("192.255.255.255", 1)) - ipv4: str = str(s.getsockname()[0]) - s.close() - return ipv4 - except Exception: - s.close() - return "127.0.0.1" - - -def download_file(url: str, target: Path, show_progress=True) -> None: - """ - Helper method for downloading files from a provided URL | - :param url: the url to the file - :param target: the target path incl filename - :param show_progress: show download progress or not - :return: None - """ - try: - if show_progress: - urllib.request.urlretrieve(url, target, download_progress) - sys.stdout.write("\n") - else: - urllib.request.urlretrieve(url, target) - except urllib.error.HTTPError as e: - Logger.print_error(f"Download failed! HTTP error occured: {e}") - raise - except urllib.error.URLError as e: - Logger.print_error(f"Download failed! URL error occured: {e}") - raise - except Exception as e: - Logger.print_error(f"Download failed! An error occured: {e}") - raise - - -def download_progress(block_num, block_size, total_size) -> None: - """ - Reporthook method for urllib.request.urlretrieve() method call in download_file() | - :param block_num: - :param block_size: - :param total_size: total filesize in bytes - :return: None - """ - downloaded = block_num * block_size - percent = 100 if downloaded >= total_size else downloaded / total_size * 100 - mb = 1024 * 1024 - progress = int(percent / 5) - remaining = "-" * (20 - progress) - dl = f"\rDownloading: [{'#' * progress}{remaining}]{percent:.2f}% ({downloaded / mb:.2f}/{total_size / mb:.2f}MB)" - sys.stdout.write(dl) - sys.stdout.flush() - - -def set_nginx_permissions() -> None: - """ - Check if permissions of the users home directory - grant execution rights to group and other and set them if not set. - Required permissions for NGINX to be able to serve Mainsail/Fluidd. - This seems to have become necessary with Ubuntu 21+. | - :return: None - """ - cmd = f"ls -ld {Path.home()} | cut -d' ' -f1" - homedir_perm = run(cmd, shell=True, stdout=PIPE, text=True) - permissions = homedir_perm.stdout - - if permissions.count("x") < 3: - Logger.print_status("Granting NGINX the required permissions ...") - run(["chmod", "og+x", Path.home()]) - Logger.print_ok("Permissions granted.") - - -def cmd_sysctl_service(name: str, action: SysCtlServiceAction) -> None: - """ - Helper method to execute several actions for a specific systemd service. | - :param name: the service name - :param action: Either "start", "stop", "restart" or "disable" - :return: None - """ - try: - Logger.print_status(f"{action.capitalize()} {name} ...") - run(["sudo", "systemctl", action, name], stderr=PIPE, check=True) - Logger.print_ok("OK!") - except CalledProcessError as e: - log = f"Failed to {action} {name}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def cmd_sysctl_manage(action: SysCtlManageAction) -> None: - try: - run(["sudo", "systemctl", action], stderr=PIPE, check=True) - except CalledProcessError as e: - log = f"Failed to run {action}: {e.stderr.decode()}" - Logger.print_error(log) - raise - - -def unit_file_exists( - name: str, suffix: Literal["service", "timer"], exclude: List[str] | None = None -) -> bool: - """ - Checks if a systemd unit file of the provided suffix exists. - :param name: the name of the unit file - :param suffix: suffix of the unit file, either "service" or "timer" - :param exclude: List of strings of names to exclude - :return: True if the unit file exists, False otherwise - """ - exclude = exclude or [] - pattern = re.compile(f"^{name}(-[0-9a-zA-Z]+)?.{suffix}$") - service_list = [ - Path(SYSTEMD, service) - for service in SYSTEMD.iterdir() - if pattern.search(service.name) and not any(s in service.name for s in exclude) - ] - return any(service_list) - - -def log_process(process: Popen) -> None: - """ - Helper method to print stdout of a process in near realtime to the console. - :param process: Process to log the output from - :return: None - """ - while True: - if process.stdout is not None: - reads = [process.stdout.fileno()] - ret = select.select(reads, [], []) - for fd in ret[0]: - if fd == process.stdout.fileno(): - line = process.stdout.readline() - if line: - print(line.strip(), flush=True) - else: - break - - if process.poll() is not None: - break - - -def create_service_file(name: str, content: str) -> None: - """ - Creates a service file at the provided path with the provided content. - :param name: the name of the service file - :param content: the content of the service file - :return: None - """ - try: - run( - ["sudo", "tee", SYSTEMD.joinpath(name)], - input=content.encode(), - stdout=DEVNULL, - check=True, - ) - Logger.print_ok(f"Service file created: {SYSTEMD.joinpath(name)}") - except CalledProcessError as e: - Logger.print_error(f"Error creating service file: {e}") - raise - - -def create_env_file(path: Path, content: str) -> None: - """ - Creates an env file at the provided path with the provided content. - :param path: the path of the env file - :param content: the content of the env file - :return: None - """ - try: - with open(path, "w") as env_file: - env_file.write(content) - Logger.print_ok(f"Env file created: {path}") - except OSError as e: - Logger.print_error(f"Error creating env file: {e}") - raise - - -def remove_system_service(service_name: str) -> None: - """ - Disables and removes a systemd service - :param service_name: name of the service unit file - must end with '.service' - :return: None - """ - try: - if not service_name.endswith(".service"): - raise ValueError(f"service_name '{service_name}' must end with '.service'") - - file: Path = SYSTEMD.joinpath(service_name) - if not file.exists() or not file.is_file(): - Logger.print_info(f"Service '{service_name}' does not exist! Skipped ...") - return - - Logger.print_status(f"Removing {service_name} ...") - cmd_sysctl_service(service_name, "stop") - cmd_sysctl_service(service_name, "disable") - remove_with_sudo(file) - cmd_sysctl_manage("daemon-reload") - cmd_sysctl_manage("reset-failed") - Logger.print_ok(f"{service_name} successfully removed!") - except Exception as e: - Logger.print_error(f"Error removing {service_name}: {e}") - raise - - -def get_service_file_path(instance_type: type, suffix: str) -> Path: - from utils.common import convert_camelcase_to_kebabcase - - if not isinstance(instance_type, type): - raise ValueError("instance_type must be a class") - - name: str = convert_camelcase_to_kebabcase(instance_type.__name__) - if suffix != "": - name += f"-{suffix}" - - file_path: Path = SYSTEMD.joinpath(f"{name}.service") - - return file_path - - -def get_distro_info() -> Tuple[str, str]: - distro_info: str = check_output(["cat", "/etc/os-release"]).decode().strip() - - if not distro_info: - raise ValueError("Error reading distro info!") - - distro_id: str = "" - distro_id_like: str = "" - distro_version: str = "" - - for line in distro_info.split("\n"): - if line.startswith("ID="): - distro_id = line.split("=")[1].strip('"').strip() - if line.startswith("ID_LIKE="): - distro_id_like = line.split("=")[1].strip('"').strip() - if line.startswith("VERSION_ID="): - distro_version = line.split("=")[1].strip('"').strip() - - if distro_id == "raspbian": - distro_id = distro_id_like - - if not distro_id: - raise ValueError("Error reading distro id!") - if not distro_version: - raise ValueError("Error reading distro version!") - - return distro_id.lower(), distro_version - - -def get_system_timezone() -> str: - timezone = "UTC" - try: - with open("/etc/timezone", "r") as f: - timezone = f.read().strip() - except FileNotFoundError: - # fallback to reading timezone from timedatectl - try: - result = run( - ["timedatectl", "show", "--property=Timezone"], - capture_output=True, - text=True, - check=True, - ) - timezone = result.stdout.strip().split("=")[1] - except CalledProcessError: - # fallback if timedatectl fails, try reading from readlink - try: - result = run( - ["readlink", "-f", "/etc/localtime"], - capture_output=True, - text=True, - check=True, - ) - timezone = result.stdout.strip().split("zoneinfo/")[1] - except (CalledProcessError, IndexError): - Logger.print_warn("Could not determine system timezone, using UTC") - return timezone diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 002b70d..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,33 +0,0 @@ -[project] -requires-python = ">=3.8" - -[project.optional-dependencies] -dev=["ruff", "mypy"] - -[tool.ruff] -required-version = ">=0.9.10" -respect-gitignore = true -exclude = [".git",".github", "./docs", "kiauh/core/submodules"] -line-length = 88 -indent-width = 4 -output-format = "full" -target-version = "py38" - -[tool.ruff.format] -indent-style = "space" -line-ending = "lf" -quote-style = "double" - -[tool.ruff.lint] -extend-select = ["I"] - -[tool.mypy] -python_version = "3.8" -platform = "linux" -# strict = true # TODO: enable this once everything is else is handled -check_untyped_defs = true -ignore_missing_imports = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_return_any = true -warn_unreachable = true diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index b76cb57..0000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pythonVersion": "3.8", - "pythonPlatform": "Linux", - "typeCheckingMode": "standard", - "venvPath": "./.kiauh-env" -} diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 93087ed..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -ruff (>=0.9.10) -pyright