From e97ad932eb623e241166b5494df2013f6b024f0d Mon Sep 17 00:00:00 2001 From: Jason Jerome Date: Sun, 5 Jul 2026 00:39:28 -0400 Subject: [PATCH] Moved codebase to Codeberg The project has been moved to: https://codeberg.org/DuckHuggles/mumimo Signed-off-by: Jason Jerome --- Dockerfile | 24 - LICENSE | 674 ------------------ __init__.py | 0 build-tools/cleanrun.sh | 52 -- build-tools/cleanrun_withpluginconfig.sh | 43 -- build-tools/codequality.sh | 17 - build-tools/run.sh | 10 - codecov.yml | 6 - config/config_template.toml | 104 --- config/logging_template.toml | 25 - conftest.py | 24 - dev_requirements.txt | 13 - mumimo.py | 10 - pyproject.toml | 28 - requirements.txt | 13 - src/__init__.py | 0 src/client_state.py | 214 ------ src/config.py | 123 ---- src/constants.py | 251 ------- src/exceptions.py | 54 -- src/lib/command.py | 100 --- src/lib/command_callbacks.py | 92 --- src/lib/command_history.py | 60 -- src/lib/command_queue.py | 56 -- .../database_connection_parameters.py | 70 -- src/lib/database/metadata.py | 5 - src/lib/database/models/alias.py | 53 -- src/lib/database/models/command.py | 49 -- src/lib/database/models/permission_group.py | 28 - src/lib/database/models/plugin.py | 38 - src/lib/database/models/user.py | 49 -- src/lib/frameworks/gui/constants.py | 7 - src/lib/frameworks/gui/gui.py | 152 ---- src/lib/frameworks/gui/utility.py | 36 - src/lib/frameworks/plugins/plugin.py | 283 -------- src/lib/singleton.py | 17 - src/log_config.py | 114 --- src/logging.py | 159 ----- src/mumimo.py | 63 -- src/murmur_connection.py | 212 ------ src/plugins/builtin_core/help_template.toml | 15 - .../builtin_core/metadata_template.toml | 12 - src/plugins/builtin_core/plugin.py | 514 ------------- .../resources/gui_custom_theme_template.toml | 15 - .../resources/gui_themes_template.toml | 42 -- .../builtin_core/tests/test_builtin_core.py | 3 - src/plugins/builtin_core/utility/constants.py | 62 -- .../builtin_core/utility/theme_utils.py | 190 ----- src/plugins/builtin_image/help_template.toml | 15 - .../builtin_image/metadata_template.toml | 12 - src/plugins/builtin_image/plugin.py | 191 ----- .../builtin_image/tests/test_builtin_image.py | 3 - .../builtin_image/utility/constants.py | 37 - .../builtin_plugin_tools/help_template.toml | 15 - .../metadata_template.toml | 12 - src/plugins/builtin_plugin_tools/plugin.py | 131 ---- .../tests/test_builtin_plugin_tools.py | 3 - .../builtin_plugin_tools/utility/constants.py | 18 - .../builtin_soundboard/help_template.toml | 15 - .../builtin_soundboard/metadata_template.toml | 12 - src/plugins/builtin_soundboard/plugin.py | 28 - .../tests/test_builtin_soundboard.py | 3 - .../builtin_soundboard/utility/constants.py | 2 - src/services/cmd_processing_service.py | 434 ----------- src/services/database_service.py | 245 ------- .../init_services/cfg_init_service.py | 31 - .../client_settings_init_service.py | 168 ----- .../init_services/gui_init_service.py | 31 - .../init_services/mumimo_init_service.py | 65 -- .../init_services/plugins_init_service.py | 248 ------- src/settings.py | 166 ----- src/system_arguments.py | 80 --- src/templates/plugins/metadata_template.toml | 12 - src/utils/__init__.py | 0 src/utils/args_validators.py | 92 --- src/utils/connection_utils.py | 16 - src/utils/log_utils.py | 34 - src/utils/mumble_utils.py | 268 ------- src/utils/parsers/__init__.py | 0 src/utils/parsers/cmd_parser.py | 92 --- src/utils/parsers/db_url_parser.py | 39 - src/utils/parsers/env_parser.py | 23 - src/version.py | 11 - tests/__init__.py | 0 tests/data/__init__.py | 0 tests/data/config/test_config.toml | 14 - tests/data/config/test_config_alt.toml | 11 - tests/data/config/test_config_broken.toml | 11 - tests/data/config/test_logging.toml | 21 - tests/data/config/test_valid_config.toml | 6 - tests/data/test.env | 15 - tests/fixtures/__init__.py | 0 tests/fixtures/config_fixtures.py | 17 - tests/fixtures/connection_fixtures.py | 35 - tests/src/__init__.py | 0 tests/src/lib/database/models/fixtures.py | 47 -- .../lib/database/models/test_alias_model.py | 210 ------ .../lib/database/models/test_command_model.py | 172 ----- .../models/test_permission_group_model.py | 135 ---- .../lib/database/models/test_plugin_model.py | 171 ----- .../lib/database/models/test_user_model.py | 203 ------ .../test_database_connection_parameters.py | 109 --- tests/src/lib/test_command.py | 81 --- tests/src/lib/test_command_history.py | 124 ---- tests/src/services/test_cfg_init_service.py | 41 -- .../test_client_settings_init_service.py | 267 ------- .../services/test_cmd_processing_service.py | 266 ------- tests/src/services/test_database_service.py | 559 --------------- .../src/services/test_mumimo_init_service.py | 62 -- tests/src/test_client_state.py | 209 ------ tests/src/test_config.py | 332 --------- tests/src/test_exceptions.py | 37 - tests/src/test_log_config.py | 332 --------- tests/src/test_logging.py | 100 --- tests/src/test_murmur_connection.py | 119 ---- tests/src/test_murmur_connection_singleton.py | 28 - tests/src/test_system_arguments.py | 269 ------- tests/src/test_version.py | 39 - tests/src/utils/__init__.py | 0 tests/src/utils/parsers/test_cmd_parser.py | 231 ------ tests/src/utils/parsers/test_env_parser.py | 63 -- tests/src/utils/test_args_validators.py | 232 ------ tests/src/utils/test_connection_utils.py | 49 -- tests/test_example.py | 2 - 124 files changed, 11352 deletions(-) delete mode 100644 Dockerfile delete mode 100644 LICENSE delete mode 100644 __init__.py delete mode 100755 build-tools/cleanrun.sh delete mode 100644 build-tools/cleanrun_withpluginconfig.sh delete mode 100755 build-tools/codequality.sh delete mode 100644 build-tools/run.sh delete mode 100644 codecov.yml delete mode 100644 config/config_template.toml delete mode 100644 config/logging_template.toml delete mode 100644 conftest.py delete mode 100644 dev_requirements.txt delete mode 100755 mumimo.py delete mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 src/__init__.py delete mode 100644 src/client_state.py delete mode 100644 src/config.py delete mode 100644 src/constants.py delete mode 100644 src/exceptions.py delete mode 100644 src/lib/command.py delete mode 100644 src/lib/command_callbacks.py delete mode 100644 src/lib/command_history.py delete mode 100644 src/lib/command_queue.py delete mode 100644 src/lib/database/database_connection_parameters.py delete mode 100644 src/lib/database/metadata.py delete mode 100644 src/lib/database/models/alias.py delete mode 100644 src/lib/database/models/command.py delete mode 100644 src/lib/database/models/permission_group.py delete mode 100644 src/lib/database/models/plugin.py delete mode 100644 src/lib/database/models/user.py delete mode 100644 src/lib/frameworks/gui/constants.py delete mode 100644 src/lib/frameworks/gui/gui.py delete mode 100644 src/lib/frameworks/gui/utility.py delete mode 100644 src/lib/frameworks/plugins/plugin.py delete mode 100644 src/lib/singleton.py delete mode 100644 src/log_config.py delete mode 100644 src/logging.py delete mode 100644 src/mumimo.py delete mode 100644 src/murmur_connection.py delete mode 100644 src/plugins/builtin_core/help_template.toml delete mode 100644 src/plugins/builtin_core/metadata_template.toml delete mode 100644 src/plugins/builtin_core/plugin.py delete mode 100644 src/plugins/builtin_core/resources/gui_custom_theme_template.toml delete mode 100644 src/plugins/builtin_core/resources/gui_themes_template.toml delete mode 100644 src/plugins/builtin_core/tests/test_builtin_core.py delete mode 100644 src/plugins/builtin_core/utility/constants.py delete mode 100644 src/plugins/builtin_core/utility/theme_utils.py delete mode 100644 src/plugins/builtin_image/help_template.toml delete mode 100644 src/plugins/builtin_image/metadata_template.toml delete mode 100644 src/plugins/builtin_image/plugin.py delete mode 100644 src/plugins/builtin_image/tests/test_builtin_image.py delete mode 100644 src/plugins/builtin_image/utility/constants.py delete mode 100644 src/plugins/builtin_plugin_tools/help_template.toml delete mode 100644 src/plugins/builtin_plugin_tools/metadata_template.toml delete mode 100644 src/plugins/builtin_plugin_tools/plugin.py delete mode 100644 src/plugins/builtin_plugin_tools/tests/test_builtin_plugin_tools.py delete mode 100644 src/plugins/builtin_plugin_tools/utility/constants.py delete mode 100644 src/plugins/builtin_soundboard/help_template.toml delete mode 100644 src/plugins/builtin_soundboard/metadata_template.toml delete mode 100644 src/plugins/builtin_soundboard/plugin.py delete mode 100644 src/plugins/builtin_soundboard/tests/test_builtin_soundboard.py delete mode 100644 src/plugins/builtin_soundboard/utility/constants.py delete mode 100644 src/services/cmd_processing_service.py delete mode 100644 src/services/database_service.py delete mode 100644 src/services/init_services/cfg_init_service.py delete mode 100644 src/services/init_services/client_settings_init_service.py delete mode 100644 src/services/init_services/gui_init_service.py delete mode 100644 src/services/init_services/mumimo_init_service.py delete mode 100644 src/services/init_services/plugins_init_service.py delete mode 100644 src/settings.py delete mode 100644 src/system_arguments.py delete mode 100644 src/templates/plugins/metadata_template.toml delete mode 100644 src/utils/__init__.py delete mode 100644 src/utils/args_validators.py delete mode 100644 src/utils/connection_utils.py delete mode 100644 src/utils/log_utils.py delete mode 100644 src/utils/mumble_utils.py delete mode 100644 src/utils/parsers/__init__.py delete mode 100644 src/utils/parsers/cmd_parser.py delete mode 100644 src/utils/parsers/db_url_parser.py delete mode 100644 src/utils/parsers/env_parser.py delete mode 100644 src/version.py delete mode 100644 tests/__init__.py delete mode 100644 tests/data/__init__.py delete mode 100644 tests/data/config/test_config.toml delete mode 100644 tests/data/config/test_config_alt.toml delete mode 100644 tests/data/config/test_config_broken.toml delete mode 100644 tests/data/config/test_logging.toml delete mode 100644 tests/data/config/test_valid_config.toml delete mode 100644 tests/data/test.env delete mode 100644 tests/fixtures/__init__.py delete mode 100644 tests/fixtures/config_fixtures.py delete mode 100644 tests/fixtures/connection_fixtures.py delete mode 100644 tests/src/__init__.py delete mode 100644 tests/src/lib/database/models/fixtures.py delete mode 100644 tests/src/lib/database/models/test_alias_model.py delete mode 100644 tests/src/lib/database/models/test_command_model.py delete mode 100644 tests/src/lib/database/models/test_permission_group_model.py delete mode 100644 tests/src/lib/database/models/test_plugin_model.py delete mode 100644 tests/src/lib/database/models/test_user_model.py delete mode 100644 tests/src/lib/database/test_database_connection_parameters.py delete mode 100644 tests/src/lib/test_command.py delete mode 100644 tests/src/lib/test_command_history.py delete mode 100644 tests/src/services/test_cfg_init_service.py delete mode 100644 tests/src/services/test_client_settings_init_service.py delete mode 100644 tests/src/services/test_cmd_processing_service.py delete mode 100644 tests/src/services/test_database_service.py delete mode 100644 tests/src/services/test_mumimo_init_service.py delete mode 100644 tests/src/test_client_state.py delete mode 100644 tests/src/test_config.py delete mode 100644 tests/src/test_exceptions.py delete mode 100644 tests/src/test_log_config.py delete mode 100644 tests/src/test_logging.py delete mode 100644 tests/src/test_murmur_connection.py delete mode 100644 tests/src/test_murmur_connection_singleton.py delete mode 100644 tests/src/test_system_arguments.py delete mode 100644 tests/src/test_version.py delete mode 100644 tests/src/utils/__init__.py delete mode 100644 tests/src/utils/parsers/test_cmd_parser.py delete mode 100644 tests/src/utils/parsers/test_env_parser.py delete mode 100644 tests/src/utils/test_args_validators.py delete mode 100644 tests/src/utils/test_connection_utils.py delete mode 100644 tests/test_example.py diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3cb16ed..0000000 --- a/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM docker.io/python:3.10-slim - -ARG DEBIAN_FRONTEND=noninteractive - -RUN apt-get update --no-install-recommends \ -&& apt-get install -y -qq apt-utils --no-install-recommends 2>&1 | grep -v "debconf: delaying package configuration, since apt-utils is not installed" \ -&& apt-get install -y -qq ffmpeg vlc --no-install-recommends \ -&& apt-get install -y -qq libopus-dev gcc openssl git \ -&& apt-get clean && rm -rf /var/lib/apt/lists/* - -# Enable VLC to be executed as root -RUN sed -i 's/geteuid/getppid/' /usr/bin/vlc - -# Expose primary web interface port -EXPOSE 7000 - -WORKDIR /app - -# Add all the requirements.txt files from the requirements folder and install them. -ADD ./dev_requirements.txt /app -ADD ./requirements.txt /app - -RUN pip install -r dev_requirements.txt --no-warn-script-location -COPY . /app diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/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/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build-tools/cleanrun.sh b/build-tools/cleanrun.sh deleted file mode 100755 index fc4a657..0000000 --- a/build-tools/cleanrun.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -e - -clear -cd "${0%/*}" -cd .. - -DB_FILE=".config/mumimo.sqlite" -PLUGIN_CONFIG_DIR=".config/plugins" -ENV_FILE=".config/.env" -LOG_DIR=".config/logs/" - - -echo "Removing old logs..." -rm -rf "$LOG_DIR" -if [ $? -ne 0 ] ; then - echo "Encountered an error removing old logs." -else - echo "Removed '$LOG_DIR'" -fi - - -echo "Removing old databases..." -if [ -f "$DB_FILE" ] ; then - rm "$DB_FILE" - if [ $? -ne 0 ] ; then - echo "Encountered an error removing the old database." - else - echo "Removed '$DB_FILE'" - fi -fi - -echo "Removing plugin config folder.." -rm -r "$PLUGIN_CONFIG_DIR" -if [ $? -ne 0 ] ; then - echo "Encountered an error removing the plugin config folder." -else - echo "Removed '$PLUGIN_CONFIG_DIR'" -fi - -# Thanks to V.Gamula/Gulzar on stackoverflow for this convenient command: -# https://stackoverflow.com/questions/28991015/python3-project-remove-pycache-folders-and-pyc-files -echo "Removing pycache files..." -find . | grep -E "(/__pycache__$|\.pyc$|\.pyo$)" | xargs rm -rf -if [ $? -ne 0 ] ; then - echo "Encountered an error removing the plugin config folder." -else - echo "Removed pycache files." -fi - - -echo "Running Mumimo with arguments: '$@'" -./mumimo.py "-vv" "--env-file=$ENV_FILE" "--config-file=.config/config.toml" "--log-config-file=.config/logging.toml" $@ diff --git a/build-tools/cleanrun_withpluginconfig.sh b/build-tools/cleanrun_withpluginconfig.sh deleted file mode 100644 index ef97ec6..0000000 --- a/build-tools/cleanrun_withpluginconfig.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -e - -clear -cd "${0%/*}" -cd .. - -DB_FILE=".config/mumimo.sqlite" -ENV_FILE=".config/.env" -LOG_DIR=".config/logs/" - - -echo "Removing old logs..." -rm -rf "$LOG_DIR" -if [ $? -ne 0 ] ; then - echo "Encountered an error removing old logs." -else - echo "Removed '$LOG_DIR'" -fi - - -echo "Removing old databases..." -if [ -f "$DB_FILE" ] ; then - rm "$DB_FILE" - if [ $? -ne 0 ] ; then - echo "Encountered an error removing the old database." - else - echo "Removed '$DB_FILE'" - fi -fi - -# Thanks to V.Gamula/Gulzar on stackoverflow for this convenient command: -# https://stackoverflow.com/questions/28991015/python3-project-remove-pycache-folders-and-pyc-files -echo "Removing pycache files..." -find . | grep -E "(/__pycache__$|\.pyc$|\.pyo$)" | xargs rm -rf -if [ $? -ne 0 ] ; then - echo "Encountered an error removing the plugin config folder." -else - echo "Removed pycache files." -fi - - -echo "Running Mumimo with arguments: '$@'" -./mumimo.py "-vv" "--env-file=$ENV_FILE" "--config-file=.config/config.toml" "--log-config-file=.config/logging.toml" $@ diff --git a/build-tools/codequality.sh b/build-tools/codequality.sh deleted file mode 100755 index 816b6a7..0000000 --- a/build-tools/codequality.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/bash - -cd "${0%/*}" -cd .. - -echo "Running black[0]: src/" -black src/ -echo "Running black[1]: tests/" -black tests/ - -echo "Running flake8[0]: src/" -echo Errors: $(flake8 src/) -echo "Running flake8[1]: tests/" -echo Errors: $(flake8 tests/) - -echo "Running isort: ." -isort . diff --git a/build-tools/run.sh b/build-tools/run.sh deleted file mode 100644 index 288c916..0000000 --- a/build-tools/run.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/bash - -cd "${0%/*}" -cd .. - -ENV_FILE=".config/.env" -LOG_DIR=".config/logs/" - -echo "Running Mumimo..." -./mumimo.py -v "--env-file=$ENV_FILE" "--config-file=.config/config.toml" "--log-config-file=.config/logging.toml" $@ diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 45c9473..0000000 --- a/codecov.yml +++ /dev/null @@ -1,6 +0,0 @@ -coverage: - status: - project: - default: - target: 80% # the required coverage value - threshold: 1% # the leniency in hitting the target diff --git a/config/config_template.toml b/config/config_template.toml deleted file mode 100644 index cc8cce5..0000000 --- a/config/config_template.toml +++ /dev/null @@ -1,104 +0,0 @@ -### Main Settings ### -[settings.connection] -mute = true -deafen = true -register = false -comment = "Hi! I am Mumimo." -default_channel = "" -auto_reconnect = false -name = "Mumimo" - -[settings.commands] -command_token = "!" -command_tick_rate = 0.1 -max_multi_command_length = 200 -max_command_queue_length = 500 -command_history_length = 25 - -### Media Settings ### -[settings.media] -volume = 0.1 -stereo_audio = true -max_queue_length = 50 -ffmpeg_path = "" -vlc_path = "" -media_directory = "" - -[settings.media.audio_ducking] -enable = false -volume = 0.05 -threshold = 2500.0 -delay = 1.0 - -[settings.media.youtube_dl] -proxy_url = "" -cookie_file = "" - -### Plugin Settings ### -[settings.plugins] -plugins_path = "src/plugins/" -plugins_config_path = "config/plugins/" -custom_plugins_path = "" -disabled_plugins = [] -safe_mode_plugins = [] - - -### GUI Settings ### -[settings.gui] -enable = true -themes_path = "config/gui_theme.toml" -selected_theme = "dark" # Default themes available: "light", "dark", "highcontrast" - -[output.gui] -enable = true - -[output.gui.canvas] -background_color = "black" -image_background_color = "black" -content_alignment = "center" -border = 0 - -[output.gui.text] -font = "Georgia" -standard_text_color = "Snow" -header_text_color = "red" -sub_header_text_color = "yellow" -index_text_color = "cyan" - -### Database Settings ### -[settings.database] -# Database connection string format: -# - dialect+driver://username:password@host/name -# - dialect+driver:///host.db -# Database connection string examples: -# - sqlite+aiosqlite://test:test@localhost:8000/mydatabase -# The database dialect, driver, username, password, host, and database name must be specified in an environment file if 'use_remote_database' is used. -use_remote_database = false -# If 'use_remote_database' is set to false, the 'local_database_path' must specify the full path of the database file. -local_database_path = "config/mumimo.sqlite" -local_database_dialect = "sqlite" -local_database_driver = "aiosqlite" - -# Default permission groups format: -# - default_permission_groups = [ -# "permission_group_1", -# "permission_group_2", -# ... -# ] -# As shown in the example above, the default permission groups must be a list of strings. -# Each string represents the name of a permission group that can control user access to commands. -# Default values: ["none", "guest", "regular", "admin"] -# It is highly recommended not to remove the default permission groups, but feel free to add more. -default_permission_groups = ["none", "guest", "regular", "admin"] - -# Default aliases format: -# - default_aliases = [ -# ["myalias", "mycommand", "list,of,permission,groups"], -# ... -# ] -# As shown in the example above, each item must contain 4 elements: -# 1. Index 0: The name of the alias as a string value. -# 2. Index 1: The command that the alias should execute as a string value. -# 3. Index 2: The list of permission groups the alias belongs to as a comma-separated string value. -# Default values: Too long to list here, please check the wiki. -default_aliases = [ ["say", "echo", "guest,regular,admin"], ["quit", "exit", "guest,regular,admin"] ] diff --git a/config/logging_template.toml b/config/logging_template.toml deleted file mode 100644 index 5ed3c5d..0000000 --- a/config/logging_template.toml +++ /dev/null @@ -1,25 +0,0 @@ -[output.file] -enable = true -level = "INFO" -path = "logs/" -format = "(%(asctime)s)[%(name)s][%(levelname)s]::%(message)s" -name = "mumimo_%s.log" - -# Please note that setting all redaction options to 'true' will prevent the entire message -# from being logged to the file. This is also a useful way to reduce log sizes. -[output.file.privacy] -redact_message = false -redact_commands = false -redact_channel = false -redact_user = false - -[output.console] -format = "[%(levelname)s]::%(message)s" - -# Please note that setting all redaction options to 'true' will prevent the entire message -# from being logged to the console. -[output.console.privacy] -redact_message = false -redact_commands = false -redact_channel = false -redact_user = false diff --git a/conftest.py b/conftest.py deleted file mode 100644 index 55acbf2..0000000 --- a/conftest.py +++ /dev/null @@ -1,24 +0,0 @@ -import pathlib - -import pytest - -# from src.logging import Logging - -pytest_plugins = [ - "tests.fixtures.config_fixtures", - "tests.fixtures.connection_fixtures", - "tests.src.lib.database.models.fixtures", -] - - -@pytest.fixture(autouse=True) -def generated_files_teardown(): - generated_files_path = pathlib.Path("tests/data/generated") - if not generated_files_path.exists(): - generated_files_path.mkdir(parents=True, exist_ok=True) - yield - for f in generated_files_path.glob("*"): - if f.is_file(): - f.unlink() - print(f"Removed generated test file: {f.name}") - generated_files_path.rmdir() diff --git a/dev_requirements.txt b/dev_requirements.txt deleted file mode 100644 index 41d6899..0000000 --- a/dev_requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ --r requirements.txt - -black==23.3.0 -flake8==6.0.0 -isort==5.12.0 -pylint==2.17.2 -pytest==7.2.2 -pytest-randomly==3.12.0 -pytest-timeout==2.1.0 -pytest-cov==4.0.0 -pytest-clarity==1.0.1 -pytest-asyncio==0.21.0 -Flake8-pyproject==1.2.3 \ No newline at end of file diff --git a/mumimo.py b/mumimo.py deleted file mode 100755 index 37814f3..0000000 --- a/mumimo.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 -from src.logging import init_logger -from src.system_arguments import args_parser - -if __name__ == "__main__": - sys_args = vars(args_parser.parse_args()) - init_logger(sys_args) - from src.mumimo import MumimoService - - mumimo = MumimoService(sys_args) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 69bd9d0..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,28 +0,0 @@ -[tool.black] -line-length = 150 -target_version = ['py38', 'py39', 'py310', 'py311'] - -[tool.isort] -profile = "black" -line_length = 150 - -[tool.pytest.ini_options] -addopts = "-s -vv --cov=." -pythonpath = "." -testpaths = "tests src/plugins/" -required_plugins = ['pytest-randomly', 'pytest-timeout'] -log_format = "(%(asctime)s)[%(name)s][%(levelname)s]:%(message)s" -log_date_format = "%Y-%m-%d %H:%M:%S" -asyncio_mode="auto" - -[tool.pyright] -include = ["src"] -exclude = ["**/__pycache__", "**/.sh", ".coveragerc"] -pythonVersion = "3.10" -pythonPlatform = "Linux" - -[tool.flake8] -exclude = ["**/__pycache__", "venv/", "build-tools/", "config/", "logs/", ".*"] -extend-ignore = ["E203"] -max-line-length = 150 -count = true diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 052622e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -# pymumble==1.7 - pymumble 1.7 was unreleased, so use the git commit for now. -git+https://github.com/azlux/pymumble/@7d203c5a9d0f1a628b6272699bf4c4a0a69fe44b - - -python-dotenv==1.0.0 -validators==0.20.0 -toml==0.10.2 -aiosqlite==0.18.0 -SqlAlchemy==2.0.8 -SQLAlchemy-Utils==0.40.0 - -thefuzz==0.19.0 -python-Levenshtein==0.20.9 diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/client_state.py b/src/client_state.py deleted file mode 100644 index a9c3b64..0000000 --- a/src/client_state.py +++ /dev/null @@ -1,214 +0,0 @@ -import asyncio -import logging -from enum import Enum -from typing import TYPE_CHECKING, Optional, Dict, Union, Any - -from .utils import mumble_utils -from .constants import LogOutputIdentifiers - - -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from pymumble_py3.mumble import Mumble - from pymumble_py3.users import User - - -class ClientState: - class AudioProperties: - class AudioState: - class MuteState(Enum): - UNMUTED = 0 - MUTED = 1 - - class DeafenState(Enum): - UNDEAFENED = 0 - DEAFENED = 1 - - _mute_state: MuteState - _deafen_state: DeafenState - - def __init__(self) -> None: - self._mute_state = self.MuteState.UNMUTED - self._deafen_state = self.DeafenState.UNDEAFENED - - @property - def mute_state(self): - return self._mute_state - - @property - def deafen_state(self): - return self._deafen_state - - def mute(self): - self._mute_state = self.MuteState.MUTED - - def unmute(self): - self._mute_state = self.MuteState.UNMUTED - - def deafen(self): - self._deafen_state = self.DeafenState.DEAFENED - - def undeafen(self): - self._deafen_state = self.DeafenState.UNDEAFENED - - _state: Optional[AudioState] - _connection: Optional["Mumble"] - - def __init__(self, mumble_instance: "Mumble") -> None: - self._state = self.AudioState() - self._connection = mumble_instance - - @property - def state(self) -> Optional[AudioState]: - return self._state - - def mute(self) -> bool: - if self._state is None: - return False - if self._check_client_mute(): - self._state.mute() - return True - return False - - def _check_client_mute(self) -> bool: - if self._connection is not None: - if self._connection.users.myself: - self._connection.users.myself.mute() - if self._connection.users.myself.get_property("self_mute") is True: - return True - return False - - def unmute(self) -> bool: - if self._state is None: - return False - if self._check_client_unmute(): - self._state.unmute() - return True - return False - - def _check_client_unmute(self) -> bool: - if self._connection is not None: - if self._connection.users.myself: - self._connection.users.myself.unmute() - if self._connection.users.myself.get_property("self_mute") is False: - return True - return False - - def deafen(self) -> bool: - if self._state is None: - return False - if self._check_client_deafen(): - self._state.deafen() - return True - return False - - def _check_client_deafen(self) -> bool: - if self._connection is not None: - if self._connection.users.myself: - self._connection.users.myself.deafen() - if self._connection.users.myself.get_property("self_deaf") is True: - return True - return False - - def undeafen(self) -> bool: - if self._state is None: - return False - if self._check_client_undeafen(): - self._state.undeafen() - return True - return False - - def _check_client_undeafen(self) -> bool: - if self._connection is not None: - if self._connection.users.myself: - self._connection.users.myself.undeafen() - if self._connection.users.myself.get_property("self_deaf") is False: - return True - return False - - class ServerProperties: - class ServerState: - _users: Dict[str, "User"] - - def __init__(self) -> None: - self._users = {} - - def add_user(self, user: Union["User", str, int]) -> bool: - if isinstance(user, str): - _user = mumble_utils.get_user_by_name(user) - elif isinstance(user, int): - _user = mumble_utils.get_user_by_id(user) - else: - _user = user - if not _user: - return False - self._users[_user["name"]] = _user - return True - - def remove_user(self, user: Union["User", str, int]) -> bool: - if isinstance(user, str): - marked = None - for _name, _user in self._users.items(): - if _name == user: - marked = _name - if marked: - del self._users[marked] - return True - elif isinstance(user, int): - marked = None - for _name, _user in self._users.items(): - if _user["session"] == user: - marked = _name - if marked: - del self._users[marked] - return True - else: - _user: "User" = user - if not _user: - return False - del self._users[_user["name"]] - return True - return False - - _state: ServerState - _connection: Optional["Mumble"] - - def __init__(self, mumble_instance: "Mumble") -> None: - self._state = self.ServerState() - self._connection = mumble_instance - - @property - def state(self) -> ServerState: - return self._state - - def on_server_connect(self, data) -> None: - print(data) - - def on_user_created(self, data: Dict[str, Any]) -> None: - asyncio.run(mumble_utils.Management.UserManagement.add_user(data)) - logger.debug(f"[{LogOutputIdentifiers.MUMBLE_ON_CONNECT}]: '{data['name']}' connected: added user '{data['name']}' to the server state.") - - def on_user_removed(self, data: Dict[str, Any], message: str) -> None: - if self.state.remove_user(user=data["name"]): - logger.debug( - f"[{LogOutputIdentifiers.MUMBLE_ON_DISCONNECT}]: '{data['name']}' disconnected: " - f"removed user '{data['name']}' from the server state." - ) - return - logger.error(f"Unable to remove user '{data['name']}' from the server state: {data}") - - _audio_properties: AudioProperties - _server_properties: ServerProperties - - def __init__(self, mumble_instance: "Mumble") -> None: - self._audio_properties = self.AudioProperties(mumble_instance) - self._server_properties = self.ServerProperties(mumble_instance) - - @property - def audio_properties(self) -> AudioProperties: - return self._audio_properties - - @property - def server_properties(self) -> ServerProperties: - return self._server_properties diff --git a/src/config.py b/src/config.py deleted file mode 100644 index d0d8624..0000000 --- a/src/config.py +++ /dev/null @@ -1,123 +0,0 @@ -import copy -import logging -import pathlib -from typing import Any, Dict, Optional, Union - -import toml - -from .exceptions import ConfigReadError, ConfigWriteError - -_logger = logging.getLogger(__name__) - - -class Config(dict): - _config_file_path: Optional[pathlib.Path] = None - _initial_config: Optional["Config"] = None - - def __init__(self, file_name: Optional[Union[str, pathlib.Path]] = None) -> None: - super().__init__() - if file_name is not None: - if isinstance(file_name, str): - self._config_file_path = (pathlib.Path.cwd() / file_name).resolve() - else: - self._config_file_path = file_name.resolve() - - def read(self, file_name: Optional[pathlib.Path] = None) -> "Config": - if not file_name: - if not self._config_file_path: - raise ConfigReadError("Unable to read config file because no file was specified.", _logger) - self._read_from_file(self._config_file_path) - return self - if file_name.is_file(): - self._read_from_file(file_name) - return self - raise ConfigReadError(f"Unable to read config file at: {file_name}", _logger) - - def _read_from_file(self, file_path: pathlib.Path): - try: - with open(str(file_path), "r", encoding="utf-8") as file_handler: - contents: Dict[str, Any] = toml.load(file_handler, _dict=dict) - self.clear() - self.update(contents) - self._config_file_path = file_path - if self._initial_config is None: - self._initial_config = Config() - self._initial_config._config_file_path = self._config_file_path - self._initial_config.clear() - self._initial_config.update(self) - except toml.TomlDecodeError as exc: - raise ConfigReadError(f"Unable to read config file at: {file_path}.", _logger) from exc - except IOError as exc: - raise ConfigReadError(f"Unable to open config file to read at: {file_path}.", _logger) from exc - - def save( - self, - file_path: Optional[pathlib.Path] = None, - modified_only: bool = False, - modified_field_name: Optional[str] = None, - ) -> str: - target_path = None - if file_path is not None: - target_path = file_path - elif self._config_file_path is not None: - target_path = self._config_file_path - else: - raise ConfigWriteError("Unable to save data to a config file because no file was specified.", _logger) - - try: - saved_data = None - with open(str(target_path), "w", encoding="utf-8") as file_handler: - if not modified_only: - saved_data = toml.dump(self, file_handler) - else: - if not modified_field_name: - raise ConfigWriteError("Unable to save modified data to a config file because the field name is invalid.", _logger) - field = self.get(modified_field_name) - if field is None: - raise ConfigWriteError("Unable to save modified data to a config file because the field name does not exist.", _logger) - if self._initial_config is None: - raise ConfigWriteError("Unable to save modified data to a config file because no file has been initialized.", _logger) - self._initial_config.set(modified_field_name, field) - saved_data = toml.dump(self._initial_config, file_handler) - if saved_data is None: - raise ConfigWriteError(f"Unable to save data to a config file at: {target_path}", _logger) - except IOError as exc: - raise ConfigWriteError(f"Unable to save config file at: {target_path} | {exc}", _logger) from exc - return saved_data - - def reset(self, field_name: str) -> bool: - return self.set(field_name, None, create_keys_if_not_exists=False) - - def get(self, field_name: str, fallback: Optional[Any] = None) -> Any: - if not field_name: - return None - field = self._get_field(field_name) - if field is None: - if fallback is not None: - return fallback - return None - return field - - def _get_field(self, field_name: str): - field_sections = field_name.split(".") - fields_copy = copy.deepcopy(self) - try: - for key in field_sections: - fields_copy = fields_copy[key] - except KeyError: - return None - return fields_copy - - def set(self, field_name: str, field_value: Optional[Any] = None, create_keys_if_not_exists: bool = False) -> bool: - if not field_name: - return False - return self._set_field(field_name, field_value, create_keys_if_not_exists) - - def _set_field(self, field_name: str, field_value: Optional[Any] = None, create_keys_if_not_exists: bool = False): - if not create_keys_if_not_exists and self.get(field_name) is None: - return False - field_sections = field_name.split(".") - for key in field_sections[:-1]: - self = self.setdefault(key, {}) - self[field_sections[-1]] = field_value - return True diff --git a/src/constants.py b/src/constants.py deleted file mode 100644 index 8074826..0000000 --- a/src/constants.py +++ /dev/null @@ -1,251 +0,0 @@ -# Verbosity Constants -VERBOSE_NONE: int = 0 -VERBOSE_MIN: int = 1 -VERBOSE_STANDARD: int = 2 -VERBOSE_HIGH: int = 3 -VERBOSE_MAX: int = 4 -VERBOSITY_MIN: int = VERBOSE_NONE -VERBOSITY_MAX: int = VERBOSE_MAX - - -# Logging Output Identifier Constants -class LogOutputIdentifiers: - PLUGINS: str = "Plugins" - PLUGINS_PERMISSIONS: str = f"{PLUGINS}.Permissions" - PLUGINS_COMMANDS: str = f"{PLUGINS}.Commands" - PLUGINS_PARAMETERS: str = f"{PLUGINS}.Parameters" - - DB: str = "Database" - DB_PERMISSIONS: str = f"{DB}.Permissions" - DB_USERS: str = f"{DB}.Users" - DB_ALIASES: str = f"{DB}.Aliases" - DB_PLUGINS: str = f"{DB}.Plugins" - DB_PLUGINS_COMMANDS: str = f"{DB}.{PLUGINS}.Commands" - DB_PLUGINS_PERMISSIONS: str = f"{DB}.{PLUGINS}.Permissions" - - MUMBLE: str = "Mumble" - MUMBLE_ON_CONNECT: str = f"{MUMBLE}.On_Connect" - MUMBLE_ON_DISCONNECT: str = f"{MUMBLE}.On_Disconnect" - - -# Env Args Constants -class EnvArgs: - ENV_HOST: str = "MUMIMO_HOST" - ENV_PORT: str = "MUMIMO_PORT" - ENV_USER: str = "MUMIMO_USER" - ENV_PASS: str = "MUMIMO_PASS" - ENV_CERT: str = "MUMIMO_CERT" - ENV_KEY: str = "MUMIMO_KEY" - ENV_TOKENS: str = "MUMIMO_TOKENS" - ENV_SUPER_USER: str = "MUMIMO_SUPER_USER" - ENV_DB_DIALECT: str = "MUMIMO_DB_DIALECT" - ENV_DB_DRIVER: str = "MUMIMO_DB_DRIVER" - ENV_DB_USER: str = "MUMIMO_DB_USER" - ENV_DB_PASS: str = "MUMIMO_DB_PASS" - ENV_DB_HOST: str = "MUMIMO_DB_HOST" - ENV_DB_PORT: str = "MUMIMO_DB_PORT" - ENV_DB_NAME: str = "MUMIMO_DB_NAME" - - -# Sys Args Constants -class SysArgs: - SYS_HOST: str = "host" - SYS_PORT: str = "port" - SYS_USER: str = "user" - SYS_PASS: str = "password" - SYS_CERT: str = "cert_file" - SYS_KEY: str = "key_file" - SYS_TOKENS: str = "tokens" - SYS_VERBOSE: str = "verbose" - SYS_RECONNECT: str = "auto_reconnect" - SYS_GEN_CERT: str = "generate_cert" - SYS_SUPER_USER: str = "superuser" - SYS_NAME: str = "name" - SYS_ENV_FILE: str = "env_file" - SYS_CONFIG_FILE: str = "config_file" - SYS_LOG_CONFIG_FILE: str = "log_config_file" - SYS_GUI_THEMES_FILE: str = "gui_themes_file" - SYS_DB_LOCALDBPATH: str = "local_database_path" - SYS_DB_LOCALDBDIALECT: str = "local_database_dialect" - SYS_DB_LOCALDBDRIVER: str = "local_database_driver" - SYS_DB_USEREMOTEDB: str = "use_remote_database" - SYS_DB_DIALECT: str = "db_dialect" - SYS_DB_DRIVER: str = "db_driver" - SYS_DB_USER: str = "db_user" - SYS_DB_PASS: str = "db_pass" - SYS_DB_HOST: str = "db_host" - SYS_DB_PORT: str = "db_port" - SYS_DB_NAME: str = "db_name" - SYS_PLUGINS_PATH: str = "plugins_path" - SYS_PLUGINS_CONFIG_PATH: str = "plugins_config_path" - - -# Config Constants -DEFAULT_PATH_CONFIG_FILE: str = "config/config.toml" -DEFAULT_PATH_LOGGING_CONFIG_FILE: str = "config/logging.toml" -DEFAULT_PATH_GUI_THEMES_FILE: str = "config/gui_themes.toml" - - -class DefaultPermissionGroups: - DEFAULT_NONE = "none" - DEFAULT_GUEST = "guest" - DEFAULT_MEMBER = "member" - DEFAULT_ADMIN = "admin" - - -class PluginCfgSections: - PLUGIN: str = "plugin" - PLUGIN_ABOUT: str = f"{PLUGIN}.about" - PLUGIN_SETTINGS: str = f"{PLUGIN}.settings" - PLUGIN_COMMANDS: str = f"{PLUGIN}.commands" - PLUGIN_HELP: str = f"{PLUGIN}.help" - - -class PluginCfgFields: - class PLUGIN: - ENABLED: str = f"{PluginCfgSections.PLUGIN}.enabled" - - class ABOUT: - NAME: str = f"{PluginCfgSections.PLUGIN_ABOUT}.name" - VERSION: str = f"{PluginCfgSections.PLUGIN_ABOUT}.version" - DESCRIPTION: str = f"{PluginCfgSections.PLUGIN_ABOUT}.description" - - class SETTINGS: - pass - - class COMMANDS: - DISABLE_COMMANDS: str = f"{PluginCfgSections.PLUGIN_COMMANDS}.disable_commands" - DISABLE_PARAMETERS: str = f"{PluginCfgSections.PLUGIN_COMMANDS}.disable_parameters" - DEFAULT_PERMISSION_GROUPS: str = f"{PluginCfgSections.PLUGIN_COMMANDS}.default_permission_groups" - - class HELP: - COMMANDS_HELP_TEXT: str = f"{PluginCfgSections.PLUGIN_HELP}.commands_help_text" - - -class LogCfgSections: - # Log Config Sections - OUTPUT: str = "output" - OUTPUT_FILE: str = f"{OUTPUT}.file" - OUTPUT_CONSOLE: str = f"{OUTPUT}.console" - OUTPUT_FILE_PRIVACY: str = f"{OUTPUT_FILE}.privacy" - OUTPUT_CONSOLE_PRIVACY: str = f"{OUTPUT_CONSOLE}.privacy" - - -class LogCfgFields: - # Log Config Fields - class OUTPUT: - class FILE: - ENABLE: str = f"{LogCfgSections.OUTPUT_FILE}.enable" - LEVEL: str = f"{LogCfgSections.OUTPUT_FILE}.level" - PATH: str = f"{LogCfgSections.OUTPUT_FILE}.path" - FORMAT: str = f"{LogCfgSections.OUTPUT_FILE}.format" - NAME: str = f"{LogCfgSections.OUTPUT_FILE}.name" - - class PRIVACY: - REDACT_MESSAGE: str = f"{LogCfgSections.OUTPUT_FILE_PRIVACY}.redact_message" - REDACT_COMMANDS: str = f"{LogCfgSections.OUTPUT_FILE_PRIVACY}.redact_commands" - REDACT_CHANNEL: str = f"{LogCfgSections.OUTPUT_FILE_PRIVACY}.redact_channel" - REDACT_USER: str = f"{LogCfgSections.OUTPUT_FILE_PRIVACY}.redact_user" - - class CONSOLE: - FORMAT: str = f"{LogCfgSections.OUTPUT_CONSOLE}.format" - - class PRIVACY: - REDACT_MESSAGE: str = f"{LogCfgSections.OUTPUT_CONSOLE_PRIVACY}.redact_message" - REDACT_COMMANDS: str = f"{LogCfgSections.OUTPUT_CONSOLE_PRIVACY}.redact_commands" - REDACT_CHANNEL: str = f"{LogCfgSections.OUTPUT_CONSOLE_PRIVACY}.redact_channel" - REDACT_USER: str = f"{LogCfgSections.OUTPUT_CONSOLE_PRIVACY}.redact_user" - - -class MumimoCfgSections: - # Mumimo Config Sections - SETTINGS: str = "settings" - SETTINGS_MEDIA: str = f"{SETTINGS}.media" - SETTINGS_CONNECTION: str = f"{SETTINGS}.connection" - SETTINGS_DATABASE: str = f"{SETTINGS}.database" - SETTINGS_COMMANDS: str = f"{SETTINGS}.commands" - SETTINGS_MEDIA: str = f"{SETTINGS}.media" - SETTINGS_MEDIA_AUDIODUCKING: str = f"{SETTINGS_MEDIA}.audio_ducking" - SETTINGS_MEDIA_YOUTUBEDL: str = f"{SETTINGS_MEDIA}.youtube_dl" - SETTINGS_PLUGINS: str = f"{SETTINGS}.plugins" - SETTINGS_GUI: str = f"{SETTINGS}.gui" - - OUTPUT: str = "output" - OUTPUT_GUI: str = f"{OUTPUT}.gui" - OUTPUT_GUI_CANVAS: str = f"{OUTPUT_GUI}.canvas" - OUTPUT_GUI_TEXT: str = f"{OUTPUT_GUI}.text" - - -class MumimoCfgFields: - # Mumimo Config Fields - class SETTINGS: - class CONNECTION: - MUTE: str = f"{MumimoCfgSections.SETTINGS_CONNECTION}.mute" - DEAFEN: str = f"{MumimoCfgSections.SETTINGS_CONNECTION}.deafen" - REGISTER: str = f"{MumimoCfgSections.SETTINGS_CONNECTION}.register" - COMMENT: str = f"{MumimoCfgSections.SETTINGS_CONNECTION}.comment" - DEFAULT_CHANNEL: str = f"{MumimoCfgSections.SETTINGS_CONNECTION}.default_channel" - AUTO_RECONNECT: str = f"{MumimoCfgSections.SETTINGS_CONNECTION}.auto_reconnect" - NAME: str = f"{MumimoCfgSections.SETTINGS_CONNECTION}.name" - - class DATABASE: - USE_REMOTE_DB: str = f"{MumimoCfgSections.SETTINGS_DATABASE}.use_remote_database" - LOCAL_DB_PATH: str = f"{MumimoCfgSections.SETTINGS_DATABASE}.local_database_path" - LOCAL_DB_DIALECT: str = f"{MumimoCfgSections.SETTINGS_DATABASE}.local_database_dialect" - LOCAL_DB_DRIVERNAME: str = f"{MumimoCfgSections.SETTINGS_DATABASE}.local_database_driver" - DEFAULT_PERMISSION_GROUPS: str = f"{MumimoCfgSections.SETTINGS_DATABASE}.default_permission_groups" - DEFAULT_ALIASES: str = f"{MumimoCfgSections.SETTINGS_DATABASE}.default_aliases" - - class COMMANDS: - TOKEN: str = f"{MumimoCfgSections.SETTINGS_COMMANDS}.command_token" - TICK_RATE: str = f"{MumimoCfgSections.SETTINGS_COMMANDS}.command_tick_rate" - MAX_MULTI_COMMAND_LENGTH: str = f"{MumimoCfgSections.SETTINGS_COMMANDS}.max_multi_command_length" - MAX_COMMAND_QUEUE_LENGTH: str = f"{MumimoCfgSections.SETTINGS_COMMANDS}.max_command_queue_length" - COMMAND_HISTORY_LENGTH: str = f"{MumimoCfgSections.SETTINGS_COMMANDS}.command_history_length" - - class MEDIA: - VOLUME: str = f"{MumimoCfgSections.SETTINGS_MEDIA}.volume" - STEREO_AUDIO: str = f"{MumimoCfgSections.SETTINGS_MEDIA}.stereo_audio" - MAX_QUEUE_LENGTH: str = f"{MumimoCfgSections.SETTINGS_MEDIA}.max_queue_length" - FFMPEG_PATH: str = f"{MumimoCfgSections.SETTINGS_MEDIA}.ffmpeg_path" - VLC_PATH: str = f"{MumimoCfgSections.SETTINGS_MEDIA}.vlc_path" - MEDIA_DIRECTORY: str = f"{MumimoCfgSections.SETTINGS_MEDIA}.media_directory" - - class AUDIODUCKING: - ENABLE: str = f"{MumimoCfgSections.SETTINGS_MEDIA_AUDIODUCKING}.enable" - VOLUME: str = f"{MumimoCfgSections.SETTINGS_MEDIA_AUDIODUCKING}.volume" - THRESHOLD: str = f"{MumimoCfgSections.SETTINGS_MEDIA_AUDIODUCKING}.threshold" - DELAY: str = f"{MumimoCfgSections.SETTINGS_MEDIA_AUDIODUCKING}.delay" - - class YOUTUBEDL: - PROXY_URL: str = f"{MumimoCfgSections.SETTINGS_MEDIA_YOUTUBEDL}.proxy_url" - COOKIE_FILE: str = f"{MumimoCfgSections.SETTINGS_MEDIA_YOUTUBEDL}.cookie_file" - - class PLUGINS: - PLUGINS_PATH: str = f"{MumimoCfgSections.SETTINGS_PLUGINS}.plugins_path" - PLUGINS_CUSTOM_PATH: str = f"{MumimoCfgSections.SETTINGS_PLUGINS}.custom_plugins_path" - PLUGINS_CONFIG_PATH: str = f"{MumimoCfgSections.SETTINGS_PLUGINS}.plugins_config_path" - DISABLED_PLUGINS: str = f"{MumimoCfgSections.SETTINGS_PLUGINS}.disabled_plugins" - SAFE_MODE_PLUGINS: str = f"{MumimoCfgSections.SETTINGS_PLUGINS}.safe_mode_plugins" - - class GUI: - ENABLE: str = f"{MumimoCfgSections.SETTINGS_GUI}.enable" - THEMES_PATH: str = f"{MumimoCfgSections.SETTINGS_GUI}.themes_path" - SELECTED_THEME: str = f"{MumimoCfgSections.SETTINGS_GUI}.selected_theme" - - class OUTPUT: - class GUI: - ENABLE: str = f"{MumimoCfgSections.OUTPUT_GUI}.enable" - - class CANVAS: - BACKGROUND_COLOR: str = f"{MumimoCfgSections.OUTPUT_GUI_CANVAS}.background_color" - IMAGE_BACKGROUND_COLOR: str = f"{MumimoCfgSections.OUTPUT_GUI_CANVAS}.image_background_color" - CONTENT_ALIGNMENT: str = f"{MumimoCfgSections.OUTPUT_GUI_CANVAS}.content_alignment" - BORDER: str = f"{MumimoCfgSections.OUTPUT_GUI_CANVAS}.border" - - class TEXT: - FONT: str = f"{MumimoCfgSections.OUTPUT_GUI_TEXT}.font" - STANDARD_TEXT_COLOR: str = f"{MumimoCfgSections.OUTPUT_GUI_TEXT}.standard_text_color" - HEADER_TEXT_COLOR: str = f"{MumimoCfgSections.OUTPUT_GUI_TEXT}.header_text_color" - SUB_HEADER_TEXT_COLOR: str = f"{MumimoCfgSections.OUTPUT_GUI_TEXT}.sub_header_text_color" - INDEX_TEXT_COLOR: str = f"{MumimoCfgSections.OUTPUT_GUI_TEXT}.index_text_Color" diff --git a/src/exceptions.py b/src/exceptions.py deleted file mode 100644 index a870adf..0000000 --- a/src/exceptions.py +++ /dev/null @@ -1,54 +0,0 @@ -import logging -from typing import Optional - - -class LoggedException(Exception): - def __init__(self, msg: str, logger: Optional[logging.Logger] = None) -> None: - if isinstance(logger, logging.Logger): - logging.exception(str(msg)) - super().__init__(msg) - - -class ValidationError(LoggedException): - def __init__(self, msg: str, logger: Optional[logging.Logger] = None) -> None: - super().__init__(msg, logger=logger) - - -class ConnectivityError(LoggedException): - def __init__(self, msg: str, logger: Optional[logging.Logger] = None) -> None: - super().__init__(msg, logger=logger) - - -class PluginError(LoggedException): - def __init__(self, msg: str, logger: Optional[logging.Logger] = None) -> None: - super().__init__(msg, logger) - - -class GUIError(LoggedException): - def __init__(self, msg: str, logger: Optional[logging.Logger] = None) -> None: - super().__init__(msg, logger) - - -class ServiceError(LoggedException): - def __init__(self, msg: str, logger: Optional[logging.Logger] = None) -> None: - super().__init__(msg, logger=logger) - - -class DatabaseServiceError(ServiceError): - def __init__(self, msg: str, logger: Optional[logging.Logger] = None) -> None: - super().__init__(msg, logger=logger) - - -class ConfigError(LoggedException): - def __init__(self, msg: str, logger: Optional[logging.Logger] = None) -> None: - super().__init__(msg, logger=logger) - - -class ConfigReadError(ConfigError): - def __init__(self, msg: str, logger: Optional[logging.Logger] = None) -> None: - super().__init__(msg, logger=logger) - - -class ConfigWriteError(ConfigError): - def __init__(self, msg: str, logger: Optional[logging.Logger] = None) -> None: - super().__init__(msg, logger=logger) diff --git a/src/lib/command.py b/src/lib/command.py deleted file mode 100644 index ea535b9..0000000 --- a/src/lib/command.py +++ /dev/null @@ -1,100 +0,0 @@ -from typing import Any, Dict, List, Optional - - -class Command: - _command: Optional[str] - _message: str - _parameters: List[str] - _channel_id: int - _session_id: int - _actor: int - - def __init__( - self, - command: Optional[str] = None, - parameters: Optional[List[str]] = None, - message: str = "", - actor: int = -1, - channel_id: Optional[int] = -1, - session_id: Optional[int] = -1, - ) -> None: - self._command = command - self._message = message - self._actor = actor - - if not isinstance(channel_id, int): - self._channel_id = -1 - else: - self._channel_id = channel_id - - if not isinstance(session_id, int): - self._session_id = -1 - else: - self._session_id = session_id - - if parameters is None: - parameters = [] - self._parameters = parameters - - @property - def command(self) -> Optional[str]: - return self._command - - @command.setter - def command(self, value: str) -> None: - self._command = value - - @property - def message(self) -> str: - return self._message - - @message.setter - def message(self, value: str) -> None: - self._message = value - - @property - def parameters(self) -> List[str]: - return self._parameters - - @parameters.setter - def parameters(self, value: List[str]): - self._parameters = value - - @property - def actor(self) -> int: - return self._actor - - @actor.setter - def actor(self, value: int) -> None: - self._actor = value - - @property - def channel_id(self) -> int: - return self._channel_id - - @channel_id.setter - def channel_id(self, value: int) -> None: - self._channel_id = value - - @property - def session_id(self) -> int: - return self._session_id - - @session_id.setter - def session_id(self, value: int) -> None: - self._session_id = value - - @property - def is_private(self) -> bool: - return self._session_id > -1 and self._channel_id == -1 - - def to_dict(self) -> Dict[str, Optional[Any]]: - return { - "command": self.command, - "parameters": self.parameters, - "message": self.message, - "actor": self.actor, - "channel_id": self.channel_id, - "session_id": self.session_id, - "is_private": self.is_private, - } diff --git a/src/lib/command_callbacks.py b/src/lib/command_callbacks.py deleted file mode 100644 index a6f27ab..0000000 --- a/src/lib/command_callbacks.py +++ /dev/null @@ -1,92 +0,0 @@ -from typing import Any, Callable, Dict, List, Optional, Tuple - - -class CommandCallbacks(Dict[str, Dict[str, Any]]): - def register_command( - self, - callback_name: str, - plugin_name: str, - command_func: Callable, - command_parameters: Optional[List[str]] = None, - parameters_required: bool = False, - exclusive_parameters: Optional[List[str]] = None, - ) -> Dict[str, Dict[str, Any]]: - if self.get(callback_name) is None: - self[callback_name] = {} - if command_parameters is None: - command_parameters = [] - if exclusive_parameters is None: - exclusive_parameters = [] - self[callback_name] = { - "command": callback_name, - "func": command_func, - "plugin": plugin_name, - "parameters": command_parameters, - "parameters_required": parameters_required, - "exclusive_parameters": exclusive_parameters, - } - return self - - def remove_command(self, callback_name: str) -> bool: - try: - del self[callback_name] - return True - except KeyError: - return False - - def unregister_command(self, callback_name: str) -> bool: - return self.remove_command(callback_name) - - def unregister_plugin(self, plugin_name: str) -> Tuple[bool, Dict[str, Dict[str, Any]]]: - to_delete = [] - failed_unregisters = {} - success_unregisters = {} - for name, callback in self.items(): - if callback["plugin"] == plugin_name: - to_delete.append((name, callback)) - for name, callback in to_delete: - _attempt = self.unregister_command(name) - if not _attempt: - failed_unregisters[name] = callback - else: - success_unregisters[name] = callback - if failed_unregisters: - return (False, failed_unregisters) - return (True, success_unregisters) - - def get_exclusive_parameters(self, callback_name: str) -> List[str]: - callback: Optional[Dict[str, Any]] = self.get(callback_name, None) - if callback is None: - return [] - parameters: List[str] = callback.get("exclusive_parameters", []) - return parameters - - def get_parameters_required(self, callback_name: str) -> bool: - callback: Optional[Dict[str, Any]] = self.get(callback_name, None) - if callback is None: - return False - return callback.get("parameters_required", False) - - def get_command_parameters(self, callback_name: str) -> List[str]: - callback: Optional[Dict[str, Any]] = self.get(callback_name, None) - if callback is None: - return [] - parameters: List[str] = callback.get("parameters", []) - return parameters - - def get_command(self, callback_name: str) -> Optional[Callable]: - callback: Optional[Dict[str, Any]] = self.get(callback_name, None) - if callback is None: - return None - command_func: Optional[Callable] = callback.get("func", None) - return command_func - - def get_plugin(self, callback_name: str) -> Optional[str]: - callback: Optional[Dict[str, Any]] = self.get(callback_name, None) - if callback is None: - return None - plugin_name: Optional[Callable] = callback.get("plugin", None) - return plugin_name - - def get_callback(self, callback_name: str) -> Optional[Dict[str, Any]]: - return self.get(callback_name, None) diff --git a/src/lib/command_history.py b/src/lib/command_history.py deleted file mode 100644 index 472e550..0000000 --- a/src/lib/command_history.py +++ /dev/null @@ -1,60 +0,0 @@ -from copy import deepcopy -from typing import TYPE_CHECKING, List, Optional - -if TYPE_CHECKING: - from .command import Command - - -class CommandHistory: - _history: List["Command"] - _limit: int - - DEFAULT_HISTORY_LIMIT: int = 100 - - @property - def history(self) -> List["Command"]: - return self._history - - @property - def length(self) -> int: - return len(self._history) - - @property - def limit(self) -> int: - return self._limit - - @limit.setter - def limit(self, value: int) -> None: - if value <= 0: - self._limit = self.DEFAULT_HISTORY_LIMIT - return - self._limit = value - - def __init__(self, history_limit: Optional[int] = DEFAULT_HISTORY_LIMIT) -> None: - self._history = [] - if history_limit is not None: - self.limit = history_limit - else: - self.limit = self.DEFAULT_HISTORY_LIMIT - - def get_last(self, last_n: int = 1) -> List["Command"]: - if last_n < 1: - return [] - reverse_history = list(reversed(self._history)) - return reverse_history[:last_n] - - def add(self, command: "Command") -> Optional["Command"]: - if self.length >= self.limit: - return None - to_add = deepcopy(command) - self._history.append(to_add) - return to_add - - def pop(self, idx: int) -> Optional["Command"]: - try: - return self._history.pop(idx) - except IndexError: - return None - - def clear(self) -> None: - self._history.clear() diff --git a/src/lib/command_queue.py b/src/lib/command_queue.py deleted file mode 100644 index d0915f6..0000000 --- a/src/lib/command_queue.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging -from typing import List, Optional - -from ..exceptions import ServiceError -from ..lib.command import Command - - -logger = logging.getLogger(__name__) - - -class CommandQueue: - _queue: List[Command] = [] - _max_size: int = 0 - - DEFAULT_COMMAND_QUEUE_LIMIT: int = 100 - - def __init__(self, max_size: Optional[int] = DEFAULT_COMMAND_QUEUE_LIMIT) -> None: - if not isinstance(max_size, int) or max_size < 0: - raise ServiceError( - "Cannot initialize command queue: the provided max size must be a non-negative number.", - logger=logger, - ) - if max_size is not None: - self._max_size = max_size - else: - self._max_size = self.DEFAULT_COMMAND_QUEUE_LIMIT - - @property - def size(self) -> int: - return len(self._queue) - - @property - def max_size(self) -> int: - return self._max_size - - @property - def queue(self) -> List[Command]: - return self._queue - - def enqueue_many(self, commands: List[Command]) -> bool: - for command in commands: - if not self.enqueue(command): - return False - return True - - def enqueue(self, command: Command) -> bool: - if self.size >= self.max_size: - logger.warning(f"Unable to enqueue command to the processing queue: {command.command}") - return False - self._queue.append(command) - return True - - def dequeue(self) -> Optional[Command]: - if self.size == 0: - return None - return self.queue.pop(0) diff --git a/src/lib/database/database_connection_parameters.py b/src/lib/database/database_connection_parameters.py deleted file mode 100644 index bc6c70e..0000000 --- a/src/lib/database/database_connection_parameters.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Any, Dict, Optional, Tuple - - -class DatabaseConnectionParameters: - def __init__( - self, - dialect: Optional[str] = None, - host: Optional[str] = None, - port: Optional[str] = None, - database_name: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - drivername: Optional[str] = None, - use_remote: bool = False, - local_database_dialect: Optional[str] = None, - local_database_path: Optional[str] = None, - local_database_driver: Optional[str] = None, - ) -> None: - self.drivername = drivername - self.dialect = dialect - self.username = username - self.host = host - self.port = port - self.database_name = database_name - self.password = password - self.use_remote = use_remote - self.local_database_dialect = local_database_dialect - self.local_database_path = local_database_path - self.local_database_driver = local_database_driver - - def validate_parameters(self) -> Tuple[bool, str]: - if self.use_remote: - if self.database_name is None or self.database_name == "": - return False, f"database_name={self.database_name}" - if self.username is None or self.username == "": - return False, f"username={self.username}" - if self.password is None or self.password == "": - return False, f"password={self.password}" - if self.port is None or self.port == "" or int(self.port) <= 0: - return False, f"port={self.port}" - if self.dialect is None or self.dialect == "": - return False, f"dialect={self.dialect}" - if self.drivername is None or self.drivername == "": - return False, f"drivername={self.drivername}" - if self.host is None or self.host == "": - return False, f"host={self.host}" - else: - if self.local_database_path is None or self.local_database_path == "": - return False, f"local_database_path={self.local_database_path}" - if self.local_database_dialect is None or self.local_database_dialect == "": - return False, f"local_database_dialect={self.local_database_dialect}" - if self.local_database_driver is None or self.local_database_driver == "": - return False, f"local_database_driver={self.local_database_driver}" - - return True, "" - - def to_dict(self) -> Dict[str, Any]: - return { - "dialect": self.dialect, - "drivername": self.drivername, - "host": self.host, - "port": self.port, - "database_name": self.database_name, - "username": self.username, - "password": self.password, - "use_remote": self.use_remote, - "local_database_path": self.local_database_path, - "local_database_dialect": self.local_database_dialect, - "local_database_driver": self.local_database_driver, - } diff --git a/src/lib/database/metadata.py b/src/lib/database/metadata.py deleted file mode 100644 index fa2b68a..0000000 --- a/src/lib/database/metadata.py +++ /dev/null @@ -1,5 +0,0 @@ -from sqlalchemy.orm import DeclarativeBase - - -class Base(DeclarativeBase): - pass diff --git a/src/lib/database/models/alias.py b/src/lib/database/models/alias.py deleted file mode 100644 index 0f772f0..0000000 --- a/src/lib/database/models/alias.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -from sqlalchemy import DateTime, ForeignKey, String, Table, Column, Boolean -from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.sql import func - -from .. import metadata - -if TYPE_CHECKING: - from .permission_group import PermissionGroupTable - - -alias_permission_association_table = Table( - "alias_permission_association_table", - metadata.Base.metadata, - Column("alias_id", ForeignKey("alias.id")), - Column("permission_group_id", ForeignKey("permission_group.id")), -) - - -class AliasTable(metadata.Base): - __tablename__ = "alias" - - id: Mapped[int] = mapped_column(primary_key=True, nullable=False) - name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) - command: Mapped[str] = mapped_column(String(1024), nullable=False) - is_generic: Mapped[bool] = mapped_column(Boolean(), nullable=False, default=False) - - # One to many to permission groups - permission_groups: Mapped[List["PermissionGroupTable"]] = relationship(secondary=alias_permission_association_table) - - # One to many nullable user - user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("user.id")) - - created_on: Mapped[DateTime] = mapped_column(DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False) - updated_on: Mapped[DateTime] = mapped_column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False) - - def to_dict(self) -> Dict[str, Any]: - return { - "id": self.id, - "name": self.name, - "command": self.command, - "is_generic": self.is_generic, - "permission_groups": [group.to_dict() for group in self.permission_groups], - "created_on": self.created_on, - "updated_on": self.updated_on, - } - - def __repr__(self) -> str: - return ( - f"Alias(id={self.id!r}, name={self.name!r}, command={self.command!r}, is_generic={self.is_generic!r}, " - f"permission_groups={self.permission_groups!r}, created_on={self.created_on!r}, updated_on={self.updated_on!r})" - ) diff --git a/src/lib/database/models/command.py b/src/lib/database/models/command.py deleted file mode 100644 index 4ac1455..0000000 --- a/src/lib/database/models/command.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -from sqlalchemy import DateTime, ForeignKey, String, Table, Column -from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.sql import func - -from .. import metadata - -if TYPE_CHECKING: - from .permission_group import PermissionGroupTable - - -command_permission_association_table = Table( - "command_permission_association_table", - metadata.Base.metadata, - Column("command_id", ForeignKey("command.id")), - Column("permission_group_id", ForeignKey("permission_group.id")), -) - - -class CommandTable(metadata.Base): - __tablename__ = "command" - - id: Mapped[int] = mapped_column(primary_key=True, nullable=False) - name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) - - # One to many to permission groups - permission_groups: Mapped[List["PermissionGroupTable"]] = relationship(secondary=command_permission_association_table) - - # One to many nullable plugin - plugin_id: Mapped[Optional[int]] = mapped_column(ForeignKey("plugin.id")) - - created_on: Mapped[DateTime] = mapped_column(DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False) - updated_on: Mapped[DateTime] = mapped_column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False) - - def to_dict(self) -> Dict[str, Any]: - return { - "id": self.id, - "name": self.name, - "permission_groups": [group.to_dict() for group in self.permission_groups], - "created_on": self.created_on, - "updated_on": self.updated_on, - } - - def __repr__(self) -> str: - return ( - f"Command(id={self.id!r}, name={self.name!r}, permission_groups={self.permission_groups!r}, " - f"created_on={self.created_on!r}, updated_on={self.updated_on!r})" - ) diff --git a/src/lib/database/models/permission_group.py b/src/lib/database/models/permission_group.py deleted file mode 100644 index 8193f73..0000000 --- a/src/lib/database/models/permission_group.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any, Dict - -from sqlalchemy import DateTime, String -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.sql import func - -from .. import metadata - - -class PermissionGroupTable(metadata.Base): - __tablename__ = "permission_group" - - id: Mapped[int] = mapped_column(primary_key=True, nullable=False) - name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) - - created_on: Mapped[DateTime] = mapped_column(DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False) - updated_on: Mapped[DateTime] = mapped_column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False) - - def to_dict(self) -> Dict[str, Any]: - return { - "id": self.id, - "name": self.name, - "created_on": self.created_on, - "updated_on": self.updated_on, - } - - def __repr__(self) -> str: - return f"PermissionGroup(id={self.id!r}, name={self.name!r}, created_on={self.created_on!r}, updated_on={self.updated_on!r})" diff --git a/src/lib/database/models/plugin.py b/src/lib/database/models/plugin.py deleted file mode 100644 index 4497432..0000000 --- a/src/lib/database/models/plugin.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, List - -from sqlalchemy import DateTime, String -from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.sql import func - -from .. import metadata - -if TYPE_CHECKING: - from .command import CommandTable - - -class PluginTable(metadata.Base): - __tablename__ = "plugin" - - id: Mapped[int] = mapped_column(primary_key=True, nullable=False) - name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) - - # One to many to commands - commands: Mapped[List["CommandTable"]] = relationship(cascade="all, delete") - - created_on: Mapped[DateTime] = mapped_column(DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False) - updated_on: Mapped[DateTime] = mapped_column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False) - - def to_dict(self) -> Dict[str, Any]: - return { - "id": self.id, - "name": self.name, - "commands": [command.to_dict() for command in self.commands], - "created_on": self.created_on, - "updated_on": self.updated_on, - } - - def __repr__(self) -> str: - return ( - f"Plugin(id={self.id!r}, name={self.name!r}, commands={self.commands!r}, " - f"created_on={self.created_on!r}, updated_on={self.updated_on!r})" - ) diff --git a/src/lib/database/models/user.py b/src/lib/database/models/user.py deleted file mode 100644 index 2a19a9b..0000000 --- a/src/lib/database/models/user.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, List - -from sqlalchemy import DateTime, String, ForeignKey, Table, Column -from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.sql import func - -from .. import metadata - -if TYPE_CHECKING: - from .alias import AliasTable - from .permission_group import PermissionGroupTable - -user_permission_association_table = Table( - "user_permission_association_table", - metadata.Base.metadata, - Column("user_id", ForeignKey("user.id")), - Column("permission_group_id", ForeignKey("permission_group.id")), -) - - -class UserTable(metadata.Base): - __tablename__ = "user" - - id: Mapped[int] = mapped_column(primary_key=True, nullable=False) - name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) - - # One to many to permission groups - permission_groups: Mapped[List["PermissionGroupTable"]] = relationship(secondary=user_permission_association_table) - # One to many to aliases - aliases: Mapped[List["AliasTable"]] = relationship() - - created_on: Mapped[DateTime] = mapped_column(DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False) - updated_on: Mapped[DateTime] = mapped_column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False) - - def to_dict(self) -> Dict[str, Any]: - return { - "id": self.id, - "name": self.name, - "permission_groups": [group.to_dict() for group in self.permission_groups], - "aliases": [alias.to_dict() for alias in self.aliases], - "created_on": self.created_on, - "updated_on": self.updated_on, - } - - def __repr__(self) -> str: - return ( - f"User(id={self.id!r}, name={self.name!r}, permission_groups={self.permission_groups!r}, " - f"aliases={self.aliases!r}, created_on={self.created_on!r}, updated_on={self.updated_on!r})" - ) diff --git a/src/lib/frameworks/gui/constants.py b/src/lib/frameworks/gui/constants.py deleted file mode 100644 index 064a92d..0000000 --- a/src/lib/frameworks/gui/constants.py +++ /dev/null @@ -1,7 +0,0 @@ -from enum import Enum - - -class TextTypes(Enum): - TITLE = "title" - HEADER = "header" - BODY = "body" diff --git a/src/lib/frameworks/gui/gui.py b/src/lib/frameworks/gui/gui.py deleted file mode 100644 index ab7b892..0000000 --- a/src/lib/frameworks/gui/gui.py +++ /dev/null @@ -1,152 +0,0 @@ -import logging - -from typing import Optional, Union, List, TYPE_CHECKING -from .constants import TextTypes -from .utility import FontModifiers, AlignmentModifiers -from ....utils import mumble_utils -from ....settings import settings -from ....constants import MumimoCfgFields -from ....exceptions import GUIError - -if TYPE_CHECKING: - from ....config import Config - - -logger = logging.getLogger(__name__) - - -class GUIFramework: - class ContentBox: - is_open: bool - settings: "Settings" - - _box_open: str - _box_close: str - _box_rows: List[str] - - def __init__(self, settings: Optional["Settings"] = None, **kwargs) -> None: - if settings is not None: - self.settings = settings - else: - self.settings = self.Settings() - self.settings.update(**kwargs) - self.is_open = False - self._box_open = "" - self._box_close = "" - self._box_rows = [] - - def open(self, settings: Optional["Settings"] = None, **kwargs) -> bool: - if self.is_open: - return False - if settings is None: - settings = self.settings - settings.update(**kwargs) - self._box_open = f'' - self.is_open = True - return True - - def close(self) -> bool: - if not self.is_open: - return False - self._box_close = "
" - self.is_open = False - return True - - def add_row(self, text: str, settings: Optional["Settings"] = None, **kwargs) -> bool: - if not self.is_open: - return False - if settings is None: - settings = self.settings - settings.update(**kwargs) - _row_type = f"{'td' if settings.text_type == TextTypes.BODY else 'th'}" - self._box_rows.append( - f"" - f"<{_row_type} {AlignmentModifiers.align(settings.row_align)}>" - f"{text}" - f"" - f"" - ) - return True - - def compile(self) -> str: - _msg = f"{self._box_open}{''.join(self._box_rows)}{self._box_close}" - self._box_open = "" - self._box_close = "" - self._box_rows = [] - self.is_open = False - - return _msg - - class Settings: - text_type: TextTypes = TextTypes.BODY - text_color: str = "yellow" - text_align: str = "center" - - table_align: str = "left" - table_bg_color: str = "black" - - row_align: str = "left" - row_bg_color: str = "black" - - def __init__(self, **kwargs) -> None: - self._from_config() - self.update(**kwargs) - - def _from_config(self) -> None: - _themes: Optional["Config"] = settings.configs.get_gui_themes() - if not _themes: - raise GUIError("Unable to load gui themes: could not retrieve gui themes from settings.") - _config: Optional["Config"] = settings.configs.get_mumimo_config() - if not _config: - raise GUIError("Unable to load gui themes: mumimo config could not be retrieved from settings.") - - _selected_theme = _config.get(MumimoCfgFields.SETTINGS.GUI.SELECTED_THEME, None) - _theme = _themes.get(_selected_theme, None) - if not _selected_theme or not _theme: - _selected_theme = "light" - logger.warning("Unable to find selected gui theme, falling back to default 'light' theme.") - - _theme = _themes.get(_selected_theme, None) - if not _theme: - raise GUIError(f"Unable to find gui theme: [{_selected_theme}], falling back to default 'light' theme.", logger=logger) - - self.update(**_theme) - - def update(self, **kwargs) -> "GUIFramework.ContentBox.Settings": - _text_type = kwargs.get("text_type", self.text_type) - if not isinstance(_text_type, TextTypes): - _text_type = TextTypes(_text_type) - self.text_type = _text_type - - self.text_color = kwargs.get("text_color", self.text_color) - self.text_align = kwargs.get("text_align", self.text_align) - - self.table_align = kwargs.get("table_align", self.table_align) - self.table_bg_color = kwargs.get("table_bg_color", self.table_bg_color) - - self.row_align = kwargs.get("row_align", self.row_align) - self.row_bg_color = kwargs.get("row_bg_color", self.row_bg_color) - return self - - @staticmethod - def gui( - text: Union[List[str], str], - settings: Optional[ContentBox.Settings] = None, - **kwargs, - ) -> None: - raw_text = text - if isinstance(text, str): - raw_text = [text] - - _content = GUIFramework.ContentBox(settings=settings) - _content.open(**kwargs) - for idx, row in enumerate(raw_text): - _content.add_row(row, **kwargs) - _content.close() - - _compiled_text: str = _content.compile() - raw_text = "".join(text) - kwargs["raw_text"] = raw_text - - logger.debug("Compiled GUI: " + _compiled_text) - mumble_utils.echo(_compiled_text, **kwargs) diff --git a/src/lib/frameworks/gui/utility.py b/src/lib/frameworks/gui/utility.py deleted file mode 100644 index fec1676..0000000 --- a/src/lib/frameworks/gui/utility.py +++ /dev/null @@ -1,36 +0,0 @@ -class TextModifiers: - @staticmethod - def bold(text: str) -> str: - return f"{text}" - - @staticmethod - def italicize(text): - return f"{text}" - - @staticmethod - def underline(text): - return f"{text}" - - @staticmethod - def subscript(text): - return f"{text}" - - -class FontModifiers: - @staticmethod - def color(color: str) -> str: - return f'color="{color}"' - - @staticmethod - def face(face: str) -> str: - return f'face="{face}"' - - @staticmethod - def bgcolor(bgcolor: str) -> str: - return f'bgcolor="{bgcolor}"' - - -class AlignmentModifiers: - @staticmethod - def align(align: str) -> str: - return f'align="{align}"' diff --git a/src/lib/frameworks/plugins/plugin.py b/src/lib/frameworks/plugins/plugin.py deleted file mode 100644 index cd27c36..0000000 --- a/src/lib/frameworks/plugins/plugin.py +++ /dev/null @@ -1,283 +0,0 @@ -import logging -import pathlib -import threading -from abc import ABC -from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union - -from ....config import Config -from ....constants import LogOutputIdentifiers, PluginCfgFields -from ....exceptions import PluginError -from ....utils import mumble_utils -from ...frameworks.gui.gui import GUIFramework - -logger = logging.getLogger(__name__) - - -if TYPE_CHECKING: - from ....lib.command import Command - - -class PluginConstants: - class Status(Enum): - NONE = 0 - OK = 1 - FAILED = 2 - - class Reason(Enum): - NONE = 0 - COMMAND_EXCLUSIVE = 1 - COMMAND_DISABLED = 2 - PARAMETER_DISABLED = 3 - PARAMETER_INVALID = 4 - - -class ParameterCompileResult: - def __init__( - self, - parameters: Optional[Union[List[str], str]] = None, - result: Optional[Dict[str, Any]] = None, - status: PluginConstants.Status = PluginConstants.Status.NONE, - reason: PluginConstants.Reason = PluginConstants.Reason.NONE, - ) -> None: - if isinstance(parameters, str): - parameters = [parameters] - self.parameters = parameters - self.result = result - self.status = status - self.reason = reason - - -class PluginBase(ABC): - _plugin_name: str = __name__ - _plugin_metadata: Config - _is_running: bool = False - _thread_stop_event: threading.Event = threading.Event() - _command_parameters: Dict[str, List[str]] = {} - _exclusive_parameters: Dict[str, List[str]] = {} - - @classmethod - def makeCommandRegister(cls): - # Thanks to ninjagecko/Palo for this idea: - # https://stackoverflow.com/questions/5707589/calling-functions-by-array-index-in-python/5707605#5707605 - registered_cmds = {} - - def register(parameters: Optional[List[str]] = None, parameters_required: bool = False, exclusive_parameters: Optional[List[str]] = None): - if parameters is None: - parameters = [] - if exclusive_parameters is None: - exclusive_parameters = [] - - def register_with_parameters(func): - registered_cmds[func.__name__] = (func, parameters, parameters_required, exclusive_parameters) - return func - - return register_with_parameters - - register.all = registered_cmds - return register - - @property - def is_running(self): - return self._is_running - - @property - def command_parameters(self): - return self._command_parameters - - @property - def exclusive_parameters(self): - return self._exclusive_parameters - - @property - def plugin_name(self): - return self._plugin_name - - @property - def plugin_metadata(self) -> Config: - return self._plugin_metadata - - def __init__(self, plugin_name: str) -> None: - super().__init__() - self._plugin_name = plugin_name - self.initialize_metadata() - - def _compile_parameters(self, func_name: str, data: "Command") -> ParameterCompileResult: - _results = {} - _command = data._command - # Raise a plugin error if the command name is missing from the command object. This should never happen. - if _command is None: - raise PluginError(f"Plugin '{self.plugin_name}' error: encountered an error compiling parameters due to a missing command value.") - - # Return a compile failure reuslt if the command is disabled. - if func_name in self.plugin_metadata.get(PluginCfgFields.PLUGIN.COMMANDS.DISABLE_COMMANDS, []): - return ParameterCompileResult( - status=PluginConstants.Status.FAILED, - reason=PluginConstants.Reason.COMMAND_DISABLED, - ) - # Return a compile failure result if the command is using more than 1 exclusive parameter at a time. - if len(self.exclusive_parameters[_command]) > 0: - if len(data.parameters) > 1: - matching_parameters = [ - param - for param in data.parameters - for param_exclusive in self.exclusive_parameters[_command] - if param.split("=")[0] == param_exclusive - ] - return ParameterCompileResult( - status=PluginConstants.Status.FAILED, - reason=PluginConstants.Reason.COMMAND_EXCLUSIVE, - parameters=[param.split("=")[0] for param in matching_parameters], - ) - - for param in data.parameters: - param_split: List[str] = param.strip().split("=", 1) - if len(param_split) == 0: - param_split.append(param.strip()) - - # Return a compile failure result if the parameter is disabled for the specified command. - if f"{_command}.{param_split[0]}" in self.plugin_metadata.get(PluginCfgFields.PLUGIN.COMMANDS.DISABLE_PARAMETERS, []): - return ParameterCompileResult( - status=PluginConstants.Status.FAILED, - reason=PluginConstants.Reason.PARAMETER_DISABLED, - parameters=param_split[0], - ) - # Return a compile failure if the parameter is invalid. - elif param_split[0] not in self.command_parameters[_command]: - return ParameterCompileResult( - status=PluginConstants.Status.FAILED, - reason=PluginConstants.Reason.PARAMETER_INVALID, - parameters=param_split[0], - ) - # Execute the parameter function. - func: Callable = getattr(self, f"_parameter_{func_name}_{param_split[0]}") - _results[param_split[0]] = func(data, param) - - return ParameterCompileResult( - status=PluginConstants.Status.OK, - result=_results, - ) - - def verify_parameters(self, func_name: str, data: "Command") -> Optional[Dict[str, Any]]: - _compiled_result: ParameterCompileResult = self._compile_parameters(func_name, data) - if _compiled_result.status == PluginConstants.Status.FAILED: - if _compiled_result.reason == PluginConstants.Reason.COMMAND_DISABLED: - GUIFramework.gui( - f"'{data.command}' command error: this command is disabled.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - elif _compiled_result.reason == PluginConstants.Reason.COMMAND_EXCLUSIVE: - if not _compiled_result.parameters: - _compiled_result.parameters = ["n/a"] - GUIFramework.gui( - f"'{data.command}' command error: [{', '.join(_compiled_result.parameters)}] are exclusive " - "parameters that must be used independently.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - elif _compiled_result.reason == PluginConstants.Reason.PARAMETER_DISABLED: - GUIFramework.gui( - f"'{data.command}' command error: the '{_compiled_result.parameters}' parameter is disabled.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - elif _compiled_result.reason == PluginConstants.Reason.PARAMETER_INVALID: - GUIFramework.gui( - f"'{data.command}' command error: the '{_compiled_result.parameters}' parameter is invalid.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _compiled_parameters: Optional[Dict[str, Any]] = _compiled_result.result - if _compiled_parameters is None: - GUIFramework.gui( - f"'{data.command}' command error: parameters failed to compile.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - return _compiled_parameters - - def initialize_metadata(self): - self._plugin_metadata = Config(pathlib.Path.cwd() / f".config/plugins/{self._plugin_name}/metadata.toml") - self._plugin_metadata.read() - - def initialize_parameters(self, callbacks: List[Dict[str, Any]]): - if not callbacks: - raise PluginError(f"Unable to initialize parameters for plugin '{self.plugin_name}'. No command callbacks provided.") - for _clbk in callbacks: - # Disable commands from plugin metadata file. - if _clbk["command"] in self.plugin_metadata.get(PluginCfgFields.PLUGIN.COMMANDS.DISABLE_COMMANDS, []): - logger.debug( - f"[{LogOutputIdentifiers.PLUGINS_PARAMETERS}]: Parameters for command '{_clbk['command']}' is disabled in the " - "plugin metadata file. Skipping parameter initialization..." - ) - continue - - # Disable parameters from plugin metadata file. - _enabled_parameters: List[str] = [] - _enabled_exclusive_parameters: List[str] = [] - _disabled_parameters: List[str] = self.plugin_metadata.get(PluginCfgFields.PLUGIN.COMMANDS.DISABLE_PARAMETERS, []) - for parameter in _clbk["parameters"]: - if f"{_clbk['command']}.{parameter}" in _disabled_parameters: - logger.debug( - f"[{LogOutputIdentifiers.PLUGINS_PARAMETERS}]: Parameter '{parameter}' for command '{_clbk['command']}' is disabled in the " - "plugin metadata file. Skipping parameter initialization..." - ) - continue - _enabled_parameters.append(parameter) - for parameter in _clbk["exclusive_parameters"]: - if parameter in _disabled_parameters: - logger.debug( - f"[{LogOutputIdentifiers.PLUGINS_PARAMETERS}]: Disabled exclusivity rules for command '{_clbk['command']}.{parameter}' as " - "the associated parameter is disabled." - ) - continue - _enabled_exclusive_parameters.append(parameter) - - self._command_parameters[_clbk["command"]] = _enabled_parameters - self._exclusive_parameters[_clbk["command"]] = _enabled_exclusive_parameters - logger.debug( - f"[{LogOutputIdentifiers.PLUGINS_PARAMETERS}]: Plugin '{self.plugin_name}' parameters [{','.join(_enabled_parameters)}] for " - f"command '{_clbk['command']}' initialized." - ) - - def start(self) -> Tuple[bool, str]: - if not self._is_running: - self._is_running = True - self._thread_stop_event.clear() - _msg = f"Plugin started: {self.plugin_name}" - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: {_msg}") - return (True, _msg) - _msg = f"Plugin '{self.plugin_name}' is already running." - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: {_msg}") - return (False, _msg) - - def stop(self) -> Tuple[bool, str]: - if self._is_running: - self._thread_stop_event.set() - self._is_running = False - _msg = f"Plugin stopped: {self.plugin_name}" - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: {_msg}") - return (True, _msg) - _msg = f"Plugin '{self.plugin_name}' is already stopped." - logger.warning(f"[{LogOutputIdentifiers.PLUGINS}]: {_msg}") - return (False, _msg) - - def restart(self) -> Tuple[bool, str]: - _plugin_stopped: Tuple[bool, str] = self.stop() - _msg = "" - if self.start(): - _msg = f"Plugin restarted: {self.plugin_name}" - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: {_msg}") - return (True, _msg) - if _plugin_stopped[0] is True: - _msg = f"Plugin '{self.plugin_name}' was stopped during a restart process and could not be started again." - logger.warning(f"[{LogOutputIdentifiers.PLUGINS}]: {_msg}") - else: - _msg = f"Plugin '{self.plugin_name}' could not be restarted." - logger.error(f"[{LogOutputIdentifiers.PLUGINS}]: {_msg}") - return (False, _msg) - - def quit(self): - self.stop() - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: Plugin closed: {self.plugin_name}") diff --git a/src/lib/singleton.py b/src/lib/singleton.py deleted file mode 100644 index 673d752..0000000 --- a/src/lib/singleton.py +++ /dev/null @@ -1,17 +0,0 @@ -# Implementation details: https://igeorgiev.eu/python/design-patterns/python-singleton-pattern-decorator/ -from functools import wraps - - -def singleton(orig_cls): - orig_new = orig_cls.__new__ - instance = None - - @wraps(orig_cls.__new__) - def __new__(cls, *args, **kwargs): - nonlocal instance - if instance is None: - instance = orig_new(cls, *args, **kwargs) - return instance - - orig_cls.__new__ = __new__ - return orig_cls diff --git a/src/log_config.py b/src/log_config.py deleted file mode 100644 index c5adc27..0000000 --- a/src/log_config.py +++ /dev/null @@ -1,114 +0,0 @@ -import copy -import pathlib -from typing import Any, Optional - -import toml - - -class LogConfig(dict): - _config_file_path: Optional[pathlib.Path] = None - _initial_config: Optional["LogConfig"] = None - - def __init__(self, file_name: Optional[str] = None) -> None: - super().__init__() - if file_name is not None: - self._config_file_path = pathlib.Path.cwd() / file_name - - def read(self, file_name: Optional[str] = None) -> "LogConfig": - if file_name is None: - if self._config_file_path is None: - raise IOError("Unable to read config file because no file was specified.") - self._read_from_file(self._config_file_path) - return self - search_path = pathlib.Path.cwd() / file_name - if search_path.exists() and search_path.is_file(): - self._read_from_file(search_path) - return self - raise IOError(f"Unable to read config file at: {search_path}") - - def _read_from_file(self, file_path: pathlib.Path): - try: - with open(str(file_path.resolve()), "r", encoding="utf-8") as file_handler: - contents = toml.load(file_handler, _dict=dict) - self.update(contents) - self._config_file_path = file_path - if self._initial_config is None: - self._initial_config = LogConfig() - self._initial_config._config_file_path = self._config_file_path - self._initial_config.update(self) - except toml.TomlDecodeError as exc: - raise IOError(f"Unable to read config file at: {file_path}.") from exc - except IOError as exc: - raise IOError(f"Unable to open config file to read at: {file_path}.") from exc - - def save( - self, - file_name: Optional[str] = None, - modified_only: bool = False, - modified_field_name: Optional[str] = None, - ) -> str: - target_path = None - if file_name is not None: - target_path = file_name - elif self._config_file_path is not None: - target_path = str(self._config_file_path.resolve()) - else: - raise IOError("Unable to save data to a config file because no file was specified.") - - try: - saved_data = None - with open(target_path, "w", encoding="utf-8") as file_handler: - if not modified_only: - saved_data = toml.dump(self, file_handler) - else: - if not modified_field_name: - raise IOError("Unable to save modified data to a config file because the field name is invalid.") - field = self.get(modified_field_name) - if field is None: - raise IOError("Unable to save modified data to a config file because the field name does not exist.") - if self._initial_config is None: - raise IOError("Unable to save modified data to a config file because no file has been initialized.") - self._initial_config.set(modified_field_name, field) - saved_data = toml.dump(self._initial_config, file_handler) - if saved_data is None: - raise IOError(f"Unable to save data to a config file at: {target_path}") - except IOError as exc: - raise IOError(f"Unable to save config file at: {target_path}") from exc - return saved_data - - def reset(self, field_name: str) -> bool: - return self.set(field_name, None, create_keys_if_not_exists=False) - - def get(self, field_name: str, fallback: Optional[Any] = None) -> Any: - if not field_name: - return None - field = self._get_field(field_name) - if field is None: - if fallback is not None: - return fallback - return None - return field - - def _get_field(self, field_name: str): - field_sections = field_name.split(".") - fields_copy = copy.deepcopy(self) - try: - for key in field_sections: - fields_copy = fields_copy[key] - except KeyError: - return None - return fields_copy - - def set(self, field_name: str, field_value: Optional[Any] = None, create_keys_if_not_exists: bool = False) -> bool: - if not field_name: - return False - return self._set_field(field_name, field_value, create_keys_if_not_exists) - - def _set_field(self, field_name: str, field_value: Optional[Any] = None, create_keys_if_not_exists: bool = False): - if not create_keys_if_not_exists and self.get(field_name) is None: - return False - field_sections = field_name.split(".") - for key in field_sections[:-1]: - self = self.setdefault(key, {}) - self[field_sections[-1]] = field_value - return True diff --git a/src/logging.py b/src/logging.py deleted file mode 100644 index f80ecc1..0000000 --- a/src/logging.py +++ /dev/null @@ -1,159 +0,0 @@ -import logging -import pathlib -import sys -from typing import Dict, Optional, TYPE_CHECKING, Mapping, Any - -from .constants import VERBOSE_HIGH, VERBOSE_MIN, VERBOSE_STANDARD, LogCfgFields, SysArgs -from .settings import settings -from .utils import log_utils -from .version import version - -_IS_INITIALIZED: bool = False -_log_file_handler: Optional[logging.Handler] = None -_log_console_handler: Optional[logging.Handler] = None -_logger = logging.getLogger(__name__) - - -if TYPE_CHECKING: - from logging import _FormatStyle - - -class LogFormatter(logging.Formatter): - _grey = "\x1b[38;20m" - _cyan = "\x1b[36:36m" - _yellow = "\x1b[33;20m" - _red = "\x1b[31;20m" - _bold_red = "\x1b[31;1m" - _reset = "\x1b[0m" - _fmt = "" - - FORMATS = { - logging.DEBUG: _fmt, - logging.INFO: _fmt, - logging.WARNING: _fmt, - logging.ERROR: _fmt, - logging.CRITICAL: _fmt, - } - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - style: "_FormatStyle" = "%", - validate: bool = True, - *, - defaults: Mapping[str, Any] | None = None, - ) -> None: - super().__init__(fmt, datefmt, style, validate, defaults=defaults) - if fmt is not None: - self.FORMATS = { - logging.DEBUG: self._cyan + fmt + self._reset, - logging.INFO: self._grey + fmt + self._reset, - logging.WARNING: self._yellow + fmt + self._reset, - logging.ERROR: self._red + fmt + self._reset, - logging.CRITICAL: self._bold_red + fmt + self._reset, - } - - def format(self, record): - log_fmt = self.FORMATS.get(record.levelno) - formatter = logging.Formatter(log_fmt) - return formatter.format(record) - - -def init_logger(sys_args: Optional[Dict[str, str]] = None) -> bool: - global _IS_INITIALIZED, _log_file_handler, _log_console_handler - if _IS_INITIALIZED: - return False - if sys_args is None: - return False - _log_config = log_utils.initialize_log_config(sys_args.get(SysArgs.SYS_LOG_CONFIG_FILE, None)) - _log_config.set(SysArgs.SYS_VERBOSE, int(sys_args.get(SysArgs.SYS_VERBOSE, VERBOSE_MIN)), create_keys_if_not_exists=True) - _log_parent_directory: pathlib.Path = pathlib.Path.cwd() / _log_config.get(LogCfgFields.OUTPUT.FILE.PATH) - _log_version_directory: pathlib.Path = _log_parent_directory / f"mumimo_{version()}/" - _enable_file_log = bool(_log_config.get(LogCfgFields.OUTPUT.FILE.ENABLE)) - if _enable_file_log: - pathlib.Path.mkdir(_log_parent_directory, exist_ok=True) - pathlib.Path.mkdir(_log_version_directory, exist_ok=True) - _enable_console_log = _log_config.get(SysArgs.SYS_VERBOSE) >= VERBOSE_MIN - _logger.root.setLevel(logging.DEBUG) - if not _logger.root.hasHandlers(): - if _enable_file_log: - file_handler = get_file_handler() - if file_handler is not None: - _logger.root.addHandler(file_handler) - _log_file_handler = file_handler - else: - print("File handler unable to initialize.") - if _enable_console_log: - console_handler = get_console_handler() - if console_handler is not None: - _logger.root.addHandler(console_handler) - _log_console_handler = console_handler - # Disable logging for asyncio/aiosqlite libraries unless the verbose level is max: - if _log_config.get(SysArgs.SYS_VERBOSE) >= VERBOSE_HIGH: - logging.getLogger("asyncio").setLevel(logging.DEBUG) - logging.getLogger("aiosqlite").setLevel(logging.DEBUG) - else: - logging.getLogger("asyncio").setLevel(logging.ERROR) - logging.getLogger("aiosqlite").setLevel(logging.ERROR) - _IS_INITIALIZED = True - return _IS_INITIALIZED - - -def get_console_handler() -> Optional[logging.StreamHandler]: - _log_config = settings.configs.get_log_config() - if _log_config is None: - return None - console_handler = logging.StreamHandler(sys.stdout) - console_handler.setLevel(logging.CRITICAL) - _verbosity = int(_log_config.get(SysArgs.SYS_VERBOSE)) - if _verbosity == VERBOSE_MIN: - console_handler.setLevel(logging.INFO) - elif _verbosity >= VERBOSE_STANDARD: - console_handler.setLevel(logging.DEBUG) - _logging_formatter = LogFormatter(_log_config.get(LogCfgFields.OUTPUT.CONSOLE.FORMAT)) - console_handler.setFormatter(_logging_formatter) - return console_handler - - -def get_file_handler() -> Optional[logging.FileHandler]: - _log_config = settings.configs.get_log_config() - if _log_config is None: - return None - _logging_formatter = logging.Formatter(_log_config.get(LogCfgFields.OUTPUT.FILE.FORMAT)) - _log_location: pathlib.Path = pathlib.Path.cwd() / _log_config.get(LogCfgFields.OUTPUT.FILE.PATH) / f"mumimo_{version()}" - _log_name: str = _log_config.get(LogCfgFields.OUTPUT.FILE.NAME) % version() - _log_level: int = _log_config.get(LogCfgFields.OUTPUT.FILE.LEVEL) - _log_location_path: pathlib.Path = (_log_location / _log_name).resolve() - - file_handler = logging.FileHandler(filename=_log_location_path) - file_handler.setLevel(_log_level) - file_handler.setFormatter(_logging_formatter) - return file_handler - - -def log_privacy(msg: str, logger: logging.Logger, level: int = logging.INFO) -> None: - if not _IS_INITIALIZED: - return - if not logger.hasHandlers(): - return - - _file_redact_all: bool = log_utils.privacy_file_redact_all_check() - _console_redact_all: bool = log_utils.privacy_console_redact_all_check() - - if _log_file_handler: - _cached_file_level: int = _log_file_handler.level - if _file_redact_all: - _log_file_handler.setLevel(logging.CRITICAL) - if _log_console_handler: - _cached_console_level: int = _log_console_handler.level - if _console_redact_all: - _log_console_handler.setLevel(logging.CRITICAL) - - if not _file_redact_all and not _console_redact_all: - logger.log(level, msg) - - if _log_file_handler: - _log_file_handler.setLevel(_cached_file_level) # type: ignore - if _log_console_handler: - _log_console_handler.setLevel(_cached_console_level) # type: ignore diff --git a/src/mumimo.py b/src/mumimo.py deleted file mode 100644 index a90e793..0000000 --- a/src/mumimo.py +++ /dev/null @@ -1,63 +0,0 @@ -import asyncio -import logging -from typing import TYPE_CHECKING, Any, Dict, Optional - -from .exceptions import ServiceError -from .murmur_connection import MurmurConnection -from .services.init_services.mumimo_init_service import MumimoInitService -from .settings import settings -from .utils import connection_utils, mumble_utils - -if TYPE_CHECKING: - from .client_state import ClientState - -logger = logging.getLogger(__name__) - - -class MumimoService: - _murmur_connection_instance: Optional[MurmurConnection] - - def __init__(self, sys_args: Dict[str, str]) -> None: - asyncio.run(self._setup(sys_args)) - - async def _setup(self, sys_args: Dict[str, str]) -> None: - if not connection_utils.is_supported_platform(): - logger.warning("Mumimo is only supported for Linux and MacOS systems. You may run into unexpected issues on Windows and other systems.") - logger.info("Initializing Mumimo client...") - _mumimo_init_service: MumimoInitService = MumimoInitService(sys_args) - await _mumimo_init_service.initialize() - _connection_params = await _mumimo_init_service.get_connection_parameters() - if not _connection_params: - raise ServiceError( - "Unable to retrieve client connection parameters: attempted to retrieve connection parameters before initializing client settings.", - logger=logger, - ) - await self.initialize_connection(_connection_params) - - async def initialize_connection(self, connection_params: Dict[str, Any]) -> None: - logger.info("Establishing Murmur connectivity...") - self._murmur_connection_instance = MurmurConnection() - if self._murmur_connection_instance is not None: - self._murmur_connection_instance.setup(connection_params) - self._murmur_connection_instance.ready().connect() - if self._murmur_connection_instance.is_connected: - settings.connection.set_murmur_connection(self._murmur_connection_instance) - _client_state: Optional["ClientState"] = settings.state.get_client_state() - if _client_state is not None: - _client_state.audio_properties.mute() - if self._murmur_connection_instance.start(): - logger.info("Established Murmur connectivity.") - logger.info("Mumimo client initialized.") - await self._murmur_connection_instance._async_post_connection_actions() - await self._wait_for_interrupt() - else: - logger.error("Failed to establish Murmur connectivity.") - else: - logger.error("Failed to initialize Mumimo connection instance singleton.") - - async def _wait_for_interrupt(self) -> None: - try: - while True: - await asyncio.sleep(0.1) - except asyncio.exceptions.CancelledError: - await mumble_utils.Management.exit_server() diff --git a/src/murmur_connection.py b/src/murmur_connection.py deleted file mode 100644 index 2ac3c0b..0000000 --- a/src/murmur_connection.py +++ /dev/null @@ -1,212 +0,0 @@ -import logging -import threading -import time -from typing import Dict, Optional, Union, List - - -import pymumble_py3 as pymumble -from pymumble_py3.users import User -from pymumble_py3.constants import PYMUMBLE_CLBK_TEXTMESSAGERECEIVED, PYMUMBLE_CLBK_CONNECTED, PYMUMBLE_CLBK_USERCREATED, PYMUMBLE_CLBK_USERREMOVED -from pymumble_py3.errors import ConnectionRejectedError - -from .client_state import ClientState -from .services.cmd_processing_service import CommandProcessingService -from .constants import VERBOSE_MAX, SysArgs, MumimoCfgFields -from .exceptions import ConnectivityError, ServiceError -from .lib.singleton import singleton -from .settings import settings -from .utils.args_validators import SystemArgumentsValidator -from .utils import mumble_utils -from .version import version - -logger = logging.getLogger(__name__) - - -@singleton -class MurmurConnection: - _thread: Optional[threading.Thread] = None - _thread_stop_event: threading.Event = threading.Event() - - _connection_instance: Optional[pymumble.Mumble] = None - _connection_params: Optional[Dict[str, Union[str, bool]]] = None - _is_connected: bool = False - - @property - def connection_instance(self) -> Optional[pymumble.Mumble]: - return self._connection_instance - - @property - def is_connected(self) -> bool: - return self._is_connected - - def setup(self, connection_params: Dict[str, Union[str, bool]]): - if not connection_params: - logger.warning("Connection parameters have not been provided during Murmur initialization.") - return - self._validate_sys_args(connection_params) - self._connection_params = connection_params - logger.debug("Validated murmur connection parameters.") - - def ready(self) -> "MurmurConnection": - if self.is_connected: - raise ConnectivityError( - "Unable to connect: a murmur connection instance is already connected. Please stop the active connection before creating a new one.", - logger=logger, - ) - if self._connection_params is None: - raise ConnectivityError("Unable to connect: connection parameters have not been specified.", logger=logger) - self._connection_instance = pymumble.Mumble( - host=self._connection_params.get(SysArgs.SYS_HOST), - port=int(self._connection_params.get(SysArgs.SYS_PORT, 64738)), - user=self._connection_params.get(SysArgs.SYS_USER), - password=str(self._connection_params.get(SysArgs.SYS_PASS)), - certfile=self._connection_params.get(SysArgs.SYS_CERT), - keyfile=self._connection_params.get(SysArgs.SYS_KEY), - tokens=self._connection_params.get(SysArgs.SYS_TOKENS), - reconnect=bool(self._connection_params.get(SysArgs.SYS_RECONNECT, False)), - debug=bool(int(self._connection_params.get(SysArgs.SYS_VERBOSE, False)) >= VERBOSE_MAX), - stereo=True, - client_type=1, - ) - self._connection_instance.set_codec_profile("audio") - self._connection_instance.set_receive_sound(True) - self._connection_instance.set_loop_rate(0.05) - - logger.debug("Murmur connection instance defined.") - return self - - def connect(self) -> "MurmurConnection": - if self.is_connected: - raise ConnectivityError( - "Unable to connect: a murmur connection instance is already connected. Please stop the active connection before creating a new one.", - logger=logger, - ) - if self._connection_instance is None: - raise ConnectivityError("Unable to connect: the murmur connection instance is not initialized.", logger=logger) - self._connect_instance() - logger.debug("Murmur connection instance established.") - self._post_connection_actions() - logger.debug("Murmur post-connection actions completed.") - return self - - def start(self) -> bool: - if self._is_connected and self._connection_instance: - if self._thread is None: - self._thread = threading.Thread(name="murmur-conn", target=self._loop, args=(self._thread_stop_event,)) - else: - self._thread_stop_event.set() - self._thread.join() - self._thread = threading.Thread(name="murmur-conn", target=self._loop, args=(self._thread_stop_event,)) - logger.debug(f"Connectivity thread: [{self._thread.name}] initialized.") - self._thread.start() - logger.debug(f"Connectivity thread: [{self._thread.name} | {self._thread.ident}] started.") - return True - return False - - def stop(self) -> bool: - if self._connection_instance is not None: - if self._thread is not None: - logger.debug(f"Connectivity thread: [{self._thread.name} | {self._thread.ident}] closing...") - self._thread_stop_event.set() - self._thread.join() - logger.debug(f"Connectivity thread: [{self._thread.name}] closed.") - try: - self._connection_instance.stop() - except AttributeError: - logger.debug("Connection instance closed prematurely.") - self._is_connected = False - self._connection_instance = None - self._thread = None - return True - return False - - def _connect_instance(self) -> None: - if self._connection_instance is None: - raise ConnectivityError("Unable to connect: the murmur connection instance is not initialized.", logger=logger) - try: - self._connection_instance.start() - self._connection_instance.is_ready() - self._is_connected = True - except ConnectionRejectedError as err: - raise ConnectivityError(str(err), logger) from err - - def _post_connection_actions(self) -> None: - if self._connection_instance is None: - raise ServiceError("Unable to conduct post connection actions: there is no active murmur connection.", logger=logger) - - # Save the client state to the settings and set callbacks. - _client_state: Optional[ClientState] = settings.state.get_client_state() - if _client_state is None: - _client_state = ClientState(self._connection_instance) - settings.state.set_client_state(_client_state) - # Set on_server_connect callback in client state. - self._connection_instance.callbacks.set_callback(PYMUMBLE_CLBK_CONNECTED, _client_state.server_properties.on_server_connect) - logger.debug(f"Added murmur callback: {PYMUMBLE_CLBK_CONNECTED}-{_client_state.server_properties.on_server_connect.__name__}") - # Set on_user_created callback in client state. - self._connection_instance.callbacks.set_callback(PYMUMBLE_CLBK_USERCREATED, _client_state.server_properties.on_user_created) - logger.debug(f"Added murmur callback: {PYMUMBLE_CLBK_USERCREATED}-{_client_state.server_properties.on_user_created.__name__}") - # Set on_user_removed callback in client state. - self._connection_instance.callbacks.set_callback(PYMUMBLE_CLBK_USERREMOVED, _client_state.server_properties.on_user_removed) - logger.debug(f"Added murmur callback: {PYMUMBLE_CLBK_USERREMOVED}-{_client_state.server_properties.on_user_removed.__name__}") - - # Save the command processing service to the settings and set command processing mumble callbacks. - _cmd_service: Optional[CommandProcessingService] = settings.commands.services.get_cmd_processing_service() - if _cmd_service is None: - _cmd_service = CommandProcessingService(self._connection_instance) - settings.commands.services.set_cmd_processing_service(_cmd_service) - self._connection_instance.callbacks.set_callback(PYMUMBLE_CLBK_TEXTMESSAGERECEIVED, _cmd_service.process_cmd) - logger.debug(f"Added murmur callback: {PYMUMBLE_CLBK_TEXTMESSAGERECEIVED}-{_cmd_service.process_cmd.__name__}") - - # Listen to all channels: - _user: Optional["User"] = self._connection_instance.users.myself - if _user: - _channels = [] - for _, channel in enumerate(self._connection_instance.channels.items()): - _channels.append(channel[1]["channel_id"]) - _user.add_listening_channels(_channels) - # Set bot client comment - _user.comment(f"Mumimo - v{version()}") - # Mute the bot on server join - _client_state.audio_properties.mute() - - # _user.myself.register() - don't implement yet - - async def _async_post_connection_actions(self) -> None: - logger.debug("Running asynchronous post connection actions.") - - # Detect all active users and add the users to the database with permissions if they don't exist: - _database_service = settings.database.get_database_instance() - if not _database_service: - raise ServiceError("Failed async post connection actions: database instance not found.", logger=logger) - _inst = self._connection_instance - if not _inst: - raise ServiceError("Failed async post connection actions: mumble instance not found.", logger=logger) - _cfg = settings.configs.get_mumimo_config() - if not _cfg: - raise ServiceError("Failed async post connection actions: mumimo config not found.", logger=logger) - _bot_name = _cfg.get(MumimoCfgFields.SETTINGS.CONNECTION.NAME, None) - if not _bot_name: - raise ServiceError("Failed async post connection actions: mumimo bot name not found in config.", logger=logger) - - _all_users: List["User"] = [user for id, user in _inst.users.items() if user["name"] != _bot_name] - for _user in _all_users: - await mumble_utils.Management.UserManagement.add_user(_user) - - logger.debug("Asynchronous post connection actions complete.") - - def _loop(self, stop_event: threading.Event) -> None: - while True: - if stop_event.is_set(): - break - time.sleep(0.1) - - def _validate_sys_args(self, params: Dict[str, Union[str, bool]]) -> None: - SystemArgumentsValidator.validate_host_param(params.get(SysArgs.SYS_HOST)) # type: ignore - SystemArgumentsValidator.validate_port_param(params.get(SysArgs.SYS_PORT)) # type: ignore - SystemArgumentsValidator.validate_user_param(params.get(SysArgs.SYS_USER)) # type: ignore - SystemArgumentsValidator.validate_password_param(params.get(SysArgs.SYS_PASS)) # type: ignore - SystemArgumentsValidator.validate_cert_param(params.get(SysArgs.SYS_CERT)) # type: ignore - SystemArgumentsValidator.validate_key_param(params.get(SysArgs.SYS_KEY)) # type: ignore - SystemArgumentsValidator.validate_tokens_param(params.get(SysArgs.SYS_TOKENS)) # type: ignore - SystemArgumentsValidator.validate_auto_reconnect_param(params.get(SysArgs.SYS_RECONNECT)) # type: ignore - SystemArgumentsValidator.validate_verbose_param(params.get(SysArgs.SYS_VERBOSE)) # type: ignore diff --git a/src/plugins/builtin_core/help_template.toml b/src/plugins/builtin_core/help_template.toml deleted file mode 100644 index db2b77b..0000000 --- a/src/plugins/builtin_core/help_template.toml +++ /dev/null @@ -1,15 +0,0 @@ -[plugin.help.page.1] -# Command help text format: -# - commands_help_text = [ -# ["command_name", "command_parameters", "command_message", "command_description_text"], -# ... -# ] -# As shown in the example above, each item must contain 4 elements: -# 1. Index 0: The name of the command (same as the method name in the plugin.py file) -# 2. Index 1: The parameters that are supported by the command. -# 3. Index 2: The message accompanying the command input, if any. -# 4. Index 3: The description/help text to help users understand what the command does. -# Default values: Too long to list here, please check the wiki. -commands_help_text = [ - ["echo", " | | ", "", "Repeats the provided message with the selected output method. Using the 'echo' command with a 'channel' parameter for example repeats the command to all users in the bot's channel."] -] diff --git a/src/plugins/builtin_core/metadata_template.toml b/src/plugins/builtin_core/metadata_template.toml deleted file mode 100644 index 87dc1e2..0000000 --- a/src/plugins/builtin_core/metadata_template.toml +++ /dev/null @@ -1,12 +0,0 @@ -[plugin] -enabled = true - -[plugin.about] -name = "Core" -version = "0.0.1" -description = "Provides core functionality to Mumimo." - -[plugin.commands] -disable_commands = [] -disable_parameters = [] -default_permission_groups = ["guest", "regular", "admin"] diff --git a/src/plugins/builtin_core/plugin.py b/src/plugins/builtin_core/plugin.py deleted file mode 100644 index d9a2bf0..0000000 --- a/src/plugins/builtin_core/plugin.py +++ /dev/null @@ -1,514 +0,0 @@ -import logging -import time -from typing import TYPE_CHECKING, List, Optional - -from pymumble_py3.channels import Channel -from pymumble_py3.users import User - -from src.constants import LogOutputIdentifiers -from src.lib.frameworks.plugins.plugin import PluginBase -from src.settings import settings -from src.utils import mumble_utils -from src.lib.frameworks.gui.gui import GUIFramework - -from .utility.constants import ParameterDefinitions -from .utility import theme_utils - -logger = logging.getLogger(__name__) - - -if TYPE_CHECKING: - from src.lib.command import Command - - -class Plugin(PluginBase): - command = PluginBase.makeCommandRegister() - - def __init__(self, plugin_name: str) -> None: - self._plugin_name = plugin_name - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: Initializing plugin: '{self._plugin_name}'") - super().__init__(self._plugin_name) - - self.initialize_parameters(settings.commands.callbacks.get_callbacks(self._plugin_name)) - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: Plugin '{self._plugin_name}' ready.") - - @command( - parameters=ParameterDefinitions.Echo.get_definitions(), - ) - def echo(self, data: "Command") -> None: - # Example: - # !echo "hello, channel!" -> Without a target parameter, by default echoes to the bot channel. - # !echo.delay=5 "hello, after delay!" -> Specify a delay of 5 seconds before sending the message to the bot channel. - # - # Display types: - # !echo.me "hello, me!" -> Echoes the message to the sender of the command. - # !echo.mychannel "hello, my channel!" -> Echoes the message to the channel the sender is in. - # !echo.channel=custom_channel "hello, specified channel!" -> Echoes the message to the specified channel. - # !echo.channels=channel1,channel2 "hello, specified channels!" -> Echoes the message to the specified channels. - # !echo.user=username "hello, specified user!" -> Echoes the message to the specified user. - # !echo.users=username1,username2 "hello, specified users!" -> Echoes the message to the specified users. - # !echo.broadcast "hello, everyone!" -> Echoes the message to all channels in the server. - _parameters = self.verify_parameters(self.echo.__name__, data) - if _parameters is None: - return - - _delay = _parameters.get(ParameterDefinitions.Echo.DELAY, None) - if _delay and _delay > 0: - time.sleep(_delay) - - if not any(x in self.command_parameters[self.echo.__name__] for x in _parameters.keys()): - if not data.message.strip(): - GUIFramework.gui( - f"'{data.command}' command error: a target channel must be specified when no parameters are used.", - target_users=mumble_utils.get_user_by_id(data.actor), - user_id=data.actor, - ) - return - _channel = mumble_utils.get_my_channel() - GUIFramework.gui( - data.message, - target_channels=_channel, - user_id=data.actor, - ) - - @command( - parameters=ParameterDefinitions.Move.get_definitions(), - exclusive_parameters=ParameterDefinitions.Move.get_definitions(), - ) - def move(self, data: "Command") -> None: - # Example: - # !move "channel name" -> Without a target parameter, by default moves the bot to the specified channel. - # !move.channel=channel_name -> Moves the bot to the specified channel. - # !move.to_user=user_name -> Moves the bot to the specified users channel. - # !move.to_me -> Moves the bot to the command senders channel. - _parameters = self.verify_parameters(self.move.__name__, data) - if _parameters is None: - return - - if not any(x in self.command_parameters[self.move.__name__] for x in _parameters.keys()): - _target_channel = data.message.strip().replace("_", " ") - if not _target_channel: - GUIFramework.gui( - f"'{data._command}' command error: a target channel must be specified when no parameters are used.", - target_users=mumble_utils.get_user_by_id(data.actor), - user_id=data.actor, - ) - return - _search_channel: Optional["Channel"] = mumble_utils.get_channel_by_name(_target_channel) - if _search_channel is None: - GUIFramework.gui( - f"'{data._command}' command error: cannot find specified channel '{_target_channel}'.", - target_users=mumble_utils.get_user_by_id(data.actor), - user_id=data.actor, - ) - return - _search_channel.move_in() - - @command( - parameters=ParameterDefinitions.Themes.get_definitions(), - exclusive_parameters=ParameterDefinitions.Themes.get_definitions(), - parameters_required=True, - ) - def themes(self, data: "Command") -> None: - # Example: - # !themes.list -> Displays a list of the available gui themes. - # !themes.switch "theme_name" -> Switches the gui theme to the selected theme. - # !themes.new "theme_name" -> Creates a new theme from a template theme. - # !themes.delete "theme_name" -> Deletes the specified theme, and falls back to a default theme if it was in use. - # !themes.show "theme_name" -> Shows the config data for the specified theme. - # !themes.update="theme_name" item1=value1... -> Updates a specified theme with the specified new values. - # !themes.reset "theme_name" -> Resets the specified theme back to default options. - # !themes.resetall -> Resets all the themes back to the default set of themes and options. - - _parameters = self.verify_parameters(self.themes.__name__, data) - if _parameters is None: - return - - def _parameter_themes_list(self, data: "Command", parameter: str) -> None: - _theme_list = theme_utils.list_themes() - if not _theme_list: - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: the themes list could not be retrieved.") - return - _msgs: List[str] = ["Available themes: "] - for idx, theme in enumerate(_theme_list): - _msgs.append(f"{idx+1}) {theme}") - GUIFramework.gui( - text=_msgs, - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_themes_switch(self, data: "Command", parameter: str) -> None: - _new_theme = data.message.strip() - _available_themes = theme_utils.list_themes() - if not _new_theme or _new_theme not in _available_themes: - _msgs: List[str] = [ - "Invalid theme. ", - "Here are the available themes to choose from: ", - ] - for idx, theme in enumerate(theme_utils.list_themes()): - _msgs.append(f"{idx+1}) {theme}") - GUIFramework.gui( - text=_msgs, - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _switched_themes = theme_utils.switch_themes(_new_theme) - if not _switched_themes: - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: the theme could not be switched.") - return - GUIFramework.gui( - text=f"Switched theme to: {data.message.strip()}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_themes_new(self, data: "Command", parameter: str) -> None: - _new_theme = data.message.strip().replace(" ", "_") - if not theme_utils.new_theme(_new_theme): - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: failed to create theme.") - GUIFramework.gui( - text=f"Failed to create new theme: {_new_theme}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - text=f"Created new theme: {_new_theme}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_themes_reset(self, data: "Command", parameter: str) -> None: - _selected_theme = data.message.strip().replace(" ", "_") - if not theme_utils.reset_theme(_selected_theme): - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: failed to reset theme.") - GUIFramework.gui( - text=f"Failed to reset theme: {_selected_theme}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - text=f"Resetted selected theme: {_selected_theme}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_themes_resetall(self, data: "Command", parameter: str) -> None: - if not theme_utils.reset_all_themes(): - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: failed to reset all themes.") - GUIFramework.gui( - text="Failed to reset all themes.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - text="Resetted all themes to defaults.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_themes_delete(self, data: "Command", parameter: str) -> None: - _delete_theme = data.message.strip().replace(" ", "_") - if not theme_utils.delete_theme(_delete_theme): - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: failed to delete theme.") - GUIFramework.gui( - text=f"Failed to delete theme: {_delete_theme}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - text=f"Deleted theme: {_delete_theme}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_themes_update(self, data: "Command", parameter: str) -> None: - parameter_split = parameter.split("=", 1) - if not len(parameter_split) == 2: - GUIFramework.gui( - f"'{data._command}' command error: a user name was not provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - - _theme_name: str = parameter_split[1].strip().replace("_", " ") - if not _theme_name: - GUIFramework.gui( - f"'{data._command}' command error: an invalid theme name was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - - _update_items = data.message.strip().replace(" ", "_") - if not _update_items: - GUIFramework.gui( - f"'{data._command}' command error: invalid update values provided. Update values must follow the format: 'item1=value1, item2=value2, ...'", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _update_items_split = _update_items.split(",") - _update_items_pairs = {} - for item in _update_items_split: - _pairs_split = item.split("=", 1) - if len(_pairs_split) != 2: - GUIFramework.gui( - f"'{data._command}' command error: invalid update values provided. Update values must follow the format: 'item1=value1, item2=value2, ...'", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _key, _value = _pairs_split[0], _pairs_split[1] - _update_items_pairs[_key] = _value - - _theme_updated: bool = theme_utils.update_theme(_theme_name, _update_items_pairs) - if not _theme_updated: - GUIFramework.gui( - f"'{data._command}' command error: unable to update theme. Ensure the theme exists and valid values are provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - f"Updated theme: {_theme_name}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_themes_show(self, data: "Command", parameter: str) -> None: - _theme_name = data.message.strip().replace(" ", "_") - _selected_theme = theme_utils._get_theme(_theme_name) - if not _selected_theme: - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: failed to show theme.") - GUIFramework.gui( - text=f"Failed to show theme: {_theme_name}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _msgs = [f"[{_theme_name}]"] - for key, value in _selected_theme.items(): - _msgs.append(f"{key}={value}") - GUIFramework.gui( - text=_msgs, - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_move_to_me(self, data: "Command", parameter: str) -> Optional["User"]: - _me = mumble_utils.get_user_by_id(data.actor) - if not _me: - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: the user could not be retrieved.") - return - _my_channel: Optional["Channel"] = mumble_utils.get_channel_by_user(_me) - if not _my_channel: - GUIFramework.gui( - f"'{data._command}' command error: unable to find the channel '{_me['name']}' belongs to.", - target_users=mumble_utils.get_user_by_id(data.actor), - user_id=data.actor, - ) - return - _my_channel.move_in() - return - - def _parameter_move_to_user(self, data: "Command", parameter: str) -> Optional["User"]: - _user = self._get_user(data, parameter) - if not _user: - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: the user could not be retrieved.") - return - _user_channel: Optional["Channel"] = mumble_utils.get_channel_by_user(_user) - if not _user_channel: - GUIFramework.gui( - f"'{data._command}' command error: unable to find the channel '{_user['name']}' belongs to.", - target_users=mumble_utils.get_user_by_id(data.actor), - user_id=data.actor, - ) - return - _user_channel.move_in() - - def _parameter_move_to_channel(self, data: "Command", parameter: str) -> Optional["Channel"]: - _channel = self._get_channel(data, parameter) - if not _channel: - return - _channel.move_in() - - def _parameter_echo_me(self, data: "Command", parameter: str) -> None: - _me = mumble_utils.get_user_by_id(data.actor) - if not _me: - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: the user was not found with the provided id.") - return - GUIFramework.gui( - data.message, - target_users=_me, - user_id=data.actor, - ) - - def _parameter_echo_broadcast(self, data: "Command", parameter: str) -> None: - _all_channels: List["Channel"] = mumble_utils.get_all_channels() - if not _all_channels: - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: the channel tree could not be retrieved.") - return - GUIFramework.gui( - data.message, - target_channels=_all_channels, - user_id=data.actor, - ) - - def _parameter_echo_user(self, data: "Command", parameter: str) -> None: - _user = self._get_user(data, parameter) - if not _user: - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: the user could not be retrieved.") - return - GUIFramework.gui( - data.message, - target_users=_user, - user_id=data.actor, - ) - - def _parameter_echo_users(self, data: "Command", parameter: str) -> None: - parameter_split = parameter.split("=", 1) - if len(parameter_split) == 2: - user_names = [user.strip().replace("_", " ") for user in parameter_split[1].split(",")] - if not user_names: - GUIFramework.gui( - f"'{data._command}' command warning: no users were provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _found_users: List["User"] = [] - for _user in user_names: - if not _user: - continue - _search_user: Optional["User"] = mumble_utils.get_user_by_name(_user) - if _search_user is not None: - _found_users.append(_search_user) - continue - GUIFramework.gui( - f"'{data._command}' command warning: cannot find specified user '{_user}'.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - GUIFramework.gui( - data.message, - target_users=_found_users, - user_id=data.actor, - ) - return - GUIFramework.gui( - f"'{data._command}' command warning: an invalid list of user names was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_echo_channel(self, data: "Command", parameter: str) -> None: - _channel = self._get_channel(data, parameter) - if not _channel: - logger.error(f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: '{data.command}' command error: the channel could not be retrieved.") - return - GUIFramework.gui( - data.message, - target_channels=_channel, - user_id=data.actor, - ) - - def _parameter_echo_mychannel(self, data: "Command", parameter: str) -> None: - _channel_user = mumble_utils.get_user_by_id(data.actor) - if not _channel_user: - return - _channel_obj = mumble_utils.get_channel_by_user(_channel_user) - if _channel_obj: - GUIFramework.gui( - data.message, - target_channels=_channel_obj, - user_id=data.actor, - ) - - def _parameter_echo_channels(self, data: "Command", parameter: str) -> None: - parameter_split = parameter.split("=", 1) - if len(parameter_split) == 2: - channel_names = [channel.strip().replace("_", " ") for channel in parameter_split[1].split(",")] - if not channel_names: - GUIFramework.gui( - f"'{data._command}' command warning: no channel names were provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _found_channels: List["Channel"] = [] - for _channel in channel_names: - if not _channel: - continue - _search_channel: Optional["Channel"] = mumble_utils.get_channel_by_name(_channel) - if _search_channel is not None: - _found_channels.append(_search_channel) - continue - GUIFramework.gui( - f"'{data._command}' command warning: cannot find specified channel '{_channel}'.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - GUIFramework.gui( - data.message, - target_channels=_found_channels, - user_id=data.actor, - ) - return - GUIFramework.gui( - f"'{data._command}' command warning: an invalid list of channel names was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_echo_delay(self, data: "Command", parameter: str) -> Optional[int]: - parameter_split = parameter.split("=", 1) - if len(parameter_split) == 2: - try: - _delay = int(parameter_split[1]) - if _delay < 0: - raise TypeError() - return _delay - except ValueError: - GUIFramework.gui( - f"'{data._command}' command warning: the 'delay' parameter must be a non-negative number.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - except TypeError: - GUIFramework.gui( - f"'{data._command}' command warning: the 'delay' parameter must be a non-negative number.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - f"'{data._command}' command warning: an invalid 'delay' value was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _get_user(self, data: "Command", parameter: str) -> Optional["User"]: - parameter_split = parameter.split("=", 1) - if len(parameter_split) == 2: - _search_term: str = parameter_split[1].strip().replace("_", " ") - if not _search_term: - GUIFramework.gui( - f"'{data._command}' command error: an invalid user name was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _search_user: Optional["User"] = mumble_utils.get_user_by_name(_search_term) - if _search_user is not None: - return _search_user - GUIFramework.gui( - f"'{data._command}' command error: cannot find specified user '{_search_term}'.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - f"'{data._command}' command error: a user name was not provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _get_channel(self, data: "Command", parameter: str) -> Optional["Channel"]: - parameter_split = parameter.split("=", 1) - if len(parameter_split) == 2: - _search_term: str = parameter_split[1].strip().replace("_", " ") - if not _search_term: - GUIFramework.gui( - f"'{data._command}' command error: an invalid channel name was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _search_channel: Optional["Channel"] = mumble_utils.get_channel_by_name(_search_term) - if _search_channel is not None: - return _search_channel - GUIFramework.gui( - f"'{data._command}' command error: cannot find specified channel '{_search_term}'.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - f"'{data._command}' command error: a channel name was not provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) diff --git a/src/plugins/builtin_core/resources/gui_custom_theme_template.toml b/src/plugins/builtin_core/resources/gui_custom_theme_template.toml deleted file mode 100644 index a3f49d6..0000000 --- a/src/plugins/builtin_core/resources/gui_custom_theme_template.toml +++ /dev/null @@ -1,15 +0,0 @@ -# -# This is a template file. -# Please do not modify this! -# - -[template] -text_type = "header" -text_color = "snow" -text_align = "center" - -table_align = "left" -table_bg_color = "black" - -row_align = "left" -row_bg_color = "black" \ No newline at end of file diff --git a/src/plugins/builtin_core/resources/gui_themes_template.toml b/src/plugins/builtin_core/resources/gui_themes_template.toml deleted file mode 100644 index a4288c7..0000000 --- a/src/plugins/builtin_core/resources/gui_themes_template.toml +++ /dev/null @@ -1,42 +0,0 @@ -# -# This is a template file. -# Please do not modify this! -# - -[light] -text_type = "header" -text_color = "black" -text_align = "center" - -table_align = "left" -table_bg_color = "blanchedalmond" - -row_align = "left" -row_bg_color = "blanchedalmond" - - -[dark] -text_type = "header" -text_color = "lightsteelblue" -text_align = "center" - -table_align = "left" -table_bg_color = "darkslategrey" - -row_align = "left" -row_bg_color = "darkslategrey" - - -[highcontrast] -text_type = "header" -text_color = "yellow" -text_align = "center" - -table_align = "left" -table_bg_color = "black" - -row_align = "left" -row_bg_color = "black" - -# Create custom themes here: -# Example: [custom1], [custom2], ... diff --git a/src/plugins/builtin_core/tests/test_builtin_core.py b/src/plugins/builtin_core/tests/test_builtin_core.py deleted file mode 100644 index 4dd07b6..0000000 --- a/src/plugins/builtin_core/tests/test_builtin_core.py +++ /dev/null @@ -1,3 +0,0 @@ -class TestPlugin_BuiltIn_Core: - def test_always_passes(self): - assert True diff --git a/src/plugins/builtin_core/utility/constants.py b/src/plugins/builtin_core/utility/constants.py deleted file mode 100644 index 62f718f..0000000 --- a/src/plugins/builtin_core/utility/constants.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import List - - -class ParameterDefinitions: - class Echo: - DELAY: str = "delay" - BROADCAST: str = "broadcast" - MYCHANNEL: str = "mychannel" - CHANNEL: str = "channel" - CHANNELS: str = "channels" - USER: str = "user" - USERS: str = "users" - ME: str = "me" - - @staticmethod - def get_definitions() -> List[str]: - return [ - ParameterDefinitions.Echo.DELAY, - ParameterDefinitions.Echo.BROADCAST, - ParameterDefinitions.Echo.MYCHANNEL, - ParameterDefinitions.Echo.CHANNEL, - ParameterDefinitions.Echo.CHANNELS, - ParameterDefinitions.Echo.USER, - ParameterDefinitions.Echo.USERS, - ParameterDefinitions.Echo.ME, - ] - - class Move: - TO_CHANNEL: str = "to_channel" - TO_USER: str = "to_user" - TO_ME: str = "to_me" - - @staticmethod - def get_definitions() -> List[str]: - return [ - ParameterDefinitions.Move.TO_CHANNEL, - ParameterDefinitions.Move.TO_USER, - ParameterDefinitions.Move.TO_ME, - ] - - class Themes: - SWITCH: str = "switch" - LIST: str = "list" - NEW: str = "new" - DELETE: str = "delete" - UPDATE: str = "update" - SHOW: str = "show" - RESET: str = "reset" - RESETALL: str = "resetall" - - @staticmethod - def get_definitions() -> List[str]: - return [ - ParameterDefinitions.Themes.SWITCH, - ParameterDefinitions.Themes.LIST, - ParameterDefinitions.Themes.NEW, - ParameterDefinitions.Themes.DELETE, - ParameterDefinitions.Themes.UPDATE, - ParameterDefinitions.Themes.SHOW, - ParameterDefinitions.Themes.RESET, - ParameterDefinitions.Themes.RESETALL, - ] diff --git a/src/plugins/builtin_core/utility/theme_utils.py b/src/plugins/builtin_core/utility/theme_utils.py deleted file mode 100644 index 37e1fc9..0000000 --- a/src/plugins/builtin_core/utility/theme_utils.py +++ /dev/null @@ -1,190 +0,0 @@ -import logging -import pathlib -from typing import List, Optional, Dict, Any -from src.settings import settings -from src.exceptions import PluginError -from src.constants import MumimoCfgFields -from src.config import Config - -logger = logging.getLogger(__name__) - - -def list_themes() -> List[str]: - _themes = settings.configs.get_gui_themes() - if _themes: - return list(_themes.keys()) - return [] - - -def switch_themes(theme: str) -> bool: - _themes: List[str] = list_themes() - if theme not in _themes: - return False - - _config: Optional["Config"] = settings.configs.get_mumimo_config() - if not _config: - raise PluginError("Unable to switch themes: mumimo config could not be retrieved from settings.", logger=logger) - - _success_switch: bool = _config.set(MumimoCfgFields.SETTINGS.GUI.SELECTED_THEME, theme) - _saved_data: str = _config.save( - modified_only=True, - modified_field_name=MumimoCfgFields.SETTINGS.GUI.SELECTED_THEME, - ) - return _success_switch and _saved_data is not None - - -def delete_theme(theme: str) -> bool: - theme = theme.strip().replace(" ", "_") - - if _get_theme(theme) is None: - logger.error(f"Failed to delete gui theme: theme '{theme}' does not exist.") - return False - - _themes: Optional["Config"] = settings.configs.get_gui_themes() - if not _themes: - logger.error("Failed to delete gui theme: the gui themes could not be retrieved from settings.") - return False - - del _themes[theme] - _themes.save() - logger.debug(f"Deleted gui theme: {theme}.") - return True - - -def new_theme(theme: str) -> bool: - _template_theme = _get_custom_theme_template() - _themes: Optional["Config"] = settings.configs.get_gui_themes() - if not _themes: - logger.error("Failed to create new gui theme: the gui themes could not be retrieved from settings.") - return False - - theme = theme.strip().replace(" ", "_") - if _themes.get(theme, None): - f"Failed to create new gui theme: theme '{theme}' already exists." - return False - - # Create new theme from default template. - _new_theme: Dict[str, Any] = {theme: _template_theme} - _themes.update(_new_theme) - # Save new theme to the toml file. - _themes.save() - logger.debug(f"Created new gui theme from template: {theme}.") - - return True - - -def reset_theme(theme: str) -> bool: - _template_theme = _get_custom_theme_template() - _themes: Optional["Config"] = settings.configs.get_gui_themes() - if not _themes: - logger.error("Failed to reset gui theme: the gui themes could not be retrieved from settings.") - return False - - theme = theme.strip().replace(" ", "_") - _selected_theme = _themes.get(theme, None) - if not _selected_theme: - logger.error(f"Failed to reset gui theme: theme '{theme}' does not exist.") - return False - _selected_theme = _template_theme - _themes.update({theme: _selected_theme}) - - _themes.save() - logger.debug(f"Reset gui theme from template: {theme}.") - - return True - - -def reset_all_themes() -> bool: - _template_default_themes = _get_default_themes_template() - _themes: Optional["Config"] = settings.configs.get_gui_themes() - if not _themes: - logger.error("Failed to reset gui themes: the gui themes could not be retrieved from settings.") - return False - _themes.clear() - _themes.update(_template_default_themes) - - _themes.save() - logger.debug("Reset all gui themes from default themes template.") - - return True - - -def update_theme(theme: str, items: Dict[str, Any]) -> bool: - _themes: Optional["Config"] = settings.configs.get_gui_themes() - if not _themes: - logger.error("Failed to update gui theme: the gui themes could not be retrieved from settings.") - return False - - theme = theme.strip().replace(" ", "_") - _selected_theme = _themes.get(theme, None) - if not _selected_theme: - logger.error(f"Failed to update gui theme: theme '{theme}' does not exist.") - return False - - for key, value in items.items(): - if key in _selected_theme.keys(): - _selected_theme[key] = value - _themes.update({theme: _selected_theme}) - - _themes.save() - logger.debug(f"Updated gui theme '{theme}' with values [{', '.join('{}={}'.format(*x) for x in items.items())}]") - - return True - - -def _get_custom_theme_template() -> "Config": - _config: Optional["Config"] = settings.configs.get_mumimo_config() - if not _config: - raise PluginError("Unable to get template theme: mumimo config could not be retrieved from settings.", logger=logger) - - _plugin_path = _config.get(MumimoCfgFields.SETTINGS.PLUGINS.PLUGINS_PATH, None) - if not _plugin_path: - raise PluginError("Unable to get template theme: mumimo config does not have a defined plugin path.") - - _theme_template_path = pathlib.Path.cwd() / _plugin_path / "builtin_core/resources/gui_custom_theme_template.toml" - _theme_cfg: "Config" = Config(_theme_template_path) - if _theme_cfg is None: - raise PluginError(f"Unable to get template theme: gui custom theme template file is missing. Expected path: {_theme_template_path}") - _theme_cfg.read() - - _template = _theme_cfg.get("template", None) - if _template is None: - raise PluginError("Unable to get template theme: 'template' section is missing in gui custom theme template file.") - - return _template - - -def _get_default_themes_template() -> "Config": - _config: Optional["Config"] = settings.configs.get_mumimo_config() - if not _config: - raise PluginError("Unable to get default template themes: mumimo config could not be retrieved from settings.", logger=logger) - - _plugin_path = _config.get(MumimoCfgFields.SETTINGS.PLUGINS.PLUGINS_PATH, None) - if not _plugin_path: - raise PluginError("Unable to get default template themes: mumimo config does not have a defined plugin path.") - - _theme_template_path = pathlib.Path.cwd() / _plugin_path / "builtin_core/resources/gui_themes_template.toml" - _themes_template: "Config" = Config(_theme_template_path) - if _themes_template is None: - raise PluginError(f"Unable to get default template themes: default template themes file is missing. Expected path: {_theme_template_path}") - _themes_template.read() - - return _themes_template - - -def _get_theme(theme: str) -> Optional["Config"]: - if not theme: - logger.error("Failed to get gui theme: no theme name provided.") - return None - - _themes: Optional["Config"] = settings.configs.get_gui_themes() - if not _themes: - logger.error("Failed to get gui theme: the gui themes could not be retrieved from settings.") - return None - - _theme = _themes.get(theme, None) - if not _theme: - logger.error(f"Failed to get gui theme: theme '{theme}' does not exist.") - return None - - return _theme diff --git a/src/plugins/builtin_image/help_template.toml b/src/plugins/builtin_image/help_template.toml deleted file mode 100644 index db2b77b..0000000 --- a/src/plugins/builtin_image/help_template.toml +++ /dev/null @@ -1,15 +0,0 @@ -[plugin.help.page.1] -# Command help text format: -# - commands_help_text = [ -# ["command_name", "command_parameters", "command_message", "command_description_text"], -# ... -# ] -# As shown in the example above, each item must contain 4 elements: -# 1. Index 0: The name of the command (same as the method name in the plugin.py file) -# 2. Index 1: The parameters that are supported by the command. -# 3. Index 2: The message accompanying the command input, if any. -# 4. Index 3: The description/help text to help users understand what the command does. -# Default values: Too long to list here, please check the wiki. -commands_help_text = [ - ["echo", " | | ", "", "Repeats the provided message with the selected output method. Using the 'echo' command with a 'channel' parameter for example repeats the command to all users in the bot's channel."] -] diff --git a/src/plugins/builtin_image/metadata_template.toml b/src/plugins/builtin_image/metadata_template.toml deleted file mode 100644 index 5b3d52c..0000000 --- a/src/plugins/builtin_image/metadata_template.toml +++ /dev/null @@ -1,12 +0,0 @@ -[plugin] -enabled = true - -[plugin.about] -name = "Image" -version = "0.0.1" -description = "Provides image processing functionality to Mumimo." - -[plugin.commands] -disable_commands = [] -disable_parameters = [] -default_permission_groups = ["guest", "regular", "admin"] diff --git a/src/plugins/builtin_image/plugin.py b/src/plugins/builtin_image/plugin.py deleted file mode 100644 index f3b5039..0000000 --- a/src/plugins/builtin_image/plugin.py +++ /dev/null @@ -1,191 +0,0 @@ -import logging -from typing import TYPE_CHECKING, List, Optional - -from pymumble_py3.channels import Channel -from pymumble_py3.users import User - -from src.constants import LogOutputIdentifiers -from src.lib.frameworks.plugins.plugin import PluginBase -from src.settings import settings -from src.utils import mumble_utils -from src.lib.frameworks.gui.gui import GUIFramework -from .utility.constants import ParameterDefinitions - -logger = logging.getLogger(__name__) - - -if TYPE_CHECKING: - from src.lib.command import Command - - -class Plugin(PluginBase): - command = PluginBase.makeCommandRegister() - - def __init__(self, plugin_name: str) -> None: - self._plugin_name = plugin_name - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: Initializing plugin: '{self._plugin_name}'") - super().__init__(self._plugin_name) - - self.initialize_parameters(settings.commands.callbacks.get_callbacks(self._plugin_name)) - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: Plugin '{self._plugin_name}' ready.") - - @command( - parameters=ParameterDefinitions.Image.get_definitions(), - ) - def image(self, data: "Command") -> None: - # Example: - # !image "image.jpg" -> Without a target parameter, by default posts the specified image in the bot channel. - # - # Filter types: - # !image.grayscale "image.jpg" -> Posts the specified image with a grayscale filter applied. - # !image.invert "image.jpg" -> Posts the specified image with an invert filter applied. - # !image.rotate=90 "image.jpg" -> Posts the specified image with a rotate filter applied with the specified rotation degrees. - # !image.compress=50 "image.jpg" -> Posts the specified image with a compress filter applied with the specified compression ratio. - # !image.brightness=50 "image.jpg" -> Posts the specified image with a brightness filter applied with the specified brightness amount. - # !image.contrast=50 "image.jpg" -> Posts the specified image with a brightness filter applied with the specified contrast amount. - # - # Display types: - # !image.channel="channel_name" "image.jpg" -> Posts the specified image in the specified channel. - # !image.channels="channel1,channel2" "image.jpg" -> Posts the specified image in the specified channels. - # !image.user="user_name" "image.jpg" -> Posts the specified image to the specified user. - # !image.users="user1,user2" "image.jpg" -> Posts the specified image to the specified users. - # !image.broadcast "image.jpg" -> Posts the specified image to all channels. - - _parameters = self.verify_parameters(self.image.__name__, data) - if _parameters is None: - return - - # Filter parameters: - def _parameter_image_grayscale(self, data: "Command", parameter: str) -> None: - pass - - def _parameter_image_invert(self, data: "Command", parameter: str) -> None: - pass - - def _parameter_image_rotate(self, data: "Command", parameter: str) -> None: - pass - - def _parameter_image_compress(self, data: "Command", parameter: str) -> None: - pass - - def _parameter_image_brightness(self, data: "Command", parameter: str) -> None: - pass - - def _parameter_image_contrast(self, data: "Command", parameter: str) -> None: - pass - - # Display type parameters: - def _parameter_image_me(self, data: "Command", parameter: str) -> bool: - return True - - def _parameter_image_broadcast(self, data: "Command", parameter: str) -> bool: - return True - - def _parameter_image_user(self, data: "Command", parameter: str) -> Optional["User"]: - return self._get_user(data, parameter) - - def _parameter_image_users(self, data: "Command", parameter: str) -> Optional[List["User"]]: - parameter_split = parameter.split("=", 1) - if len(parameter_split) == 2: - user_names = [user.strip().replace("_", " ") for user in parameter_split[1].split(",")] - if not user_names: - GUIFramework.gui( - f"'{data._command}' command warning: no users were provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _found_users: List["User"] = [] - for _user in user_names: - if not _user: - continue - _search_user: Optional["User"] = mumble_utils.get_user_by_name(_user) - if _search_user is not None: - _found_users.append(_search_user) - continue - GUIFramework.gui( - f"'{data._command}' command warning: cannot find specified user '{_user}'.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return _found_users - GUIFramework.gui( - f"'{data._command}' command warning: an invalid list of user names was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_image_channel(self, data: "Command", parameter: str) -> Optional["Channel"]: - return self._get_channel(data, parameter) - - def _parameter_image_channels(self, data: "Command", parameter: str) -> Optional[List["Channel"]]: - parameter_split = parameter.split("=", 1) - if len(parameter_split) == 2: - channel_names = [channel.strip().replace("_", " ") for channel in parameter_split[1].split(",")] - if not channel_names: - GUIFramework.gui( - f"'{data._command}' command warning: no channel names were provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _found_channels: List["Channel"] = [] - for _channel in channel_names: - if not _channel: - continue - _search_channel: Optional["Channel"] = mumble_utils.get_channel_by_name(_channel) - if _search_channel is not None: - _found_channels.append(_search_channel) - continue - GUIFramework.gui( - f"'{data._command}' command warning: cannot find specified channel '{_channel}'.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return _found_channels - GUIFramework.gui( - f"'{data._command}' command warning: an invalid list of channel names was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - # Utility methods: - def _get_user(self, data: "Command", parameter: str): - parameter_split = parameter.split("=", 1) - if len(parameter_split) == 2: - _search_term: str = parameter_split[1].strip().replace("_", " ") - if not _search_term: - GUIFramework.gui( - f"'{data._command}' command warning: an invalid user name was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _search_user: Optional["User"] = mumble_utils.get_user_by_name(_search_term) - if _search_user is not None: - return _search_user - GUIFramework.gui( - f"'{data._command}' command warning: cannot find specified user '{_search_term}'.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - f"'{data._command}' command warning: a user name was not provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _get_channel(self, data: "Command", parameter: str): - parameter_split = parameter.split("=", 1) - if len(parameter_split) == 2: - _search_term: str = parameter_split[1].strip().replace("_", " ") - if not _search_term: - GUIFramework.gui( - f"'{data._command}' command warning: an invalid channel name was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _search_channel: Optional["Channel"] = mumble_utils.get_channel_by_name(_search_term) - if _search_channel is not None: - return _search_channel - GUIFramework.gui( - f"'{data._command}' command warning: cannot find specified channel '{_search_term}'.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - f"'{data._command}' command warning: a channel name was not provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) diff --git a/src/plugins/builtin_image/tests/test_builtin_image.py b/src/plugins/builtin_image/tests/test_builtin_image.py deleted file mode 100644 index e97a3d3..0000000 --- a/src/plugins/builtin_image/tests/test_builtin_image.py +++ /dev/null @@ -1,3 +0,0 @@ -class TestPlugin_BuiltIn_Image: - def test_always_passes(self): - assert True diff --git a/src/plugins/builtin_image/utility/constants.py b/src/plugins/builtin_image/utility/constants.py deleted file mode 100644 index 2a20680..0000000 --- a/src/plugins/builtin_image/utility/constants.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import List - - -class ParameterDefinitions: - class Image: - GRAYSCALE: str = "grayscale" - INVERT: str = "invert" - COMPRESS: str = "compress" - COLORSHIFT: str = "colorshift" - BLUR: str = "blur" - BRIGHTNESS: str = "brightness" - CONTRAST: str = "contrast" - - BROADCAST: str = "broadcast" - CHANNEL: str = "channel" - CHANNELS: str = "channels" - USER: str = "user" - USERS: str = "users" - ME: str = "me" - - @staticmethod - def get_definitions() -> List[str]: - return [ - ParameterDefinitions.Image.GRAYSCALE, - ParameterDefinitions.Image.INVERT, - ParameterDefinitions.Image.COMPRESS, - ParameterDefinitions.Image.COLORSHIFT, - ParameterDefinitions.Image.BLUR, - ParameterDefinitions.Image.BRIGHTNESS, - ParameterDefinitions.Image.CONTRAST, - ParameterDefinitions.Image.BROADCAST, - ParameterDefinitions.Image.CHANNEL, - ParameterDefinitions.Image.CHANNELS, - ParameterDefinitions.Image.USER, - ParameterDefinitions.Image.USERS, - ParameterDefinitions.Image.ME, - ] diff --git a/src/plugins/builtin_plugin_tools/help_template.toml b/src/plugins/builtin_plugin_tools/help_template.toml deleted file mode 100644 index c4c0bc7..0000000 --- a/src/plugins/builtin_plugin_tools/help_template.toml +++ /dev/null @@ -1,15 +0,0 @@ -[plugin.help.page.1] -# Command help text format: -# - commands_help_text = [ -# ["command_name", "command_parameters", "command_message", "command_description_text"], -# ... -# ] -# As shown in the example above, each item must contain 4 elements: -# 1. Index 0: The name of the command (same as the method name in the plugin.py file) -# 2. Index 1: The parameters that are supported by the command. -# 3. Index 2: The message accompanying the command input, if any. -# 4. Index 3: The description/help text to help users understand what the command does. -# Default values: Too long to list here, please check the wiki. -commands_help_text = [ - ["plugin", "", "", "Displays a message containing the active/inactive status of a registered plugin."] -] diff --git a/src/plugins/builtin_plugin_tools/metadata_template.toml b/src/plugins/builtin_plugin_tools/metadata_template.toml deleted file mode 100644 index 9754c7f..0000000 --- a/src/plugins/builtin_plugin_tools/metadata_template.toml +++ /dev/null @@ -1,12 +0,0 @@ -[plugin] -enabled = true - -[plugin.about] -name = "Plugin Tools" -version = "0.0.1" -description = "Provides functionality to manage plugins." - -[plugin.commands] -disable_commands = [] -disable_parameters = [] -default_permission_groups = ["guest", "regular", "admin"] diff --git a/src/plugins/builtin_plugin_tools/plugin.py b/src/plugins/builtin_plugin_tools/plugin.py deleted file mode 100644 index a620ad0..0000000 --- a/src/plugins/builtin_plugin_tools/plugin.py +++ /dev/null @@ -1,131 +0,0 @@ -import logging -from typing import TYPE_CHECKING, Optional - -from src.constants import LogOutputIdentifiers -from src.lib.frameworks.plugins.plugin import PluginBase -from src.settings import settings -from src.utils import mumble_utils -from src.lib.frameworks.gui.gui import GUIFramework - -from .utility.constants import ParameterDefinitions - -logger = logging.getLogger(__name__) - - -if TYPE_CHECKING: - from src.lib.command import Command - - -class Plugin(PluginBase): - command = PluginBase.makeCommandRegister() - - def __init__(self, plugin_name: str) -> None: - self._plugin_name = plugin_name - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: Initializing plugin: '{self._plugin_name}'") - super().__init__(self._plugin_name) - - self.initialize_parameters(settings.commands.callbacks.get_callbacks(self._plugin_name)) - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: Plugin '{self._plugin_name}' ready.") - - @command( - parameters=ParameterDefinitions.Plugin.get_definitions(), - parameters_required=True, - exclusive_parameters=ParameterDefinitions.Plugin.get_definitions(), - ) - def plugin(self, data: "Command"): - # Example: - # !plugin.active "builtin_core" -> Displays a message showing the status of the specified plugin. - # !plugin.stop "plugin1" -> Stops the specified plugin if it is running. - # !plugin.start "plugin1" -> Starts the specified plugin if it is not running. - # !plugin.restart "plugin1" -> Restarts the specified plugin. - _parameters = self.verify_parameters(self.plugin.__name__, data) - if _parameters is None: - return - - def _get_plugin(self, data: "Command") -> Optional["PluginBase"]: - _plugin_name: str = data.message.strip() - if not _plugin_name: - GUIFramework.gui( - f"'{data._command}' command warning: an invalid plugin name was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _plugin: Optional["PluginBase"] = settings.plugins.get_registered_plugin(_plugin_name) - if _plugin is None: - GUIFramework.gui( - f"'{data._command}' command error: the plugin '{_plugin_name}' could not be found.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - return _plugin - - def _parameter_plugin_stop(self, data: "Command", parameter: str) -> None: - _plugin = self._get_plugin(data) - if not _plugin: - return - - _status, _message = _plugin.stop() - if not _status: - GUIFramework.gui( - f"'{data._command}' command error: {_message}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - _message, - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_plugin_start(self, data: "Command", parameter: str) -> None: - _plugin = self._get_plugin(data) - if not _plugin: - return - - _status, _message = _plugin.start() - if not _status: - GUIFramework.gui( - f"'{data._command}' command error: {_message}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - _message, - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_plugin_restart(self, data: "Command", parameter: str) -> None: - _plugin = self._get_plugin(data) - if not _plugin: - return - - _status, _message = _plugin.restart() - if not _status: - GUIFramework.gui( - f"'{data._command}' command error: {_message}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - _message, - target_users=mumble_utils.get_user_by_id(data.actor), - ) - - def _parameter_plugin_active(self, data: "Command", parameter: str) -> None: - _registered_plugins = settings.plugins.get_registered_plugins() - if not _registered_plugins: - GUIFramework.gui( - f"'{data._command}' command warning: no active plugins were found.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - _plugin = _registered_plugins.get(data.message.strip()) - if not _plugin: - GUIFramework.gui( - f"'{data._command}' command warning: no plugin name was provided.", - target_users=mumble_utils.get_user_by_id(data.actor), - ) - return - GUIFramework.gui( - f"Plugin '{data.message.strip()}' active: {_plugin.is_running}", - target_users=mumble_utils.get_user_by_id(data.actor), - ) diff --git a/src/plugins/builtin_plugin_tools/tests/test_builtin_plugin_tools.py b/src/plugins/builtin_plugin_tools/tests/test_builtin_plugin_tools.py deleted file mode 100644 index 633b6b0..0000000 --- a/src/plugins/builtin_plugin_tools/tests/test_builtin_plugin_tools.py +++ /dev/null @@ -1,3 +0,0 @@ -class TestPlugin_BuiltIn_PluginTools: - def test_always_passes(self): - assert True diff --git a/src/plugins/builtin_plugin_tools/utility/constants.py b/src/plugins/builtin_plugin_tools/utility/constants.py deleted file mode 100644 index 03b3753..0000000 --- a/src/plugins/builtin_plugin_tools/utility/constants.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import List - - -class ParameterDefinitions: - class Plugin: - ACTIVE: str = "active" - STOP: str = "stop" - START: str = "start" - RESTART: str = "restart" - - @staticmethod - def get_definitions() -> List[str]: - return [ - ParameterDefinitions.Plugin.ACTIVE, - ParameterDefinitions.Plugin.STOP, - ParameterDefinitions.Plugin.START, - ParameterDefinitions.Plugin.RESTART, - ] diff --git a/src/plugins/builtin_soundboard/help_template.toml b/src/plugins/builtin_soundboard/help_template.toml deleted file mode 100644 index db2b77b..0000000 --- a/src/plugins/builtin_soundboard/help_template.toml +++ /dev/null @@ -1,15 +0,0 @@ -[plugin.help.page.1] -# Command help text format: -# - commands_help_text = [ -# ["command_name", "command_parameters", "command_message", "command_description_text"], -# ... -# ] -# As shown in the example above, each item must contain 4 elements: -# 1. Index 0: The name of the command (same as the method name in the plugin.py file) -# 2. Index 1: The parameters that are supported by the command. -# 3. Index 2: The message accompanying the command input, if any. -# 4. Index 3: The description/help text to help users understand what the command does. -# Default values: Too long to list here, please check the wiki. -commands_help_text = [ - ["echo", " | | ", "", "Repeats the provided message with the selected output method. Using the 'echo' command with a 'channel' parameter for example repeats the command to all users in the bot's channel."] -] diff --git a/src/plugins/builtin_soundboard/metadata_template.toml b/src/plugins/builtin_soundboard/metadata_template.toml deleted file mode 100644 index a6465c1..0000000 --- a/src/plugins/builtin_soundboard/metadata_template.toml +++ /dev/null @@ -1,12 +0,0 @@ -[plugin] -enabled = false - -[plugin.about] -name = "Sound Board" -version = "0.0.1" -description = "Provides sound board functionality to Mumimo." - -[plugin.commands] -disable_commands = [] -disable_parameters = [] -default_permission_groups = ["guest", "regular", "admin"] diff --git a/src/plugins/builtin_soundboard/plugin.py b/src/plugins/builtin_soundboard/plugin.py deleted file mode 100644 index 19d5479..0000000 --- a/src/plugins/builtin_soundboard/plugin.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging -from typing import TYPE_CHECKING - -from src.constants import LogOutputIdentifiers -from src.lib.frameworks.plugins.plugin import PluginBase -from src.settings import settings - -logger = logging.getLogger(__name__) - - -if TYPE_CHECKING: - from src.lib.command import Command - - -class Plugin(PluginBase): - command = PluginBase.makeCommandRegister() - - def __init__(self, plugin_name: str) -> None: - self._plugin_name = plugin_name - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: Initializing plugin: '{self._plugin_name}'") - super().__init__(self._plugin_name) - - self.initialize_parameters(settings.commands.callbacks.get_callbacks(self._plugin_name)) - logger.debug(f"[{LogOutputIdentifiers.PLUGINS}]: Plugin '{self._plugin_name}' ready.") - - @command(parameters=[]) - def soundboard(self, data: "Command") -> None: - pass diff --git a/src/plugins/builtin_soundboard/tests/test_builtin_soundboard.py b/src/plugins/builtin_soundboard/tests/test_builtin_soundboard.py deleted file mode 100644 index dd7b688..0000000 --- a/src/plugins/builtin_soundboard/tests/test_builtin_soundboard.py +++ /dev/null @@ -1,3 +0,0 @@ -class TestPlugin_BuiltIn_SoundBoard: - def test_always_passes(self): - assert True diff --git a/src/plugins/builtin_soundboard/utility/constants.py b/src/plugins/builtin_soundboard/utility/constants.py deleted file mode 100644 index efd507b..0000000 --- a/src/plugins/builtin_soundboard/utility/constants.py +++ /dev/null @@ -1,2 +0,0 @@ -class ParameterDefinitions: - pass diff --git a/src/services/cmd_processing_service.py b/src/services/cmd_processing_service.py deleted file mode 100644 index c4ad5d2..0000000 --- a/src/services/cmd_processing_service.py +++ /dev/null @@ -1,434 +0,0 @@ -import logging -import threading -import asyncio -from copy import deepcopy -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional -from thefuzz import process - - -from sqlalchemy import select -from sqlalchemy.orm import selectinload -from ..lib.database.models.user import UserTable -from ..lib.database.models.permission_group import PermissionGroupTable -from ..lib.database.models.command import CommandTable -from ..lib.database.models.alias import AliasTable - -from ..constants import LogCfgFields, MumimoCfgFields, LogOutputIdentifiers -from ..exceptions import ServiceError -from ..lib.frameworks.gui.gui import GUIFramework -from ..lib.command_history import CommandHistory -from ..logging import log_privacy -from ..settings import settings -from ..utils import mumble_utils -from ..utils.parsers import cmd_parser -from ..lib.command_queue import CommandQueue - -if TYPE_CHECKING: - from pymumble_py3.mumble import Mumble - from pymumble_py3.users import User - - from ..config import Config - from ..lib.command import Command - from ..log_config import LogConfig - - from ..services.database_service import DatabaseService - - -logger = logging.getLogger(__name__) - - -class CommandProcessingService: - class OutputPrivacyFilter: - def get_privacy_template(self) -> Dict[str, Any]: - return { - "file": { - "message": None, - "command": None, - "actor": None, - "channel": None, - "parameters": None, - }, - "console": { - "message": None, - "command": None, - "actor": None, - "channel": None, - "parameters": None, - }, - } - - def compile_file_privacy_checked_message(self, redacted_output: Dict[str, Any]): - return ( - f"Command Received::{redacted_output['file']['channel']}::{redacted_output['file']['actor']}::" - f"[Cmd:{redacted_output['file']['command']} | Params:{redacted_output['file']['parameters']}]::" - f"{redacted_output['file']['message']}" - ) - - def compile_console_privacy_checked_message(self, redacted_output: Dict[str, Any]): - return ( - f"Command Received::{redacted_output['console']['channel']}::{redacted_output['console']['actor']}::" - f"[Cmd:{redacted_output['console']['command']} | Params:{redacted_output['console']['parameters']}]::" - f"{redacted_output['console']['message']}" - ) - - def _redact_commands(self, cmd: "Command", log_cfg: "LogConfig") -> Dict[str, Any]: - _redacted_text = "Redacted" - _dict = {"console": {}, "file": {}} - if log_cfg.get(LogCfgFields.OUTPUT.CONSOLE.PRIVACY.REDACT_COMMANDS): - _dict["console"]["command"] = _redacted_text - _dict["console"]["parameters"] = _redacted_text - else: - _dict["console"]["command"] = cmd.command - _dict["console"]["parameters"] = cmd.parameters - if log_cfg.get(LogCfgFields.OUTPUT.FILE.PRIVACY.REDACT_COMMANDS): - _dict["file"]["command"] = _redacted_text - _dict["file"]["parameters"] = _redacted_text - else: - _dict["file"]["command"] = cmd.command - _dict["file"]["parameters"] = cmd.parameters - return _dict - - def _redact_actor(self, cmd: "Command", log_cfg: "LogConfig", connection_instance: "Mumble") -> Dict[str, Any]: - _redacted_text = "Redacted" - _dict = {"console": {}, "file": {}} - _parsed_actor_name = cmd_parser.parse_actor_name(cmd, connection_instance) - if log_cfg.get(LogCfgFields.OUTPUT.CONSOLE.PRIVACY.REDACT_USER): - _dict["console"]["actor"] = _redacted_text - else: - _dict["console"]["actor"] = _parsed_actor_name - if log_cfg.get(LogCfgFields.OUTPUT.FILE.PRIVACY.REDACT_USER): - _dict["file"]["actor"] = _redacted_text - else: - _dict["file"]["actor"] = _parsed_actor_name - return _dict - - def _redact_channel(self, cmd: "Command", log_cfg: "LogConfig", connection_instance: "Mumble") -> Dict[str, Any]: - _redacted_text = "Redacted" - _dict = {"console": {}, "file": {}} - _parsed_channel_name = cmd_parser.parse_channel_name(cmd, connection_instance) - if log_cfg.get(LogCfgFields.OUTPUT.CONSOLE.PRIVACY.REDACT_CHANNEL): - _dict["console"]["channel"] = _redacted_text - else: - _dict["console"]["channel"] = _parsed_channel_name - if log_cfg.get(LogCfgFields.OUTPUT.FILE.PRIVACY.REDACT_CHANNEL): - _dict["file"]["channel"] = _redacted_text - else: - _dict["file"]["channel"] = _parsed_channel_name - return _dict - - def _redact_message(self, cmd: "Command", log_cfg: "LogConfig") -> Dict[str, Any]: - _redacted_message_text = "[Redacted Message]" - _dict = {"console": {}, "file": {}} - _parsed_message = cmd_parser.parse_message_image_data(cmd) - _parsed_message = cmd_parser.parse_message_hyperlink_data(cmd) - if log_cfg.get(LogCfgFields.OUTPUT.CONSOLE.PRIVACY.REDACT_MESSAGE): - _dict["console"]["message"] = _redacted_message_text - else: - _dict["console"]["message"] = _parsed_message - if log_cfg.get(LogCfgFields.OUTPUT.FILE.PRIVACY.REDACT_MESSAGE): - _dict["file"]["message"] = _redacted_message_text - else: - _dict["file"]["message"] = _parsed_message - return _dict - - def get_privacy_checked_output( - self, - cmd: "Command", - log_cfg: "LogConfig", - connection_instance: "Mumble", - ): - _dict = self.get_privacy_template() - - # Redact commands for file and console output: - res = self._redact_commands(cmd, log_cfg) - _dict["file"]["command"] = res["file"]["command"] - _dict["console"]["command"] = res["console"]["command"] - _dict["file"]["parameters"] = res["file"]["parameters"] - _dict["console"]["parameters"] = res["console"]["parameters"] - - # Redact actor for file and console output: - res = self._redact_actor(cmd, log_cfg, connection_instance) - _dict["file"]["actor"] = res["file"]["actor"] - _dict["console"]["actor"] = res["console"]["actor"] - - # Redact channel for file and console output: - res = self._redact_channel(cmd, log_cfg, connection_instance) - _dict["file"]["channel"] = res["file"]["channel"] - _dict["console"]["channel"] = res["console"]["channel"] - - # Redact message for file and console output: - res = self._redact_message(cmd, log_cfg) - _dict["file"]["message"] = res["file"]["message"] - _dict["console"]["message"] = res["console"]["message"] - - return _dict - - _connection_instance: "Mumble" - _privacy_filter: "OutputPrivacyFilter" - _cfg_instance: "Config" - _log_cfg: "LogConfig" - _cmd_queue: "CommandQueue" - - @property - def connection_instance(self) -> "Mumble": - return self._connection_instance - - @property - def log_cfg(self) -> "LogConfig": - return self._log_cfg - - @property - def command_queue(self) -> "CommandQueue": - return self._cmd_queue - - def __init__(self, murmur_connection: "Mumble") -> None: - self._connection_instance = murmur_connection - if self.connection_instance is None: - raise ServiceError("Unable to retrieve murmur connection: the murmur instance is not connected to a server.", logger=logger) - _cfg_instance: Optional["Config"] = settings.configs.get_mumimo_config() - if _cfg_instance is None: - raise ServiceError("Unable to create command processing service: mumimo config could not be retrieved.", logger=logger) - _log_cfg: Optional["LogConfig"] = settings.configs.get_log_config() - if _log_cfg is None: - raise ServiceError("Unable to create command processing service: log config could not be retrieved.", logger=logger) - self._log_cfg = _log_cfg - settings.commands.history.set_command_history( - CommandHistory(history_limit=_cfg_instance.get(MumimoCfgFields.SETTINGS.COMMANDS.COMMAND_HISTORY_LENGTH, None)) - ) - self._privacy_filter = self.OutputPrivacyFilter() - self._cmd_queue = CommandQueue(max_size=_cfg_instance.get(MumimoCfgFields.SETTINGS.COMMANDS.COMMAND_HISTORY_LENGTH, None)) - - def process_cmd(self, text) -> None: - if text is None: - raise ServiceError("Received text message with a 'None' value.", logger=logger) - - asyncio.run(self._process_alias(text)) - asyncio.run(self._process_cmd()) - - async def _process_alias(self, text) -> None: - parsed_cmd: Optional["Command"] = cmd_parser.parse_command(text) - if parsed_cmd is not None: - # Exit command processing if the user message does not contain a command. - _cmd_name = parsed_cmd.command - if _cmd_name is None: - return - - # Retrieve the database service. - _db_service: Optional["DatabaseService"] = settings.database.get_database_instance() - if not _db_service: - raise ServiceError("Unable to process command: the database service could not retrieve the database instance.", logger=logger) - - async with _db_service.session() as session: - # Check if the command is an alias. - _alias_query = await session.execute(select(AliasTable).filter_by(name=_cmd_name).options(selectinload(AliasTable.permission_groups))) - _alias_info: Optional[AliasTable] = _alias_query.scalar() - if not _alias_info: - logger.debug(f"No aliases found for '{_cmd_name}' in the database. Continuing to process as command...") - if self.command_queue.enqueue(parsed_cmd): - logger.debug(f"Enqueued command: '{parsed_cmd.command}'.") - return - logger.error(f"Encountered an error enqueueing command: '{parsed_cmd.command}'") - return - - # Process a generic alias: - if _alias_info.is_generic: - logger.debug(f"Detected generic alias '{_cmd_name}'. Processing generic commands...") - _generic_messages: List[str] = _alias_info.command.split("|") - for _message in _generic_messages: - logger.debug(f"Processing generic command: '{_message}'") - _new_text_object = deepcopy(text) - _new_text_object.message = _message - _new_parsed_cmd: Optional["Command"] = cmd_parser.parse_command(_new_text_object) - if not _new_parsed_cmd: - logger.warning(f"Generic command not recognized: '{_message}'. Skipping command...") - continue - self.command_queue.enqueue(_new_parsed_cmd) - await self._process_cmd() - return - # Process a non-generic alias: - _alias_cmd_query = await session.execute(select(CommandTable).filter_by(name=_alias_info.command)) - _alias_cmd_info: Optional[CommandTable] = _alias_cmd_query.scalar() - if not _alias_cmd_info: - logger.warning( - f"[{LogOutputIdentifiers.DB_ALIASES}]: Unable to execute non-generic alias '{_alias_info.name}': the command for this " - "alias does not exist in the database." - ) - return - parsed_cmd.command = _alias_cmd_info.name - if self.command_queue.enqueue(parsed_cmd): - logger.debug(f"Detected non-generic alias '{_cmd_name}'. Enqueued command: '{parsed_cmd.command}'.") - return - logger.error(f"Encountered an error enqueueing command: '{parsed_cmd.command}'") - - async def _process_cmd(self) -> None: - for _ in range(self.command_queue.size): - # Retrieve the command from the queue if the queue is not empty. - command = self.command_queue.dequeue() - if command is None: - return - logger.debug(f"Processing command: '{command.command}'...") - - # Retrieve the command name. - _cmd_name = command.command - if _cmd_name is None: - return - - # Retrieve all the registered command callbacks to process the command. - _callbacks = settings.commands.callbacks.get_command_callbacks() - if _callbacks is None: - raise ServiceError("Unable to process command: cannot retrieve registered command callbacks.", logger=logger) - - # Retrieve the registered command information. - _cmd_info: Optional[Dict[str, Any]] = _callbacks.get(_cmd_name) - if _cmd_info is None: - # If the command does not exist, suggest similar commands using a fuzzy search. - logger.warning(f"The command: [{_cmd_name}] is not a registered command.") - _all_command_names = [command for command in _callbacks.keys()] - _command_suggestions = process.extract(_cmd_name, _all_command_names, limit=3) - logger.debug(f"Found command suggestions: {_command_suggestions}") - _command_suggestions = [x[0] for x in _command_suggestions if x[1] >= 70] - # Only display suggestions if there is a closely matched ratio. - if _command_suggestions: - _msgs = [ - f"The command: [{_cmd_name}] could not be found. ", - "Did you mean any of the following: ", - ] - for idx, cmd in enumerate(_command_suggestions): - _msgs.append(f"{idx+1}) {cmd}") - - GUIFramework.gui( - _msgs, - target_users=mumble_utils.get_user_by_id(command.actor), - ) - return - - # Retrieve the user's and command's permission groups to determine if the user can use this commnad. - _db_service: Optional["DatabaseService"] = settings.database.get_database_instance() - if not _db_service: - raise ServiceError("Unable to process command: the database service could not retrieve the database instance.", logger=logger) - - _actor_name: Optional["User"] = mumble_utils.get_user_by_id(command.actor) - if not _actor_name: - raise ServiceError("Unable to process command: the user name could not be retrieved from the actor id.") - - async with _db_service.session() as session: - _user_query = await session.execute( - select(UserTable).filter_by(name=_actor_name["name"]).options(selectinload(UserTable.permission_groups)) - ) - _user_info: Optional[UserTable] = _user_query.scalar() - if not _user_info: - logger.error("Unable to process command: the user that sent this command was not found in the database.") - return - - _command_query = await session.execute( - select(CommandTable).filter_by(name=_cmd_name).options(selectinload(CommandTable.permission_groups)) - ) - _command_info: Optional[CommandTable] = _command_query.scalar() - if not _command_info: - logger.error("Unable to process command: the command was not found in the database.") - return - - _user_permissions_groups: List[PermissionGroupTable] = _user_info.permission_groups - _command_permission_groups: List[PermissionGroupTable] = _command_info.permission_groups - _user_permissions_list = [perm.to_dict() for perm in _user_permissions_groups] - - _command_permissions_list = [perm.to_dict() for perm in _command_permission_groups] - _command_permissions_list = [cmd_perm["name"] for cmd_perm in _command_permissions_list] - permission_found: bool = False - for _user_permission in _user_permissions_list: - if _user_permission["name"] in _command_permissions_list: - permission_found = True - break - if not permission_found: - GUIFramework.gui( - f"Unable to process command: the user '{_user_info.name}' does not have permissions to use the '{_cmd_name}' command.", - target_users=mumble_utils.get_user_by_id(command.actor), - log_severity=logging.WARNING, - ) - return - - # Check if the plugin is currently active/running: - _plugin_name: Optional[str] = _cmd_info.get("plugin", None) - if _plugin_name is None: - raise ServiceError(f"Unable to process command: registered command [{_cmd_name}] is missing a registered plugin name.", logger=logger) - _registered_plugins = settings.plugins.get_registered_plugins() - if _registered_plugins is None: - raise ServiceError("Unable to process command: could not find any registered plugins.", logger=logger) - if not _registered_plugins[_plugin_name].is_running: - _inactive_msg = f"The command '{_cmd_name}' could not be executed because the plugin '{_plugin_name}' is not running." - logger.warning(_inactive_msg) - GUIFramework.gui( - _inactive_msg, - target_users=mumble_utils.get_user_by_id(command.actor), - ) - return - - # Retrieve the callable method for the command. - _cmd_callable: Optional[Callable] = _cmd_info.get("func", None) - if _cmd_callable is None: - raise ServiceError(f"The command: [{_cmd_name}] does not contain a registered callable method.", logger=logger) - - # Ignore the command if the provided parameters are invalid or do not exist: - _cmd_params: Optional[List[str]] = _cmd_info.get("parameters", []) - if len(_cmd_params) > 0: - _parameters_required = _cmd_info.get("parameters_required", False) - if _parameters_required and not command.parameters: - logger.warning(f"The command: [{_cmd_name}] requires parameters and no parameters were provided.") - _msgs = [ - f"Invalid '{_cmd_name}' command. This command requires the usage of parameters. ", - "Please use one of the available parameters: ", - ] - for idx, param in enumerate(_cmd_params): - _msgs.append(f"{idx+1}) {param}") - GUIFramework.gui( - text=_msgs, - target_users=mumble_utils.get_user_by_id(command.actor), - ) - return - if any(param.split("=", 1)[0] not in _cmd_params for param in command.parameters): - logger.warning(f"The command: [{_cmd_name}] could not be executed because one or more provided parameters do not exist.") - _msgs = [ - f"Invalid '{_cmd_name}' command. ", - "Please use one of the available parameters: ", - ] - for idx, param in enumerate(_cmd_params): - _msgs.append(f"{idx+1}) {param}") - GUIFramework.gui( - text=_msgs, - target_users=mumble_utils.get_user_by_id(command.actor), - ) - return - - # Handle the redaction of actor names, commands, messages, and channel names: - if self.log_cfg is None: - raise ServiceError("Unable to process command privacy checks: log config could not be retrieved.", logger=logger) - _privacy_checked_output: Dict[str, Any] = self._privacy_filter.get_privacy_checked_output( - command, self.log_cfg, self._connection_instance - ) - - # Add command to command history: - if settings.commands.history.get_command_history() is None: - raise ServiceError("Unable to add command to uninitialized command history.", logger=logger) - if settings.commands.history.add_command_to_history(command) is None: - logger.warning(f"The command: [{command.message}] could not be added to the command history.") - - # Debug the command: - log_privacy( - msg=self._privacy_filter.compile_file_privacy_checked_message(_privacy_checked_output), - logger=logger, - level=logging.DEBUG, - ) - - # Execute the command's callable method in a new thread and pass in all command data. - _cmd_thread = threading.Thread( - name=f"mumimo-{_plugin_name}-{_cmd_name}", - target=_cmd_callable, - args=(_registered_plugins[_plugin_name], command), - ) - logger.debug(f"Command thread: [{_cmd_thread.name}] initialized.") - _cmd_thread.start() - logger.debug(f"Command thread: [{_cmd_thread.name} | {_cmd_thread.ident}] starting...") - - logger.debug(f"Command thread: [{_cmd_thread.name} | {_cmd_thread.ident}] completing...") - _cmd_thread.join() - logger.debug(f"Command thread: [{_cmd_thread.name}] closed.") diff --git a/src/services/database_service.py b/src/services/database_service.py deleted file mode 100644 index bbf7268..0000000 --- a/src/services/database_service.py +++ /dev/null @@ -1,245 +0,0 @@ -import asyncio -import contextlib -import logging -from typing import TYPE_CHECKING, AsyncGenerator, List, Optional, Tuple - -import sqlalchemy_utils -from sqlalchemy import select -from sqlalchemy.exc import NoSuchModuleError -from sqlalchemy.ext.asyncio import AsyncSession, async_scoped_session, async_sessionmaker, create_async_engine - -from ..constants import LogOutputIdentifiers, MumimoCfgFields -from ..exceptions import DatabaseServiceError -from ..lib.database import metadata -from ..lib.database.database_connection_parameters import DatabaseConnectionParameters -from ..lib.database.models.alias import AliasTable # noqa -from ..lib.database.models.command import CommandTable # noqa -from ..lib.database.models.permission_group import PermissionGroupTable # noqa -from ..lib.database.models.plugin import PluginTable # noqa -from ..lib.database.models.user import UserTable # noqa -from ..lib.singleton import singleton -from ..settings import settings -from ..utils.parsers.db_url_parser import get_url - -if TYPE_CHECKING: - from sqlalchemy.ext.asyncio import AsyncEngine - -logger = logging.getLogger(__name__) - - -@singleton -class DatabaseService: - _engine: Optional["AsyncEngine"] = None - _connection_parameters: Optional[DatabaseConnectionParameters] = None - _session_factory: Optional[async_scoped_session] = None - - async def initialize_database( - self, - dialect: Optional[str] = None, - host: Optional[str] = None, - port: Optional[str] = None, - database_name: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - drivername: Optional[str] = None, - use_remote: bool = False, - local_database_path: Optional[str] = None, - local_database_dialect: Optional[str] = None, - local_database_driver: Optional[str] = None, - ) -> None: - db_connection_opts: DatabaseConnectionParameters = DatabaseConnectionParameters( - dialect=dialect, - drivername=drivername, - database_name=database_name, - host=host, - port=port, - username=username, - password=password, - use_remote=use_remote, - local_database_path=local_database_path, - local_database_dialect=local_database_dialect, - local_database_driver=local_database_driver, - ) - await self.setup(db_connection_opts) - await self.import_default_values() - - async def _import_default_permission_groups(self, cfg): - logger.debug(f"[{LogOutputIdentifiers.DB_PERMISSIONS}]: Importing default permission groups...") - _default_permission_groups: List[str] = cfg.get(MumimoCfgFields.SETTINGS.DATABASE.DEFAULT_PERMISSION_GROUPS, []) - async with self.session() as session: - for permission_group in _default_permission_groups: - # Check if the permission group is a valid string: - if not isinstance(permission_group, str) or permission_group.strip() == "": - logger.warning( - f"[{LogOutputIdentifiers.DB_PERMISSIONS}]: Unable to import default permission group. Invalid string - " - f"'{permission_group}'" - ) - continue - permission_group = permission_group.strip() - # Check if the permission group already exists to prevent duplicate imports. - exists = await session.execute(select(PermissionGroupTable).filter_by(name=permission_group)) - if len(exists.scalars().all()) > 0: - logger.debug( - f"[{LogOutputIdentifiers.DB_PERMISSIONS}]: Permission already exists. Skipped importing default permission group - " - f"'{permission_group}'" - ) - continue - # Add the new imported permission group. - _new_permission: PermissionGroupTable = PermissionGroupTable(name=permission_group) - session.add(_new_permission) - await session.commit() - logger.debug(f"[{LogOutputIdentifiers.DB_PERMISSIONS}]: Imported default permission group - '{permission_group}'") - logger.debug(f"[{LogOutputIdentifiers.DB_PERMISSIONS}]: Finished adding default permission groups.") - - async def _import_default_aliases(self, cfg): - logger.debug(f"[{LogOutputIdentifiers.DB_ALIASES}]: Importing default aliases...") - _default_aliases: List[List[str]] = cfg.get(MumimoCfgFields.SETTINGS.DATABASE.DEFAULT_ALIASES, "") - async with self.session() as session: - for alias in _default_aliases: - # Validate the alias parameters provided in the config file. - if len(alias) != 3: - logger.warning( - f"[{LogOutputIdentifiers.DB_ALIASES}]: Invalid alias - unable to import default alias due to missing alias parameters. \ - Please check your config file." - ) - continue - if not isinstance(alias[0], str): - logger.warning( - f"[{LogOutputIdentifiers.DB_ALIASES}]: Invalid alias - unable to import default alias. \ - The alias name must be a string value." - ) - continue - if not isinstance(alias[1], str): - logger.warning( - f"[{LogOutputIdentifiers.DB_ALIASES}]: Invalid alias - unable to import default alias '{alias[0]}'. \ - The alias command must be a string value." - ) - continue - if not isinstance(alias[2], str): - logger.warning( - f"[{LogOutputIdentifiers.DB_ALIASES}]: Invalid alias - unable to import default alias '{alias[0]}'. \ - The alias permission groups must be a comma-separated string value." - ) - continue - # Check if the alias already exists to prevent duplicate imports. - _alias_exists = await session.execute(select(AliasTable).filter_by(name=alias[0])) - if len(_alias_exists.scalars().all()) > 0: - logger.debug(f"[{LogOutputIdentifiers.DB_ALIASES}]: Alias '{alias[0]}' already exists. Skipped import.") - continue - # Filter permissions that exist in the permission group table and add it to the new alias object. - _new_alias: AliasTable = AliasTable(name=alias[0], command=alias[1]) - # Get the permissions string and separate into a list of strings. - _alias_permission_groups: List[str] = [permission.strip() for permission in alias[2].split(",")] - if not _alias_permission_groups: - logger.debug(f"[{LogOutputIdentifiers.DB_ALIASES}]: No permission groups added for alias - '{alias[0]}'") - else: - _permissions_query = await session.execute( - select(PermissionGroupTable).filter(PermissionGroupTable.name.in_(_alias_permission_groups)) - ) - _all_matching_permissions = _permissions_query.scalars().all() - for permission_group in _all_matching_permissions: - _new_alias.permission_groups.append(permission_group) - logger.debug( - f"[{LogOutputIdentifiers.DB_ALIASES}]: Added permission group for imported default alias - " - f"'{alias[0]}':[{permission_group.name}]" - ) - # Do not attempt to add imported alias if no valid permission groups are found. - if len(_new_alias.permission_groups) == 0: - logger.warning( - f"[{LogOutputIdentifiers.DB_ALIASES}]: Unable to add default alias '{alias[0]}'. No valid permission groups detected." - ) - continue - # Add the new imported alias. - session.add(_new_alias) - await session.commit() - logger.debug(f"[{LogOutputIdentifiers.DB_ALIASES}]: Imported default alias - '{alias[0]}'") - - logger.debug(f"[{LogOutputIdentifiers.DB_ALIASES}]: Finished adding default aliases.") - - async def import_default_values(self): - if self._engine is None: - raise DatabaseServiceError( - f"[{LogOutputIdentifiers.DB}]: Cannot import default values. Database connection engine is not initialized.", logger=logger - ) - _cfg = settings.configs.get_mumimo_config() - if _cfg is None: - raise DatabaseServiceError( - f"[{LogOutputIdentifiers.DB}]: Cannot import default values. The mumimo config has not been initialized.", logger=logger - ) - - # Import default permission groups: - await self._import_default_permission_groups(_cfg) - - # Import default aliases: - await self._import_default_aliases(_cfg) - - async def setup(self, connection_parameters: DatabaseConnectionParameters): - # Ensure that there are no existing engine connections initialized. - if self._engine is not None: - raise DatabaseServiceError( - f"[{LogOutputIdentifiers.DB}]: Connection engine is already initialized. Close the connection first.", logger=logger - ) - - # Validate connection parameters. - if connection_parameters is None: - raise DatabaseServiceError(f"[{LogOutputIdentifiers.DB}]: Connection parameters were not provided.", logger=logger) - _validation_result: Tuple[bool, str] = await self._validate_connection_parameters(connection_parameters) - if not _validation_result[0]: - raise DatabaseServiceError(f"[{LogOutputIdentifiers.DB}]: Connection parameters are invalid - '{_validation_result[1]}'.", logger=logger) - self._connection_parameters = connection_parameters - - # Create or connect to the database, initialize the async engine and create all missing tables. - _create_db_url: str = get_url(self._connection_parameters, create_url=True) - _async_url: str = get_url(self._connection_parameters, create_url=False) - try: - if not self._connection_parameters.use_remote: - if not sqlalchemy_utils.database_exists(_create_db_url): - sqlalchemy_utils.create_database(_create_db_url) - self._engine = create_async_engine(_async_url, echo=False) - async with self._engine.begin() as conn: - await conn.run_sync(metadata.Base.metadata.create_all) - except NoSuchModuleError as exc: - raise DatabaseServiceError( - f"[{LogOutputIdentifiers.DB}]: Database connection dialect could not be found. Please check your dialect connection parameters.", - logger=logger, - ) from exc - - # Initialize the async session factory with the initialized async engine. - self._session_factory = async_scoped_session( - session_factory=async_sessionmaker(self._engine, class_=AsyncSession, expire_on_commit=False), - scopefunc=asyncio.current_task, - ) - - # Save the database service instance to the settings. - settings.database.set_database_instance(self) - - async def _validate_connection_parameters(self, connection_parameters: DatabaseConnectionParameters) -> Tuple[bool, str]: - _validation_result: Tuple[bool, str] = connection_parameters.validate_parameters() - return _validation_result - - async def close(self, clean: bool = False) -> None: - logger.debug(f"[{LogOutputIdentifiers.DB}]: Attempting to close mumimo database connection.") - if self._engine is not None: - await self._engine.dispose(close=True) - if clean: - self._connection_parameters = None - self._session_factory = None - self._engine = None - logger.debug(f"[{LogOutputIdentifiers.DB}]: Mumimo database connection closed.") - - @contextlib.asynccontextmanager - async def session(self) -> AsyncGenerator[AsyncSession, None]: - if self._session_factory is None: - raise DatabaseServiceError( - f"[{LogOutputIdentifiers.DB}]: Unable to retrieve database session due to an uninitialized session factory.", logger=logger - ) - async with self._session_factory() as session: - try: - yield session - except Exception as exc: - await session.rollback() - raise DatabaseServiceError( - f"[{LogOutputIdentifiers.DB}]: Encountered an error with the database session. Rolled back changes.", logger=logger - ) from exc - finally: - await session.close() diff --git a/src/services/init_services/cfg_init_service.py b/src/services/init_services/cfg_init_service.py deleted file mode 100644 index 6cb57d4..0000000 --- a/src/services/init_services/cfg_init_service.py +++ /dev/null @@ -1,31 +0,0 @@ -import logging -from typing import Dict, Optional - -from ...config import Config -from ...constants import DEFAULT_PATH_CONFIG_FILE -from ...exceptions import ConfigError -from ...settings import settings - -logger = logging.getLogger(__name__) - - -class ConfigInitService: - def __init__(self, sys_args: Dict[str, str]) -> None: - self._sys_args = sys_args - - def initialize_config(self, cfg_path: Optional[str] = None) -> "Config": - # Load in configurations from custom config file path if present. - # Otherwise load configurations from default config file path. - _cfg_instance: Optional["Config"] = settings.configs.get_mumimo_config() - if _cfg_instance is None: - if not cfg_path: - cfg_path = DEFAULT_PATH_CONFIG_FILE - logger.warning("Mumimo config file path not provided. Reading config from default path.") - _cfg_instance = Config(cfg_path) - _cfg_instance.read() - settings.configs.set_mumimo_config(_cfg_instance) - - _cfg_instance = settings.configs.get_mumimo_config() - if _cfg_instance is None: - raise ConfigError("An unexpected error occurred where the config file was not read during initialization.", logger=logger) - return _cfg_instance diff --git a/src/services/init_services/client_settings_init_service.py b/src/services/init_services/client_settings_init_service.py deleted file mode 100644 index b43a660..0000000 --- a/src/services/init_services/client_settings_init_service.py +++ /dev/null @@ -1,168 +0,0 @@ -import logging -from typing import TYPE_CHECKING, Any, Dict, Optional - -from ...constants import VERBOSITY_MIN, EnvArgs, MumimoCfgFields, SysArgs -from ...utils import connection_utils -from ...utils.parsers import env_parser - -if TYPE_CHECKING: - from ...config import Config - - -logger = logging.getLogger(__name__) - - -class ClientSettingsInitService: - _sys_args: Dict[str, str] = {} - _prioritized_env_opts: Dict[str, Any] = {} - _prioritized_cfg_opts: Dict[str, Any] = {} - - def __init__(self, sys_args: Dict[str, str]) -> None: - self._sys_args = sys_args - - def initialize_client_settings(self, cfg_instance: "Config") -> None: - # Initialize client settings. - self._prioritized_cfg_opts = self._get_prioritized_client_config_options(cfg_instance) - self._prioritized_env_opts = self._get_prioritized_client_env_options() - - def get_connection_parameters(self) -> Dict[str, Any]: - _prioritized_env_opts = self.get_prioritized_env_options() - _prioritized_cfg_opts = self.get_prioritized_cfg_options() - if not _prioritized_cfg_opts or not _prioritized_env_opts: - return {} - - _sys_args = self._get_sys_args() - return { - SysArgs.SYS_HOST: _prioritized_env_opts.get(SysArgs.SYS_HOST), - SysArgs.SYS_PORT: _prioritized_env_opts.get(SysArgs.SYS_PORT), - SysArgs.SYS_USER: _prioritized_env_opts.get(SysArgs.SYS_USER), - SysArgs.SYS_PASS: _prioritized_env_opts.get(SysArgs.SYS_PASS), - SysArgs.SYS_CERT: _prioritized_env_opts.get(SysArgs.SYS_CERT), - SysArgs.SYS_KEY: _prioritized_env_opts.get(SysArgs.SYS_KEY), - SysArgs.SYS_TOKENS: _prioritized_env_opts.get(SysArgs.SYS_TOKENS), - SysArgs.SYS_SUPER_USER: _prioritized_env_opts.get(SysArgs.SYS_SUPER_USER), - SysArgs.SYS_VERBOSE: _sys_args.get(SysArgs.SYS_VERBOSE) or VERBOSITY_MIN, - SysArgs.SYS_RECONNECT: _prioritized_cfg_opts.get(SysArgs.SYS_RECONNECT, False), - } - - def get_prioritized_cfg_options(self) -> Dict[str, Any]: - if not self._prioritized_cfg_opts: - return {} - return self._prioritized_cfg_opts - - def get_prioritized_env_options(self) -> Dict[str, Any]: - if not self._prioritized_env_opts: - return {} - return self._prioritized_env_opts - - def _get_sys_args(self) -> Dict[str, str]: - return self._sys_args - - def _get_prioritized_client_config_options(self, cfg_instance: "Config") -> Dict[str, Any]: - # Consolidate priority between system arguments and config file. - # System arguments should have higher priority than config file. - prioritized_options: Dict[str, Any] = {} - - # Prioritize mumimo name from system argument over config option. - name = self._get_sys_args().get(SysArgs.SYS_NAME) or cfg_instance.get( - MumimoCfgFields.SETTINGS.CONNECTION.NAME, - fallback="Mumimo", - ) - prioritized_options[SysArgs.SYS_NAME] = name - cfg_instance.set(MumimoCfgFields.SETTINGS.CONNECTION.NAME, name) - - # Prioritize auto-reconnect from system argument over config option. - reconnect = self._get_sys_args().get(SysArgs.SYS_RECONNECT) or cfg_instance.get( - MumimoCfgFields.SETTINGS.CONNECTION.AUTO_RECONNECT, fallback=False - ) - prioritized_options[SysArgs.SYS_RECONNECT] = reconnect - cfg_instance.set(MumimoCfgFields.SETTINGS.CONNECTION.AUTO_RECONNECT, reconnect) - - # Prioritize database parameters from system arguments over config options. - use_remote_db = self._get_sys_args().get(SysArgs.SYS_DB_USEREMOTEDB) or cfg_instance.get( - MumimoCfgFields.SETTINGS.DATABASE.USE_REMOTE_DB, fallback=False - ) - prioritized_options[SysArgs.SYS_DB_USEREMOTEDB] = use_remote_db - cfg_instance.set(MumimoCfgFields.SETTINGS.DATABASE.USE_REMOTE_DB, use_remote_db) - - db_local_path = self._get_sys_args().get(SysArgs.SYS_DB_LOCALDBPATH) or cfg_instance.get(MumimoCfgFields.SETTINGS.DATABASE.LOCAL_DB_PATH) - prioritized_options[SysArgs.SYS_DB_LOCALDBPATH] = db_local_path - cfg_instance.set(MumimoCfgFields.SETTINGS.DATABASE.LOCAL_DB_PATH, db_local_path) - - db_local_dialect = self._get_sys_args().get(SysArgs.SYS_DB_LOCALDBDIALECT) or cfg_instance.get( - MumimoCfgFields.SETTINGS.DATABASE.LOCAL_DB_DIALECT - ) - prioritized_options[SysArgs.SYS_DB_LOCALDBDIALECT] = db_local_dialect - cfg_instance.set(MumimoCfgFields.SETTINGS.DATABASE.LOCAL_DB_DIALECT, db_local_dialect) - - db_local_drivername = self._get_sys_args().get(SysArgs.SYS_DB_LOCALDBDRIVER) or cfg_instance.get( - MumimoCfgFields.SETTINGS.DATABASE.LOCAL_DB_DRIVERNAME - ) - prioritized_options[SysArgs.SYS_DB_LOCALDBDRIVER] = db_local_drivername - cfg_instance.set(MumimoCfgFields.SETTINGS.DATABASE.LOCAL_DB_DRIVERNAME, db_local_drivername) - - # Prioritize plugins-path from system argument over config option. - plugins_path = self._get_sys_args().get(SysArgs.SYS_PLUGINS_PATH) or cfg_instance.get(MumimoCfgFields.SETTINGS.PLUGINS.PLUGINS_PATH) - prioritized_options[SysArgs.SYS_PLUGINS_PATH] = plugins_path - cfg_instance.set(MumimoCfgFields.SETTINGS.PLUGINS.PLUGINS_PATH, plugins_path) - - # Prioritize plugins-config-path from system argument over config option. - plugins_config_path = self._get_sys_args().get(SysArgs.SYS_PLUGINS_CONFIG_PATH) or cfg_instance.get( - MumimoCfgFields.SETTINGS.PLUGINS.PLUGINS_CONFIG_PATH - ) - prioritized_options[SysArgs.SYS_PLUGINS_CONFIG_PATH] = plugins_config_path - cfg_instance.set(MumimoCfgFields.SETTINGS.PLUGINS.PLUGINS_CONFIG_PATH, plugins_config_path) - - # Prioritize gui-theme-path from system argument over config options. - gui_theme_path = self._get_sys_args().get(SysArgs.SYS_GUI_THEMES_FILE) or cfg_instance.get(MumimoCfgFields.SETTINGS.GUI.THEMES_PATH) - prioritized_options[SysArgs.SYS_GUI_THEMES_FILE] = gui_theme_path - cfg_instance.set(MumimoCfgFields.SETTINGS.GUI.THEMES_PATH, gui_theme_path) - - return prioritized_options - - def _get_prioritized_client_env_options(self) -> Dict[str, Any]: - # Load in options from .env file if present. - available_env_file: Optional[str] = self._get_sys_args().get(SysArgs.SYS_ENV_FILE, None) or ".env" - env_args: Dict[str, Any] = {} - if available_env_file is not None: - env_args = env_parser.read_env_file(available_env_file) - if env_args is None: - env_args = {} - - # Prioritize system args from system arguments over environment file. - prioritized_options: Dict[str, Any] = {} - - # Prioritize channel access tokens. - sys_tokens = self._get_sys_args().get(SysArgs.SYS_TOKENS) or env_args.get(EnvArgs.ENV_TOKENS) - if isinstance(sys_tokens, str): - sys_tokens = connection_utils.parse_channel_tokens(sys_tokens) - prioritized_options[SysArgs.SYS_TOKENS] = sys_tokens - - # Prioritize cert/key files. - sys_cert_file = self._get_sys_args().get(SysArgs.SYS_CERT) or env_args.get(EnvArgs.ENV_CERT) - prioritized_options[SysArgs.SYS_CERT] = sys_cert_file - sys_key_file = self._get_sys_args().get(SysArgs.SYS_KEY) or env_args.get(EnvArgs.ENV_KEY) - prioritized_options[SysArgs.SYS_KEY] = sys_key_file - - prioritized_options.update( - { - # Prioritize connection args from system arguments over environment file: - SysArgs.SYS_HOST: self._get_sys_args().get(SysArgs.SYS_HOST) or env_args.get(EnvArgs.ENV_HOST), - SysArgs.SYS_PORT: self._get_sys_args().get(SysArgs.SYS_PORT) or env_args.get(EnvArgs.ENV_PORT), - SysArgs.SYS_USER: self._get_sys_args().get(SysArgs.SYS_USER) or env_args.get(EnvArgs.ENV_USER), - SysArgs.SYS_PASS: self._get_sys_args().get(SysArgs.SYS_PASS) or env_args.get(EnvArgs.ENV_PASS), - SysArgs.SYS_CERT: self._get_sys_args().get(SysArgs.SYS_CERT) or env_args.get(EnvArgs.ENV_CERT), - SysArgs.SYS_KEY: self._get_sys_args().get(SysArgs.SYS_KEY) or env_args.get(EnvArgs.ENV_KEY), - SysArgs.SYS_SUPER_USER: self._get_sys_args().get(SysArgs.SYS_SUPER_USER) or env_args.get(EnvArgs.ENV_SUPER_USER), - # Prioritize database args from system arguments over environment file: - SysArgs.SYS_DB_DIALECT: self._get_sys_args().get(SysArgs.SYS_DB_DIALECT) or env_args.get(EnvArgs.ENV_DB_DIALECT), - SysArgs.SYS_DB_DRIVER: self._get_sys_args().get(SysArgs.SYS_DB_DRIVER) or env_args.get(EnvArgs.ENV_DB_DRIVER), - SysArgs.SYS_DB_USER: self._get_sys_args().get(SysArgs.SYS_DB_USER) or env_args.get(EnvArgs.ENV_DB_USER), - SysArgs.SYS_DB_PASS: self._get_sys_args().get(SysArgs.SYS_DB_PASS) or env_args.get(EnvArgs.ENV_DB_PASS), - SysArgs.SYS_DB_HOST: self._get_sys_args().get(SysArgs.SYS_DB_HOST) or env_args.get(EnvArgs.ENV_DB_HOST), - SysArgs.SYS_DB_PORT: self._get_sys_args().get(SysArgs.SYS_DB_PORT) or env_args.get(EnvArgs.ENV_DB_PORT), - SysArgs.SYS_DB_NAME: self._get_sys_args().get(SysArgs.SYS_DB_NAME) or env_args.get(EnvArgs.ENV_DB_NAME), - } - ) - - return prioritized_options diff --git a/src/services/init_services/gui_init_service.py b/src/services/init_services/gui_init_service.py deleted file mode 100644 index c8514c9..0000000 --- a/src/services/init_services/gui_init_service.py +++ /dev/null @@ -1,31 +0,0 @@ -import logging -from typing import Dict, Optional - -from ...config import Config -from ...constants import DEFAULT_PATH_GUI_THEMES_FILE -from ...exceptions import ConfigError -from ...settings import settings - -logger = logging.getLogger(__name__) - - -class GUIInitService: - def __init__(self, sys_args: Dict[str, str]) -> None: - self._sys_args = sys_args - - def initialize_gui(self, themes_path: Optional[str] = None) -> "Config": - # Load in themes from custom themes file path if present. - # Otherwise load themes from default themes file path. - _themes_instance: Optional["Config"] = settings.configs.get_gui_themes() - if _themes_instance is None: - if not themes_path: - themes_path = DEFAULT_PATH_GUI_THEMES_FILE - logger.warning("Mumimo gui themes file path not provided. Reading gui themes from default path.") - _themes_instance = Config(themes_path) - _themes_instance.read() - settings.configs.set_gui_themes(_themes_instance) - - _themes_instance = settings.configs.get_gui_themes() - if _themes_instance is None: - raise ConfigError("An unexpected error occurred where the gui themes file was not read during initialization.", logger=logger) - return _themes_instance diff --git a/src/services/init_services/mumimo_init_service.py b/src/services/init_services/mumimo_init_service.py deleted file mode 100644 index 716e2ec..0000000 --- a/src/services/init_services/mumimo_init_service.py +++ /dev/null @@ -1,65 +0,0 @@ -import logging -from typing import TYPE_CHECKING, Any, Dict - -from ...constants import SysArgs -from ...services.database_service import DatabaseService -from ...services.init_services.cfg_init_service import ConfigInitService -from ...services.init_services.client_settings_init_service import ClientSettingsInitService -from ...services.init_services.plugins_init_service import PluginsInitService -from ...services.init_services.gui_init_service import GUIInitService - -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from ...config import Config - - -class MumimoInitService: - def __init__(self, sys_args: Dict[str, str]) -> None: - self._sys_args: Dict[str, str] = sys_args - self._cfg_init_service: ConfigInitService = ConfigInitService(sys_args) - self._client_settings_init_service: ClientSettingsInitService = ClientSettingsInitService(sys_args) - self._db_init_service: DatabaseService = DatabaseService() - self._plugins_init_service: PluginsInitService = PluginsInitService(sys_args) - self._gui_init_service: GUIInitService = GUIInitService(sys_args) - - async def initialize(self) -> None: - # Initialize the mumimo configuration file. - logger.info("Initializing configuration file...") - cfg: "Config" = self._cfg_init_service.initialize_config(self._sys_args.get(SysArgs.SYS_CONFIG_FILE)) - logger.info("Mumimo client configuration initialized.") - # Initialize mumimo client settings. - logger.info("Initializing client settings...") - self._client_settings_init_service.initialize_client_settings(cfg) - logger.info("Mumimo client settings initialized.") - # Initialize mumimo gui settings. - logger.info("Initializing client gui settings...") - self._gui_init_service.initialize_gui(self._sys_args.get(SysArgs.SYS_GUI_THEMES_FILE)) - logger.info("Client gui settings initialized.") - # Initialize the internal database. - logger.info("Initializing internal database...") - # print(cfg) - _prioritized_cfg_opts = self._client_settings_init_service.get_prioritized_cfg_options() - # print(_prioritized_cfg_opts) - _prioritized_env_opts = self._client_settings_init_service.get_prioritized_env_options() - await self._db_init_service.initialize_database( - dialect=_prioritized_env_opts.get(SysArgs.SYS_DB_DIALECT, None), - username=_prioritized_env_opts.get(SysArgs.SYS_DB_USER, None), - host=_prioritized_env_opts.get(SysArgs.SYS_DB_HOST, None), - port=_prioritized_env_opts.get(SysArgs.SYS_DB_PORT, None), - password=_prioritized_env_opts.get(SysArgs.SYS_DB_PASS, None), - database_name=_prioritized_env_opts.get(SysArgs.SYS_DB_NAME, None), - drivername=_prioritized_env_opts.get(SysArgs.SYS_DB_DRIVER, None), - use_remote=_prioritized_cfg_opts.get(SysArgs.SYS_DB_USEREMOTEDB, None), - local_database_dialect=_prioritized_cfg_opts.get(SysArgs.SYS_DB_LOCALDBDIALECT, None), - local_database_path=_prioritized_cfg_opts.get(SysArgs.SYS_DB_LOCALDBPATH, None), - local_database_driver=_prioritized_cfg_opts.get(SysArgs.SYS_DB_LOCALDBDRIVER, None), - ) - logger.info("Mumimo internal database initialized.") - # Initialize the plugins. - logger.info("Mumimo plugins initializing...") - await self._plugins_init_service.initialize_plugins(self._db_init_service) - logger.info("Mumimo plugins initialized.") - - async def get_connection_parameters(self) -> Dict[str, Any]: - return self._client_settings_init_service.get_connection_parameters() diff --git a/src/services/init_services/plugins_init_service.py b/src/services/init_services/plugins_init_service.py deleted file mode 100644 index 2629933..0000000 --- a/src/services/init_services/plugins_init_service.py +++ /dev/null @@ -1,248 +0,0 @@ -import logging -import pathlib -import shutil -import sys -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -from sqlalchemy import select - -from ...config import Config -from ...constants import LogOutputIdentifiers, MumimoCfgFields, PluginCfgFields -from ...exceptions import ServiceError -from ...lib.command_callbacks import CommandCallbacks -from ...lib.database.models.command import CommandTable -from ...lib.database.models.permission_group import PermissionGroupTable -from ...lib.database.models.plugin import PluginTable -from ...settings import settings - -if TYPE_CHECKING: - from ...services.database_service import DatabaseService - - -logger = logging.getLogger(__name__) - - -class PluginsInitService: - _sys_args: Dict[str, str] = {} - - def __init__(self, sys_args: Dict[str, str]) -> None: - self._sys_args = sys_args - - async def initialize_plugins(self, db_service: "DatabaseService"): - # Ensure the config and client state are both initialized. - _cfg = settings.configs.get_mumimo_config() - if _cfg is None: - raise ServiceError("Unable to initialize plugins: the mumimo config is uninitialized.", logger=logger) - # Ensure the plugin path exists. - _plugin_path: pathlib.Path = pathlib.Path.cwd() / _cfg.get(MumimoCfgFields.SETTINGS.PLUGINS.PLUGINS_PATH) - if not _plugin_path.exists() or not _plugin_path.is_dir(): - raise ServiceError( - f"Unable to initialize plugins: the plugins path is invalid. Please check your config settings: '{_plugin_path}'", logger=logger - ) - - # Retrieve all the plugin directories in the root plugin folder. - _plugin_dirs: List[pathlib.Path] = [] - for dir in _plugin_path.iterdir(): - if dir.is_dir() and dir.name != "__pycache__": - _plugin_dirs.append(dir) - - # Iterate over each plugin directory and ensure each plugin has a metadata file. - # Skip any directories that do not have a metadata file. - - _skipped_plugins = [] - for dir in _plugin_dirs: - _plugin_name = dir.name - - if not (dir / "plugin.py").is_file(): - logger.error( - f"[{LogOutputIdentifiers.PLUGINS}]: Plugin '{_plugin_name}' does not contain a 'plugin.py' file. " - "Skipping plugin initialization..." - ) - _skipped_plugins.append(_plugin_name) - continue - - _plugin_config_path: pathlib.Path = pathlib.Path.cwd() / _cfg.get(MumimoCfgFields.SETTINGS.PLUGINS.PLUGINS_CONFIG_PATH) / _plugin_name - try: - # Create the plugin's config directory if it does not exist and create the 'metadata.toml' from the template file. - if not pathlib.Path.is_dir(_plugin_config_path): - pathlib.Path.mkdir(_plugin_config_path, parents=True, exist_ok=True) - logger.warning( - f"[{LogOutputIdentifiers.PLUGINS}]: Config directory not found for '{_plugin_name}' plugin. " - "Generating config directory now..." - ) - if not (dir / "metadata_template.toml").is_file(): - logger.error( - f"[{LogOutputIdentifiers.PLUGINS}]: Plugin '{_plugin_name}' does not contain a 'metadata_template.toml' file. " - "Skipping plugin initialization..." - ) - _skipped_plugins.append(_plugin_name) - continue - shutil.copy(dir / "metadata_template.toml", _plugin_config_path / "metadata.toml") - # If the plugin config directory already exists but the 'metadata.toml' file is missing, then skip plugin initialization. - elif not (_plugin_config_path / "metadata.toml").is_file(): - logger.warning( - f"[{LogOutputIdentifiers.PLUGINS}]: '{_plugin_name}' plugin does not contain a 'metadata.toml' file. " - "Skipping plugin initialization..." - ) - continue - except IOError as exc: - raise ServiceError(f"Unable to create config directory for '{_plugin_name}' plugin.", logger=logger) from exc - - # Attempt to load in plugin metadata. Skip the plugin initialization if this step fails. - _plugin_cfg = Config(_plugin_config_path / "metadata.toml") - _plugin_cfg.read() - if not _plugin_cfg: - logger.error( - f"[{LogOutputIdentifiers.PLUGINS}]: Plugin '{_plugin_name}' encountered an error while processing the metadata file. " - "Skipping plugin initialization..." - ) - _skipped_plugins.append(_plugin_name) - continue - # Skip the plugin initialization if the plugin is not enabled in the plugin metadata file. - if not _plugin_cfg.get(PluginCfgFields.PLUGIN.ENABLED, False): - logger.info( - f"[{LogOutputIdentifiers.PLUGINS}]: Plugin '{_plugin_name}' is not enabled in the plugin 'metadata.toml' file. " - "Skipping plugin initialization..." - ) - _skipped_plugins.append(_plugin_name) - continue - - sys.path.insert(0, str(_plugin_path.resolve())) - _registered_plugin = __import__(f"{_plugin_name}.plugin", fromlist=["*"]).Plugin - sys.path.pop(0) - - _plugin_commands: CommandCallbacks = CommandCallbacks() - # Register all the commands belonging to this plugin. - try: - for command_name, command_value in _registered_plugin.command.all.items(): - _plugin_commands.register_command( - callback_name=command_name, - plugin_name=_plugin_name, - command_func=command_value[0], - command_parameters=command_value[1], - parameters_required=command_value[2], - exclusive_parameters=command_value[3], - ) - logger.debug( - f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: Registered '{_plugin_name}.{command_name}.[{'|'.join(command_value[1])}]' " - "plugin command." - ) - settings.commands.callbacks.add_command_callbacks(_plugin_commands) - except AttributeError as exc: - raise ServiceError( - f"Unable to initialize '{_plugin_name}' plugin. The command attribute does not exist in the plugin class.", logger=logger - ) from exc - - # Import the plugin to the database and associate it with the plugin commands. - _abort_import: bool = False - async with db_service.session() as session: - _query = await session.execute(select(PluginTable).filter_by(name=_plugin_name)) - _db_plugin: Optional[PluginTable] = _query.scalar() - _plugin_modified: bool = False - if _db_plugin is not None: - logger.debug(f"[{LogOutputIdentifiers.DB_PLUGINS}]: Plugin '{_plugin_name}' already exists in the database. Skipping import...") - else: - _db_plugin = PluginTable(name=_plugin_name) - _plugin_modified = True - logger.debug( - f"[{LogOutputIdentifiers.DB_PLUGINS}]: Plugin '{_plugin_name}' not detected in the database. This plugin will be imported." - ) - # Import plugin commands to the database with plugin-specified default permissions if the command does not already exist. - _imported_cmd_count: int = 0 - _cfg_disabled_commands: List[str] = _plugin_cfg.get(PluginCfgFields.PLUGIN.COMMANDS.DISABLE_COMMANDS, []) - for command_name, command_value in _plugin_commands.items(): - if command_name in _cfg_disabled_commands: - logger.debug( - f"[{LogOutputIdentifiers.PLUGINS_COMMANDS}]: Command '{command_name}' is disabled in the " - "plugin '{_plugin_name}' metadata file. Skipping import..." - ) - continue - - _query = await session.execute(select(CommandTable).filter_by(name=command_name)) - _command: Optional[CommandTable] = _query.scalar() - if _command is not None: - logger.debug( - f"[{LogOutputIdentifiers.DB_PLUGINS_COMMANDS}]: Command '{command_name}' already exists in the database. " - "Skipping import..." - ) - continue - - _cfg_plugin_cmd_permission_names: List[str] = _plugin_cfg.get(PluginCfgFields.PLUGIN.COMMANDS.DEFAULT_PERMISSION_GROUPS, []) - - _new_command = CommandTable(name=command_name) - - for _permission_name in _cfg_plugin_cmd_permission_names: - _query = await session.execute(select(PermissionGroupTable).filter_by(name=_permission_name)) - _permission: Optional[PermissionGroupTable] = _query.scalar() - if not _permission: - logger.error( - f"[{LogOutputIdentifiers.PLUGINS_PERMISSIONS}]: Default permission group '{_permission_name}' from the " - f"plugin '{_plugin_name}' does not exist in the database. Aborting plugin initialization..." - ) - _skipped_plugins.append(_plugin_name) - _abort_import = True - break - _new_command.permission_groups.append(_permission) - logger.debug( - f"[{LogOutputIdentifiers.PLUGINS_PERMISSIONS}]: Added permission group '{_permission_name}' to '{command_name}' command." - ) - if _abort_import: - break - - _db_plugin.commands.append(_new_command) - _imported_cmd_count += 1 - _plugin_modified = True - - logger.debug( - f"[{LogOutputIdentifiers.DB_PLUGINS_COMMANDS}]: Command '{command_name}' not detected in the database. " - "This command will be imported." - ) - - # Abort the plugin import process and revert registering command callbacks if there was an error during database initialization. - if _abort_import: - logger.debug( - f"[{LogOutputIdentifiers.DB_PLUGINS_COMMANDS}]: Aborting plugin commands database import process " - f"for '{_plugin_name}' plugin: rolling back database modifications and unregistering plugin commands." - ) - await session.rollback() - _unregister_status, _unregisters = _plugin_commands.unregister_plugin(_plugin_name) - if not _unregister_status: - _failed: List[Dict[str, Any]] = [dict_val for dict_key, dict_val in _unregisters.items()] - logger.error( - f"[{LogOutputIdentifiers.DB_PLUGINS_COMMANDS}]: Encountered an error unregistering plugin '{_plugin_name}' during " - f"abort process. The following commands failed to unregister: [{', '.join([item['command'] for item in _failed])}]" - ) - else: - _success: List[Dict[str, Any]] = [dict_val for dict_key, dict_val in _unregisters.items()] - logger.debug( - f"[{LogOutputIdentifiers.DB_PLUGINS_COMMANDS}]: Rolling back plugin '{_plugin_name}' command registrations. " - f"The following commands have been unregistered: [{', '.join([item['command'] for item in _success])}]" - ) - settings.commands.callbacks.set_command_callbacks(_plugin_commands) - continue - - # Add the plugin and associated commands to the database if any modifications were detected since the previous run. - if _plugin_modified: - session.add(_db_plugin) - await session.commit() - logger.debug( - f"[{LogOutputIdentifiers.DB_PLUGINS}]: Imported plugin '{_plugin_name}' in the database " - f"with {_imported_cmd_count} new commands." - ) - else: - logger.debug( - f"[{LogOutputIdentifiers.DB_PLUGINS}]: Plugin '{_plugin_name}' already exists in the database and was " - "unmodified from the previous run. No new database imports conducted." - ) - await session.rollback() - - if not _abort_import: - _plugin = _registered_plugin(_plugin_name) - _plugin.start() - settings.plugins.set_registered_plugin(_plugin_name, _plugin) - - logger.info( - f"[{LogOutputIdentifiers.PLUGINS}]: Initialized plugins: [{', '.join([k for k, v in settings.plugins.get_registered_plugins().items()])}]" - ) - if _skipped_plugins: - logger.warning(f"[{LogOutputIdentifiers.PLUGINS}]: The following plugins were not initialized: [{', '.join(_skipped_plugins)}]") diff --git a/src/settings.py b/src/settings.py deleted file mode 100644 index 4289b0f..0000000 --- a/src/settings.py +++ /dev/null @@ -1,166 +0,0 @@ -import logging -from typing import TYPE_CHECKING, Any, Dict, List, Optional - - -from .lib.command_callbacks import CommandCallbacks -from .lib.singleton import singleton - -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from .client_state import ClientState - from .config import Config - from .lib.command import Command - from .lib.command_history import CommandHistory - from .lib.frameworks.plugins.plugin import PluginBase - from .log_config import LogConfig - from .murmur_connection import MurmurConnection - from .services.database_service import DatabaseService - from .services.cmd_processing_service import CommandProcessingService - - -@singleton -class MumimoSettings: - connection: "Connection" - plugins: "Plugins" - configs: "Configs" - commands: "Commands" - state: "State" - database: "Database" - - class Connection: - _murmur_connection: Optional["MurmurConnection"] = None - - def set_murmur_connection(self, murmur_connection: "MurmurConnection") -> None: - self._murmur_connection = murmur_connection - - def get_murmur_connection(self) -> Optional["MurmurConnection"]: - return self._murmur_connection - - class Plugins: - _registered_plugins: Dict[str, "PluginBase"] = {} - - def get_registered_plugins(self) -> Dict[str, "PluginBase"]: - return self._registered_plugins - - def get_registered_plugin(self, plugin_name: str) -> Optional["PluginBase"]: - return self._registered_plugins.get(plugin_name) - - def set_registered_plugin(self, plugin_name: str, plugin: "PluginBase") -> None: - self._registered_plugins[plugin_name] = plugin - - class Configs: - _mumimo_cfg: Optional["Config"] = None - _log_cfg: Optional["LogConfig"] = None - _gui_themes: Optional["Config"] = None - - def get_mumimo_config(self) -> Optional["Config"]: - return self._mumimo_cfg - - def set_mumimo_config(self, cfg: "Config") -> None: - self._mumimo_cfg = cfg - - def get_log_config(self) -> Optional["LogConfig"]: - return self._log_cfg - - def set_log_config(self, cfg: "LogConfig") -> None: - self._log_cfg = cfg - - def get_gui_themes(self) -> Optional["Config"]: - return self._gui_themes - - def set_gui_themes(self, themes: "Config") -> Optional["Config"]: - self._gui_themes = themes - - class State: - _client_state: Optional["ClientState"] = None - - def get_client_state(self) -> Optional["ClientState"]: - return self._client_state - - def set_client_state(self, client_state: "ClientState") -> None: - self._client_state = client_state - - class Commands: - history: "History" - callbacks: "Callbacks" - services: "Services" - - def __init__(self) -> None: - self.history = self.History() - self.callbacks = self.Callbacks() - self.services = self.Services() - - class Services: - _cmd_processing_service: Optional["CommandProcessingService"] = None - - def set_cmd_processing_service(self, service: "CommandProcessingService") -> None: - self._cmd_processing_service = service - - def get_cmd_processing_service(self) -> Optional["CommandProcessingService"]: - return self._cmd_processing_service - - class History: - _cmd_history: Optional["CommandHistory"] = None - - def get_command_history(self) -> Optional["CommandHistory"]: - return self._cmd_history - - def set_command_history(self, history: "CommandHistory") -> None: - self._cmd_history = history - - def add_command_to_history(self, command: "Command") -> Optional["Command"]: - if self._cmd_history: - return self._cmd_history.add(command) - return None - - class Callbacks: - _cmd_callbacks: Optional["CommandCallbacks"] = None - - def get_command_callbacks(self) -> Optional["CommandCallbacks"]: - return self._cmd_callbacks - - def set_command_callbacks(self, callbacks: "CommandCallbacks") -> None: - self._cmd_callbacks = callbacks - - def add_command_callbacks(self, callbacks: "CommandCallbacks") -> None: - if self._cmd_callbacks is None: - self._cmd_callbacks = CommandCallbacks() - self._cmd_callbacks.update(callbacks) - - def get_callback(self, plugin: str, command: str) -> Optional[Dict[str, Any]]: - _all_callbacks = self.get_command_callbacks() - if _all_callbacks is None: - return None - return _all_callbacks.get(f"{plugin}.{command}", None) - - def get_callbacks(self, plugin: str) -> List[Dict[str, Any]]: - _all_callbacks = self.get_command_callbacks() - if _all_callbacks is None: - logger.warning(f"Unable to retrieve callbacks for plugin '{plugin}'.") - return [] - _plugin_callbacks = [] - for idx, (_cmd, _clbk) in enumerate(_all_callbacks.items()): - if _clbk["plugin"] == plugin: - _plugin_callbacks.append(_clbk) - return _plugin_callbacks - - class Database: - _database_instance: Optional["DatabaseService"] = None - - def get_database_instance(self) -> Optional["DatabaseService"]: - return self._database_instance - - def set_database_instance(self, instance: "DatabaseService") -> None: - self._database_instance = instance - - def __init__(self) -> None: - self.commands = self.Commands() - self.configs = self.Configs() - self.connection = self.Connection() - self.plugins = self.Plugins() - self.state = self.State() - self.database = self.Database() - - -settings = MumimoSettings() diff --git a/src/system_arguments.py b/src/system_arguments.py deleted file mode 100644 index 4a77a5c..0000000 --- a/src/system_arguments.py +++ /dev/null @@ -1,80 +0,0 @@ -import argparse - -from . import version - -args_parser = argparse.ArgumentParser( - prog="Mumimo", - usage="mumimo ...", - description="A plugin-based All-In-One mumble bot client solution in python 3.8+ with extensive features and support for custom plugins.", -) -args_parser.add_argument( - "-version", - "--version", - help="displays the application version", - action="version", - version=f"Mumimo v{version.version()}", -) - -# Connection system arguments -group_connection = args_parser.add_argument_group("connection") -group_connection.add_argument("-sh", "--host", help="host IP of murmur server", type=str) -group_connection.add_argument("-sp", "--port", help="host port of murmur server", type=int) -group_connection.add_argument("-us", "--user", help="the client username", type=str) -group_connection.add_argument("-ps", "--password", help="the client password", type=str) -group_connection.add_argument("-c", "--cert-file", help="path to user certificate", type=str) -group_connection.add_argument("-k", "--key-file", help="path to private key associated with user certificate", type=str) -group_connection.add_argument("-t", "--tokens", help="channel access tokens as a list of strings", type=str) -group_connection.add_argument("-ar", "--auto-reconnect", help="attempt to reconnect to the server if disconnected", action="store_true") -group_connection.add_argument("-su", "--superuser", help="the user profile that has full access to this client", type=str) -group_connection.add_argument("-n", "--name", help="changes the display name of the bot client to the specified name", type=str) - -# Other system arguments -group_other = args_parser.add_argument_group("other") -group_other.add_argument("-pp", "--plugins-path", help="use a custom path for plugins", type=str) -group_other.add_argument("-cpp", "--custom-plugins-path", help="specify a path for custom plugins", type=str) -group_other.add_argument("-pcp", "--plugins-config-path", help="use a custom path for plugin metadata file storage", type=str) -group_other.add_argument("-cf", "--config-file", help="use a custom config file from the given path", type=str) -group_other.add_argument("-lcf", "--log-config-file", help="use a custom config file for logging from the given path", type=str) -group_other.add_argument("-gtf", "--gui-themes-file", help="ust a custom gui themes file for loading gui themes", type=str) -group_other.add_argument("-e", "--env-file", help="use connection options from an environment file", type=str) -group_other.add_argument("-v", "--verbose", help="enables verbose and debug messages to output to the console", action="count", default=0) -group_other.add_argument( - "-gc", - "--generate-cert", - help="automatically generates a self-signed certificate if none exists", - action="store_true", -) - -# Database system arguments -group_database = args_parser.add_argument_group("database") -group_database.add_argument( - "-rdb", - "--use-remote-database", - help="enables usage of a custom remote database instead of the default local sqlite database", - action="store_true", -) -group_database.add_argument("-ldbp", "--local-database-path", help="specify the local database path to use for non-remote database usage", type=str) -group_database.add_argument( - "-ldbdr", "--local-database-dialect", help="specify the local database dialect to use for non-remote database usage", type=str -) -group_database.add_argument( - "-ldbdi", "--local-database-driver", help="specify the local database driver to use for non-remote database usage", type=str -) - -group_database.add_argument( - "-dbdi", - "--db-dialect", - help="specify the database dialect based on the database type for remote connections (ex: sqlite, postgresql, mysql, etc.)", - type=str, -) -group_database.add_argument( - "-dbdr", - "--db-driver", - help="specify the database connection driver based on the database type for remote connections (ex: aiosqlite, pymyssql, pyodbc, pymysql, etc.)", - type=str, -) -group_database.add_argument("-dbus", "--db-user", help="specify the database connection username for remote connections", type=str) -group_database.add_argument("-dbps", "--db-pass", help="specify the database connection password for remote connections", type=str) -group_database.add_argument("-dbh", "--db-host", help="specify the database connection host for remote connections", type=str) -group_database.add_argument("-dbp", "--db-port", help="specify the database connection port for remote connections", type=str) -group_database.add_argument("-dbn", "--db-name", help="specify the database name for remote connections", type=str) diff --git a/src/templates/plugins/metadata_template.toml b/src/templates/plugins/metadata_template.toml deleted file mode 100644 index 07ee3b5..0000000 --- a/src/templates/plugins/metadata_template.toml +++ /dev/null @@ -1,12 +0,0 @@ -[plugin] -enabled = false - -[plugin.about] -name = "placeholder" -version = "0.0.1" -description = "placeholder" - -[plugin.commands] -disable_commands = [] -disable_parameters = [] -default_permission_groups = [] diff --git a/src/utils/__init__.py b/src/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/args_validators.py b/src/utils/args_validators.py deleted file mode 100644 index 2b84ee1..0000000 --- a/src/utils/args_validators.py +++ /dev/null @@ -1,92 +0,0 @@ -import logging -from typing import Union - -import validators - -from ..constants import VERBOSITY_MAX, VERBOSITY_MIN -from ..exceptions import ValidationError - -logger = logging.getLogger(__name__) - - -class SystemArgumentsValidator: - @staticmethod - def validate_host_param(host: str) -> None: - if host is None: - raise ValidationError("The server host ip/url must be provided.", logger) - host = host.strip() - if len(host) == 0: - raise ValidationError("The server host ip/url cannot be empty.", logger) - _validate = validators.ipv4(host) or validators.ipv6(host) or validators.url(host) # pyright: reportGeneralTypeIssues=false - if isinstance(_validate, validators.ValidationFailure): - raise ValidationError(f"The provided host: '{host}' is not a valid host ip or url.", logger) - - @staticmethod - def validate_port_param(port: Union[str, int]) -> None: - if port is None: - raise ValidationError("The server port must be provided.", logger) - if isinstance(port, str) and len(port.strip()) == 0: - raise ValidationError("The server port cannot be empty.", logger) - try: - int_port: int = int(port) - except ValueError as exc: - raise ValidationError(f"The server port: '{port}' is not a valid port number.", logger) from exc - if int_port < 1 or int_port > 65535: - raise ValidationError(f"The server port: '{port}' is not a valid port number.", logger) - - @staticmethod - def validate_user_param(user: str) -> None: - if user is None: - raise ValidationError("The client user must be provided.", logger) - if not isinstance(user, str): - raise ValidationError("The client user must be a valid string.", logger) - if len(user.strip()) == 0: - raise ValidationError("The client username cannot be empty.", logger) - - @staticmethod - def validate_password_param(password: str) -> None: - if password is not None: - if not isinstance(password, str): - raise ValidationError("The server password must be a valid string.", logger) - if len(password.strip()) == 0: - raise ValidationError("The server password cannot be empty.", logger) - - @staticmethod - def validate_cert_param(cert: str) -> None: - if cert is not None: - if not isinstance(cert, str): - raise ValidationError("The client certificate file path must be a valid string.", logger) - if len(cert.strip()) == 0: - raise ValidationError("The client certificate file path cannot be empty.", logger) - - @staticmethod - def validate_key_param(key: str) -> None: - if key is not None: - if not isinstance(key, str): - raise ValidationError("The client key file path must be a valid string.", logger) - if len(key.strip()) == 0: - raise ValidationError("The client key file path cannot be empty.", logger) - - @staticmethod - def validate_tokens_param(tokens: str) -> None: - if tokens is not None: - if not isinstance(tokens, str): - raise ValidationError("The client channels access tokens must be a valid string.", logger) - if len(tokens.strip()) == 0: - raise ValidationError("The client channels access tokens cannot be empty.", logger) - - @staticmethod - def validate_auto_reconnect_param(reconnect: bool) -> None: - if reconnect is None: - raise ValidationError("The reconnect option can only be True or False, not None.", logger) - if not isinstance(reconnect, bool): - raise ValidationError("The reconnect option can only be True or False.", logger) - - @staticmethod - def validate_verbose_param(verbose: int) -> None: - if verbose is None: - raise ValidationError(f"The verbose option can only be a number from {VERBOSITY_MIN} to {VERBOSITY_MAX}, not None.", logger) - if not isinstance(verbose, int): - raise ValidationError(f"The verbose option can only be a number from {VERBOSITY_MIN} to {VERBOSITY_MAX}.", logger) - if VERBOSITY_MIN > verbose or verbose > VERBOSITY_MAX: - raise ValidationError(f"The verbose option can only be a number from {VERBOSITY_MIN} to {VERBOSITY_MAX}.", logger) diff --git a/src/utils/connection_utils.py b/src/utils/connection_utils.py deleted file mode 100644 index 7eadc1a..0000000 --- a/src/utils/connection_utils.py +++ /dev/null @@ -1,16 +0,0 @@ -import platform -from typing import List, Optional - - -def is_supported_platform() -> bool: - return platform.system() in ("Linux", "Darwin") - - -def parse_channel_tokens(tokens: str) -> Optional[List[str]]: - if not isinstance(tokens, str): - return None - tokens = tokens.strip() - if not tokens: - return None - tokens = " ".join(tokens.split()) - return [token.strip() for token in tokens.split()] diff --git a/src/utils/log_utils.py b/src/utils/log_utils.py deleted file mode 100644 index 8b7ad0d..0000000 --- a/src/utils/log_utils.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Any, Dict, Optional - -from ..constants import DEFAULT_PATH_LOGGING_CONFIG_FILE, LogCfgSections -from ..log_config import LogConfig -from ..settings import settings - - -def initialize_log_config(cfg_path: Optional[str] = None) -> "LogConfig": - # Load in configurations from custom config file path if present. - # Otherwise load configurations from default config file path. - _cfg_instance: Optional["LogConfig"] = settings.configs.get_log_config() - if _cfg_instance is None: - if not cfg_path: - cfg_path = DEFAULT_PATH_LOGGING_CONFIG_FILE - _cfg_instance = LogConfig(cfg_path) - _cfg_instance.read(cfg_path) - settings.configs.set_log_config(_cfg_instance) - return _cfg_instance - - -def privacy_file_redact_all_check(): - _log_config = settings.configs.get_log_config() - if _log_config is None: - return False - file_privacy_dict: Dict[str, Any] = _log_config.get(LogCfgSections.OUTPUT_FILE_PRIVACY, {}) - return all(file_privacy_dict.values()) - - -def privacy_console_redact_all_check(): - _log_config = settings.configs.get_log_config() - if _log_config is None: - return False - console_privacy_dict: Dict[str, Any] = _log_config.get(LogCfgSections.OUTPUT_CONSOLE_PRIVACY, {}) - return all(console_privacy_dict.values()) diff --git a/src/utils/mumble_utils.py b/src/utils/mumble_utils.py deleted file mode 100644 index 8a85953..0000000 --- a/src/utils/mumble_utils.py +++ /dev/null @@ -1,268 +0,0 @@ -import asyncio -import logging -import time -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union - -from pymumble_py3.channels import Channel -from pymumble_py3.errors import UnknownChannelError -from pymumble_py3.users import User - -from sqlalchemy import select - -from ..lib.database.models.permission_group import PermissionGroupTable -from ..lib.database.models.user import UserTable -from ..exceptions import ServiceError -from ..settings import settings -from ..constants import DefaultPermissionGroups, LogOutputIdentifiers - -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from pymumble_py3 import Mumble - - from ..murmur_connection import MurmurConnection - from ..services.database_service import DatabaseService - - -def echo( - text: str, - user_id: Optional[int] = None, - delay: Optional[int] = None, - target_channels: Optional[Union[List["Channel"], "Channel"]] = None, - target_users: Optional[Union[List["User"], "User"]] = None, - log_severity: int = logging.DEBUG, - raw_text: Optional[str] = None, - **kwargs, -) -> None: - if text is None: - logger.warning("Unable to echo message: 'text' value is missing.") - return - if target_channels is None: - target_channels = [] - if target_users is None: - target_users = [] - if isinstance(target_channels, Channel): - target_channels = [target_channels] - if isinstance(target_users, User): - target_users = [target_users] - if raw_text is None: - raw_text = text - - if user_id: - _user = get_user_by_id(user_id) - if not _user: - _user = "n/a" - else: - _user = _user["name"] - else: - _user = "Mumimo" - - _delay = 0 - if delay is not None and delay > 0: - _delay = delay - - if target_channels: - if _delay > 0: - time.sleep(_delay) - for channel in target_channels: - channel.send_text_message(text.strip()) - logger.log( - level=log_severity, - msg=f"'{_user}' echoed [Channel->{channel['name']}]: {raw_text.strip()}", - ) - elif target_users: - if _delay > 0: - time.sleep(_delay) - for user in target_users: - user.send_text_message(text.strip()) - logger.log( - level=log_severity, - msg=f"'{_user}' echoed [User->{user['name']}]: {raw_text.strip()}", - ) - else: - _channel: Optional["Channel"] = get_my_channel() - if _channel is None: - logger.warning("Channel object was not found: the current channel could not be retrieved.") - return None - if _delay > 0: - time.sleep(_delay) - _channel.send_text_message(text.strip()) - logger.log( - level=log_severity, - msg=f"'{_user}' echoed [Channel->{_channel['name']}]: {raw_text.strip()}", - ) - - -def get_user_by_id(id: int) -> Optional["User"]: - _inst: Optional["Mumble"] = Management.get_connection_instance() - if _inst is not None: - try: - return _inst.users[id] - except KeyError: - return None - return None - - -def get_user_by_name(name: str) -> Optional["User"]: - _inst: Optional["Mumble"] = Management.get_connection_instance() - if _inst is not None: - try: - for user in _inst.users.values(): - if user["name"] == name: - return user - except KeyError: - return None - return None - - -def get_channel_by_user(user: Union["User", str]) -> Optional["Channel"]: - if isinstance(user, str): - _user: Optional["User"] = get_user_by_name(user) - if _user is None: - return None - else: - _user: Optional["User"] = user - - _inst: Optional["Mumble"] = Management.get_connection_instance() - if _inst is not None: - try: - return _inst.channels[_user["channel_id"]] - except KeyError: - return None - return None - - -def get_my_channel() -> Optional["Channel"]: - _inst: Optional["Mumble"] = Management.get_connection_instance() - if _inst is not None: - _myself = _inst.users.myself - if _myself is not None: - return _inst.channels[_myself["channel_id"]] - return None - - -def get_all_channels() -> List["Channel"]: - _channels: List["Channel"] = [] - _inst: Optional["Mumble"] = Management.get_connection_instance() - if _inst is not None: - for _, channel in enumerate(_inst.channels.items()): - _channel: "Channel" = channel[1] - _channels.append(_channel) - return _channels - return [] - - -def get_channel_by_name(channel_name: str) -> Optional["Channel"]: - _inst: Optional["Mumble"] = Management.get_connection_instance() - if _inst is not None: - try: - return _inst.channels.find_by_name(channel_name.strip()) - except UnknownChannelError: - return None - return None - - -class Management: - class UserManagement: - @staticmethod - async def add_user(actor: Dict[str, Any]) -> None: - _db_service: Optional["DatabaseService"] = settings.database.get_database_instance() - if not _db_service: - raise ServiceError("Unable to add new user: the database service could not retrieve the database instance.", logger=logger) - - _actor: Optional["User"] = get_user_by_name(actor["name"]) - if not _actor: - raise ServiceError("Unable to add new user: the user could not be retrieved from the actor name.") - - async with _db_service.session() as session: - _user_query = await session.execute(select(UserTable).filter_by(name=_actor["name"])) - _user_info: Optional[UserTable] = _user_query.scalar() - if _user_info: - logger.warning(f"Unable to add new user: the user '{_actor['name']}' already exists in the database.") - else: - _user_info = UserTable(name=actor["name"]) - _permission_query = await session.execute(select(PermissionGroupTable).filter_by(name=DefaultPermissionGroups.DEFAULT_GUEST)) - _permission_info: Optional[PermissionGroupTable] = _permission_query.scalar() - if not _permission_info: - raise ServiceError( - f"Unable to add new user: the default permission '{DefaultPermissionGroups.DEFAULT_GUEST}' is not in the database." - ) - _user_info.permission_groups.append(_permission_info) - session.add(_user_info) - await session.commit() - logger.debug(f"[{LogOutputIdentifiers.DB_USERS}]: Added new user to the database -> [{_actor['name']}]") - - _client_state = settings.state.get_client_state() - if not _client_state: - raise ServiceError("Unable to add new user: the client state could not be retrieved.") - _server_state = _client_state.server_properties.state - if _server_state.add_user(_actor): - logger.debug(f"Added new user '{_actor['name']}' to the server state.") - else: - logger.error(f"Unable to add new user '{_actor['name']}' to the server state.") - await session.rollback() - return - - @staticmethod - async def remove_user(actor: Dict[str, Any]) -> None: - _db_service: Optional["DatabaseService"] = settings.database.get_database_instance() - if not _db_service: - raise ServiceError("Unable to remove user: the database service could not retrieve the database instance.", logger=logger) - - _actor: Optional["User"] = get_user_by_name(actor["name"]) - if not _actor: - raise ServiceError("Unable to remove user: the user could not be retrieved from the actor name.") - - async with _db_service.session() as session: - _user_query = await session.execute(select(UserTable).filter_by(name=_actor["name"])) - _user_info: Optional[UserTable] = _user_query.scalar() - if not _user_info: - logger.warning(f"Unable to remove user: the user '{_actor['name']}' does not exist in the database.") - else: - _user_info = UserTable(name=actor["name"]) - await session.delete(_user_info) - await session.commit() - logger.debug(f"[{LogOutputIdentifiers.DB_USERS}]: Removed user from the database -> [{_actor['name']}]") - - _client_state = settings.state.get_client_state() - if not _client_state: - raise ServiceError("Unable to remove user: the client state could not be retrieved.") - _server_state = _client_state.server_properties.state - if _server_state.remove_user(_actor): - logger.debug(f"Removed user '{_actor['name']}' to the server state.") - else: - logger.error(f"Unable to remove user '{_actor['name']}' from the server state.") - await session.rollback() - return - - @staticmethod - def get_connection_instance() -> Optional["Mumble"]: - _conn: Optional["MurmurConnection"] = settings.connection.get_murmur_connection() - if _conn is not None: - _inst = _conn.connection_instance - if _inst is not None: - return _inst - return None - - @staticmethod - async def exit_server(): - import sys - - logger.info("Gracefully exiting Mumimo client...") - all_plugins: Dict[str, Any] = settings.plugins.get_registered_plugins() - logger.info("Gracefully exiting plugins...") - for _, plugin in all_plugins.items(): - plugin.quit() - _murmur_instance = settings.connection.get_murmur_connection() - if _murmur_instance is not None: - logger.info("Disconnecting from Murmur server...") - if _murmur_instance.stop(): - logger.info("Disconnected from Murmur server.") - _db_service = settings.database.get_database_instance() - if _db_service: - await _db_service.close(clean=True) - _async_runners = await asyncio.gather() - for runner in _async_runners: - runner.cancel("Mumimo shutting down.") - logger.info("Mumimo client closed.") - sys.exit(0) diff --git a/src/utils/parsers/__init__.py b/src/utils/parsers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/parsers/cmd_parser.py b/src/utils/parsers/cmd_parser.py deleted file mode 100644 index ed7895a..0000000 --- a/src/utils/parsers/cmd_parser.py +++ /dev/null @@ -1,92 +0,0 @@ -from typing import TYPE_CHECKING, List, Optional - -from ...constants import MumimoCfgFields -from ...exceptions import ServiceError -from ...lib.command import Command -from ...settings import settings - -if TYPE_CHECKING: - from pymumble_py3.mumble import Mumble - - -def parse_command(text) -> Optional[Command]: - if not text: - return None - message = text.message - if not message: - return None - - actor = text.actor - channel_id = text.channel_id - if channel_id: - channel_id = channel_id[0] - session_id = text.session - if session_id: - session_id = session_id[0] - message: str = message.strip() - if not message: - return None - - _cfg_instance = settings.configs.get_mumimo_config() - if _cfg_instance is None: - raise ServiceError("Unable to process commands: mumimo config is not initialized.") - if message[0] != _cfg_instance.get(MumimoCfgFields.SETTINGS.COMMANDS.TOKEN, "!"): - return Command(message=message, actor=actor, channel_id=channel_id, session_id=session_id) - - text_parse_list: List[str] = message.split(" ", 1) - cmd_parse_list: List[str] = text_parse_list[0].split(".", 1) - - cmd: Optional[str] = cmd_parse_list[0][1:] - if not cmd: - cmd = None - - try: - parameters = cmd_parse_list[1].split(".") - parameters = [opt for opt in parameters if opt] - except IndexError: - parameters = None - - try: - message_body = text_parse_list[1] - except IndexError: - message_body = "" - - return Command(command=cmd, parameters=parameters, message=message_body, actor=actor, channel_id=channel_id, session_id=session_id) - - -def parse_actor_name(command: "Command", connection_instance: "Mumble") -> str: - try: - actor_name: Optional[str] = connection_instance.users[command.actor]["name"] - except KeyError: - actor_name = "Server" - if actor_name is None: - actor_name = "Unavailable" - return actor_name - - -def parse_channel_name(command: "Command", connection_instance: "Mumble") -> str: - try: - channel_name: Optional[str] = connection_instance.channels[command.channel_id]["name"] - except Exception: # protobuf error raised here if the channel_id does not exist. - channel_name = "Server" - if command.is_private: - channel_name = "Private" - if channel_name is None: - channel_name = "Unavailable" - return channel_name - - -def parse_message_image_data(command: "Command") -> str: - if " str: - if " str: - _port = None - _host = None - _user = None - if create_url: - if connection_params.local_database_dialect is None: - connection_params.local_database_dialect = "sqlite" # Set a default dialect if none is set. - _drivername = connection_params.local_database_dialect - _database = connection_params.local_database_path - else: - if connection_params.use_remote: - _drivername = f"{connection_params.dialect}+{connection_params.drivername}" - _database = connection_params.database_name - _user = connection_params.username - _host = connection_params.host - if connection_params.port is not None: - _port = int(connection_params.port) - else: - if connection_params.local_database_dialect is None: - connection_params.local_database_dialect = "sqlite" # Set a default dialect if none is set. - if connection_params.local_database_driver is None: - connection_params.local_database_driver = "aiosqlite" - _drivername = f"{connection_params.local_database_dialect}+{connection_params.local_database_driver}" - _database = connection_params.local_database_path - - url = URL.create( - drivername=_drivername, - username=_user, - password=connection_params.password, - host=_host, - port=_port, - database=_database, - ) - return url.render_as_string(hide_password=False) diff --git a/src/utils/parsers/env_parser.py b/src/utils/parsers/env_parser.py deleted file mode 100644 index 958cff3..0000000 --- a/src/utils/parsers/env_parser.py +++ /dev/null @@ -1,23 +0,0 @@ -import pathlib -from typing import Dict, Optional - -from dotenv import dotenv_values - - -def get_env_file(file_name: str) -> Optional[pathlib.Path]: - # Check file path is valid: - if file_name is None or len(file_name) == 0: - return None - # Check if file exists: - rel_path = pathlib.Path.cwd() / file_name - if rel_path.is_file(): - return rel_path - return None - - -def read_env_file(file_name: str) -> Dict[str, Optional[str]]: - env_path = get_env_file(file_name) - if env_path is None: - raise Exception(f"Could not find environment file at: {file_name}") - env_vars = dotenv_values(dotenv_path=env_path) - return env_vars diff --git a/src/version.py b/src/version.py deleted file mode 100644 index d8abb63..0000000 --- a/src/version.py +++ /dev/null @@ -1,11 +0,0 @@ -import re - -VERSION: str = "0.0.1" - - -def version(): - return VERSION - - -def validate_version() -> bool: - return re.fullmatch(r"\d{1,3}.\d{1,3}.\d{1,3}", version()) is not None diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/data/__init__.py b/tests/data/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/data/config/test_config.toml b/tests/data/config/test_config.toml deleted file mode 100644 index dbe66e7..0000000 --- a/tests/data/config/test_config.toml +++ /dev/null @@ -1,14 +0,0 @@ -[test_section_1] -test_1 = "test_1" -test_2 = 2 -test_3 = ["test_3"] -test_4 = true - -[test_section_2] -test_1 = "test_1" -test_2 = 2 -test_3 = ["test_3"] -test_4 = true - -[test_section_1.nested] -test_1 = "test_1" diff --git a/tests/data/config/test_config_alt.toml b/tests/data/config/test_config_alt.toml deleted file mode 100644 index 3ca176e..0000000 --- a/tests/data/config/test_config_alt.toml +++ /dev/null @@ -1,11 +0,0 @@ -[test_section_0] -test_1 = "test_1" -test_2 = 2 -test_3 = ["test_3"] -test_4 = true - -[test_section_1] -test_1 = "test_1" -test_2 = 2 -test_3 = ["test_3"] -test_4 = true diff --git a/tests/data/config/test_config_broken.toml b/tests/data/config/test_config_broken.toml deleted file mode 100644 index 99252fb..0000000 --- a/tests/data/config/test_config_broken.toml +++ /dev/null @@ -1,11 +0,0 @@ -test_section_0 -test_1 = "test_1 -test_2 = 2 -test-3 = ["test_3",] -test_4 = True - -[test_section_1] -test_1 = "test_1" -test_2 = 2 -test_3 = ["test_3",] -test_4 = true diff --git a/tests/data/config/test_logging.toml b/tests/data/config/test_logging.toml deleted file mode 100644 index 05b910f..0000000 --- a/tests/data/config/test_logging.toml +++ /dev/null @@ -1,21 +0,0 @@ -[output.file] -enable = true -level = "DEBUG" -path = "tests/data/generated/logs/" -format = "(%(asctime)s)[%(name)s][%(levelname)s]::%(message)s" -name = "mumimo_test_%s.log" - -[output.file.privacy] -redact_message = true -redact_commands = true -redact_channel = true -redact_user = true - -[output.console] -format = "[%(levelname)s]::%(message)s" - -[output.console.privacy] -redact_message = false -redact_commands = false -redact_channel = false -redact_user = false diff --git a/tests/data/config/test_valid_config.toml b/tests/data/config/test_valid_config.toml deleted file mode 100644 index c4d6b84..0000000 --- a/tests/data/config/test_valid_config.toml +++ /dev/null @@ -1,6 +0,0 @@ -[main] -mute = true -deafen = true -register = false -comment = "" -reconnect = false diff --git a/tests/data/test.env b/tests/data/test.env deleted file mode 100644 index ec780c5..0000000 --- a/tests/data/test.env +++ /dev/null @@ -1,15 +0,0 @@ -MUMIMO_HOST=test_host -MUMIMO_PORT=12345 -MUMIMO_USER=test_user -MUMIMO_PASS=test_pass -MUMIMO_CERT=test_cert -MUMIMO_KEY=test_key -MUMIMO_TOKENS=test_token_1 test_token2 -MUMIMO_SUPER_USER=test_super_user -MUMIMO_DB_DIALECT=sqlite -MUMIMO_DB_DRIVER=aiosqlite -MUMIMO_DB_USER=test_mumimo -MUMIMO_DB_PASS=test_mumimo -MUMIMO_DB_HOST=test_mumimo_db -MUMIMO_DB_NAME=test_mumimo -MUMIMO_DB_PORT=12345 \ No newline at end of file diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/config_fixtures.py b/tests/fixtures/config_fixtures.py deleted file mode 100644 index e85c45e..0000000 --- a/tests/fixtures/config_fixtures.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any, Dict - -import pytest - -from src.constants import MumimoCfgFields - - -@pytest.fixture(autouse=True) -def valid_config_params() -> Dict[str, Any]: - _connection_params = { - MumimoCfgFields.SETTINGS.CONNECTION.MUTE: True, - MumimoCfgFields.SETTINGS.CONNECTION.DEAFEN: True, - MumimoCfgFields.SETTINGS.CONNECTION.REGISTER: False, - MumimoCfgFields.SETTINGS.CONNECTION.COMMENT: "test", - MumimoCfgFields.SETTINGS.CONNECTION.AUTO_RECONNECT: False, - } - return _connection_params diff --git a/tests/fixtures/connection_fixtures.py b/tests/fixtures/connection_fixtures.py deleted file mode 100644 index 71d477c..0000000 --- a/tests/fixtures/connection_fixtures.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Any, Dict - -import pytest - -from src.constants import SysArgs - - -@pytest.fixture(autouse=True) -def valid_connection_params() -> Dict[str, Any]: - return { - SysArgs.SYS_HOST: "127.0.0.1", - SysArgs.SYS_PORT: "64738", - SysArgs.SYS_USER: "test", - SysArgs.SYS_PASS: "test", - SysArgs.SYS_CERT: "path/to/cert", - SysArgs.SYS_TOKENS: "token1 token2", - SysArgs.SYS_KEY: "path/to/key", - SysArgs.SYS_RECONNECT: False, - SysArgs.SYS_VERBOSE: 0, - } - - -@pytest.fixture(autouse=True) -def invalid_connection_params() -> Dict[str, Any]: - return { - SysArgs.SYS_HOST: "hello", - SysArgs.SYS_PORT: "hello", - SysArgs.SYS_USER: "test", - SysArgs.SYS_PASS: "test", - SysArgs.SYS_CERT: "path/to/cert", - SysArgs.SYS_TOKENS: "token1 token2", - SysArgs.SYS_KEY: "path/to/key", - SysArgs.SYS_RECONNECT: False, - SysArgs.SYS_VERBOSE: 0, - } diff --git a/tests/src/__init__.py b/tests/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/src/lib/database/models/fixtures.py b/tests/src/lib/database/models/fixtures.py deleted file mode 100644 index 5883c77..0000000 --- a/tests/src/lib/database/models/fixtures.py +++ /dev/null @@ -1,47 +0,0 @@ -import asyncio -from typing import AsyncGenerator - -import pytest -import sqlalchemy_utils -from sqlalchemy.ext.asyncio import AsyncSession, async_scoped_session, async_sessionmaker, create_async_engine - -from src.lib.database.database_connection_parameters import DatabaseConnectionParameters -from src.lib.database.metadata import Base - -# Imports are required here so tables can be created for tests. -from src.lib.database.models.alias import AliasTable # noqa -from src.lib.database.models.command import CommandTable # noqa -from src.lib.database.models.permission_group import PermissionGroupTable # noqa -from src.lib.database.models.plugin import PluginTable # noqa -from src.lib.database.models.user import UserTable # noqa -from src.utils.parsers.db_url_parser import get_url - - -@pytest.fixture(scope="function") -async def get_db_session_factory() -> AsyncGenerator[async_scoped_session, None]: - _dialect: str = "sqlite" - _host: str = "tests/data/generated/mumimo_test.db" - _drivername: str = "aiosqlite" - _connection_parameters = DatabaseConnectionParameters( - dialect=_dialect, - host=_host, - drivername=_drivername, - ) - _create_db_url: str = get_url(_connection_parameters, create_url=True) - _async_url: str = get_url(_connection_parameters, create_url=False) - if not sqlalchemy_utils.database_exists(_create_db_url): - sqlalchemy_utils.create_database(_create_db_url) - _engine = create_async_engine(_async_url, echo=False) - async with _engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - _session_factory = async_scoped_session( - session_factory=async_sessionmaker(_engine, class_=AsyncSession, expire_on_commit=False), - scopefunc=asyncio.current_task, - ) - yield _session_factory - # Teardown: - if _engine is not None: - await _engine.dispose(close=True) - _connection_parameters = None - _session_factory = None - _engine = None diff --git a/tests/src/lib/database/models/test_alias_model.py b/tests/src/lib/database/models/test_alias_model.py deleted file mode 100644 index 60b1e45..0000000 --- a/tests/src/lib/database/models/test_alias_model.py +++ /dev/null @@ -1,210 +0,0 @@ -import pytest -from sqlalchemy import delete, select -from sqlalchemy.exc import IntegrityError -from sqlalchemy.ext.asyncio import async_scoped_session - -from src.lib.database.models.alias import AliasTable -from src.lib.database.models.permission_group import PermissionGroupTable - - -class TestCommandModel: - @pytest.fixture(autouse=True) - async def alias(self) -> AliasTable: - alias: AliasTable = AliasTable( - name="test", - command="test", - permission_groups=[PermissionGroupTable(name="test")], - ) - return alias - - @pytest.mark.asyncio - async def test_to_dict(self, alias: AliasTable) -> None: - assert alias.to_dict() == { - "id": alias.id, - "name": alias.name, - "command": alias.command, - "permission_groups": [x.to_dict() for x in alias.permission_groups], - "created_on": alias.created_on, - "updated_on": alias.updated_on, - } - - @pytest.mark.asyncio - async def test_repr(self, alias: AliasTable) -> None: - assert ( - str(alias) == f"Alias(id={alias.id!r}, name={alias.name!r}, command={alias.command!r}, permission_groups={alias.permission_groups}, " - f"created_on={alias.created_on!r}, updated_on={alias.updated_on!r})" - ) - - class TestModelIO: - @pytest.mark.asyncio - async def test_create_alias(self): - alias: AliasTable = AliasTable( - name="test", - command="test", - permission_groups=[PermissionGroupTable(name="test")], - ) - assert alias.name == "test" - assert alias.command == "test" - assert len(alias.permission_groups) > 0 - assert isinstance(alias.permission_groups[0], PermissionGroupTable) is True - assert alias.permission_groups[0].name == "test" - - @pytest.mark.asyncio - async def test_update_alias(self, alias: AliasTable): - assert alias.name == "test" - assert alias.command == "test" - assert len(alias.permission_groups) > 0 - assert isinstance(alias.permission_groups[0], PermissionGroupTable) is True - assert alias.permission_groups[0].name == "test" - alias.name = "new_test" - alias.command = "new_test" - alias.permission_groups[0].name = "new_test" - assert alias.name == "new_test" - assert alias.command == "new_test" - assert alias.permission_groups[0].name == "new_test" - - class TestDatabaseIO: - @pytest.mark.asyncio - async def test_add_alias(self, alias: AliasTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new alias "test" - session.add(alias) - await session.commit() - # Assert the "test" alias exists - _query = select(AliasTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("alias not found, aborting test.") - assert _result.name == "test" - assert _result.command == "test" - assert len(_result.permission_groups) > 0 - assert isinstance(_result.permission_groups[0], PermissionGroupTable) is True - assert _result.permission_groups[0].name == "test" - # Assert the "test" permission group added by the alias exists - _query = select(PermissionGroupTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("permission group not found, aborting test.") - assert _result.name == "test" - - @pytest.mark.asyncio - async def test_add_and_update_alias(self, alias: AliasTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new alias "test" - session.add(alias) - await session.commit() - # Assert the "test" alias exists - _query = select(AliasTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("alias not found, aborting test.") - assert _result.name == "test" - assert _result.command == "test" - assert len(_result.permission_groups) > 0 - assert isinstance(_result.permission_groups[0], PermissionGroupTable) is True - assert _result.permission_groups[0].name == "test" - # Update the "test" alias name and command to "new_test" - _result.name = "new_test" - _result.command = "new_test" - await session.commit() - # Assert the "new_test" alias group exists - _query = select(AliasTable).where(AliasTable.name == "new_test") - _result = await session.execute(_query) - _result = _result.scalars().first() - if _result is None: - pytest.fail("updated command not found, aborting test.") - assert _result.name == "new_test" - assert _result.command == "new_test" - - @pytest.mark.asyncio - async def test_add_and_delete_alias(self, alias: AliasTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new alias "test" - session.add(alias) - await session.commit() - # Assert the "test" alias exists - _query = select(AliasTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("command not found, aborting test.") - assert _result.name == "test" - assert _result.command == "test" - assert len(_result.permission_groups) > 0 - assert isinstance(_result.permission_groups[0], PermissionGroupTable) is True - assert _result.permission_groups[0].name == "test" - # Delete the "test" alias - _query = delete(AliasTable).where(AliasTable.name == "test") - _result = await session.execute(_query) - await session.commit() - # Assert the "test" alias group does not exist - _query = select(AliasTable).where(AliasTable.name == "new_test") - _result = await session.execute(_query) - _result = _result.scalars().first() - assert _result is None - - class TestFailures: - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_alias_fails_null_constraints(self, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - alias: AliasTable = AliasTable() - session.add(alias) - try: - await session.commit() - except IntegrityError: - await session.rollback() - - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_alias_fails_null_constraint_name(self, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - alias: AliasTable = AliasTable(name="test") - session.add(alias) - try: - await session.commit() - except IntegrityError: - await session.rollback() - - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_alias_fails_null_constraint_command(self, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - alias: AliasTable = AliasTable(command="test") - session.add(alias) - try: - await session.commit() - except IntegrityError: - await session.rollback() - - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_alias_fails_unique_constraint(self, get_db_session_factory: async_scoped_session): - session: async_scoped_session - async with get_db_session_factory() as session: - alias1: AliasTable = AliasTable( - name="test", - command="test", - permission_groups=[PermissionGroupTable(name="test")], - ) - session.add(alias1) - await session.flush() - alias2: AliasTable = AliasTable( - name="test", - command="test", - permission_groups=[PermissionGroupTable(name="test")], - ) - session.add(alias2) - try: - await session.commit() - except IntegrityError: - await session.rollback() diff --git a/tests/src/lib/database/models/test_command_model.py b/tests/src/lib/database/models/test_command_model.py deleted file mode 100644 index b245548..0000000 --- a/tests/src/lib/database/models/test_command_model.py +++ /dev/null @@ -1,172 +0,0 @@ -import pytest -from sqlalchemy import delete, select -from sqlalchemy.exc import IntegrityError -from sqlalchemy.ext.asyncio import async_scoped_session - -from src.lib.database.models.command import CommandTable -from src.lib.database.models.permission_group import PermissionGroupTable - - -class TestCommandModel: - @pytest.fixture(autouse=True) - async def command(self) -> CommandTable: - command: CommandTable = CommandTable( - name="test", - permission_groups=[PermissionGroupTable(name="test")], - ) - return command - - @pytest.mark.asyncio - async def test_to_dict(self, command: CommandTable) -> None: - assert command.to_dict() == { - "id": command.id, - "name": command.name, - "permission_groups": [x.to_dict() for x in command.permission_groups], - "created_on": command.created_on, - "updated_on": command.updated_on, - } - - @pytest.mark.asyncio - async def test_repr(self, command: CommandTable) -> None: - assert ( - str(command) == f"Command(id={command.id!r}, name={command.name!r}, permission_groups={command.permission_groups}, " - f"created_on={command.created_on!r}, updated_on={command.updated_on!r})" - ) - - class TestModelIO: - @pytest.mark.asyncio - async def test_create_command(self): - command: CommandTable = CommandTable( - name="test", - permission_groups=[PermissionGroupTable(name="test")], - ) - assert command.name == "test" - assert len(command.permission_groups) > 0 - assert isinstance(command.permission_groups[0], PermissionGroupTable) is True - assert command.permission_groups[0].name == "test" - - @pytest.mark.asyncio - async def test_update_command(self, command: CommandTable): - assert command.name == "test" - assert len(command.permission_groups) > 0 - assert isinstance(command.permission_groups[0], PermissionGroupTable) is True - assert command.permission_groups[0].name == "test" - command.name = "new_test" - command.permission_groups[0].name = "new_test" - assert command.name == "new_test" - assert command.permission_groups[0].name == "new_test" - - class TestDatabaseIO: - @pytest.mark.asyncio - async def test_add_command(self, command: CommandTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new command "test" - session.add(command) - await session.commit() - # Assert the "test" command exists - _query = select(CommandTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("command not found, aborting test.") - assert _result.name == "test" - assert len(_result.permission_groups) > 0 - assert isinstance(_result.permission_groups[0], PermissionGroupTable) is True - assert _result.permission_groups[0].name == "test" - # Assert the "test" permission group added by the command exists - _query = select(PermissionGroupTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("command not found, aborting test.") - assert _result.name == "test" - - @pytest.mark.asyncio - async def test_add_and_update_command(self, command: CommandTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new command "test" - session.add(command) - await session.commit() - # Assert the "test" command exists - _query = select(CommandTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("command not found, aborting test.") - assert _result.name == "test" - assert len(_result.permission_groups) > 0 - assert isinstance(_result.permission_groups[0], PermissionGroupTable) is True - assert _result.permission_groups[0].name == "test" - # Update the "test" command name to "new_test" - _result.name = "new_test" - await session.commit() - # Assert the "new_test" command group exists - _query = select(CommandTable).where(CommandTable.name == "new_test") - _result = await session.execute(_query) - _result = _result.scalars().first() - if _result is None: - pytest.fail("updated command not found, aborting test.") - assert _result.name == "new_test" - - @pytest.mark.asyncio - async def test_add_and_delete_command(self, command: CommandTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new command "test" - session.add(command) - await session.commit() - # Assert the "test" command exists - _query = select(CommandTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("command not found, aborting test.") - assert _result.name == "test" - assert len(_result.permission_groups) > 0 - assert isinstance(_result.permission_groups[0], PermissionGroupTable) is True - assert _result.permission_groups[0].name == "test" - # Delete the "test" command - _query = delete(CommandTable).where(CommandTable.name == "test") - _result = await session.execute(_query) - await session.commit() - # Assert the "test" command group does not exist - _query = select(CommandTable).where(CommandTable.name == "new_test") - _result = await session.execute(_query) - _result = _result.scalars().first() - assert _result is None - - class TestFailures: - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_command_fails_null_constraint(self, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - command: CommandTable = CommandTable() - session.add(command) - try: - await session.commit() - except IntegrityError: - await session.rollback() - - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_command_fails_unique_constraint(self, get_db_session_factory: async_scoped_session): - session: async_scoped_session - async with get_db_session_factory() as session: - command1: CommandTable = CommandTable( - name="test", - permission_groups=[PermissionGroupTable(name="test")], - ) - session.add(command1) - await session.flush() - command2: CommandTable = CommandTable( - name="test", - permission_groups=[PermissionGroupTable(name="test")], - ) - session.add(command2) - try: - await session.commit() - except IntegrityError: - await session.rollback() diff --git a/tests/src/lib/database/models/test_permission_group_model.py b/tests/src/lib/database/models/test_permission_group_model.py deleted file mode 100644 index 8e57b96..0000000 --- a/tests/src/lib/database/models/test_permission_group_model.py +++ /dev/null @@ -1,135 +0,0 @@ -import pytest -from sqlalchemy import delete, select -from sqlalchemy.exc import IntegrityError -from sqlalchemy.ext.asyncio import async_scoped_session - -from src.lib.database.models.permission_group import PermissionGroupTable # noqa - - -class TestPermissionGroupModel: - @pytest.mark.asyncio - async def test_to_dict(self) -> None: - permission_group: PermissionGroupTable = PermissionGroupTable(name="test") - assert permission_group.to_dict() == { - "id": permission_group.id, - "name": permission_group.name, - "created_on": permission_group.created_on, - "updated_on": permission_group.updated_on, - } - - @pytest.mark.asyncio - async def test_repr(self) -> None: - permission_group: PermissionGroupTable = PermissionGroupTable(name="test") - assert ( - str(permission_group) == f"PermissionGroup(id={permission_group.id!r}, name={permission_group.name!r}, " - f"created_on={permission_group.created_on!r}, updated_on={permission_group.updated_on!r})" - ) - - class TestModelIO: - @pytest.mark.asyncio - async def test_create_permission_group(self): - permission_group: PermissionGroupTable = PermissionGroupTable(name="test") - assert permission_group.name == "test" - - @pytest.mark.asyncio - async def test_create_and_update_permission_group(self): - permission_group: PermissionGroupTable = PermissionGroupTable(name="test") - assert permission_group.name == "test" - permission_group.name = "new_test" - assert permission_group.name == "new_test" - - class TestDatabaseIO: - @pytest.mark.asyncio - async def test_add_permission_group(self, get_db_session_factory: async_scoped_session): - session: async_scoped_session - permission_group: PermissionGroupTable = PermissionGroupTable(name="test") - async with get_db_session_factory() as session: - # Add permission group "test" - session.add(permission_group) - await session.commit() - # Assert the "test" permission group exists - _query = select(PermissionGroupTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("permission not found, aborting test.") - assert _result.name == "test" - - @pytest.mark.asyncio - async def test_add_and_update_permission_group(self, get_db_session_factory: async_scoped_session): - session: async_scoped_session - async with get_db_session_factory() as session: - # Add permission group "test" - permission_group: PermissionGroupTable = PermissionGroupTable(name="test") - session.add(permission_group) - await session.commit() - # Assert the "test" permission group exists - _query = select(PermissionGroupTable).where(PermissionGroupTable.name == "test") - _row = await session.execute(_query) - _result = _row.scalar() - if _result is None: - pytest.fail("permission not found, aborting test.") - assert _result.name == "test" - # Update the "test" permission group name to "new_test" - _result.name = "new_test" - await session.commit() - # Assert the "new_test" permission group exists - _query = select(PermissionGroupTable).where(PermissionGroupTable.name == "new_test") - _result = await session.execute(_query) - _result = _result.scalars().first() - if _result is None: - pytest.fail("updated permission not found, aborting test.") - assert _result.name == "new_test" - - @pytest.mark.asyncio - async def test_add_and_delete_permission_group(self, get_db_session_factory: async_scoped_session): - session: async_scoped_session - async with get_db_session_factory() as session: - # Add permission group "test" - permission_group: PermissionGroupTable = PermissionGroupTable(name="test") - session.add(permission_group) - await session.commit() - # Assert the "test" permission group exists - _query = select(PermissionGroupTable).where(PermissionGroupTable.name == "test") - _row = await session.execute(_query) - _result = _row.scalar() - if _result is None: - pytest.fail("permission not found, aborting test.") - assert _result.name == "test" - # Delete the "test" permission group - _query = delete(PermissionGroupTable).where(PermissionGroupTable.name == "test") - _result = await session.execute(_query) - await session.commit() - # Assert the "test" permission group does not exist - _query = select(PermissionGroupTable).where(PermissionGroupTable.name == "test") - _result = await session.execute(_query) - _result = _result.scalars().first() - assert _result is None - - class TestFailures: - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_permission_group_fails_null_constraint(self, get_db_session_factory: async_scoped_session): - session: async_scoped_session - async with get_db_session_factory() as session: - permission_group: PermissionGroupTable = PermissionGroupTable() - session.add(permission_group) - try: - await session.commit() - except IntegrityError: - await session.rollback() - - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_permission_group_fails_unique_constraint(self, get_db_session_factory: async_scoped_session): - session: async_scoped_session - async with get_db_session_factory() as session: - permission_group1: PermissionGroupTable = PermissionGroupTable(name="test") - session.add(permission_group1) - await session.flush() - permission_group2: PermissionGroupTable = PermissionGroupTable(name="test") - session.add(permission_group2) - try: - await session.commit() - except IntegrityError: - await session.rollback() diff --git a/tests/src/lib/database/models/test_plugin_model.py b/tests/src/lib/database/models/test_plugin_model.py deleted file mode 100644 index 31bca54..0000000 --- a/tests/src/lib/database/models/test_plugin_model.py +++ /dev/null @@ -1,171 +0,0 @@ -import pytest -from sqlalchemy import delete, select -from sqlalchemy.exc import IntegrityError -from sqlalchemy.ext.asyncio import async_scoped_session - -from src.lib.database.models.command import CommandTable -from src.lib.database.models.plugin import PluginTable - - -class TestPluginModel: - @pytest.fixture(autouse=True) - async def plugin(self): - plugin: PluginTable = PluginTable( - name="test", - commands=[CommandTable(name="test")], - ) - return plugin - - @pytest.mark.asyncio - async def test_to_dict(self, plugin: PluginTable) -> None: - assert plugin.to_dict() == { - "id": plugin.id, - "name": plugin.name, - "commands": [x.to_dict() for x in plugin.commands], - "created_on": plugin.created_on, - "updated_on": plugin.updated_on, - } - - @pytest.mark.asyncio - async def test_repr(self, plugin: PluginTable) -> None: - assert ( - str(plugin) == f"Plugin(id={plugin.id!r}, name={plugin.name!r}, commands={plugin.commands}, " - f"created_on={plugin.created_on!r}, updated_on={plugin.updated_on!r})" - ) - - class TestModelIO: - @pytest.mark.asyncio - async def test_create_plugin(self): - plugin: PluginTable = PluginTable( - name="test", - commands=[CommandTable(name="test")], - ) - assert plugin.name == "test" - assert len(plugin.commands) > 0 - assert isinstance(plugin.commands[0], CommandTable) is True - assert plugin.commands[0].name == "test" - - @pytest.mark.asyncio - async def test_update_plugin(self, plugin: PluginTable): - assert plugin.name == "test" - assert len(plugin.commands) > 0 - assert isinstance(plugin.commands[0], CommandTable) is True - assert plugin.commands[0].name == "test" - plugin.name = "new_test" - plugin.commands[0].name = "new_test" - assert plugin.name == "new_test" - assert plugin.commands[0].name == "new_test" - - class TestDatabaseIO: - @pytest.mark.asyncio - async def test_add_plugin(self, plugin: PluginTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new plugin "test" - session.add(plugin) - await session.commit() - # Assert the "test" plugin exists - _query = select(PluginTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("plugin not found, aborting test.") - assert _result.name == "test" - assert len(_result.commands) > 0 - assert isinstance(_result.commands[0], CommandTable) is True - assert _result.commands[0].name == "test" - # Assert the "test" command added by the plugin exists - _query = select(CommandTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("command not found, aborting test.") - assert _result.name == "test" - - @pytest.mark.asyncio - async def test_add_and_update_plugin(self, plugin: PluginTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new plugin "test" - session.add(plugin) - await session.commit() - # Assert the "test" plugin exists - _query = select(PluginTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("plugin not found, aborting test.") - assert _result.name == "test" - assert len(_result.commands) > 0 - assert isinstance(_result.commands[0], CommandTable) is True - assert _result.commands[0].name == "test" - # Update the "test" plugin name to "new_test" - _result.name = "new_test" - await session.commit() - # Assert the "new_test" plugin group exists - _query = select(PluginTable).where(PluginTable.name == "new_test") - _result = await session.execute(_query) - _result = _result.scalars().first() - if _result is None: - pytest.fail("updated plugin not found, aborting test.") - assert _result.name == "new_test" - - @pytest.mark.asyncio - async def test_add_and_delete_plugin(self, plugin: PluginTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - session.add(plugin) - await session.commit() - # Assert the "test" plugin exists - _query = select(PluginTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("plugin not found, aborting test.") - assert _result.name == "test" - assert len(_result.commands) > 0 - assert isinstance(_result.commands[0], CommandTable) is True - assert _result.commands[0].name == "test" - # Delete the "test" plugin - _query = delete(PluginTable).where(PluginTable.name == "test") - _result = await session.execute(_query) - await session.commit() - # Assert the "test" plugin does not exist - _query = select(PluginTable).where(PluginTable.name == "new_test") - _result = await session.execute(_query) - _result = _result.scalars().first() - assert _result is None - - class TestFailures: - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_plugin_fails_null_constraint(self, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - plugin: PluginTable = PluginTable() - session.add(plugin) - try: - await session.commit() - except IntegrityError: - await session.rollback() - - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_plugin_fails_unique_constraint(self, get_db_session_factory: async_scoped_session): - session: async_scoped_session - async with get_db_session_factory() as session: - plugin1: PluginTable = PluginTable( - name="test", - commands=[CommandTable(name="test")], - ) - session.add(plugin1) - await session.flush() - plugin2: PluginTable = PluginTable( - name="test", - commands=[CommandTable(name="test2")], - ) - session.add(plugin2) - try: - await session.commit() - except IntegrityError: - await session.rollback() diff --git a/tests/src/lib/database/models/test_user_model.py b/tests/src/lib/database/models/test_user_model.py deleted file mode 100644 index a0066d2..0000000 --- a/tests/src/lib/database/models/test_user_model.py +++ /dev/null @@ -1,203 +0,0 @@ -import pytest -from sqlalchemy import delete, select -from sqlalchemy.exc import IntegrityError -from sqlalchemy.ext.asyncio import async_scoped_session - -from src.lib.database.models.alias import AliasTable -from src.lib.database.models.permission_group import PermissionGroupTable -from src.lib.database.models.user import UserTable - - -class TestUserModel: - @pytest.fixture(autouse=True) - async def user(self) -> UserTable: - user: UserTable = UserTable( - name="test", - permission_groups=[PermissionGroupTable(name="test")], - aliases=[AliasTable(name="test", command="test")], - ) - return user - - @pytest.mark.asyncio - async def test_to_dict(self, user: UserTable) -> None: - assert user.to_dict() == { - "id": user.id, - "name": user.name, - "permission_groups": [x.to_dict() for x in user.permission_groups], - "aliases": [alias.to_dict() for alias in user.aliases], - "created_on": user.created_on, - "updated_on": user.updated_on, - } - - @pytest.mark.asyncio - async def test_repr(self, user: UserTable) -> None: - assert ( - str(user) == f"User(id={user.id!r}, name={user.name!r}, permission_groups={user.permission_groups}, " - f"aliases={user.aliases!r}, created_on={user.created_on!r}, updated_on={user.updated_on!r})" - ) - - class TestModelIO: - @pytest.mark.asyncio - async def test_create_user(self): - user: UserTable = UserTable( - name="test", - permission_groups=[PermissionGroupTable(name="test")], - aliases=[AliasTable(name="test", command="test")], - ) - assert user.name == "test" - assert len(user.permission_groups) > 0 - assert isinstance(user.permission_groups[0], PermissionGroupTable) is True - assert user.permission_groups[0].name == "test" - assert len(user.aliases) > 0 - assert isinstance(user.aliases[0], AliasTable) is True - assert user.aliases[0].name == "test" - - @pytest.mark.asyncio - async def test_update_user(self, user: UserTable): - assert user.name == "test" - assert len(user.permission_groups) > 0 - assert isinstance(user.permission_groups[0], PermissionGroupTable) is True - assert user.permission_groups[0].name == "test" - assert len(user.aliases) > 0 - assert isinstance(user.aliases[0], AliasTable) is True - assert user.aliases[0].name == "test" - - user.name = "new_test" - user.permission_groups[0].name = "new_test" - user.aliases[0].name = "new_test" - assert user.name == "new_test" - assert user.permission_groups[0].name == "new_test" - assert len(user.aliases) > 0 - assert isinstance(user.aliases[0], AliasTable) is True - assert user.aliases[0].name == "new_test" - - class TestDatabaseIO: - @pytest.mark.asyncio - async def test_add_user(self, user: UserTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new user "test" - session.add(user) - await session.commit() - # Assert the "test" user exists - _query = select(UserTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("user not found, aborting test.") - assert _result.name == "test" - assert len(_result.permission_groups) > 0 - assert isinstance(_result.permission_groups[0], PermissionGroupTable) is True - assert _result.permission_groups[0].name == "test" - assert len(user.aliases) > 0 - assert isinstance(user.aliases[0], AliasTable) is True - assert user.aliases[0].name == "test" - # Assert the "test" permission group added by the user exists - _query = select(PermissionGroupTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("permission not found, aborting test.") - assert _result.name == "test" - # Assert the "test" alias added by the user exists - _query = select(AliasTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("alias not found, aborting test.") - assert _result.name == "test" - - @pytest.mark.asyncio - async def test_add_and_update_command(self, user: UserTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new user "test" - session.add(user) - await session.commit() - # Assert the "test" user exists - _query = select(UserTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("user not found, aborting test.") - assert _result.name == "test" - assert len(_result.permission_groups) > 0 - assert isinstance(_result.permission_groups[0], PermissionGroupTable) is True - assert _result.permission_groups[0].name == "test" - assert len(_result.aliases) > 0 - assert isinstance(_result.aliases[0], AliasTable) is True - assert _result.aliases[0].name == "test" - # Update the "test" user name to "new_test" - _result.name = "new_test" - await session.commit() - # Assert the "new_test" user group exists - _query = select(UserTable).filter_by(name="new_test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("updated user not found, aborting test.") - assert _result.name == "new_test" - - @pytest.mark.asyncio - async def test_add_and_delete_command(self, user: UserTable, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - # Add new user "test" - session.add(user) - await session.commit() - # Assert the "test" user exists - _query = select(UserTable).filter_by(name="test") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("user not found, aborting test.") - assert _result.name == "test" - assert len(_result.permission_groups) > 0 - assert isinstance(_result.permission_groups[0], PermissionGroupTable) is True - assert _result.permission_groups[0].name == "test" - assert len(_result.aliases) > 0 - assert isinstance(_result.aliases[0], AliasTable) is True - assert _result.aliases[0].name == "test" - # Delete the "test" user - _query = delete(UserTable).where(UserTable.name == "test") - _result = await session.execute(_query) - await session.commit() - # Assert the "test" user does not exist - _query = select(UserTable).where(UserTable.name == "new_test") - _result = await session.execute(_query) - _result = _result.scalars().first() - assert _result is None - - class TestFailures: - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_user_fails_null_constraint(self, get_db_session_factory: async_scoped_session) -> None: - session: async_scoped_session - async with get_db_session_factory() as session: - user: UserTable = UserTable() - session.add(user) - try: - await session.commit() - except IntegrityError: - await session.rollback() - - @pytest.mark.asyncio - @pytest.mark.xfail(raises=IntegrityError) - async def test_create_invalid_user_fails_unique_constraint(self, get_db_session_factory: async_scoped_session): - session: async_scoped_session - async with get_db_session_factory() as session: - user1: UserTable = UserTable( - name="test", - permission_groups=[PermissionGroupTable(name="test")], - ) - session.add(user1) - await session.flush() - user2: UserTable = UserTable( - name="test", - permission_groups=[PermissionGroupTable(name="test")], - ) - session.add(user2) - try: - await session.commit() - except IntegrityError: - await session.rollback() diff --git a/tests/src/lib/database/test_database_connection_parameters.py b/tests/src/lib/database/test_database_connection_parameters.py deleted file mode 100644 index bee74d8..0000000 --- a/tests/src/lib/database/test_database_connection_parameters.py +++ /dev/null @@ -1,109 +0,0 @@ -from typing import Any, Tuple - -import pytest - -from src.lib.database.database_connection_parameters import DatabaseConnectionParameters - - -class TestDatabaseConnectionParameters: - @pytest.fixture(autouse=True) - def get_connection_params(self) -> Tuple[Any, ...]: - return ( - "mumimo_dialect", # dialect - "mumimo_host.db", # host - "mumimo_port", # port - "mumimo_database_name", # database - "mumimo_user", # username - "mumimo_pass", # password - "mumimo_drivername", # drivername - False, # use remote - "mumimo_localdb_dialect", # local db dialect - "mumimo_localdb_path.db", # local db path - "mumimo_localdb_drivername", # local db drivername - ) - - @pytest.fixture(autouse=True) - def get_params_object(self, get_connection_params: Tuple[str, ...]) -> DatabaseConnectionParameters: - _params: DatabaseConnectionParameters = DatabaseConnectionParameters(*get_connection_params) # type: ignore - return _params - - def test_init_database_connection_parameters(self, get_connection_params: Tuple[str, ...]) -> None: - _params: DatabaseConnectionParameters = DatabaseConnectionParameters(*get_connection_params) # type: ignore - assert _params.dialect == get_connection_params[0] - assert _params.host == get_connection_params[1] - assert _params.port == get_connection_params[2] - assert _params.database_name == get_connection_params[3] - assert _params.username == get_connection_params[4] - assert _params.password == get_connection_params[5] - assert _params.drivername == get_connection_params[6] - assert _params.use_remote == get_connection_params[7] - assert _params.local_database_dialect == get_connection_params[8] - assert _params.local_database_path == get_connection_params[9] - assert _params.local_database_driver == get_connection_params[10] - - def test_to_dict(self, get_params_object: DatabaseConnectionParameters) -> None: - _params: DatabaseConnectionParameters = get_params_object - assert _params.to_dict() == { - "dialect": _params.dialect, - "drivername": _params.drivername, - "host": _params.host, - "port": _params.port, - "database_name": _params.database_name, - "username": _params.username, - "password": _params.password, - "use_remote": _params.use_remote, - "local_database_path": _params.local_database_path, - "local_database_dialect": _params.local_database_dialect, - "local_database_driver": _params.local_database_driver, - } - - class TestValidateParameters: - @pytest.fixture(autouse=True) - def get_params(self, get_params_object: DatabaseConnectionParameters) -> DatabaseConnectionParameters: - _params: DatabaseConnectionParameters = get_params_object - _params.database_name = "test" - _params.dialect = "test" - _params.host = "test" - _params.port = "12345" - _params.username = "test" - _params.password = "test" - _params.drivername = "test" - _params.local_database_dialect = "test" - _params.local_database_driver = "test" - _params.local_database_path = "test" - _params.use_remote = False - return _params - - def test_validate_parameters_default(self, get_params_object: DatabaseConnectionParameters) -> None: - _params: DatabaseConnectionParameters = get_params_object - _params.use_remote = False - assert _params.validate_parameters() == (True, "") - _params.use_remote = True - assert _params.validate_parameters() == (True, "") - - class TestRemote: - remote_vars = ["database_name", "username", "password", "port", "dialect", "drivername", "host"] - - @pytest.mark.parametrize("name", remote_vars) - def test_validate_parameters_remote_invalid_parameters(self, name: str, get_params: DatabaseConnectionParameters) -> None: - _params: DatabaseConnectionParameters = get_params - _params.use_remote = True - - print(_params.to_dict(), name) - setattr(_params, name, None) - assert _params.validate_parameters() == (False, f"{name}=None") - setattr(_params, name, "") - assert _params.validate_parameters() == (False, f"{name}=") - - class TestLocal: - local_vars = ["local_database_path", "local_database_dialect", "local_database_driver"] - - @pytest.mark.parametrize("name", local_vars) - def test_validate_parameters_local_invalid_parameters(self, name: str, get_params: DatabaseConnectionParameters) -> None: - _params: DatabaseConnectionParameters = get_params - _params.use_remote = False - - setattr(_params, name, None) - assert _params.validate_parameters() == (False, f"{name}=None") - setattr(_params, name, "") - assert _params.validate_parameters() == (False, f"{name}=") diff --git a/tests/src/lib/test_command.py b/tests/src/lib/test_command.py deleted file mode 100644 index 7d65558..0000000 --- a/tests/src/lib/test_command.py +++ /dev/null @@ -1,81 +0,0 @@ -import pytest - -from src.lib.command import Command - - -class TestCommand: - class TestCommandInit: - def test_command_init_empty(self): - cmd: Command = Command() - assert cmd is not None - assert cmd.actor == -1 - assert cmd.command is None - assert cmd.parameters == [] - assert cmd.message == "" - assert cmd.channel_id == -1 - assert cmd.session_id == -1 - - def test_command_init_channel_id_is_none(self): - cmd: Command = Command(channel_id=None) - assert cmd.channel_id == -1 - - def test_command_init_session_id_is_none(self): - cmd: Command = Command(session_id=None) - assert cmd.session_id == -1 - - class TestCommandGetterSetters: - @pytest.fixture(autouse=True) - def cmd(self): - return Command() - - def test_command_getter(self, cmd): - assert cmd.command is None - - def test_command_setter(self, cmd): - cmd.command = "test" - assert cmd.command == "test" - - def test_message_getter(self, cmd): - assert cmd.message == "" - - def test_message_setter(self, cmd): - cmd.message = "test" - assert cmd.message == "test" - - def test_parameters_getter(self, cmd): - assert cmd.parameters == [] - - def test_parameters_setter(self, cmd): - cmd.parameters = ["test"] - assert cmd.parameters == ["test"] - - def test_actor_getter(self, cmd): - assert cmd.actor == -1 - - def test_actor_setter(self, cmd): - cmd.actor = 0 - assert cmd.actor == 0 - - def test_channel_id_getter(self, cmd): - assert cmd.channel_id == -1 - - def test_channel_id_setter(self, cmd): - cmd.channel_id = 0 - assert cmd.channel_id == 0 - - def test_session_id_getter(self, cmd): - assert cmd.session_id == -1 - - def test_session_id_setter(self, cmd): - cmd.session_id = 0 - assert cmd.session_id == 0 - - def test_is_private_property_true(self, cmd): - cmd.session_id = 0 - cmd.channel_id = -1 - assert cmd.is_private is True - - def test_is_private_property_false(self, cmd): - cmd.session_id = -1 - cmd.channel_id = 0 - assert cmd.is_private is False diff --git a/tests/src/lib/test_command_history.py b/tests/src/lib/test_command_history.py deleted file mode 100644 index 627bed2..0000000 --- a/tests/src/lib/test_command_history.py +++ /dev/null @@ -1,124 +0,0 @@ -from typing import List, Optional - -import pytest - -from src.lib.command import Command -from src.lib.command_history import CommandHistory - - -class TestCommandHistory: - @pytest.fixture(autouse=True) - def mock_cmd_history_list(self) -> List[Command]: - history: List[Command] = [ - Command("test_1", ["param_1", "param_2"], "test_msg_1"), - Command("test_2", ["param_1", "param_2"], "test_msg_2"), - Command("test_3", ["param_1", "param_2"], "test_msg_3"), - Command("test_4", ["param_1", "param_2"], "test_msg_4"), - ] - return history - - @pytest.fixture(autouse=True) - def mock_empty_cmd_history(self) -> CommandHistory: - history: CommandHistory = CommandHistory() - return history - - @pytest.fixture(autouse=True) - def mock_filled_cmd_history(self, mock_cmd_history_list) -> CommandHistory: - history: CommandHistory = CommandHistory() - for cmd in mock_cmd_history_list: - history.add(cmd) - return history - - class TestCommandHistoryInit: - def test_command_history_init(self) -> None: - history: CommandHistory = CommandHistory() - assert history is not None - - def test_command_history_init_with_limit(self) -> None: - history: CommandHistory = CommandHistory(history_limit=10) - assert history is not None - assert history.limit == 10 - - def test_command_history_init_with_invalid_limit(self) -> None: - history: CommandHistory = CommandHistory(history_limit=-5) - assert history is not None - assert history.limit == CommandHistory.DEFAULT_HISTORY_LIMIT - - def test_command_history_init_with_none_limit(self) -> None: - history: CommandHistory = CommandHistory(history_limit=None) - assert history is not None - assert history.limit == CommandHistory.DEFAULT_HISTORY_LIMIT - - class TestCommandHistoryProperties: - def test_command_history_history_property(self, mock_filled_cmd_history: CommandHistory) -> None: - history: CommandHistory = mock_filled_cmd_history - assert history.history is not None - - def test_command_history_length_property(self, mock_filled_cmd_history: CommandHistory, mock_cmd_history_list: List[Command]) -> None: - history: CommandHistory = mock_filled_cmd_history - assert history.length == len(mock_cmd_history_list) - - def test_command_history_limit_property(self, mock_empty_cmd_history: CommandHistory) -> None: - history: CommandHistory = mock_empty_cmd_history - assert history.limit == mock_empty_cmd_history.DEFAULT_HISTORY_LIMIT - - class TestCommandHistorySetters: - def test_command_history_limit_setter_valid_limit(self, mock_empty_cmd_history: CommandHistory) -> None: - history: CommandHistory = mock_empty_cmd_history - history.limit = 10 - assert history.limit == 10 - - def test_command_history_limit_setter_invalid_limit(self, mock_empty_cmd_history: CommandHistory) -> None: - history: CommandHistory = mock_empty_cmd_history - history.limit = -5 - assert history.limit == mock_empty_cmd_history.DEFAULT_HISTORY_LIMIT - - class TestCommandHistoryMethods: - class TestGetLast: - def test_command_history_get_last_valid(self, mock_filled_cmd_history: CommandHistory) -> None: - history: CommandHistory = mock_filled_cmd_history - history.add(Command("test_5", message="test_msg_5")) - cmd_result: List[Command] = history.get_last(1) - assert cmd_result - assert cmd_result[0].command == "test_5" - - def test_command_history_get_last_invalid_last_n(self, mock_filled_cmd_history: CommandHistory) -> None: - history: CommandHistory = mock_filled_cmd_history - cmd_result: List[Command] = history.get_last(0) - assert not cmd_result - - def test_command_history_get_last_with_empty_history(self, mock_empty_cmd_history: CommandHistory) -> None: - history: CommandHistory = mock_empty_cmd_history - cmd_result: List[Command] = history.get_last(0) - assert not cmd_result - - class TestAdd: - def test_command_history_add(self, mock_filled_cmd_history) -> None: - history: CommandHistory = mock_filled_cmd_history - cmd_result: Optional[Command] = history.add(Command("test_5", message="test_msg_5")) - assert cmd_result is not None - assert cmd_result.command == "test_5" - - def test_command_history_add_above_limit_fails(self, mock_filled_cmd_history) -> None: - history: CommandHistory = mock_filled_cmd_history - mock_filled_cmd_history.limit = mock_filled_cmd_history.length - cmd_result: Optional[Command] = history.add(Command("test_5", message="test_msg_5")) - assert cmd_result is None - - class TestPop: - def test_command_history_pop_valid_index(self, mock_filled_cmd_history: CommandHistory) -> None: - history: CommandHistory = mock_filled_cmd_history - cmd_result: Optional[Command] = history.pop(0) - assert cmd_result is not None - assert cmd_result.command == "test_1" - - def test_command_history_pop_invalid_index(self, mock_filled_cmd_history: CommandHistory) -> None: - history: CommandHistory = mock_filled_cmd_history - cmd_result: Optional[Command] = history.pop(999) - assert cmd_result is None - - class TestClear: - def test_command_history_clear(self, mock_filled_cmd_history) -> None: - history: CommandHistory = mock_filled_cmd_history - history.clear() - assert len(history.history) == 0 diff --git a/tests/src/services/test_cfg_init_service.py b/tests/src/services/test_cfg_init_service.py deleted file mode 100644 index 1b1c617..0000000 --- a/tests/src/services/test_cfg_init_service.py +++ /dev/null @@ -1,41 +0,0 @@ -import pathlib -from unittest.mock import patch - -import pytest - -from src.constants import DEFAULT_PATH_CONFIG_FILE -from src.exceptions import ConfigReadError -from src.services.init_services.cfg_init_service import ConfigInitService -from src.settings import settings - - -class TestCfgInitService: - @pytest.fixture(autouse=True) - def get_cfg_service(self): - yield ConfigInitService({}) - - @pytest.fixture(autouse=True) - def valid_cfg_path(self) -> str: - return "tests/data/config/test_valid_config.toml" - - @pytest.fixture(autouse=True) - def invalid_cfg_path(self) -> str: - return "invalid/path/to/config/test_invalid_config.toml" - - def test_initialize_mumimo_config_valid_cfg_path(self, valid_cfg_path: str, get_cfg_service: ConfigInitService) -> None: - settings.configs.set_mumimo_config(None) # type: ignore - _cfg_instance = get_cfg_service.initialize_config(valid_cfg_path) - assert _cfg_instance._config_file_path == (pathlib.Path.cwd() / valid_cfg_path).resolve() - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - def test_initialize_mumimo_config_invalid_cfg_path(self, mock_get_cfg, invalid_cfg_path: str, get_cfg_service: ConfigInitService) -> None: - mock_get_cfg.return_value = None - with pytest.raises(ConfigReadError, match="^Unable to open config file to read"): - _ = get_cfg_service.initialize_config(invalid_cfg_path) - - def test_initialization_mumimo_config_no_cfg_path_uses_default_path(self, get_cfg_service: ConfigInitService, caplog) -> None: - settings.configs.set_mumimo_config(None) # type: ignore - _cfg_instance = get_cfg_service.initialize_config(None) - assert "Mumimo config file path not provided." in caplog.text - assert _cfg_instance is not None - assert _cfg_instance._config_file_path == (pathlib.Path.cwd() / DEFAULT_PATH_CONFIG_FILE).resolve() diff --git a/tests/src/services/test_client_settings_init_service.py b/tests/src/services/test_client_settings_init_service.py deleted file mode 100644 index c10fef5..0000000 --- a/tests/src/services/test_client_settings_init_service.py +++ /dev/null @@ -1,267 +0,0 @@ -from typing import Any, Dict -from unittest.mock import patch - -import pytest - -from src.config import Config -from src.constants import VERBOSITY_MAX, EnvArgs, MumimoCfgFields, SysArgs -from src.services.init_services.client_settings_init_service import ClientSettingsInitService - - -class TestClientSettingsInitService: - @pytest.fixture(autouse=True) - def get_service(self) -> ClientSettingsInitService: - return ClientSettingsInitService({}) - - @pytest.fixture(autouse=True) - def get_config(self) -> Config: - _cfg_instance = Config("tests/data/config/test_config.toml") - _cfg_instance.read() - return _cfg_instance - - @pytest.fixture(autouse=True) - def get_expected_prioritized_client_opts(self): - return { - SysArgs.SYS_RECONNECT: True, - } - - @pytest.fixture(autouse=True) - def get_expected_prioritized_env_opts(self): - return { - SysArgs.SYS_HOST: "host", - SysArgs.SYS_PORT: 12345, - SysArgs.SYS_USER: "user", - SysArgs.SYS_PASS: "pass", - SysArgs.SYS_CERT: "cert", - SysArgs.SYS_KEY: "key", - SysArgs.SYS_TOKENS: "tokens", - SysArgs.SYS_SUPER_USER: "super", - } - - @patch.object(ClientSettingsInitService, "_get_prioritized_client_config_options") - @patch.object(ClientSettingsInitService, "_get_prioritized_client_env_options") - @patch.object(ClientSettingsInitService, "_get_sys_args") - def test_initialize_client_settings( - self, - mock_sys_args, - mock_env_opts, - mock_config_opts, - get_service: ClientSettingsInitService, - get_config: Config, - get_expected_prioritized_env_opts: Dict[str, Any], - get_expected_prioritized_client_opts, - ) -> None: - # mock_init_mumimo_cfg.return_value.set(f"{CFG_SECTION.SETTINGS.CONNECTION}.{CFG_FIELD.SETTINGS.CONNECTION.AUTO_RECONNECT}", True) - mock_sys_args.return_value = {SysArgs.SYS_VERBOSE: VERBOSITY_MAX} - mock_env_opts.return_value = get_expected_prioritized_env_opts - mock_config_opts.return_value = get_expected_prioritized_client_opts - _ = get_service.initialize_client_settings(get_config) - - expected_connection_opts = { - **mock_config_opts.return_value, - **mock_env_opts.return_value, - **mock_sys_args.return_value, - } - assert expected_connection_opts is not None - - class TestGetPrioritizedOptions: - def test_get_prioritized_cfg_options_empty(self, get_service: ClientSettingsInitService) -> None: - get_service._prioritized_cfg_opts = {} - assert get_service.get_prioritized_cfg_options() == {} - - def test_get_prioritized_cfg_options(self, get_service: ClientSettingsInitService) -> None: - get_service._prioritized_cfg_opts = {"test": "test"} - assert get_service.get_prioritized_cfg_options() == {"test": "test"} - - def test_get_prioritized_env_options_empty(self, get_service: ClientSettingsInitService) -> None: - get_service._prioritized_env_opts = {} - assert get_service.get_prioritized_env_options() == {} - - def test_get_prioritized_env_options(self, get_service: ClientSettingsInitService) -> None: - get_service._prioritized_env_opts = {"test": "test"} - assert get_service.get_prioritized_env_options() == {"test": "test"} - - class TestGetConnectionParameters: - @patch.object(ClientSettingsInitService, "get_prioritized_cfg_options") - @patch.object(ClientSettingsInitService, "get_prioritized_env_options") - def test_get_connection_parameters_prioritized_options_are_none( - self, - mock_env_opts, - mock_cfg_opts, - get_service: ClientSettingsInitService, - ) -> None: - mock_env_opts.return_value = {} - mock_cfg_opts.return_value = {} - assert get_service.get_connection_parameters() == {} - - @patch.object(ClientSettingsInitService, "_get_sys_args") - @patch.object(ClientSettingsInitService, "get_prioritized_cfg_options") - @patch.object(ClientSettingsInitService, "get_prioritized_env_options") - def test_get_connection_parameters_prioritized_options( - self, - mock_env_opts, - mock_cfg_opts, - mock_sys_args, - get_service: ClientSettingsInitService, - ) -> None: - mock_env_opts.return_value = { - SysArgs.SYS_HOST: "host", - SysArgs.SYS_PORT: 12345, - SysArgs.SYS_USER: "mumimo", - SysArgs.SYS_PASS: "pass", - SysArgs.SYS_SUPER_USER: "superuser", - SysArgs.SYS_CERT: "cert", - SysArgs.SYS_TOKENS: "token1 token2", - SysArgs.SYS_KEY: "key", - } - mock_sys_args.return_value = { - SysArgs.SYS_VERBOSE: 0, - } - mock_cfg_opts.return_value = { - SysArgs.SYS_RECONNECT: True, - } - assert get_service.get_connection_parameters() == { - SysArgs.SYS_HOST: "host", - SysArgs.SYS_PORT: 12345, - SysArgs.SYS_USER: "mumimo", - SysArgs.SYS_PASS: "pass", - SysArgs.SYS_SUPER_USER: "superuser", - SysArgs.SYS_CERT: "cert", - SysArgs.SYS_TOKENS: "token1 token2", - SysArgs.SYS_KEY: "key", - SysArgs.SYS_RECONNECT: True, - SysArgs.SYS_VERBOSE: 0, - } - - class TestPrioritizedOptions: - @pytest.fixture(autouse=True) - def get_expected_env_mock_sys_args(self): - return { - SysArgs.SYS_HOST: "host", - SysArgs.SYS_PORT: 12345, - SysArgs.SYS_USER: "mumimo", - SysArgs.SYS_PASS: "pass", - SysArgs.SYS_SUPER_USER: "superuser", - SysArgs.SYS_CERT: "cert", - SysArgs.SYS_TOKENS: "token1 token2", - SysArgs.SYS_KEY: "key", - SysArgs.SYS_DB_DIALECT: "sqlite", - SysArgs.SYS_DB_DRIVER: "aiosqlite", - SysArgs.SYS_DB_USER: "mumimo", - SysArgs.SYS_DB_PASS: "pass", - SysArgs.SYS_DB_HOST: "mumimo_db", - SysArgs.SYS_DB_PORT: 12345, - SysArgs.SYS_DB_NAME: "mumimo", - } - - @pytest.fixture(autouse=True) - def get_expected_env_mock_no_sys_args(self): - return { - EnvArgs.ENV_HOST: "test_host", - EnvArgs.ENV_PORT: 12345, - EnvArgs.ENV_USER: "test_user", - EnvArgs.ENV_PASS: "test_pass", - EnvArgs.ENV_SUPER_USER: "test_super_user", - EnvArgs.ENV_CERT: "test_cert", - EnvArgs.ENV_TOKENS: ["test_token_1", "test_token_2"], - EnvArgs.ENV_KEY: "test_key", - EnvArgs.ENV_DB_DIALECT: "sqlite", - EnvArgs.ENV_DB_DRIVER: "aiosqlite", - EnvArgs.ENV_DB_USER: "test_mumimo", - EnvArgs.ENV_DB_PASS: "test_mumimo", - EnvArgs.ENV_DB_HOST: "test_mumimo_db", - EnvArgs.ENV_DB_NAME: "test_mumimo", - EnvArgs.ENV_DB_PORT: 12345, - } - - @patch.object(ClientSettingsInitService, "_get_sys_args") - def test__get_prioritized_client_config_options_with_sys_args( - self, - mock_sys_args, - get_config: Config, - get_service: ClientSettingsInitService, - ) -> None: - mock_sys_args.return_value = { - SysArgs.SYS_RECONNECT: True, - SysArgs.SYS_DB_LOCALDBDIALECT: None, - SysArgs.SYS_DB_LOCALDBDRIVER: None, - SysArgs.SYS_DB_LOCALDBPATH: None, - SysArgs.SYS_PLUGINS_CONFIG_PATH: None, - SysArgs.SYS_PLUGINS_PATH: None, - SysArgs.SYS_DB_USEREMOTEDB: False, - } - client_settings = get_service._get_prioritized_client_config_options(get_config) - assert client_settings == mock_sys_args.return_value - - @patch.object(ClientSettingsInitService, "_get_sys_args") - def test__get_prioritized_client_config_options_without_sys_args( - self, - mock_sys_args, - get_config: Config, - get_service: ClientSettingsInitService, - ) -> None: - mock_sys_args.return_value = {} - if get_config is not None: - get_config.set(MumimoCfgFields.SETTINGS.CONNECTION.AUTO_RECONNECT, True) - client_settings = get_service._get_prioritized_client_config_options(get_config) - assert client_settings[SysArgs.SYS_RECONNECT] is False - - @patch.object(ClientSettingsInitService, "_get_sys_args") - @patch("src.utils.parsers.env_parser.read_env_file") - def test__get_prioritized_client_env_options_with_sys_args( - self, - mock_env_args, - mock_sys_args, - get_service: ClientSettingsInitService, - get_expected_env_mock_sys_args: Dict[str, Any], - ) -> None: - mock_env_args.return_value = {} - mock_sys_args.return_value = get_expected_env_mock_sys_args - client_settings = get_service._get_prioritized_client_env_options() - assert client_settings == { - SysArgs.SYS_HOST: mock_sys_args.return_value[SysArgs.SYS_HOST], - SysArgs.SYS_PORT: mock_sys_args.return_value[SysArgs.SYS_PORT], - SysArgs.SYS_USER: mock_sys_args.return_value[SysArgs.SYS_USER], - SysArgs.SYS_PASS: mock_sys_args.return_value[SysArgs.SYS_PASS], - SysArgs.SYS_SUPER_USER: mock_sys_args.return_value[SysArgs.SYS_SUPER_USER], - SysArgs.SYS_CERT: mock_sys_args.return_value[SysArgs.SYS_CERT], - SysArgs.SYS_TOKENS: ["token1", "token2"], - SysArgs.SYS_KEY: mock_sys_args.return_value[SysArgs.SYS_KEY], - SysArgs.SYS_DB_DIALECT: mock_sys_args.return_value[SysArgs.SYS_DB_DIALECT], - SysArgs.SYS_DB_DRIVER: mock_sys_args.return_value[SysArgs.SYS_DB_DRIVER], - SysArgs.SYS_DB_USER: mock_sys_args.return_value[SysArgs.SYS_DB_USER], - SysArgs.SYS_DB_PASS: mock_sys_args.return_value[SysArgs.SYS_DB_PASS], - SysArgs.SYS_DB_HOST: mock_sys_args.return_value[SysArgs.SYS_DB_HOST], - SysArgs.SYS_DB_NAME: mock_sys_args.return_value[SysArgs.SYS_DB_NAME], - SysArgs.SYS_DB_PORT: mock_sys_args.return_value[SysArgs.SYS_DB_PORT], - } - - @patch.object(ClientSettingsInitService, "_get_sys_args") - @patch("src.utils.parsers.env_parser.read_env_file") - def test__get_prioritized_client_env_options_without_sys_args( - self, - mock_env_args, - mock_sys_args, - get_service: ClientSettingsInitService, - get_expected_env_mock_no_sys_args: Dict[str, Any], - ) -> None: - mock_env_args.return_value = get_expected_env_mock_no_sys_args - mock_sys_args.return_value = {SysArgs.SYS_ENV_FILE: "data/test.env"} - client_settings = get_service._get_prioritized_client_env_options() - assert client_settings == { - SysArgs.SYS_HOST: mock_env_args.return_value[EnvArgs.ENV_HOST], - SysArgs.SYS_PORT: mock_env_args.return_value[EnvArgs.ENV_PORT], - SysArgs.SYS_USER: mock_env_args.return_value[EnvArgs.ENV_USER], - SysArgs.SYS_PASS: mock_env_args.return_value[EnvArgs.ENV_PASS], - SysArgs.SYS_SUPER_USER: mock_env_args.return_value[EnvArgs.ENV_SUPER_USER], - SysArgs.SYS_CERT: mock_env_args.return_value[EnvArgs.ENV_CERT], - SysArgs.SYS_TOKENS: mock_env_args.return_value[EnvArgs.ENV_TOKENS], - SysArgs.SYS_KEY: mock_env_args.return_value[EnvArgs.ENV_KEY], - SysArgs.SYS_DB_DIALECT: mock_env_args.return_value[EnvArgs.ENV_DB_DIALECT], - SysArgs.SYS_DB_DRIVER: mock_env_args.return_value[EnvArgs.ENV_DB_DRIVER], - SysArgs.SYS_DB_USER: mock_env_args.return_value[EnvArgs.ENV_DB_USER], - SysArgs.SYS_DB_PASS: mock_env_args.return_value[EnvArgs.ENV_DB_PASS], - SysArgs.SYS_DB_HOST: mock_env_args.return_value[EnvArgs.ENV_DB_HOST], - SysArgs.SYS_DB_NAME: mock_env_args.return_value[EnvArgs.ENV_DB_NAME], - SysArgs.SYS_DB_PORT: mock_env_args.return_value[EnvArgs.ENV_DB_PORT], - } diff --git a/tests/src/services/test_cmd_processing_service.py b/tests/src/services/test_cmd_processing_service.py deleted file mode 100644 index 89daa58..0000000 --- a/tests/src/services/test_cmd_processing_service.py +++ /dev/null @@ -1,266 +0,0 @@ -from typing import Any, Dict -from unittest.mock import Mock, PropertyMock, patch - -import pytest - -from src.config import Config -from src.constants import LogCfgFields -from src.exceptions import ServiceError -from src.lib.command import Command -from src.log_config import LogConfig -from src.services.cmd_processing_service import CommandProcessingService - - -class TestCommandProcessingService: - @pytest.fixture(autouse=True) - @patch("src.settings.MumimoSettings.Configs.get_log_config") - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - @patch("pymumble_py3.mumble.Mumble") - def get_cmd_processing_service(self, mock_mumble, mock_cfg, mock_log_cfg) -> CommandProcessingService: - mock_cfg.return_value = Config("tests/data/config/test_config.toml") - mock_log_cfg.return_value = LogConfig("tests/data/config/test_logging.toml") - return CommandProcessingService(mock_mumble) - - class TestOutputPrivacyFilter: - @pytest.fixture(autouse=True) - def get_privacy_filter(self) -> "CommandProcessingService.OutputPrivacyFilter": - return CommandProcessingService.OutputPrivacyFilter() - - @pytest.fixture(autouse=True) - def filled_privacy_dict(self, get_privacy_filter) -> Dict[str, Any]: - filter = get_privacy_filter.get_privacy_template() - filter.update( - { - "file": { - "message": "test_file_message", - "command": "test_file_command", - "actor": "test_file_actor", - "channel": "test_file_channel", - "parameters": ["test_file_param"], - }, - "console": { - "message": "test_console_message", - "command": "test_console_command", - "actor": "test_console_actor", - "channel": "test_console_channel", - "parameters": ["test_console_param"], - }, - } - ) - return filter - - @pytest.fixture(autouse=True) - def filled_redacted_privacy_dict(self, get_privacy_filter) -> Dict[str, Any]: - filter = get_privacy_filter.get_privacy_template() - filter.update( - { - "file": { - "message": "Redacted", - "command": "Redacted", - "actor": "Redacted", - "channel": "Redacted", - "parameters": "[Redacted]", - }, - "console": { - "message": "Redacted", - "command": "Redacted", - "actor": "Redacted", - "channel": "Redacted", - "parameters": "[Redacted]", - }, - } - ) - return filter - - def test_get_privacy_template(self, get_privacy_filter) -> None: - assert isinstance(get_privacy_filter.get_privacy_template(), dict) is True - - def test_compile_file_privacy_checked_message(self, get_privacy_filter, filled_redacted_privacy_dict) -> None: - assert ( - get_privacy_filter.compile_file_privacy_checked_message(filled_redacted_privacy_dict) - == f"{filled_redacted_privacy_dict['file']['channel']}::{filled_redacted_privacy_dict['file']['actor']}::" - f"[Cmd:{filled_redacted_privacy_dict['file']['command']} | Params:{filled_redacted_privacy_dict['file']['parameters']}]::" - f"{filled_redacted_privacy_dict['file']['message']}" - ) - - def test_compile_console_privacy_checked_message(self, get_privacy_filter, filled_redacted_privacy_dict) -> None: - assert ( - get_privacy_filter.compile_file_privacy_checked_message(filled_redacted_privacy_dict) - == f"{filled_redacted_privacy_dict['console']['channel']}::{filled_redacted_privacy_dict['console']['actor']}::" - f"[Cmd:{filled_redacted_privacy_dict['console']['command']} | Params:{filled_redacted_privacy_dict['console']['parameters']}]::" - f"{filled_redacted_privacy_dict['console']['message']}" - ) - - @patch("src.services.cmd_processing_service.CommandProcessingService.OutputPrivacyFilter._redact_message") - @patch("src.services.cmd_processing_service.CommandProcessingService.OutputPrivacyFilter._redact_channel") - @patch("src.services.cmd_processing_service.CommandProcessingService.OutputPrivacyFilter._redact_actor") - @patch("src.services.cmd_processing_service.CommandProcessingService.OutputPrivacyFilter._redact_commands") - @patch("src.lib.command.Command") - @patch("src.log_config.LogConfig") - @patch("pymumble_py3.mumble.Mumble") - def test_get_privacy_checked_output( - self, - mock_connection_instance, - mock_log_cfg, - mock_cmd, - mock_redact_cmd, - mock_redact_actor, - mock_redact_channel, - mock_redact_msg, - get_privacy_filter, - ) -> None: - mock_redact_cmd.return_value = { - "file": {"command": "Redacted", "parameters": "Redacted"}, - "console": {"command": "Redacted", "parameters": "Redacted"}, - } - mock_redact_actor.return_value = { - "file": {"actor": "Redacted"}, - "console": {"actor": "Redacted"}, - } - mock_redact_channel.return_value = { - "file": {"channel": "Redacted"}, - "console": {"channel": "Redacted"}, - } - mock_redact_msg.return_value = { - "file": {"message": "[Redacted Message]"}, - "console": {"message": "[Redacted Message]"}, - } - result: Dict[str, Any] = get_privacy_filter.get_privacy_checked_output(mock_cmd, mock_log_cfg, mock_connection_instance) - assert all("Redacted" in val for val in result["file"].values()) is True - assert all("Redacted" in val for val in result["console"].values()) is True - - @patch("src.lib.command.Command") - @patch("src.log_config.LogConfig") - def test__redact_commands(self, mock_log_cfg, mock_cmd, get_privacy_filter) -> None: - mock_log_cfg.set(LogCfgFields.OUTPUT.FILE.PRIVACY.REDACT_COMMANDS, True) - mock_log_cfg.set(LogCfgFields.OUTPUT.CONSOLE.PRIVACY.REDACT_COMMANDS, True) - mock_cmd.return_value = Command("test_redacted_command", ["redact_1", "redact_2"]) - result: Dict[str, Any] = get_privacy_filter._redact_commands(mock_cmd, mock_log_cfg) - assert result["console"]["command"] == "Redacted" - assert result["console"]["parameters"] == "Redacted" - assert result["file"]["command"] == "Redacted" - assert result["file"]["parameters"] == "Redacted" - - @patch("src.utils.parsers.cmd_parser.parse_actor_name") - @patch("src.lib.command.Command") - @patch("src.log_config.LogConfig") - @patch("pymumble_py3.mumble.Mumble") - def test__redact_actor(self, mock_connection_instance, mock_log_cfg, mock_cmd, mock_parser, get_privacy_filter) -> None: - mock_log_cfg.set(LogCfgFields.OUTPUT.FILE.PRIVACY.REDACT_USER, True) - mock_log_cfg.set(LogCfgFields.OUTPUT.CONSOLE.PRIVACY.REDACT_USER, True) - mock_cmd.return_value = Command(actor=0) - mock_parser.return_value = "test_redacted_actor" - result: Dict[str, Any] = get_privacy_filter._redact_actor(mock_cmd, mock_log_cfg, mock_connection_instance) - assert result["console"]["actor"] == "Redacted" - assert result["file"]["actor"] == "Redacted" - - @patch("src.utils.parsers.cmd_parser.parse_channel_name") - @patch("src.lib.command.Command") - @patch("src.log_config.LogConfig") - @patch("pymumble_py3.mumble.Mumble") - def test__redact_channel(self, mock_connection_instance, mock_log_cfg, mock_cmd, mock_parser, get_privacy_filter) -> None: - mock_log_cfg.set(LogCfgFields.OUTPUT.FILE.PRIVACY.REDACT_CHANNEL, True) - mock_log_cfg.set(LogCfgFields.OUTPUT.CONSOLE.PRIVACY.REDACT_CHANNEL, True) - mock_cmd.return_value = Command(channel_id=0) - mock_parser.return_value = "test_redacted_channel" - result: Dict[str, Any] = get_privacy_filter._redact_channel(mock_cmd, mock_log_cfg, mock_connection_instance) - assert result["console"]["channel"] == "Redacted" - assert result["file"]["channel"] == "Redacted" - - @patch("src.utils.parsers.cmd_parser.parse_message_hyperlink_data") - @patch("src.utils.parsers.cmd_parser.parse_message_image_data") - @patch("src.lib.command.Command") - @patch("src.log_config.LogConfig") - def test__redact_message(self, mock_log_cfg, mock_cmd, mock_image_parser, mock_link_parser, get_privacy_filter) -> None: - msg = "test_redacted_message" - mock_log_cfg.set(LogCfgFields.OUTPUT.FILE.PRIVACY.REDACT_MESSAGE, True) - mock_log_cfg.set(LogCfgFields.OUTPUT.CONSOLE.PRIVACY.REDACT_MESSAGE, True) - mock_cmd.return_value = Command(message=msg) - mock_image_parser.return_value = msg - mock_link_parser.return_value = msg - result: Dict[str, Any] = get_privacy_filter._redact_message(mock_cmd, mock_log_cfg) - assert result["console"]["message"] == "[Redacted Message]" - assert result["file"]["message"] == "[Redacted Message]" - - class TestServiceInit: - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - @patch("src.settings.MumimoSettings.Configs.get_log_config") - @patch("src.config.Config") - @patch("pymumble_py3.mumble.Mumble") - def test_init_command_processing_service(self, mock_mumble, mock_mumimo_config, mock_log_cfg, mock_config) -> None: - mock_mumimo_config.get.return_value = 10 - mock_config.return_value = Config("tests/data/config/test_config.toml") - mock_log_cfg.return_value = LogConfig("tests/data/config/test_logging.toml") - service: CommandProcessingService = CommandProcessingService(mock_mumble) - assert service is not None - - @patch("src.services.cmd_processing_service.CommandProcessingService.connection_instance", new_callable=PropertyMock) - @patch("pymumble_py3.mumble.Mumble") - def test_init_command_processing_service_no_murmur_connection(self, mock_mumble, mock_connection_instance) -> None: - mock_connection_instance.return_value = None - with pytest.raises(ServiceError, match="^Unable to retrieve murmur connection:"): - _ = CommandProcessingService(mock_mumble) - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - @patch("pymumble_py3.mumble.Mumble") - def test_init_command_processing_service_no_cfg_instance(self, mock_mumble, mock_cfg_instance) -> None: - mock_cfg_instance.return_value = None - with pytest.raises(ServiceError, match="^Unable to create command processing service:"): - _ = CommandProcessingService(mock_mumble) - - class TestProcessCmd: - class MockMumble: - users = {0: {"name": "test_user"}} - channels = {0: {"name": "test_channel"}} - - @patch("src.settings.MumimoSettings.Commands.get_command_callbacks") - @patch("src.logging.log_privacy", Mock()) - @patch("src.services.cmd_processing_service.CommandProcessingService.connection_instance", new_callable=PropertyMock) - @patch("src.settings.MumimoSettings.Configs.get_log_config") - @patch("src.utils.parsers.cmd_parser.parse_command") - def test_process_cmd_valid_text( - self, mock_parse_command, mock_log_cfg, mock_connection_instance, mock_cmd_callbacks, get_cmd_processing_service - ) -> None: - mock_cmd_callbacks.return_value = {"test_cmd": {"func": Mock()}} - mock_connection_instance.return_value = self.MockMumble() - mock_log_cfg.return_value = LogConfig("tests/data/config/test_logging.toml") - mock_parse_command.return_value = Command("test_cmd", ["param_1", "param_2"], "test_msg", 0, 0, -1) - service: CommandProcessingService = get_cmd_processing_service - assert service.process_cmd("") is None - - @patch("src.settings.MumimoSettings.Commands.get_command_callbacks") - @patch("src.services.cmd_processing_service.CommandProcessingService.connection_instance", new_callable=PropertyMock) - @patch("src.services.cmd_processing_service.CommandProcessingService.log_cfg", new_callable=PropertyMock) - @patch("src.utils.parsers.cmd_parser.parse_command") - def test_process_cmd_valid_text_log_cfg_is_none( - self, mock_parse_command, mock_log_cfg, mock_connection_instance, mock_cmd_callbacks, get_cmd_processing_service - ) -> None: - mock_cmd_callbacks.return_value = {"test_cmd": {"func": Mock()}} - mock_connection_instance.return_value = self.MockMumble() - mock_log_cfg.return_value = None - mock_parse_command.return_value = Command("test_cmd", ["param_1", "param_2"], "test_msg", 0, 0, -1) - with pytest.raises(ServiceError, match="^Unable to process command privacy checks"): - service: CommandProcessingService = get_cmd_processing_service - service.process_cmd("") - - @patch("src.settings.MumimoSettings.Commands.get_command_callbacks") - @patch("src.services.cmd_processing_service.CommandProcessingService.cmd_history", new_callable=PropertyMock) - @patch("src.services.cmd_processing_service.CommandProcessingService.connection_instance", new_callable=PropertyMock) - @patch("src.settings.MumimoSettings.Configs.get_log_config") - @patch("src.utils.parsers.cmd_parser.parse_command") - def test_process_cmd_valid_text_cmd_history_is_none( - self, mock_parse_command, mock_log_cfg, mock_connection_instance, mock_cmd_history, mock_cmd_callbacks, get_cmd_processing_service - ) -> None: - mock_cmd_callbacks.return_value = {"test_cmd": {"func": Mock()}} - mock_cmd_history.return_value = None - mock_connection_instance.return_value = self.MockMumble() - mock_log_cfg.return_value = LogConfig("tests/data/config/test_logging.toml") - mock_parse_command.return_value = Command("test_cmd", ["param_1", "param_2"], "test_msg", 0, 0, -1) - with pytest.raises(ServiceError, match="^Unable to add command"): - service: CommandProcessingService = get_cmd_processing_service - service.process_cmd("") - - def test_process_cmd_text_is_none(self, get_cmd_processing_service) -> None: - with pytest.raises(ServiceError, match="^Received text message"): - service: CommandProcessingService = get_cmd_processing_service - service.process_cmd(None) diff --git a/tests/src/services/test_database_service.py b/tests/src/services/test_database_service.py deleted file mode 100644 index eeeec3e..0000000 --- a/tests/src/services/test_database_service.py +++ /dev/null @@ -1,559 +0,0 @@ -import asyncio -from typing import Dict, Tuple -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession, async_scoped_session, async_sessionmaker - -from src.exceptions import DatabaseServiceError -from src.lib.database.database_connection_parameters import DatabaseConnectionParameters -from src.lib.database.models.alias import AliasTable -from src.lib.database.models.permission_group import PermissionGroupTable -from src.services.database_service import DatabaseService - - -class TestDatabaseService: - @pytest.fixture(autouse=True) - def get_connection_params(self) -> Dict[str, str]: - return { - "dialect": "sqlite", # dialect - "host": "tests/data/generated/mumimo_test.db", # host - "drivername": "aiosqlite", # drivername - } - - @pytest.fixture(autouse=True) - def get_connection_params_object(self, get_connection_params: Dict[str, str]) -> DatabaseConnectionParameters: - return DatabaseConnectionParameters( - local_database_dialect=get_connection_params["dialect"], - local_database_path=get_connection_params["host"], - local_database_driver=get_connection_params["drivername"], - ) - - @pytest.mark.asyncio - @pytest.fixture(autouse=True) - @patch.object(DatabaseService, "import_default_values") - @patch.object(DatabaseService, "setup") - async def get_database_service(self, mock_setup, mock_import, get_connection_params: Dict[str, str]) -> DatabaseService: - mock_setup.return_value = None - mock_import.return_value = None - _dialect: str = get_connection_params["dialect"] - _host: str = get_connection_params["host"] - _drivername: str = get_connection_params["drivername"] - _db_service: DatabaseService = DatabaseService() - await _db_service.initialize_database(local_database_dialect=_dialect, local_database_path=_host, local_database_driver=_drivername) - return _db_service - - class TestInit: - @pytest.mark.asyncio - @patch.object(DatabaseService, "import_default_values") - @patch.object(DatabaseService, "setup") - async def test_initialize_database(self, mock_setup: "MagicMock", mock_import: "MagicMock", get_connection_params: Dict[str, str]) -> None: - mock_setup.return_value = None - mock_import.return_value = None - _dialect: str = get_connection_params["dialect"] - _host: str = get_connection_params["host"] - _drivername: str = get_connection_params["drivername"] - _db_service: DatabaseService = DatabaseService() - await _db_service.initialize_database(local_database_dialect=_dialect, local_database_path=_host, local_database_driver=_drivername) - mock_setup.assert_called_once() - mock_import.assert_called_once() - - class TestValidateParameters: - @pytest.mark.asyncio - async def test_validate_no_checks( - self, get_connection_params_object: DatabaseConnectionParameters, get_database_service: DatabaseService - ) -> None: - _params: DatabaseConnectionParameters = get_connection_params_object - _db_service: DatabaseService = get_database_service - _params.username = "test" - _params.password = "test" - _params.database_name = "test" - _params.dialect = "test" - _params.drivername = "test" - _params.host = "test" - _params.port = "12345" - - _params.local_database_dialect = "test" - _params.local_database_driver = "test" - _params.local_database_path = "test" - - _params.use_remote = False - result_non_remote: Tuple[bool, str] = await _db_service._validate_connection_parameters(_params) - assert result_non_remote == (True, "") - - _params.use_remote = True - result_remote: Tuple[bool, str] = await _db_service._validate_connection_parameters(_params) - assert result_remote == (True, "") - - @pytest.mark.asyncio - @patch.object(DatabaseConnectionParameters, "validate_parameters") - async def test_validate_check_credentials( - self, mock_validate, get_connection_params_object: DatabaseConnectionParameters, get_database_service: DatabaseService - ) -> None: - _params: DatabaseConnectionParameters = get_connection_params_object - _db_service: DatabaseService = get_database_service - - _params.database_name = "test" - _params.dialect = "test" - _params.drivername = "test" - _params.host = "test" - _params.port = "12345" - _params.local_database_dialect = "test" - _params.local_database_driver = "test" - _params.local_database_path = "test" - _params.use_remote = True - - _params.username = "test" - _params.password = None - await _db_service._validate_connection_parameters(_params) - assert mock_validate.called is True - - _params.username = None - _params.password = "test" - await _db_service._validate_connection_parameters(_params) - assert mock_validate.called is True - - @pytest.mark.asyncio - @patch.object(DatabaseConnectionParameters, "validate_parameters") - async def test_validate_check_drivername( - self, mock_validate, get_connection_params_object: DatabaseConnectionParameters, get_database_service: DatabaseService - ) -> None: - _params: DatabaseConnectionParameters = get_connection_params_object - _db_service: DatabaseService = get_database_service - - _params.username = "test" - _params.password = "test" - _params.database_name = "test" - _params.dialect = "test" - _params.drivername = "test" - _params.host = "test" - _params.port = "12345" - _params.local_database_dialect = "test" - _params.local_database_driver = "test" - _params.local_database_path = "test" - _params.use_remote = True - - _params.drivername = None - await _db_service._validate_connection_parameters(_params) - assert mock_validate.called is True - - _params.drivername = "test" - await _db_service._validate_connection_parameters(_params) - assert mock_validate.called is True - - @pytest.mark.asyncio - @patch.object(DatabaseConnectionParameters, "validate_parameters") - async def test_validate_check_database_name( - self, mock_validate, get_connection_params_object: DatabaseConnectionParameters, get_database_service: DatabaseService - ) -> None: - _params: DatabaseConnectionParameters = get_connection_params_object - _db_service: DatabaseService = get_database_service - _params.username = "test" - _params.password = "test" - _params.database_name = "test" - _params.dialect = "test" - _params.host = "test" - _params.port = "12345" - _params.local_database_dialect = "test" - _params.local_database_driver = "test" - _params.local_database_path = "test" - _params.use_remote = True - - _params.database_name = None # type: ignore - await _db_service._validate_connection_parameters(_params) - assert mock_validate.called is True - - _params.database_name = "test" - await _db_service._validate_connection_parameters(_params) - assert mock_validate.called is True - - class TestSession: - @pytest.mark.asyncio - async def test_get_session_no_session_factory(self, get_database_service: DatabaseService) -> None: - _db_service: DatabaseService = get_database_service - _db_service._session_factory = None - with pytest.raises(DatabaseServiceError, match=r"an uninitialized session factory.$"): - async with _db_service.session() as _: - pass - - @pytest.mark.asyncio - async def test_get_session_exception_raised(self, get_database_service: DatabaseService) -> None: - _db_service: DatabaseService = get_database_service - _db_service._session_factory = async_scoped_session( - session_factory=async_sessionmaker(_db_service._engine, class_=AsyncSession, expire_on_commit=False), - scopefunc=asyncio.current_task, - ) - with pytest.raises(DatabaseServiceError, match=r"Rolled back changes.$"): - async with _db_service.session() as _: - raise Exception("something went wrong with getting a session") - - @pytest.mark.asyncio - async def test_get_session(self, get_database_service: DatabaseService) -> None: - _db_service: DatabaseService = get_database_service - _db_service._session_factory = async_scoped_session( - session_factory=async_sessionmaker(_db_service._engine, class_=AsyncSession, expire_on_commit=False), - scopefunc=asyncio.current_task, - ) - async with _db_service.session() as session: - assert isinstance(session, AsyncSession) is True - - class TestClose: - @pytest.mark.asyncio - @patch("sqlalchemy.ext.asyncio.AsyncEngine.dispose") - async def test_close_no_engine(self, mock_dispose, get_database_service: DatabaseService) -> None: - _db_service: DatabaseService = get_database_service - _db_service._engine = None - await _db_service.close() - assert _db_service._engine is None - assert not mock_dispose.called - - @pytest.mark.asyncio - async def test_close_no_clean(self, get_database_service: DatabaseService) -> None: - _db_service: DatabaseService = get_database_service - _db_service._engine = AsyncMock() - _db_service._connection_parameters = AsyncMock() - _db_service._session_factory = AsyncMock() - await _db_service.close(clean=False) - assert _db_service._connection_parameters is not None - assert _db_service._session_factory is not None - assert _db_service._engine is not None - - @pytest.mark.asyncio - async def test_close_clean(self, get_database_service: DatabaseService) -> None: - _db_service: DatabaseService = get_database_service - _db_service._engine = AsyncMock() - _db_service._connection_parameters = AsyncMock() - _db_service._session_factory = AsyncMock() - await _db_service.close(clean=True) - assert _db_service._connection_parameters is None - assert _db_service._session_factory is None - assert _db_service._engine is None - - class TestSetup: - @pytest.mark.asyncio - async def test_setup_engine_exists( - self, get_connection_params_object: DatabaseConnectionParameters, get_database_service: DatabaseService - ) -> None: - _db_connection_params: DatabaseConnectionParameters = get_connection_params_object - _db_service: DatabaseService = get_database_service - _db_service._engine = MagicMock() - with pytest.raises(DatabaseServiceError, match=r"Close the connection first.$"): - _ = await _db_service.setup(_db_connection_params) - - @pytest.mark.asyncio - async def test_setup_connection_parameters_is_none(self, get_database_service: DatabaseService) -> None: - _db_connection_params = None - _db_service: DatabaseService = get_database_service - _db_service._engine = None - with pytest.raises(DatabaseServiceError, match=r"Connection parameters were not provided.$"): - _ = await _db_service.setup(_db_connection_params) # type: ignore - - @pytest.mark.asyncio - async def test_setup_connection_parameters_fails_validation_remote( - self, get_connection_params_object: DatabaseConnectionParameters, get_database_service: DatabaseService - ) -> None: - _db_connection_params: DatabaseConnectionParameters = get_connection_params_object - _db_connection_params.dialect = None # type: ignore - _db_connection_params.username = "test" - _db_connection_params.password = "test" - _db_connection_params.database_name = "test" - _db_connection_params.host = "test" - _db_connection_params.port = "12345" - _db_connection_params.local_database_dialect = "test" - _db_connection_params.local_database_driver = "test" - _db_connection_params.local_database_path = "test" - _db_connection_params.use_remote = True - - _db_service: DatabaseService = get_database_service - _db_service._engine = None - with pytest.raises(DatabaseServiceError, match=r"Connection parameters are invalid - 'dialect=None'.$"): - _ = await _db_service.setup(_db_connection_params) - - @pytest.mark.asyncio - async def test_setup_connection_parameters_fails_validation_local( - self, get_connection_params_object: DatabaseConnectionParameters, get_database_service: DatabaseService - ) -> None: - _db_connection_params: DatabaseConnectionParameters = get_connection_params_object - _db_connection_params.local_database_dialect = None # type: ignore - _db_connection_params.local_database_driver = "test" - _db_connection_params.local_database_path = "test" - _db_connection_params.use_remote = False - - _db_service: DatabaseService = get_database_service - _db_service._engine = None - with pytest.raises(DatabaseServiceError, match=r"Connection parameters are invalid - 'local_database_dialect=None'.$"): - _ = await _db_service.setup(_db_connection_params) - - @pytest.mark.asyncio - @patch("src.utils.parsers.db_url_parser.get_url") - async def test_setup_database_connection_fails_to_open( - self, mock_get_url, get_connection_params_object: DatabaseConnectionParameters, get_database_service: DatabaseService - ) -> None: - _db_connection_params: DatabaseConnectionParameters = get_connection_params_object - _db_service: DatabaseService = get_database_service - _db_service._engine = None - _db_connection_params.dialect = "test" - _db_connection_params.username = "test" - _db_connection_params.password = "test" - _db_connection_params.database_name = "test" - _db_connection_params.drivername = "test" - _db_connection_params.host = "test" - _db_connection_params.port = "12345" - _db_connection_params.use_remote = True - mock_get_url.return_value = "" - with pytest.raises(DatabaseServiceError, match=r"Please check your dialect connection parameters.$"): - _ = await _db_service.setup(_db_connection_params) - - class TestImportDefaultValues: - @pytest.mark.asyncio - async def test_import_engine_is_none(self, get_database_service: DatabaseService) -> None: - _db_service: DatabaseService = get_database_service - _db_service._engine = None - with pytest.raises(DatabaseServiceError, match=r"Database connection engine is not initialized.$"): - await _db_service.import_default_values() - - @pytest.mark.asyncio - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - async def test_import_config_is_none(self, mock_cfg, get_database_service: DatabaseService) -> None: - _db_service: DatabaseService = get_database_service - _db_service._engine = AsyncMock() - mock_cfg.return_value = None - with pytest.raises(DatabaseServiceError, match=r"The mumimo config has not been initialized.$"): - await _db_service.import_default_values() - - @pytest.mark.asyncio - @patch.object(DatabaseService, "_import_default_aliases") - @patch.object(DatabaseService, "_import_default_permission_groups") - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - async def test_import_success(self, mock_cfg, mock_import_permissions, mock_import_aliases, get_database_service: DatabaseService) -> None: - _db_service: DatabaseService = get_database_service - _db_service._engine = AsyncMock() - mock_cfg.return_value = AsyncMock() - mock_import_permissions.return_value = None - mock_import_aliases.return_value = None - await _db_service.import_default_values() - - class TestImportDefaultPermissionGroups: - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_success_does_not_exist( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = ["test1", "test2", "test3"] - mock_session.return_value = get_db_session_factory() - _db_service: DatabaseService = get_database_service - await _db_service._import_default_permission_groups(mock_cfg) - session: async_scoped_session - async with get_db_session_factory() as session: - _query = select(PermissionGroupTable).filter(PermissionGroupTable.name.in_(["test1", "test2", "test3"])) - _result = await session.execute(_query) - _result = _result.scalars().all() - if _result is None: - pytest.fail("permissions not found, aborting test.") - assert len(_result) == 3 - assert [x.name.startswith("test") for x in _result] - - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_permission_exists_already( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = ["test1", "test2", "test3"] - mock_session.return_value = get_db_session_factory() - _db_service: DatabaseService = get_database_service - async with get_db_session_factory() as session: - for i in range(1, 3): - _permission_group = PermissionGroupTable(name=f"test{i}") - session.add(_permission_group) - await session.commit() - await _db_service._import_default_permission_groups(mock_cfg) - session: async_scoped_session - async with get_db_session_factory() as session: - _query = select(PermissionGroupTable).filter(PermissionGroupTable.name.in_(["test1", "test2", "test3"])) - _result = await session.execute(_query) - _result = _result.scalars().all() - if _result is None: - pytest.fail("permissions not found, aborting test.") - assert len(_result) == 3 - assert [x.name.startswith("test") for x in _result] - - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_no_default_permissions( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = [] - mock_session.return_value = get_db_session_factory() - _db_service: DatabaseService = get_database_service - await _db_service._import_default_permission_groups(mock_cfg) - session: async_scoped_session - async with get_db_session_factory() as session: - _query = select(PermissionGroupTable).filter(PermissionGroupTable.name.in_(["test1", "test2", "test3"])) - _result = await session.execute(_query) - _result = _result.scalars().all() - if _result is None: - pytest.fail("permissions not found, aborting test.") - assert len(_result) == 0 - - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_invalid_default_permissions( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = [True, None, 15] - mock_session.return_value = get_db_session_factory() - _db_service: DatabaseService = get_database_service - await _db_service._import_default_permission_groups(mock_cfg) - session: async_scoped_session - async with get_db_session_factory() as session: - _query = select(PermissionGroupTable).filter(PermissionGroupTable.name.in_(["test1", "test2", "test3"])) - _result = await session.execute(_query) - _result = _result.scalars().all() - if _result is None: - pytest.fail("permissions not found, aborting test.") - assert len(_result) == 0 - - class TestImportDefaultAliases: - class TestImportDefaultAliasesInvalidParameters: - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_invalid_alias_parameters_count( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = [["test1", "test1"]] - mock_session.return_value = get_db_session_factory() - _db_service: DatabaseService = get_database_service - await _db_service._import_default_aliases(mock_cfg) - async with get_db_session_factory() as session: - _query = select(AliasTable).filter(AliasTable.name.in_(["test1", "test2", "test3"])) - _result = await session.execute(_query) - _result = _result.scalars().all() - if _result is None: - pytest.fail("aliases not found, aborting test.") - assert len(_result) == 0 - - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_invalid_alias_name( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = [[5, "test1", "test1"]] - mock_session.return_value = get_db_session_factory() - _db_service: DatabaseService = get_database_service - await _db_service._import_default_aliases(mock_cfg) - async with get_db_session_factory() as session: - _query = select(AliasTable).filter(AliasTable.name.in_(["test1", "test2", "test3"])) - _result = await session.execute(_query) - _result = _result.scalars().all() - if _result is None: - pytest.fail("aliases not found, aborting test.") - assert len(_result) == 0 - - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_invalid_alias_command( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = [["test1", 5, "test1"]] - mock_session.return_value = get_db_session_factory() - _db_service: DatabaseService = get_database_service - await _db_service._import_default_aliases(mock_cfg) - async with get_db_session_factory() as session: - _query = select(AliasTable).filter(AliasTable.name.in_(["test1", "test2", "test3"])) - _result = await session.execute(_query) - _result = _result.scalars().all() - if _result is None: - pytest.fail("aliases not found, aborting test.") - assert len(_result) == 0 - - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_invalid_alias_permissions( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = [["test1", "test1", 5]] - mock_session.return_value = get_db_session_factory() - _db_service: DatabaseService = get_database_service - await _db_service._import_default_aliases(mock_cfg) - async with get_db_session_factory() as session: - _query = select(AliasTable).filter(AliasTable.name.in_(["test1", "test2", "test3"])) - _result = await session.execute(_query) - _result = _result.scalars().all() - if _result is None: - pytest.fail("aliases not found, aborting test.") - assert len(_result) == 0 - - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_aliases_already_exist( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = [["test1", "test1", "test1,test2,test3"]] - mock_session.return_value = get_db_session_factory() - async with get_db_session_factory() as session: - _alias = AliasTable(name="test1", command="test1") - session.add(_alias) - await session.commit() - - _db_service: DatabaseService = get_database_service - await _db_service._import_default_aliases(mock_cfg) - session: async_scoped_session - async with get_db_session_factory() as session: - _query = select(AliasTable).filter_by(name="test1") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("aliases not found, aborting test.") - assert _result.name == "test1" - - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_aliases_permissions_do_not_exist( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = [["test1", "test1", "test1,test2,test3"]] - mock_session.return_value = get_db_session_factory() - _db_service: DatabaseService = get_database_service - await _db_service._import_default_aliases(mock_cfg) - session: async_scoped_session - async with get_db_session_factory() as session: - _query = select(AliasTable).filter_by(name="test1") - _result = await session.execute(_query) - _result = _result.scalar() - assert _result is None - - @pytest.mark.asyncio - @patch.object(DatabaseService, "session") - @patch("src.config.Config") - async def test_import_aliases_permissions_exist( - self, mock_cfg, mock_session, get_db_session_factory: async_scoped_session, get_database_service: DatabaseService - ) -> None: - mock_cfg.get.return_value = [["test1", "test1", "test1,test2,test3"]] - mock_session.return_value = get_db_session_factory() - async with get_db_session_factory() as session: - for i in range(1, 3): - _alias = PermissionGroupTable(name=f"test{i}") - session.add(_alias) - await session.commit() - - _db_service: DatabaseService = get_database_service - await _db_service._import_default_aliases(mock_cfg) - session: async_scoped_session - async with get_db_session_factory() as session: - _query = select(AliasTable).filter_by(name="test1") - _result = await session.execute(_query) - _result = _result.scalar() - if _result is None: - pytest.fail("aliases not found, aborting test.") - assert _result.name == "test1" diff --git a/tests/src/services/test_mumimo_init_service.py b/tests/src/services/test_mumimo_init_service.py deleted file mode 100644 index 18bfe91..0000000 --- a/tests/src/services/test_mumimo_init_service.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import Any, Dict -from unittest.mock import patch - -import pytest - -from src.config import Config -from src.services.init_services.mumimo_init_service import MumimoInitService - - -class TestMumimoInitService: - @pytest.fixture(autouse=True) - def get_service(self): - return MumimoInitService({}) - - @pytest.fixture(autouse=True) - def get_config(self): - return Config("tests/data/config/test_config.toml") - - def test_init(self, get_service) -> None: - init_service: MumimoInitService = get_service - assert init_service._sys_args == {} - assert init_service._cfg_init_service is not None - assert init_service._client_settings_init_service is not None - - @pytest.mark.asyncio - @patch("src.services.init_services.plugins_init_service.PluginsInitService.initialize_plugins") - @patch("src.services.init_services.client_settings_init_service.ClientSettingsInitService.get_prioritized_env_options") - @patch("src.services.database_service.DatabaseService.initialize_database") - @patch("src.services.init_services.client_settings_init_service.ClientSettingsInitService.initialize_client_settings") - @patch("src.services.init_services.cfg_init_service.ConfigInitService.initialize_config") - async def test_initialize( - self, - mock_init_cfg, - mock_init_settings, - mock_db_init, - mock_prioritized_env, - mock_init_plugins, - get_config: Dict[str, Any], - get_service: MumimoInitService, - ) -> None: - init_service: MumimoInitService = get_service - init_service._sys_args = {} - - mock_init_cfg.return_value = get_config - mock_init_settings.return_value = None - mock_prioritized_env.return_value = {} - mock_db_init.return_value = None - mock_init_plugins.return_value = None - - await init_service.initialize() - - mock_init_cfg.assert_called_once() - mock_init_settings.assert_called_once() - mock_prioritized_env.assert_called_once() - mock_db_init.assert_called_once() - - @pytest.mark.asyncio - @patch("src.services.init_services.client_settings_init_service.ClientSettingsInitService.get_connection_parameters") - async def test_get_connection_parameters_prioritized_options_are_none(self, mock_conn_params, get_service: MumimoInitService) -> None: - mock_conn_params.return_value = {"test": "test"} - init_service: MumimoInitService = get_service - assert await init_service.get_connection_parameters() == {"test": "test"} diff --git a/tests/src/test_client_state.py b/tests/src/test_client_state.py deleted file mode 100644 index b834613..0000000 --- a/tests/src/test_client_state.py +++ /dev/null @@ -1,209 +0,0 @@ -from unittest.mock import patch - -import pytest -from pymumble_py3 import Mumble - -from src.client_state import ClientState -from src.config import Config -from src.log_config import LogConfig -from src.murmur_connection import MurmurConnection - - -class TestClientState: - @pytest.fixture(autouse=True) - @patch("src.settings.MumimoSettings.Configs.get_log_config") - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - @patch.object(MurmurConnection, "connection_instance") - def get_client_state(self, mumble_instance: Mumble, mock_mumimo_cfg, mock_log_cfg) -> ClientState: - mock_mumimo_cfg.return_value = Config("tests/data/config/test_config.toml") - mock_log_cfg.return_value = LogConfig("tests/data/config/test_logging.toml") - return ClientState(mumble_instance) - - @patch("src.settings.MumimoSettings.Configs.get_log_config") - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - @patch.object(MurmurConnection, "connection_instance") - def test_client_state_init(self, mumble_instance: Mumble, mock_mumimo_cfg, mock_log_cfg) -> None: - mock_mumimo_cfg.return_value = Config("tests/data/config/test_config.toml") - mock_log_cfg.return_value = LogConfig("tests/data/config/test_logging.toml") - client_state = ClientState(mumble_instance) - assert client_state is not None - assert client_state.audio_properties is not None - assert client_state.cmd_service is not None - - class TestAudioState: - def test_audio_state_init(self) -> None: - audio_state = ClientState.AudioProperties.AudioState() - assert audio_state is not None - - def test_audio_state_mute(self) -> None: - audio_state = ClientState.AudioProperties.AudioState() - audio_state._mute_state = ClientState.AudioProperties.AudioState.MuteState.UNMUTED - - audio_state.mute() - assert audio_state.mute_state == ClientState.AudioProperties.AudioState.MuteState.MUTED - - def test_audio_state_unmute(self) -> None: - audio_state = ClientState.AudioProperties.AudioState() - audio_state._mute_state = ClientState.AudioProperties.AudioState.MuteState.MUTED - - audio_state.unmute() - assert audio_state.mute_state == ClientState.AudioProperties.AudioState.MuteState.UNMUTED - - def test_audio_state_deafen(self) -> None: - audio_state = ClientState.AudioProperties.AudioState() - audio_state._deafen_state = ClientState.AudioProperties.AudioState.DeafenState.UNDEAFENED - - audio_state.deafen() - assert audio_state.deafen_state == ClientState.AudioProperties.AudioState.DeafenState.DEAFENED - - def test_audio_state_undeafen(self) -> None: - audio_state = ClientState.AudioProperties.AudioState() - audio_state._deafen_state = ClientState.AudioProperties.AudioState.DeafenState.DEAFENED - - audio_state.undeafen() - assert audio_state.deafen_state == ClientState.AudioProperties.AudioState.DeafenState.UNDEAFENED - - class TestAudioProperties: - def test_audio_properties_init(self, get_client_state: ClientState) -> None: - audio_properties = get_client_state.audio_properties - if audio_properties._state: - assert audio_properties._state.mute_state.value == ClientState.AudioProperties.AudioState.MuteState.UNMUTED.value - assert audio_properties._state.deafen_state.value == ClientState.AudioProperties.AudioState.DeafenState.UNDEAFENED.value - else: - pytest.fail("Something went wrong: the _state value is None") - - def test_audio_properties_state_exists(self, get_client_state: ClientState) -> None: - audio_properties = get_client_state.audio_properties - assert audio_properties.state is not None - - def test_audio_properties_state_does_not_exist(self, get_client_state: ClientState) -> None: - audio_properties = get_client_state.audio_properties - audio_properties._state = None - assert audio_properties.state is None - - class TestMute: - @patch.object(ClientState.AudioProperties, "_check_client_mute") - def test_audio_properties_mute(self, client_mute_mock, get_client_state: ClientState) -> None: - client_mute_mock.return_value = True - - audio_properties = get_client_state.audio_properties - if audio_properties._state: - audio_properties._state._mute_state = ClientState.AudioProperties.AudioState.MuteState.UNMUTED - - assert audio_properties.mute() is True - assert audio_properties._state.mute_state.value == ClientState.AudioProperties.AudioState.MuteState.MUTED.value - else: - pytest.fail("Something went wrong: the _state value is None") - - @patch.object(ClientState.AudioProperties, "_check_client_mute") - def test_audio_properties_mute_invalid_state(self, client_mute_mock, get_client_state: ClientState) -> None: - client_mute_mock.return_value = True - - audio_properties = get_client_state.audio_properties - audio_properties._state = None - - assert audio_properties.mute() is False - - @patch.object(ClientState.AudioProperties, "_check_client_mute") - def test_audio_properties_mute_invalid_mute_check(self, client_mute_mock, get_client_state: ClientState) -> None: - client_mute_mock.return_value = False - - audio_properties = get_client_state.audio_properties - - assert audio_properties.mute() is False - - class TestUnmute: - @patch.object(ClientState.AudioProperties, "_check_client_unmute") - def test_audio_properties_unmute(self, client_unmute_mock, get_client_state: ClientState) -> None: - client_unmute_mock.return_value = True - - audio_properties = get_client_state.audio_properties - if audio_properties._state: - audio_properties._state._mute_state = ClientState.AudioProperties.AudioState.MuteState.MUTED - - assert audio_properties.unmute() is True - assert audio_properties._state.mute_state.value == ClientState.AudioProperties.AudioState.MuteState.UNMUTED.value - else: - pytest.fail("Something went wrong: the _state value is None") - - @patch.object(ClientState.AudioProperties, "_check_client_unmute") - def test_audio_properties_unmute_invalid_state(self, client_unmute_mock, get_client_state: ClientState) -> None: - client_unmute_mock.return_value = True - - audio_properties = get_client_state.audio_properties - audio_properties._state = None - - assert audio_properties.unmute() is False - - @patch.object(ClientState.AudioProperties, "_check_client_unmute") - def test_audio_properties_unmute_invalid_unmute_check(self, client_unmute_mock, get_client_state: ClientState) -> None: - client_unmute_mock.return_value = False - - audio_properties = get_client_state.audio_properties - audio_properties._state = None - - assert audio_properties.unmute() is False - - class TestDeafen: - @patch.object(ClientState.AudioProperties, "_check_client_deafen") - def test_audio_properties_deafen(self, client_deafen_mock, get_client_state: ClientState) -> None: - client_deafen_mock.return_value = True - - audio_properties = get_client_state.audio_properties - if audio_properties._state: - audio_properties._state._deafen_state = ClientState.AudioProperties.AudioState.DeafenState.UNDEAFENED - - assert audio_properties.deafen() is True - assert audio_properties._state.deafen_state.value == ClientState.AudioProperties.AudioState.DeafenState.DEAFENED.value - else: - pytest.fail("Something went wrong: the _state value is None") - - @patch.object(ClientState.AudioProperties, "_check_client_deafen") - def test_audio_properties_deafen_invalid_state(self, client_deafen_mock, get_client_state: ClientState) -> None: - client_deafen_mock.return_value = True - - audio_properties = get_client_state.audio_properties - audio_properties._state = None - - assert audio_properties.deafen() is False - - @patch.object(ClientState.AudioProperties, "_check_client_deafen") - def test_audio_properties_deafen_invalid_deafen_check(self, client_deafen_mock, get_client_state: ClientState) -> None: - client_deafen_mock.return_value = False - - audio_properties = get_client_state.audio_properties - audio_properties._state = None - - assert audio_properties.deafen() is False - - class TestUndeafen: - @patch.object(ClientState.AudioProperties, "_check_client_undeafen") - def test_audio_properties_undeafen(self, client_undeafen_mock, get_client_state: ClientState) -> None: - client_undeafen_mock.return_value = True - - audio_properties = get_client_state.audio_properties - if audio_properties._state: - audio_properties._state._deafen_state = ClientState.AudioProperties.AudioState.DeafenState.DEAFENED - - assert audio_properties.undeafen() is True - assert audio_properties._state.deafen_state.value == ClientState.AudioProperties.AudioState.DeafenState.UNDEAFENED.value - else: - pytest.fail("Something went wrong: the _state value is None") - - @patch.object(ClientState.AudioProperties, "_check_client_undeafen") - def test_audio_properties_undeafen_invalid_state(self, client_undeafen_mock, get_client_state: ClientState) -> None: - client_undeafen_mock.return_value = True - - audio_properties = get_client_state.audio_properties - audio_properties._state = None - - assert audio_properties.undeafen() is False - - @patch.object(ClientState.AudioProperties, "_check_client_undeafen") - def test_audio_properties_undeafen_invalid_undeafen_check(self, client_undeafen_mock, get_client_state: ClientState) -> None: - client_undeafen_mock.return_value = False - - audio_properties = get_client_state.audio_properties - audio_properties._state = None - - assert audio_properties.undeafen() is False diff --git a/tests/src/test_config.py b/tests/src/test_config.py deleted file mode 100644 index a62432b..0000000 --- a/tests/src/test_config.py +++ /dev/null @@ -1,332 +0,0 @@ -import pathlib -import re -from typing import Any, Dict -from unittest.mock import patch - -import pytest -import toml - -from src.config import Config -from src.exceptions import ConfigReadError, ConfigWriteError - - -class TestConfig: - @pytest.fixture(autouse=True) - def config(self, config_path: str) -> Config: - return Config(config_path) - - @pytest.fixture(autouse=True) - def empty_config(self) -> Config: - return Config() - - @pytest.fixture(autouse=True) - def config_path(self) -> pathlib.Path: - return pathlib.Path.cwd() / "tests/data/config/test_config.toml" - - @pytest.fixture(autouse=True) - def alt_config_path(self) -> pathlib.Path: - return pathlib.Path.cwd() / "tests/data/config/test_config_alt.toml" - - @pytest.fixture(autouse=True) - def broken_config_path(self) -> pathlib.Path: - return pathlib.Path.cwd() / "tests/data/config/test_config_broken.toml" - - @pytest.fixture(autouse=True) - def invalid_config_path(self) -> pathlib.Path: - return pathlib.Path.cwd() / "invalid/path/to/config/test_config.toml" - - @pytest.fixture(autouse=True) - def save_config_path(self) -> pathlib.Path: - return pathlib.Path.cwd() / "tests/data/generated/generated_config.toml" - - @pytest.fixture(autouse=True) - def expected_config_data(self) -> Dict[str, Any]: - return { - "test_section_1": {"test_1": "test_1", "test_2": 2, "test_3": ["test_3"], "test_4": True, "nested": {"test_1": "test_1"}}, - "test_section_2": {"test_1": "test_1", "test_2": 2, "test_3": ["test_3"], "test_4": True}, - } - - @pytest.fixture(autouse=True) - def expected_alt_config_data(self) -> Dict[str, Any]: - return { - "test_section_0": {"test_1": "test_1", "test_2": 2, "test_3": ["test_3"], "test_4": True}, - "test_section_1": {"test_1": "test_1", "test_2": 2, "test_3": ["test_3"], "test_4": True}, - } - - @pytest.fixture(autouse=True) - def config_save_data(self, expected_config_data: Dict[str, Any]) -> Dict[str, Any]: - return expected_config_data - - @pytest.fixture(autouse=True) - def expected_save_data(self): - return re.sub( - r"[\t\s]*", - "", - """ - [test_section_1] - test_1 = "test_1" - test_2 = 2 - test_3 = ["test_3",] - test_4 = true - - [test_section_2] - test_1 = "test_1" - test_2 = 2 - test_3 = ["test_3",] - test_4 = true - - [test_section_1.nested] - test_1 = "test_1" - """.strip(), - ) - - @pytest.fixture(autouse=True) - def expected_save_data_modified_only(self): - return re.sub( - r"[\t\s]*", - "", - """ - [test] - test_1 = false - test_2 = true - """.strip(), - ) - - def test_config_init_without_file_path(self, empty_config: Config, caplog) -> None: - assert empty_config._config_file_path is None - - def test_config_init_with_file_path(self, config: Config) -> None: - assert config._config_file_path is not None - - class TestConfigRead: - def test_config_read_from_init(self, config: Config, expected_config_data: Dict[str, Any]) -> None: - config = config.read() - assert config.items() == expected_config_data.items() - - def test_config_read_from_valid_path(self, empty_config: Config, config_path: pathlib.Path, expected_config_data: Dict[str, Any]) -> None: - empty_config = empty_config.read(config_path) - assert empty_config.items() == expected_config_data.items() - - def test_config_read_from_missing_path(self, empty_config: Config) -> None: - empty_config._config_file_path = None - with pytest.raises(ConfigReadError, match=r".*\ no file was specified.$"): - empty_config.read() - - def test_config_read_from_invalid_path(self, empty_config: Config, invalid_config_path: pathlib.Path) -> None: - with pytest.raises(ConfigReadError, match=r"^Unable to read config file at:"): - empty_config.read(invalid_config_path) - - def test_config__read_from_file_valid_path(self, empty_config: Config, config_path: str, expected_config_data: Dict[str, Any]) -> None: - search_path = pathlib.Path.cwd() / config_path - empty_config._read_from_file(search_path) - assert empty_config.items() == expected_config_data.items() - - def test_config__read_from_file_empty_path(self, empty_config: Config) -> None: - search_path = pathlib.Path.cwd() / "" - with pytest.raises(ConfigReadError, match=r"^Unable to open config file to read at:"): - empty_config._read_from_file(search_path) - - def test_config__read_from_file_invalid_path(self, empty_config: Config, invalid_config_path: str) -> None: - search_path = pathlib.Path.cwd() / invalid_config_path - with pytest.raises(ConfigReadError, match=r"^Unable to open config file to read at:"): - empty_config._read_from_file(search_path) - - def test_config__read_from_file_invalid_toml(self, empty_config: Config, broken_config_path: str) -> None: - search_path = pathlib.Path.cwd() / broken_config_path - with pytest.raises(ConfigReadError, match=r"^Unable to read config file at:"): - empty_config._read_from_file(search_path) - - class TestConfigSave: - def _validate_saved_data(self, actual, expected) -> bool: - return re.sub(r"[\t\s]*", "", actual) == re.sub(r"[\t\s]*", "", expected) - - def test_config_save_no_target_paths(self, empty_config: Config) -> None: - empty_config._config_file_path = None - with pytest.raises(ConfigWriteError, match=r"^Unable to save data to a config file"): - empty_config.save() - - def test_config_save_valid_data_no_file_path( - self, empty_config: Config, save_config_path: str, config_save_data: Dict[str, Any], expected_save_data: str - ) -> None: - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config.update(config_save_data) - saved_data: str = empty_config.save() - assert self._validate_saved_data(saved_data, expected_save_data) is True - - def test_config_save_valid_data_with_file_path( - self, empty_config: Config, save_config_path: str, config_save_data: Dict[str, Any], expected_save_data: str - ) -> None: - empty_config.update(config_save_data) - saved_data: str = empty_config.save(save_config_path) - assert self._validate_saved_data(saved_data, expected_save_data) is True - - def test_config_save_modified_data_only(self, empty_config: Config, save_config_path: str, expected_save_data_modified_only: str) -> None: - original_config = {"test": {"test_1": True, "test_2": True}} - updates = {"test": {"test_1": False, "test_2": True}} - empty_config.update(original_config) - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config._initial_config = Config() - empty_config._initial_config._config_file_path = empty_config._config_file_path - empty_config._initial_config.update(empty_config) - empty_config.update(updates) - saved_data: str = empty_config.save(modified_only=True, modified_field_name="test.test_1") - assert self._validate_saved_data(saved_data, expected_save_data_modified_only) is True - - def test_config_save_modified_data_only_missing_initial_config(self, empty_config: Config, save_config_path: str) -> None: - original_config = {"test": {"test_1": True, "test_2": True}} - updates = {"test": {"test_1": False, "test_2": True}} - empty_config._initial_config = None - empty_config.update(original_config) - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config.update(updates) - with pytest.raises(ConfigWriteError, match=r"because no file has been initialized.$"): - empty_config.save(modified_only=True, modified_field_name="test.test_1") - - def test_config_save_modified_data_only_no_section_provided(self, empty_config: Config, save_config_path: str) -> None: - original_config = {"test": {"test_1": True, "test_2": True}} - updates = {"test": {"test_1": False, "test_2": True}} - empty_config.update(original_config) - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config._initial_config = Config() - empty_config._initial_config._config_file_path = empty_config._config_file_path - empty_config._initial_config.update(empty_config) - empty_config.update(updates) - with pytest.raises(ConfigWriteError, match=r"the field name is invalid.$"): - empty_config.save(modified_only=True, modified_field_name="") - - def test_config_save_modified_data_only_invalid_field_provided(self, empty_config: Config, save_config_path: str) -> None: - original_config = {"test": {"test_1": True, "test_2": True}} - updates = {"test": {"test_1": False, "test_2": True}} - empty_config.update(original_config) - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config._initial_config = Config() - empty_config._initial_config._config_file_path = empty_config._config_file_path - empty_config._initial_config.update(empty_config) - empty_config.update(updates) - with pytest.raises(ConfigWriteError, match=r"the field name does not exist.$"): - empty_config.save(modified_only=True, modified_field_name="testing123") - - def test_config_save_modified_data_only_invalid_nested_field_provided(self, empty_config: Config, save_config_path: str) -> None: - original_config = {"test": {"test_1": True, "test_2": True}} - updates = {"test": {"test_1": False, "test_2": True}} - empty_config.update(original_config) - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config._initial_config = Config() - empty_config._initial_config._config_file_path = empty_config._config_file_path - empty_config._initial_config.update(empty_config) - empty_config.update(updates) - with pytest.raises(ConfigWriteError, match=r"the field name does not exist.$"): - empty_config.save(modified_only=True, modified_field_name="test.testing123") - - @patch.object(toml, "dump") - def test_config_save_toml_dump_failed(self, toml_return, empty_config: Config, save_config_path: str) -> None: - toml_return.return_value = None - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - with pytest.raises(ConfigWriteError, match=r"^Unable to save data to a config file at:"): - empty_config.save() - - def test_config_save_invalid_path(self, empty_config: Config, invalid_config_path: str) -> None: - empty_config._config_file_path = pathlib.Path.cwd() / invalid_config_path - with pytest.raises(ConfigWriteError, match=r"^Unable to save config file at:"): - empty_config.save() - - class TestConfigReset: - def test_config_reset_valid_field(self, empty_config: Config) -> None: - empty_config.update({"field": "test"}) - assert empty_config.reset("field") is True - assert empty_config.get("field") is None - - def test_config_reset_valid_nested_field(self, empty_config: Config) -> None: - empty_config.update({"outer": {"inner": "test"}}) - assert empty_config.reset("outer.inner") is True - assert empty_config.get("outer.inner") is None - - def test_config_reset_invalid_field_is_empty(self, empty_config: Config) -> None: - assert empty_config.reset("") is False - - def test_config_reset_invalid_field_does_not_exist(self, empty_config: Config) -> None: - assert empty_config.reset("field") is False - - def test_config_reset_invalid_nested_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"outer": {"inner": "test"}}) - assert empty_config.reset("outer.invalid_inner") is False - - class TestConfigSet: - def test_config_set_field_value_when_section_exists(self, empty_config: Config) -> None: - empty_config.update({"main": {"field": "test1"}}) - assert empty_config.set("main.field", "test2") is True - assert empty_config.get("main.field") == "test2" - - def test_config_set_field_value_fails_when_nested_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {"field": "test1"}}) - assert empty_config.set("main.new_field", "test2") is False - assert empty_config.get("main.new_field") is None - - def test_config_set_new_nested_field_with_value_when_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {}}) - assert empty_config.set("main.new_field", "test2", create_keys_if_not_exists=True) is True - assert empty_config.get("main.new_field") == "test2" - - def test_config_set_new_nested_field_with_none_when_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {}}) - assert empty_config.set("main.field", None, create_keys_if_not_exists=True) is True - assert empty_config.get("main.field") is None - assert empty_config == {"main": {"field": None}} - - def test_config_set_new_nested_field_with_empty_string_when_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {}}) - assert empty_config.set("main.field", "", create_keys_if_not_exists=True) is True - assert empty_config.get("main.field") == "" - assert empty_config == {"main": {"field": ""}} - - def test_config_set_new_nested_field_with_empty_dict_when_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {}}) - assert empty_config.set("main.field", {}, create_keys_if_not_exists=True) is True - assert empty_config.get("main.field") == {} - assert empty_config == {"main": {"field": {}}} - - def test_config_set_new_section_and_nested_field_when_not_exists(self, empty_config: Config) -> None: - assert empty_config.set("new_section.field", "test", create_keys_if_not_exists=True) is True - assert empty_config.get("new_section.field") == "test" - assert empty_config == {"new_section": {"field": "test"}} - - def test_config_set_new_section_when_not_exists(self, empty_config: Config) -> None: - assert empty_config.set("new_section", {}, create_keys_if_not_exists=True) is True - assert empty_config.get("new_section") == {} - assert empty_config == {"new_section": {}} - - def test_config_set_empty_field_name(self, empty_config: Config) -> None: - assert empty_config.set("") is False - - def test_config_set_none_field_name(self, empty_config: Config) -> None: - assert empty_config.set(None) is False # type: ignore - - class TestConfigGet: - def test_config_get_field(self, empty_config: Config) -> None: - empty_config.update({"main": {"test": None}}) - assert empty_config.get("main") == {"test": None} - - def test_config_get_field_does_not_exist(self, empty_config: Config) -> None: - assert empty_config.get("main") is None - - def test_config_get_field_does_not_exist_fallback(self, empty_config: Config) -> None: - assert empty_config.get("main", fallback={"test": None}) == {"test": None} - - def test_config_get_exception_when_field_name_is_none(self, empty_config: Config) -> None: - assert empty_config.get(field_name=None) is None # type: ignore - - def test_config_get_nested_field_when_parent_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {"test": None}}) - assert empty_config.get("test.test") is None - - def test_config_get_nested_field(self, empty_config: Config) -> None: - empty_config.update({"main": {"test": "value"}}) - assert empty_config.get("main.test") == "value" - - def test_config_get_nested_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {"test": "val"}}) - assert empty_config.get("main.new_test") is None - - def test_config_get_nested_field_does_not_exist_fallback(self, empty_config: Config) -> None: - empty_config.update({"main": {"test": "val"}}) - assert empty_config.get("main.new_test", fallback={"new_test": "val"}) == {"new_test": "val"} diff --git a/tests/src/test_exceptions.py b/tests/src/test_exceptions.py deleted file mode 100644 index fb5a4f0..0000000 --- a/tests/src/test_exceptions.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest - -from src import exceptions - - -class TestExceptions: - def test_raise_logged_error(self): - with pytest.raises(exceptions.LoggedException, match="test"): - raise exceptions.LoggedException("test", logger=None) - - def test_raise_validation_error(self): - with pytest.raises(exceptions.ValidationError, match="test"): - raise exceptions.ValidationError("test", logger=None) - - def test_raise_connectivity_error(self): - with pytest.raises(exceptions.ConnectivityError, match="test"): - raise exceptions.ConnectivityError("test", logger=None) - - def test_raise_service_error(self): - with pytest.raises(exceptions.ServiceError, match="test"): - raise exceptions.ServiceError("test", logger=None) - - def test_raise_database_service_error(self): - with pytest.raises(exceptions.DatabaseServiceError, match="test"): - raise exceptions.DatabaseServiceError("test", logger=None) - - def test_raise_config_error(self): - with pytest.raises(exceptions.ConfigError, match="test"): - raise exceptions.ConfigError("test", logger=None) - - def test_raise_config_read_error(self): - with pytest.raises(exceptions.ConfigReadError, match="test"): - raise exceptions.ConfigReadError("test", logger=None) - - def test_raise_config_write_error(self): - with pytest.raises(exceptions.ConfigWriteError, match="test"): - raise exceptions.ConfigWriteError("test", logger=None) diff --git a/tests/src/test_log_config.py b/tests/src/test_log_config.py deleted file mode 100644 index a62432b..0000000 --- a/tests/src/test_log_config.py +++ /dev/null @@ -1,332 +0,0 @@ -import pathlib -import re -from typing import Any, Dict -from unittest.mock import patch - -import pytest -import toml - -from src.config import Config -from src.exceptions import ConfigReadError, ConfigWriteError - - -class TestConfig: - @pytest.fixture(autouse=True) - def config(self, config_path: str) -> Config: - return Config(config_path) - - @pytest.fixture(autouse=True) - def empty_config(self) -> Config: - return Config() - - @pytest.fixture(autouse=True) - def config_path(self) -> pathlib.Path: - return pathlib.Path.cwd() / "tests/data/config/test_config.toml" - - @pytest.fixture(autouse=True) - def alt_config_path(self) -> pathlib.Path: - return pathlib.Path.cwd() / "tests/data/config/test_config_alt.toml" - - @pytest.fixture(autouse=True) - def broken_config_path(self) -> pathlib.Path: - return pathlib.Path.cwd() / "tests/data/config/test_config_broken.toml" - - @pytest.fixture(autouse=True) - def invalid_config_path(self) -> pathlib.Path: - return pathlib.Path.cwd() / "invalid/path/to/config/test_config.toml" - - @pytest.fixture(autouse=True) - def save_config_path(self) -> pathlib.Path: - return pathlib.Path.cwd() / "tests/data/generated/generated_config.toml" - - @pytest.fixture(autouse=True) - def expected_config_data(self) -> Dict[str, Any]: - return { - "test_section_1": {"test_1": "test_1", "test_2": 2, "test_3": ["test_3"], "test_4": True, "nested": {"test_1": "test_1"}}, - "test_section_2": {"test_1": "test_1", "test_2": 2, "test_3": ["test_3"], "test_4": True}, - } - - @pytest.fixture(autouse=True) - def expected_alt_config_data(self) -> Dict[str, Any]: - return { - "test_section_0": {"test_1": "test_1", "test_2": 2, "test_3": ["test_3"], "test_4": True}, - "test_section_1": {"test_1": "test_1", "test_2": 2, "test_3": ["test_3"], "test_4": True}, - } - - @pytest.fixture(autouse=True) - def config_save_data(self, expected_config_data: Dict[str, Any]) -> Dict[str, Any]: - return expected_config_data - - @pytest.fixture(autouse=True) - def expected_save_data(self): - return re.sub( - r"[\t\s]*", - "", - """ - [test_section_1] - test_1 = "test_1" - test_2 = 2 - test_3 = ["test_3",] - test_4 = true - - [test_section_2] - test_1 = "test_1" - test_2 = 2 - test_3 = ["test_3",] - test_4 = true - - [test_section_1.nested] - test_1 = "test_1" - """.strip(), - ) - - @pytest.fixture(autouse=True) - def expected_save_data_modified_only(self): - return re.sub( - r"[\t\s]*", - "", - """ - [test] - test_1 = false - test_2 = true - """.strip(), - ) - - def test_config_init_without_file_path(self, empty_config: Config, caplog) -> None: - assert empty_config._config_file_path is None - - def test_config_init_with_file_path(self, config: Config) -> None: - assert config._config_file_path is not None - - class TestConfigRead: - def test_config_read_from_init(self, config: Config, expected_config_data: Dict[str, Any]) -> None: - config = config.read() - assert config.items() == expected_config_data.items() - - def test_config_read_from_valid_path(self, empty_config: Config, config_path: pathlib.Path, expected_config_data: Dict[str, Any]) -> None: - empty_config = empty_config.read(config_path) - assert empty_config.items() == expected_config_data.items() - - def test_config_read_from_missing_path(self, empty_config: Config) -> None: - empty_config._config_file_path = None - with pytest.raises(ConfigReadError, match=r".*\ no file was specified.$"): - empty_config.read() - - def test_config_read_from_invalid_path(self, empty_config: Config, invalid_config_path: pathlib.Path) -> None: - with pytest.raises(ConfigReadError, match=r"^Unable to read config file at:"): - empty_config.read(invalid_config_path) - - def test_config__read_from_file_valid_path(self, empty_config: Config, config_path: str, expected_config_data: Dict[str, Any]) -> None: - search_path = pathlib.Path.cwd() / config_path - empty_config._read_from_file(search_path) - assert empty_config.items() == expected_config_data.items() - - def test_config__read_from_file_empty_path(self, empty_config: Config) -> None: - search_path = pathlib.Path.cwd() / "" - with pytest.raises(ConfigReadError, match=r"^Unable to open config file to read at:"): - empty_config._read_from_file(search_path) - - def test_config__read_from_file_invalid_path(self, empty_config: Config, invalid_config_path: str) -> None: - search_path = pathlib.Path.cwd() / invalid_config_path - with pytest.raises(ConfigReadError, match=r"^Unable to open config file to read at:"): - empty_config._read_from_file(search_path) - - def test_config__read_from_file_invalid_toml(self, empty_config: Config, broken_config_path: str) -> None: - search_path = pathlib.Path.cwd() / broken_config_path - with pytest.raises(ConfigReadError, match=r"^Unable to read config file at:"): - empty_config._read_from_file(search_path) - - class TestConfigSave: - def _validate_saved_data(self, actual, expected) -> bool: - return re.sub(r"[\t\s]*", "", actual) == re.sub(r"[\t\s]*", "", expected) - - def test_config_save_no_target_paths(self, empty_config: Config) -> None: - empty_config._config_file_path = None - with pytest.raises(ConfigWriteError, match=r"^Unable to save data to a config file"): - empty_config.save() - - def test_config_save_valid_data_no_file_path( - self, empty_config: Config, save_config_path: str, config_save_data: Dict[str, Any], expected_save_data: str - ) -> None: - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config.update(config_save_data) - saved_data: str = empty_config.save() - assert self._validate_saved_data(saved_data, expected_save_data) is True - - def test_config_save_valid_data_with_file_path( - self, empty_config: Config, save_config_path: str, config_save_data: Dict[str, Any], expected_save_data: str - ) -> None: - empty_config.update(config_save_data) - saved_data: str = empty_config.save(save_config_path) - assert self._validate_saved_data(saved_data, expected_save_data) is True - - def test_config_save_modified_data_only(self, empty_config: Config, save_config_path: str, expected_save_data_modified_only: str) -> None: - original_config = {"test": {"test_1": True, "test_2": True}} - updates = {"test": {"test_1": False, "test_2": True}} - empty_config.update(original_config) - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config._initial_config = Config() - empty_config._initial_config._config_file_path = empty_config._config_file_path - empty_config._initial_config.update(empty_config) - empty_config.update(updates) - saved_data: str = empty_config.save(modified_only=True, modified_field_name="test.test_1") - assert self._validate_saved_data(saved_data, expected_save_data_modified_only) is True - - def test_config_save_modified_data_only_missing_initial_config(self, empty_config: Config, save_config_path: str) -> None: - original_config = {"test": {"test_1": True, "test_2": True}} - updates = {"test": {"test_1": False, "test_2": True}} - empty_config._initial_config = None - empty_config.update(original_config) - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config.update(updates) - with pytest.raises(ConfigWriteError, match=r"because no file has been initialized.$"): - empty_config.save(modified_only=True, modified_field_name="test.test_1") - - def test_config_save_modified_data_only_no_section_provided(self, empty_config: Config, save_config_path: str) -> None: - original_config = {"test": {"test_1": True, "test_2": True}} - updates = {"test": {"test_1": False, "test_2": True}} - empty_config.update(original_config) - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config._initial_config = Config() - empty_config._initial_config._config_file_path = empty_config._config_file_path - empty_config._initial_config.update(empty_config) - empty_config.update(updates) - with pytest.raises(ConfigWriteError, match=r"the field name is invalid.$"): - empty_config.save(modified_only=True, modified_field_name="") - - def test_config_save_modified_data_only_invalid_field_provided(self, empty_config: Config, save_config_path: str) -> None: - original_config = {"test": {"test_1": True, "test_2": True}} - updates = {"test": {"test_1": False, "test_2": True}} - empty_config.update(original_config) - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config._initial_config = Config() - empty_config._initial_config._config_file_path = empty_config._config_file_path - empty_config._initial_config.update(empty_config) - empty_config.update(updates) - with pytest.raises(ConfigWriteError, match=r"the field name does not exist.$"): - empty_config.save(modified_only=True, modified_field_name="testing123") - - def test_config_save_modified_data_only_invalid_nested_field_provided(self, empty_config: Config, save_config_path: str) -> None: - original_config = {"test": {"test_1": True, "test_2": True}} - updates = {"test": {"test_1": False, "test_2": True}} - empty_config.update(original_config) - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - empty_config._initial_config = Config() - empty_config._initial_config._config_file_path = empty_config._config_file_path - empty_config._initial_config.update(empty_config) - empty_config.update(updates) - with pytest.raises(ConfigWriteError, match=r"the field name does not exist.$"): - empty_config.save(modified_only=True, modified_field_name="test.testing123") - - @patch.object(toml, "dump") - def test_config_save_toml_dump_failed(self, toml_return, empty_config: Config, save_config_path: str) -> None: - toml_return.return_value = None - empty_config._config_file_path = pathlib.Path.cwd() / save_config_path - with pytest.raises(ConfigWriteError, match=r"^Unable to save data to a config file at:"): - empty_config.save() - - def test_config_save_invalid_path(self, empty_config: Config, invalid_config_path: str) -> None: - empty_config._config_file_path = pathlib.Path.cwd() / invalid_config_path - with pytest.raises(ConfigWriteError, match=r"^Unable to save config file at:"): - empty_config.save() - - class TestConfigReset: - def test_config_reset_valid_field(self, empty_config: Config) -> None: - empty_config.update({"field": "test"}) - assert empty_config.reset("field") is True - assert empty_config.get("field") is None - - def test_config_reset_valid_nested_field(self, empty_config: Config) -> None: - empty_config.update({"outer": {"inner": "test"}}) - assert empty_config.reset("outer.inner") is True - assert empty_config.get("outer.inner") is None - - def test_config_reset_invalid_field_is_empty(self, empty_config: Config) -> None: - assert empty_config.reset("") is False - - def test_config_reset_invalid_field_does_not_exist(self, empty_config: Config) -> None: - assert empty_config.reset("field") is False - - def test_config_reset_invalid_nested_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"outer": {"inner": "test"}}) - assert empty_config.reset("outer.invalid_inner") is False - - class TestConfigSet: - def test_config_set_field_value_when_section_exists(self, empty_config: Config) -> None: - empty_config.update({"main": {"field": "test1"}}) - assert empty_config.set("main.field", "test2") is True - assert empty_config.get("main.field") == "test2" - - def test_config_set_field_value_fails_when_nested_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {"field": "test1"}}) - assert empty_config.set("main.new_field", "test2") is False - assert empty_config.get("main.new_field") is None - - def test_config_set_new_nested_field_with_value_when_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {}}) - assert empty_config.set("main.new_field", "test2", create_keys_if_not_exists=True) is True - assert empty_config.get("main.new_field") == "test2" - - def test_config_set_new_nested_field_with_none_when_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {}}) - assert empty_config.set("main.field", None, create_keys_if_not_exists=True) is True - assert empty_config.get("main.field") is None - assert empty_config == {"main": {"field": None}} - - def test_config_set_new_nested_field_with_empty_string_when_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {}}) - assert empty_config.set("main.field", "", create_keys_if_not_exists=True) is True - assert empty_config.get("main.field") == "" - assert empty_config == {"main": {"field": ""}} - - def test_config_set_new_nested_field_with_empty_dict_when_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {}}) - assert empty_config.set("main.field", {}, create_keys_if_not_exists=True) is True - assert empty_config.get("main.field") == {} - assert empty_config == {"main": {"field": {}}} - - def test_config_set_new_section_and_nested_field_when_not_exists(self, empty_config: Config) -> None: - assert empty_config.set("new_section.field", "test", create_keys_if_not_exists=True) is True - assert empty_config.get("new_section.field") == "test" - assert empty_config == {"new_section": {"field": "test"}} - - def test_config_set_new_section_when_not_exists(self, empty_config: Config) -> None: - assert empty_config.set("new_section", {}, create_keys_if_not_exists=True) is True - assert empty_config.get("new_section") == {} - assert empty_config == {"new_section": {}} - - def test_config_set_empty_field_name(self, empty_config: Config) -> None: - assert empty_config.set("") is False - - def test_config_set_none_field_name(self, empty_config: Config) -> None: - assert empty_config.set(None) is False # type: ignore - - class TestConfigGet: - def test_config_get_field(self, empty_config: Config) -> None: - empty_config.update({"main": {"test": None}}) - assert empty_config.get("main") == {"test": None} - - def test_config_get_field_does_not_exist(self, empty_config: Config) -> None: - assert empty_config.get("main") is None - - def test_config_get_field_does_not_exist_fallback(self, empty_config: Config) -> None: - assert empty_config.get("main", fallback={"test": None}) == {"test": None} - - def test_config_get_exception_when_field_name_is_none(self, empty_config: Config) -> None: - assert empty_config.get(field_name=None) is None # type: ignore - - def test_config_get_nested_field_when_parent_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {"test": None}}) - assert empty_config.get("test.test") is None - - def test_config_get_nested_field(self, empty_config: Config) -> None: - empty_config.update({"main": {"test": "value"}}) - assert empty_config.get("main.test") == "value" - - def test_config_get_nested_field_does_not_exist(self, empty_config: Config) -> None: - empty_config.update({"main": {"test": "val"}}) - assert empty_config.get("main.new_test") is None - - def test_config_get_nested_field_does_not_exist_fallback(self, empty_config: Config) -> None: - empty_config.update({"main": {"test": "val"}}) - assert empty_config.get("main.new_test", fallback={"new_test": "val"}) == {"new_test": "val"} diff --git a/tests/src/test_logging.py b/tests/src/test_logging.py deleted file mode 100644 index e7e474c..0000000 --- a/tests/src/test_logging.py +++ /dev/null @@ -1,100 +0,0 @@ -import logging -import pathlib -import shutil -import sys -from unittest.mock import patch - -import pytest - -import src.logging as log -from src.log_config import LogConfig - - -class TestLogging: - @pytest.fixture(autouse=True) - def get_logger(self): - log._IS_INITIALIZED = False - assert log.init_logger({"log_config_file": "tests/data/config/test_logging.toml"}) is True - assert log._IS_INITIALIZED is True - yield logging.getLogger(__name__) - log._IS_INITIALIZED = False - generated_logs_path = pathlib.Path("tests/data/generated/logs") - if generated_logs_path.exists(): - shutil.rmtree(generated_logs_path) - - @pytest.fixture(autouse=True) - def mock_log_config(self): - _cfg = { - "output": { - "file": { - "enable": False, - "level": "DEBUG", - "path": "tests/data/generated/logs/", - "format": "(%(asctime)s)[%(name)s][%(levelname)s]::%(message)s", - "name": "mumimo_test_%s.log", - "message_privacy": True, - }, - "console": { - "format": "[%(levelname)s]::%(message)s", - "message_privacy": True, - }, - } - } - cfg = LogConfig("/tmp/mumimo_test.toml") - cfg.update(_cfg) - return cfg - - def test_logger_already_initialized(self): - log._IS_INITIALIZED = True - assert log.init_logger() is False - - @patch("src.logging._logger.root.addHandler") - @patch("src.logging._logger.root.hasHandlers") - def test_init_logger(self, mock_logger_handlers, mock_add_handler): - log._IS_INITIALIZED = False - mock_logger_handlers.return_value = False - assert log.init_logger({"log_config_file": "tests/data/config/test_logging.toml"}) is True - assert mock_add_handler.called is True - - def test_init_logger_no_sys_args(self): - log._IS_INITIALIZED = False - assert log.init_logger() is False - - @patch("src.logging._logger.root.addHandler") - @patch("src.logging._logger.root.hasHandlers") - def test_init_logger_no_handlers(self, mock_logger_handlers, mock_add_handler): - log._IS_INITIALIZED = False - mock_logger_handlers.return_value = True - log.init_logger({"log_config_file": "tests/data/config/test_logging.toml"}) - assert not mock_add_handler.called - - @patch("src.utils.log_utils.initialize_log_config") - @patch("src.logging.get_file_handler") - def test_init_logger_file_log_disabled(self, mock_get_file_handler, mock_init_config, mock_log_config): - log._IS_INITIALIZED = False - mock_init_config.return_value = mock_log_config - mock_init_config.set.return_value = None - assert log.init_logger({"log_config_file": "tests/data/config/test_logging.toml"}) is True - assert not mock_get_file_handler.called - - @patch("src.utils.log_utils.privacy_console_redact_all_check") - @patch("src.utils.log_utils.privacy_file_redact_all_check") - def test_log_privacy_fully_redacted(self, mock_file_redact, mock_console_redact, get_logger, caplog): - mock_file_redact.return_value = True - mock_console_redact.return_value = True - log.log_privacy(msg="this should not show up", logger=get_logger, level=logging.INFO) - assert len(caplog.records) == 0 - - @patch("src.logging._log_console_handler") - @patch("src.logging._log_file_handler") - @patch("src.utils.log_utils.privacy_console_redact_all_check") - @patch("src.utils.log_utils.privacy_file_redact_all_check") - def test_log_privacy(self, mock_file_redact, mock_console_redact, mock_file_handler, mock_console_handler, get_logger, caplog): - mock_file_redact.return_value = False - mock_console_redact.return_value = False - mock_file_handler.return_value = logging.FileHandler("/tmp/mumimo_temp.log") - mock_console_handler.return_value = logging.StreamHandler(sys.stdout) - - log.log_privacy(msg="test_print_privacy", logger=get_logger, level=logging.INFO) - assert caplog.records[0].levelno == logging.INFO - assert "test_print_privacy" in caplog.text diff --git a/tests/src/test_murmur_connection.py b/tests/src/test_murmur_connection.py deleted file mode 100644 index e9b697f..0000000 --- a/tests/src/test_murmur_connection.py +++ /dev/null @@ -1,119 +0,0 @@ -from typing import Dict, Union -from unittest.mock import patch - -import pytest - -from src.exceptions import ValidationError -from src.murmur_connection import MurmurConnection - - -class TestMurmurConnection: - @pytest.fixture(autouse=True) - def murmur_connection(self): - instance = MurmurConnection() - yield instance - if instance is not None: - if instance._thread is not None: - if instance._thread_stop_event is not None: - instance._thread_stop_event.set() - instance._thread.join() - instance._thread = None - instance._connection_instance = None - instance._connection_params = None - instance._is_connected = False - - def test_murmur_connection_setup_without_init_params(self, murmur_connection: MurmurConnection) -> None: - murmur_connection.setup({}) # type: ignore - assert murmur_connection._connection_params is None - - @patch.object(MurmurConnection, "_validate_sys_args") - def test_murmur_connection_setup_with_init_params( - self, mock_validate, murmur_connection: MurmurConnection, valid_connection_params: Dict[str, Union[str, bool]] - ) -> None: - mock_validate.return_value = None - murmur_connection.setup(valid_connection_params) - assert murmur_connection._connection_params is not None - - @patch.object(MurmurConnection, "setup") - def test_murmur_connection_validate_correct_connection_sys_args( - self, mock_setup, murmur_connection: MurmurConnection, valid_connection_params: Dict[str, Union[str, bool]] - ) -> None: - mock_setup.return_value = None - murmur_connection._validate_sys_args(valid_connection_params) - assert murmur_connection is not None - - @patch.object(MurmurConnection, "setup") - def test_murmur_connection_validate_incorrect_connection_sys_args( - self, mock_setup, murmur_connection: MurmurConnection, invalid_connection_params: Dict[str, Union[str, bool]] - ) -> None: - mock_setup.return_value = None - with pytest.raises(ValidationError, match=r".*\ is not a valid host ip or url.$"): - murmur_connection._validate_sys_args(invalid_connection_params) - - @patch.object(MurmurConnection, "_connect_instance") - @patch.object(MurmurConnection, "_loop") - @patch.object(MurmurConnection, "start") - @patch.object(MurmurConnection, "stop") - @patch.object(MurmurConnection, "connect") - def test_murmur_connection_connect_to_murmur_with_init_params( - self, - mock_connect, - mock_stop, - mock_start, - mock_loop, - mock_instance, - murmur_connection: MurmurConnection, - valid_connection_params: Dict[str, Union[str, bool]], - ) -> None: - mock_instance.return_value = None - mock_loop.return_value = None - mock_start.return_value = True - mock_stop.return_value = True - mock_connect.return_value = None - murmur_connection.setup(valid_connection_params) - murmur_connection.ready() - murmur_connection.connect() - murmur_connection.start() - murmur_connection.stop() - assert murmur_connection._connection_params == valid_connection_params - - @patch.object(MurmurConnection, "_loop") - @patch.object(MurmurConnection, "start") - @patch.object(MurmurConnection, "stop") - @patch.object(MurmurConnection, "connect") - def test_murmur_connection_connect_to_murmur_with_connect_params( - self, - mock_connect, - mock_stop, - mock_start, - mock_loop, - murmur_connection: MurmurConnection, - valid_connection_params: Dict[str, Union[str, bool]], - ) -> None: - mock_loop.return_value = None - mock_start.return_value = True - mock_stop.return_value = True - mock_connect.return_value = None - murmur_connection.setup(valid_connection_params) - murmur_connection.ready().connect() - murmur_connection.start() - murmur_connection.stop() - assert murmur_connection._connection_params == valid_connection_params - - @patch.object(MurmurConnection, "_connect_instance") - @patch.object(MurmurConnection, "connect") - @patch("pymumble_py3.Mumble") - def test_murmur_connection_threading( - self, mock_mumble, mock_connect, mock_instance, murmur_connection: MurmurConnection, valid_connection_params: Dict[str, Union[str, bool]] - ) -> None: - mock_instance.return_value = None - mock_connect.return_value = None - murmur_connection.setup(valid_connection_params) - murmur_connection.ready() - murmur_connection.connect() - murmur_connection._is_connected = True - murmur_connection.start() - assert murmur_connection._thread is not None - murmur_connection._connection_instance = mock_mumble - murmur_connection.stop() - assert murmur_connection._thread is None diff --git a/tests/src/test_murmur_connection_singleton.py b/tests/src/test_murmur_connection_singleton.py deleted file mode 100644 index 60199de..0000000 --- a/tests/src/test_murmur_connection_singleton.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Tuple - -import pytest - -from src.murmur_connection import MurmurConnection - - -class TestMurmurConnectionSingleton: - @pytest.fixture(autouse=True) - def murmur_singletons(self) -> Tuple[MurmurConnection, MurmurConnection]: - murmur_connection_1 = MurmurConnection() - murmur_connection_2 = MurmurConnection() - return (murmur_connection_1, murmur_connection_2) - - def test_murmur_connection_singleton_without_params(self, murmur_singletons: Tuple[MurmurConnection, MurmurConnection]) -> None: - murmur_connection_1 = murmur_singletons[0] - murmur_connection_2 = murmur_singletons[1] - assert murmur_connection_1 is murmur_connection_2 - - def test_murmur_connection_singleton_with_params( - self, murmur_singletons: Tuple[MurmurConnection, MurmurConnection], valid_connection_params - ) -> None: - murmur_connection_1 = murmur_singletons[0] - murmur_connection_2 = murmur_singletons[1] - - murmur_connection_1._connection_params = valid_connection_params - - assert murmur_connection_1._connection_params == murmur_connection_2._connection_params diff --git a/tests/src/test_system_arguments.py b/tests/src/test_system_arguments.py deleted file mode 100644 index 3123507..0000000 --- a/tests/src/test_system_arguments.py +++ /dev/null @@ -1,269 +0,0 @@ -import pytest - -from src.constants import VERBOSITY_MAX, VERBOSITY_MIN -from src.system_arguments import args_parser - - -class TestSystemArguments: - @pytest.fixture() - def sys_args_parser(self): - return args_parser - - class TestOptionConfigFile: - def test_option_valid_config_file(self, sys_args_parser): - options = ["-cf", "path/to/config"] - parser = sys_args_parser.parse_args(options) - assert parser.config_file == "path/to/config" - - def test_option_invalid_config_file_value(self, sys_args_parser): - options = ["-cf", 99] - with pytest.raises(TypeError): - sys_args_parser.parse_args(options) - - def test_option_invalid_config_file_empty(self, sys_args_parser): - options = ["-cf"] - with pytest.raises(SystemExit): - sys_args_parser.parse_args(options) - - class TestOptionHost: - def test_option_valid_host(self, sys_args_parser): - options = ["-sh", "127.0.0.1"] - parser = sys_args_parser.parse_args(options) - assert parser.host == "127.0.0.1" - - def test_option_invalid_host_value(self, sys_args_parser): - options = ["-sh", 99] - with pytest.raises(TypeError): - sys_args_parser.parse_args(options) - - def test_option_invalid_host_empty(self, sys_args_parser): - options = ["-sh"] - with pytest.raises(SystemExit): - sys_args_parser.parse_args(options) - - class TestOptionPort: - def test_option_valid_port(self, sys_args_parser): - # The type is an integer, but the input value is a string. - # argsparse internally converts it to an integer. - options = ["-sp", "64738"] - parser = sys_args_parser.parse_args(options) - assert parser.port == 64738 - - def test_option_invalid_port_value(self, sys_args_parser): - # The type is an integer, but the input value must be a string. - options = ["-sp", 64738] - with pytest.raises(TypeError): - sys_args_parser.parse_args(options) - - def test_option_invalid_port_empty(self, sys_args_parser): - options = ["-sp"] - with pytest.raises(SystemExit): - sys_args_parser.parse_args(options) - - class TestOptionUser: - def test_option_valid_user(self, sys_args_parser): - options = ["-u", "testuser"] - parser = sys_args_parser.parse_args(options) - assert parser.user == "testuser" - - def test_option_invalid_user_value(self, sys_args_parser): - options = ["-u", 99] - with pytest.raises(TypeError): - sys_args_parser.parse_args(options) - - def test_option_invalid_user_empty(self, sys_args_parser): - options = ["-u"] - with pytest.raises(SystemExit): - sys_args_parser.parse_args(options) - - class TestOptionPassword: - def test_option_valid_password(self, sys_args_parser): - options = ["-ps", "testpassword"] - parser = sys_args_parser.parse_args(options) - assert parser.password == "testpassword" - - def test_option_invalid_password_value(self, sys_args_parser): - options = ["-ps", 99] - with pytest.raises(TypeError): - sys_args_parser.parse_args(options) - - def test_option_invalid_password_empty(self, sys_args_parser): - options = ["-ps"] - with pytest.raises(SystemExit): - sys_args_parser.parse_args(options) - - class TestOptionCertFile: - def test_option_valid_cert_file(self, sys_args_parser): - options = ["-c", "path/to/cert"] - parser = sys_args_parser.parse_args(options) - assert parser.cert_file == "path/to/cert" - - def test_option_invalid_cert_file_value(self, sys_args_parser): - options = ["-c", 99] - with pytest.raises(TypeError): - sys_args_parser.parse_args(options) - - def test_option_invalid_cert_file_empty(self, sys_args_parser): - options = ["-c"] - with pytest.raises(SystemExit): - sys_args_parser.parse_args(options) - - class TestOptionKeyFile: - def test_option_valid_key_file(self, sys_args_parser): - options = ["-k", "path/to/key"] - parser = sys_args_parser.parse_args(options) - assert parser.key_file == "path/to/key" - - def test_option_invalid_key_file_value(self, sys_args_parser): - options = ["-k", 99] - with pytest.raises(TypeError): - sys_args_parser.parse_args(options) - - def test_option_invalid_key_file_empty(self, sys_args_parser): - options = ["-k"] - with pytest.raises(SystemExit): - sys_args_parser.parse_args(options) - - class TestOptionTokens: - def test_option_valid_tokens(self, sys_args_parser): - options = ["-t", "token1 token2"] - parser = sys_args_parser.parse_args(options) - assert parser.tokens == "token1 token2" - - def test_option_invalid_tokens_value(self, sys_args_parser): - options = ["-t", 99] - with pytest.raises(TypeError): - sys_args_parser.parse_args(options) - - def test_option_invalid_tokens_empty(self, sys_args_parser): - options = ["-t"] - with pytest.raises(SystemExit): - sys_args_parser.parse_args(options) - - class TestOptionReconnect: - def test_option_auto_reconnect(self, sys_args_parser): - options = ["-ar"] - parser = sys_args_parser.parse_args(options) - assert parser.auto_reconnect is True - - def test_option_no_auto_reconnect(self, sys_args_parser): - options = [] - parser = sys_args_parser.parse_args(options) - assert parser.auto_reconnect is False - - class TestOptionVerbose: - def test_option_no_verbose(self, sys_args_parser): - options = [] - parser = sys_args_parser.parse_args(options) - assert parser.verbose == VERBOSITY_MIN - - def test_option_min_verbose(self, sys_args_parser): - options = ["-v"] - parser = sys_args_parser.parse_args(options) - assert parser.verbose == 1 - - def test_option_max_verbose(self, sys_args_parser): - options = ["-vvvv"] - parser = sys_args_parser.parse_args(options) - assert parser.verbose == VERBOSITY_MAX - - class TestOptionGenerateCert: - def test_option_generate_cert(self, sys_args_parser): - options = ["-gc"] - parser = sys_args_parser.parse_args(options) - assert parser.generate_cert is True - - def test_option_no_generatecert(self, sys_args_parser): - options = [] - parser = sys_args_parser.parse_args(options) - assert parser.generate_cert is False - - class TestOptionSuperUser: - def test_option_valid_superuser(self, sys_args_parser): - options = ["-su", "superuser"] - parser = sys_args_parser.parse_args(options) - assert parser.superuser == "superuser" - - def test_option_invalid_superuser_value(self, sys_args_parser): - options = ["-su", 99] - with pytest.raises(TypeError): - sys_args_parser.parse_args(options) - - def test_option_invalid_superuser_empty(self, sys_args_parser): - options = ["-su"] - with pytest.raises(SystemExit): - sys_args_parser.parse_args(options) - - class TestOptionEnvFile: - def test_option_valid_env_file(self, sys_args_parser): - options = ["-e", "path/to/env"] - parser = sys_args_parser.parse_args(options) - assert parser.env_file == "path/to/env" - - def test_option_invalid_superuser_value(self, sys_args_parser): - options = ["-e", 99] - with pytest.raises(TypeError): - sys_args_parser.parse_args(options) - - def test_option_invalid_superuser_empty(self, sys_args_parser): - options = ["-e"] - with pytest.raises(SystemExit): - sys_args_parser.parse_args(options) - - class TestOptionDatabase: - def test_option_database_use_remote(self, sys_args_parser): - options = ["-rdb"] - parser = sys_args_parser.parse_args(options) - assert parser.use_remote_database is True - - class Remote: - def test_option_database_dialect(self, sys_args_parser): - options = ["-dbdi", "sqlite"] - parser = sys_args_parser.parse_args(options) - assert parser.db_dialect == "sqlite" - - def test_option_database_driver(self, sys_args_parser): - options = ["-dbdr", "aiosqlite"] - parser = sys_args_parser.parse_args(options) - assert parser.db_driver == "aiosqlite" - - def test_option_database_user(self, sys_args_parser): - options = ["-dbus", "mumimo"] - parser = sys_args_parser.parse_args(options) - assert parser.db_user == "mumimo" - - def test_option_database_pass(self, sys_args_parser): - options = ["-dbps", "mumimo"] - parser = sys_args_parser.parse_args(options) - assert parser.db_pass == "mumimo" - - def test_option_database_host(self, sys_args_parser): - options = ["-dbh", "mumimo.db"] - parser = sys_args_parser.parse_args(options) - assert parser.db_host == "mumimo.db" - - def test_option_database_name(self, sys_args_parser): - options = ["-dbn", "mumimo"] - parser = sys_args_parser.parse_args(options) - assert parser.db_name == "mumimo" - - def test_option_database_port(self, sys_args_parser): - options = ["-dbp", "12345"] - parser = sys_args_parser.parse_args(options) - assert parser.db_port == 12345 - - class Local: - def test_option_local_database_path(self, sys_args_parser): - options = ["-ldbp", "mumimo.db"] - parser = sys_args_parser.parse_args(options) - assert parser.local_database_path == "mumimo.db" - - def test_option_local_database_dialect(self, sys_args_parser): - options = ["-ldbdi", "sqlite"] - parser = sys_args_parser.parse_args(options) - assert parser.local_database_dialect == "sqlite" - - def test_option_local_database_driver(self, sys_args_parser): - options = ["-ldbdr", "aiosqlite"] - parser = sys_args_parser.parse_args(options) - assert parser.local_database_driver == "aiosqlite" diff --git a/tests/src/test_version.py b/tests/src/test_version.py deleted file mode 100644 index adf4d39..0000000 --- a/tests/src/test_version.py +++ /dev/null @@ -1,39 +0,0 @@ -from unittest.mock import patch - -from src.version import VERSION, validate_version, version - - -class TestVersion: - def test_version_exists(self): - assert isinstance(version(), str) - - def test_version_returns_correct_version(self): - assert version() == VERSION - - def test_version_is_valid(self): - with patch("src.version.version", return_value="0.0.1"): - assert validate_version() is True - - def test_version_is_invalid_number_of_digits(self): - with patch("src.version.version", return_value="0.0.0.1"): - assert validate_version() is False - - def test_version_is_invalid_characters(self): - with patch("src.version.version", return_value="y.e.s"): - assert validate_version() is False - - def test_version_is_invalid_too_many_digits(self): - with patch("src.version.version", return_value="0000.1.1"): - assert validate_version() is False - - def test_version_is_invalid_negative_digits(self): - with patch("src.version.version", return_value="0.-1.1"): - assert validate_version() is False - - def test_version_is_invalid_not_enough_digits(self): - with patch("src.version.version", return_value="0.1"): - assert validate_version() is False - - def test_version_is_invalid_wrong_dot_placement(self): - with patch("src.version.version", return_value=".0.0.1"): - assert validate_version() is False diff --git a/tests/src/utils/__init__.py b/tests/src/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/src/utils/parsers/test_cmd_parser.py b/tests/src/utils/parsers/test_cmd_parser.py deleted file mode 100644 index 1c866af..0000000 --- a/tests/src/utils/parsers/test_cmd_parser.py +++ /dev/null @@ -1,231 +0,0 @@ -from typing import Optional -from unittest.mock import patch - -import pytest - -from src.constants import MumimoCfgFields -from src.exceptions import ServiceError -from src.lib.command import Command -from src.utils.parsers import cmd_parser - - -class TestCmdParser: - class TestParseCommand: - @pytest.fixture(autouse=True) - def std_mock_text(self): - mock_text = self.MockText() - mock_text.actor = 0 - mock_text.channel_id = 0 - mock_text.session = None - mock_text.message = "test_message" - return mock_text - - class MockText: - message: Optional[str] - actor: Optional[int] - channel_id: Optional[int] - session: Optional[int] - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - def test_parse_command_cfg_does_not_exist(self, mock_cfg, std_mock_text): - mock_cfg.return_value = None - mock_text = std_mock_text - with pytest.raises(ServiceError, match="^Unable to process commands:"): - _ = cmd_parser.parse_command(mock_text) - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - def test_parse_command_valid_text_channel(self, mock_cfg_instance, std_mock_text) -> None: - mock_text = std_mock_text - mock_cfg_instance.return_value = {MumimoCfgFields.SETTINGS.COMMANDS.TOKEN: "!"} - cmd_result = cmd_parser.parse_command(mock_text) - assert cmd_result is not None - assert cmd_result.actor == 0 - assert cmd_result.channel_id == 0 - assert cmd_result.session_id == -1 - assert cmd_result.message == "test_message" - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - def test_parse_command_valid_text_private(self, mock_cfg_instance, std_mock_text) -> None: - mock_text = std_mock_text - mock_text.channel_id = None - mock_text.session = 0 - mock_cfg_instance.return_value = {MumimoCfgFields.SETTINGS.COMMANDS.TOKEN: "!"} - cmd_result = cmd_parser.parse_command(mock_text) - assert cmd_result is not None - assert cmd_result.actor == 0 - assert cmd_result.channel_id == -1 - assert cmd_result.session_id == 0 - assert cmd_result.message == "test_message" - - def test_parse_command_text_is_none(self) -> None: - assert cmd_parser.parse_command(None) is None - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - def test_parse_command_message_is_none(self, mock_cfg_instance) -> None: - mock_text = self.MockText() - mock_text.message = None - mock_cfg_instance.return_value = {MumimoCfgFields.SETTINGS.COMMANDS.TOKEN: "!"} - cmd_result = cmd_parser.parse_command(mock_text) - assert cmd_result is None - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - def test_parse_command_message_is_not_command(self, mock_cfg_instance, std_mock_text) -> None: - mock_text = std_mock_text - mock_text.message = "test_message" - mock_cfg_instance.return_value = {MumimoCfgFields.SETTINGS.COMMANDS.TOKEN: "!"} - - cmd_result = cmd_parser.parse_command(mock_text) - assert cmd_result is not None - assert cmd_result.message == mock_text.message - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - def test_parse_command_message_is_empty_spaces(self, mock_cfg_instance, std_mock_text) -> None: - mock_text = std_mock_text - mock_text.message = " " - mock_cfg_instance.return_value = {MumimoCfgFields.SETTINGS.COMMANDS.TOKEN: "!"} - - cmd_result = cmd_parser.parse_command(mock_text) - assert cmd_result is None - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - def test_parse_command_message_command_parse_error(self, mock_cfg_instance, std_mock_text) -> None: - mock_text = std_mock_text - mock_text.message = "!" - mock_cfg_instance.return_value = {MumimoCfgFields.SETTINGS.COMMANDS.TOKEN: "!"} - - cmd_result = cmd_parser.parse_command(mock_text) - assert cmd_result is not None - assert cmd_result.command is None - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - def test_parse_command_message_parameters_parse_error(self, mock_cfg_instance, std_mock_text) -> None: - mock_text = std_mock_text - mock_text.message = "!test.param" - mock_cfg_instance.return_value = {MumimoCfgFields.SETTINGS.COMMANDS.TOKEN: "!"} - - cmd_result = cmd_parser.parse_command(mock_text) - assert cmd_result is not None - assert cmd_result.parameters == ["param"] - - @patch("src.settings.MumimoSettings.Configs.get_mumimo_config") - def test_parse_command_message_body_parse_error(self, mock_cfg_instance, std_mock_text) -> None: - mock_text = std_mock_text - mock_text.message = "!test" - mock_cfg_instance.return_value = {MumimoCfgFields.SETTINGS.COMMANDS.TOKEN: "!"} - - cmd_result = cmd_parser.parse_command(mock_text) - assert cmd_result is not None - assert cmd_result.message == "" - - class TestParseActorName: - class MockConnectionInstance: - users = {0: {"name": "test_user"}} - - @patch("pymumble_py3.mumble.Mumble", new_callable=MockConnectionInstance) - def test_parse_actor_name_valid(self, mock_mumble) -> None: - mock_command: Command = Command() - mock_command.actor = 0 - mock_mumble.users[0]["name"] = "test_user" - - cmd_result: str = cmd_parser.parse_actor_name(mock_command, mock_mumble) - assert cmd_result == self.MockConnectionInstance().users[0]["name"] - - @patch("pymumble_py3.mumble.Mumble", new_callable=MockConnectionInstance) - def test_parse_actor_name_invalid_actor_id(self, mock_mumble) -> None: - mock_command: Command = Command() - mock_command.actor = 1 - - cmd_result: str = cmd_parser.parse_actor_name(mock_command, mock_mumble) - assert cmd_result == "Server" - - @patch("pymumble_py3.mumble.Mumble", new_callable=MockConnectionInstance) - def test_parse_actor_name_actor_is_none(self, mock_mumble) -> None: - mock_command: Command = Command() - mock_command.actor = 0 - mock_mumble.users[0]["name"] = None - - cmd_result: str = cmd_parser.parse_actor_name(mock_command, mock_mumble) - assert cmd_result == "Unavailable" - - class TestParseChannelName: - class MockConnectionInstance: - channels = {0: {"name": "test_channel"}} - - @patch("pymumble_py3.mumble.Mumble", new_callable=MockConnectionInstance) - def test_parse_channel_name_valid(self, mock_mumble) -> None: - mock_command: Command = Command() - mock_command.channel_id = 0 - mock_mumble.channels[0]["name"] = "test_channel" - - cmd_result: str = cmd_parser.parse_channel_name(mock_command, mock_mumble) - assert cmd_result == self.MockConnectionInstance().channels[0]["name"] - - @patch("pymumble_py3.mumble.Mumble", new_callable=MockConnectionInstance) - def test_parse_channel_name_invalid_channel_id(self, mock_mumble) -> None: - mock_command: Command = Command() - mock_command.channel_id = 1 - - cmd_result: str = cmd_parser.parse_channel_name(mock_command, mock_mumble) - assert cmd_result == "Server" - - @patch("pymumble_py3.mumble.Mumble", new_callable=MockConnectionInstance) - def test_parse_channel_name_invalid_private_message(self, mock_mumble) -> None: - mock_command: Command = Command() - mock_command.channel_id = -1 - mock_command.session_id = 0 - - cmd_result: str = cmd_parser.parse_channel_name(mock_command, mock_mumble) - assert cmd_result == "Private" - - @patch("pymumble_py3.mumble.Mumble", new_callable=MockConnectionInstance) - def test_parse_channel_name_channel_is_none(self, mock_mumble) -> None: - mock_command: Command = Command() - mock_command.channel_id = 0 - mock_mumble.channels[0]["name"] = None - - cmd_result: str = cmd_parser.parse_channel_name(mock_command, mock_mumble) - assert cmd_result == "Unavailable" - - class TestParseMessageImageData: - def test_parse_message_image_data_message_has_image_data(self) -> None: - mock_command: Command = Command() - mock_command.message = 'image' - - cmd_result: str = cmd_parser.parse_message_image_data(mock_command) - assert cmd_result == "Image Data" - - def test_parse_message_image_data_message_has_no_image_data(self) -> None: - mock_command: Command = Command() - mock_command.message = "regular_message" - - cmd_result: str = cmd_parser.parse_message_image_data(mock_command) - assert cmd_result == "regular_message" - - def test_parse_message_image_data_message_is_empty(self) -> None: - mock_command: Command = Command() - mock_command.message = "" - - cmd_result: str = cmd_parser.parse_message_image_data(mock_command) - assert cmd_result == "" - - class TestParseMessageHyperlinkData: - def test_parse_message_hyperlink_data_message_has_hyperlink_data(self) -> None: - mock_command: Command = Command() - mock_command.message = 'hyperlink' - - cmd_result: str = cmd_parser.parse_message_hyperlink_data(mock_command) - assert cmd_result == "Hyperlink Data" - - def test_parse_message_hyperlink_data_message_has_no_hyperlink_data(self) -> None: - mock_command: Command = Command() - mock_command.message = "regular_message" - - cmd_result: str = cmd_parser.parse_message_hyperlink_data(mock_command) - assert cmd_result == "regular_message" - - def test_parse_message_hyperlink_data_message_is_empty(self) -> None: - mock_command: Command = Command() - mock_command.message = "" - - cmd_result: str = cmd_parser.parse_message_hyperlink_data(mock_command) - assert cmd_result == "" diff --git a/tests/src/utils/parsers/test_env_parser.py b/tests/src/utils/parsers/test_env_parser.py deleted file mode 100644 index 216e46f..0000000 --- a/tests/src/utils/parsers/test_env_parser.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import Any, Dict - -import pytest - -from src.constants import EnvArgs -from src.utils.parsers import env_parser - - -class TestEnvParserIO: - @pytest.fixture(autouse=True) - def env_path(self) -> str: - return "tests/data/test.env" - - @pytest.fixture(autouse=True) - def env_dict(self) -> Dict[str, Any]: - return { - EnvArgs.ENV_HOST: "test_host", - EnvArgs.ENV_PORT: "12345", - EnvArgs.ENV_USER: "test_user", - EnvArgs.ENV_PASS: "test_pass", - EnvArgs.ENV_CERT: "test_cert", - EnvArgs.ENV_KEY: "test_key", - EnvArgs.ENV_TOKENS: "test_token_1 test_token2", - EnvArgs.ENV_SUPER_USER: "test_super_user", - EnvArgs.ENV_DB_DIALECT: "sqlite", - EnvArgs.ENV_DB_DRIVER: "aiosqlite", - EnvArgs.ENV_DB_USER: "test_mumimo", - EnvArgs.ENV_DB_PASS: "test_mumimo", - EnvArgs.ENV_DB_HOST: "test_mumimo_db", - EnvArgs.ENV_DB_NAME: "test_mumimo", - EnvArgs.ENV_DB_PORT: "12345", - } - - class TestGetEnvFile: - def test_get_env_file_invalid_empty_path(self) -> None: - env_path = "" - env_file = env_parser.get_env_file(env_path) - assert env_file is None - - def test_get_env_file_invalid_path(self) -> None: - env_path = "mytestpath" - env_file = env_parser.get_env_file(env_path) - assert env_file is None - - def test_get_env_file_exists(self, env_path: str) -> None: - env_file = env_parser.get_env_file(env_path) - assert env_file is not None - - class TestReadEnvFile: - def test_read_env_file_contents_and_matches_test_data(self, env_path: str, env_dict: Dict[str, str]) -> None: - env_contents = env_parser.read_env_file(env_path) - assert env_contents is not None - assert env_contents.items() == env_dict.items() - - def test_read_env_file_invalid_path(self) -> None: - env_path = "mytestpath" - with pytest.raises(Exception, match=r"^Could not find environment file at"): - env_parser.read_env_file(env_path) - - def test_read_env_file_invalid_empty_path(self) -> None: - env_path = "" - with pytest.raises(Exception, match=r"^Could not find environment file at"): - env_parser.read_env_file(env_path) diff --git a/tests/src/utils/test_args_validators.py b/tests/src/utils/test_args_validators.py deleted file mode 100644 index d417453..0000000 --- a/tests/src/utils/test_args_validators.py +++ /dev/null @@ -1,232 +0,0 @@ -from typing import Dict, Union - -import pytest - -from src.constants import SysArgs -from src.exceptions import ValidationError -from src.utils.args_validators import SystemArgumentsValidator as validator - - -class TestSystemArgumentsValidators: - @pytest.fixture(autouse=True) - def sys_args(self) -> Dict[str, Union[str, bool, None]]: - return { - "host": "", - "port": "", - "user": "", - "password": "", - "cert_file": "", - "key_file": "", - "tokens": "", - "auto_reconnect": "", - "verbose": "", - } - - @pytest.fixture(autouse=True) - def host(self, sys_args): - yield sys_args[SysArgs.SYS_HOST] - sys_args[SysArgs.SYS_HOST] = "" - - @pytest.fixture(autouse=True) - def port(self, sys_args): - yield sys_args[SysArgs.SYS_PORT] - sys_args[SysArgs.SYS_PORT] = "" - - @pytest.fixture(autouse=True) - def user(self, sys_args): - yield sys_args[SysArgs.SYS_USER] - sys_args[SysArgs.SYS_USER] = "" - - @pytest.fixture(autouse=True) - def password(self, sys_args): - yield sys_args[SysArgs.SYS_PASS] - sys_args[SysArgs.SYS_PASS] = "" - - @pytest.fixture(autouse=True) - def cert(self, sys_args): - yield sys_args[SysArgs.SYS_CERT] - sys_args[SysArgs.SYS_CERT] = "" - - @pytest.fixture(autouse=True) - def key(self, sys_args): - yield sys_args[SysArgs.SYS_KEY] - sys_args[SysArgs.SYS_KEY] = "" - - @pytest.fixture(autouse=True) - def tokens(self, sys_args): - yield sys_args[SysArgs.SYS_TOKENS] - sys_args[SysArgs.SYS_TOKENS] = "" - - @pytest.fixture(autouse=True) - def reconnect(self, sys_args): - yield sys_args[SysArgs.SYS_RECONNECT] - sys_args[SysArgs.SYS_RECONNECT] = "" - - @pytest.fixture(autouse=True) - def verbose(self, sys_args): - yield sys_args[SysArgs.SYS_VERBOSE] - sys_args[SysArgs.SYS_VERBOSE] = "" - - class TestHostParam: - def test_host_is_none(self, host) -> None: - host = None - with pytest.raises(ValidationError, match=r".*\ must be provided.$"): - validator.validate_host_param(host) # type: ignore - - def test_host_is_empty(self, host) -> None: - host = "" - with pytest.raises(ValidationError, match=r".*\ cannot be empty.$"): - validator.validate_host_param(host) - - def test_host_is_invalid(self, host) -> None: - host = "invalid_host" - with pytest.raises(ValidationError, match=r".*\ is not a valid host ip or url.$"): - validator.validate_host_param(host) - - def test_host_is_valid_ipv4(self, host) -> None: - host = "127.0.0.1" - assert validator.validate_host_param(host) is None - - def test_host_is_invalid_ipv4(self, host) -> None: - host = "127.0.0133.12" - with pytest.raises(ValidationError, match=r".*\ is not a valid host ip or url.$"): - validator.validate_host_param(host) - - def test_host_is_valid_ipv6(self, host) -> None: - host = "0:0:0:0:0:0:0:1" - assert validator.validate_host_param(host) is None - - def test_host_is_invalid_ipv6(self, host) -> None: - host = "1545:23:0:0:155" - with pytest.raises(ValidationError, match=r".*\ is not a valid host ip or url.$"): - validator.validate_host_param(host) - - def test_host_is_valid_url(self, host) -> None: - host = "https://myhost.ddns.net" - assert validator.validate_host_param(host) is None - - def test_host_is_invalid_url(self, host) -> None: - host = "localhost" - with pytest.raises(ValidationError, match=r".*\ is not a valid host ip or url.$"): - validator.validate_host_param(host) - - class TestPortParam: - def test_port_valid_number(self, port) -> None: - port = 64738 - assert validator.validate_port_param(port) is None - - def test_port_valid_string(self, port) -> None: - port = "64738" - assert validator.validate_port_param(port) is None - - def test_port_is_none(self, port) -> None: - port = None - with pytest.raises(ValidationError, match=r".*\ must be provided.$"): - validator.validate_port_param(port) # type: ignore - - def test_port_is_empty(self, port) -> None: - port = "" - with pytest.raises(ValidationError, match=r".*\ cannot be empty.$"): - validator.validate_port_param(port) - - def test_port_is_invalid_string(self, port) -> None: - port = "invalid_port" - with pytest.raises(ValidationError, match=r".*\ is not a valid port number.$"): - validator.validate_port_param(port) - - def test_port_is_invalid_number_lower_range(self, port) -> None: - port = -64738 - with pytest.raises(ValidationError, match=r".*\ is not a valid port number.$"): - validator.validate_port_param(port) - - def test_port_is_invalid_number_upper_range(self, port) -> None: - port = 65536 - with pytest.raises(ValidationError, match=r".*\ is not a valid port number.$"): - validator.validate_port_param(port) - - class TestUserParam: - def test_user_is_none(self, user) -> None: - user = None - with pytest.raises(ValidationError, match=r".*\ must be provided.$"): - validator.validate_user_param(user) # type: ignore - - def test_user_is_empty(self, user) -> None: - user = " " - with pytest.raises(ValidationError, match=r".*\ cannot be empty.$"): - validator.validate_user_param(user) - - def test_user_is_invalid(self, user) -> None: - user = True - with pytest.raises(ValidationError, match=r".*\ must be a valid string.$"): - validator.validate_user_param(user) # type: ignore - - class TestPasswordParam: - def test_password_is_empty(self, password) -> None: - password = " " - with pytest.raises(ValidationError, match=r".*\ cannot be empty.$"): - validator.validate_password_param(password) - - def test_password_is_invalid(self, password) -> None: - password = True - with pytest.raises(ValidationError, match=r".*\ must be a valid string.$"): - validator.validate_password_param(password) # type: ignore - - class TestCertParam: - def test_cert_is_empty(self, cert) -> None: - cert = " " - with pytest.raises(ValidationError, match=r".*\ cannot be empty.$"): - validator.validate_cert_param(cert) - - def test_cert_is_invalid(self, cert) -> None: - cert = True - with pytest.raises(ValidationError, match=r".*\ must be a valid string.$"): - validator.validate_cert_param(cert) # type: ignore - - class TestKeyParam: - def test_key_is_empty(self, key) -> None: - key = " " - with pytest.raises(ValidationError, match=r".*\ cannot be empty.$"): - validator.validate_key_param(key) - - def test_key_is_invalid(self, key) -> None: - key = True - with pytest.raises(ValidationError, match=r".*\ must be a valid string.$"): - validator.validate_key_param(key) # type: ignore - - class TestTokensParam: - def test_tokens_is_empty(self, tokens) -> None: - tokens = " " - with pytest.raises(ValidationError, match=r".*\ cannot be empty.$"): - validator.validate_tokens_param(tokens) - - def test_tokens_is_invalid(self, tokens) -> None: - tokens = True - with pytest.raises(ValidationError, match=r".*\ must be a valid string.$"): - validator.validate_tokens_param(tokens) # type: ignore - - class TestAutoReconnectParam: - def test_auto_reconnect_is_none(self, reconnect) -> None: - reconnect = None - with pytest.raises(ValidationError, match=r".*\ can only be True or False, not None.$"): - validator.validate_auto_reconnect_param(reconnect) # type: ignore - - def test_auto_reconnect_is_invalid(self, reconnect) -> None: - reconnect = "invalid_auto_reconnect" - with pytest.raises(ValidationError, match=r".*\ can only be True or False.$"): - validator.validate_auto_reconnect_param(reconnect) # type: ignore - - class TestVerboseParam: - def test_verbose_is_empty(self, verbose) -> None: - verbose = None - with pytest.raises(ValidationError, match=r"^The verbose option can only be a number from"): - validator.validate_verbose_param(verbose) # type: ignore - - def test_verbose_is_invalid(self, verbose) -> None: - verbose = "invalid_verbose" - with pytest.raises(ValidationError, match=r"^The verbose option can only be a number from"): - validator.validate_verbose_param(verbose) # type: ignore - - def test_verbose_is_above_limit(self, verbose) -> None: - verbose = 9999 - with pytest.raises(ValidationError, match=r"^The verbose option can only be a number from"): - validator.validate_verbose_param(verbose) diff --git a/tests/src/utils/test_connection_utils.py b/tests/src/utils/test_connection_utils.py deleted file mode 100644 index fcf14c6..0000000 --- a/tests/src/utils/test_connection_utils.py +++ /dev/null @@ -1,49 +0,0 @@ -from unittest.mock import patch - -from src.utils import connection_utils - - -class TestSupportedPlatforms: - @patch("platform.system") - def test_is_supported_platform_linux(self, mock_platform): - mock_platform.return_value = "Linux" - is_supported = connection_utils.is_supported_platform() - assert is_supported is True - - @patch("platform.system") - def test_is_supported_platform_macos(self, mock_platform): - mock_platform.return_value = "Darwin" - is_supported = connection_utils.is_supported_platform() - assert is_supported is True - - @patch("platform.system") - def test_is_not_supported_platform_windows(self, mock_platform): - mock_platform.return_value = "Windows" - is_supported = connection_utils.is_supported_platform() - assert is_supported is False - - @patch("platform.system") - def test_is_not_supported_platform_java(self, mock_platform): - mock_platform.return_value = "Java" - is_supported = connection_utils.is_supported_platform() - assert is_supported is False - - -class TestParseChannelTokens: - def test_parse_channel_tokens_is_valid(self) -> None: - assert connection_utils.parse_channel_tokens("token1 token2") == ["token1", "token2"] - - def test_parse_channel_tokens_is_none(self) -> None: - assert connection_utils.parse_channel_tokens(None) is None # type: ignore - - def test_parse_channel_tokens_invalid_values(self) -> None: - assert connection_utils.parse_channel_tokens(5) is None # type: ignore - - def test_parse_channel_tokens_extra_spaces(self) -> None: - assert connection_utils.parse_channel_tokens(" token1 token2 ") == ["token1", "token2"] - - def test_parse_channel_tokens_empty(self) -> None: - assert connection_utils.parse_channel_tokens("") is None - - def test_parse_channel_tokens_spaces(self) -> None: - assert connection_utils.parse_channel_tokens(" ") is None diff --git a/tests/test_example.py b/tests/test_example.py deleted file mode 100644 index 2e4b39b..0000000 --- a/tests/test_example.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_always_passes() -> None: - assert True