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"{_row_type}>"
- 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