From 3e6a08231c7dc41796d847c2ba6e4ad68dbb5d12 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 31 Oct 2025 16:00:54 +1100 Subject: [PATCH] Initial commit --- .gitignore | 5 + LICENSE | 674 +++++++++++++++++++++++++++++++++++++ README.md | 61 ++++ bouquin/__init__.py | 1 + bouquin/__main__.py | 4 + bouquin/db.py | 92 +++++ bouquin/highlighter.py | 112 ++++++ bouquin/key_prompt.py | 41 +++ bouquin/main.py | 15 + bouquin/main_window.py | 245 ++++++++++++++ bouquin/settings.py | 29 ++ bouquin/settings_dialog.py | 72 ++++ poetry.lock | 544 ++++++++++++++++++++++++++++++ pyproject.toml | 25 ++ screenshot.png | Bin 0 -> 55893 bytes tests/conftest.py | 56 +++ tests/test_ui.py | 78 +++++ 17 files changed, 2054 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bouquin/__init__.py create mode 100644 bouquin/__main__.py create mode 100644 bouquin/db.py create mode 100644 bouquin/highlighter.py create mode 100644 bouquin/key_prompt.py create mode 100644 bouquin/main.py create mode 100644 bouquin/main_window.py create mode 100644 bouquin/settings.py create mode 100644 bouquin/settings_dialog.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 screenshot.png create mode 100644 tests/conftest.py create mode 100644 tests/test_ui.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8652982 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.swp +*.pyc +__pycache__ +.pytest_cache +dist diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + 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/README.md b/README.md new file mode 100644 index 0000000..8b20e14 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Bouquin + + +## Introduction + +Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher. + +It uses [SQLCipher bindings](https://pypi.org/project/sqlcipher3-wheels) as a drop-in replacement +for SQLite3. This means that the underlying database for the notebook is encrypted at rest. + +To increase security, the SQLCipher key is requested when the app is opened, and is not written +to disk. + +There is deliberately no network connectivity or syncing intended. + +## Screenshot + +![Screenshot of Bouquin](./screenshot.png) + +## Features + + * Every 'page' is linked to the calendar day + * Basic markdown + * Automatic periodic saving (or explicitly save) + * Navigating from one day to the next automatically saves + * Basic keyboard shortcuts + * Transparent integrity checking of the database when it opens + + +## Yet to do + + * Search + * Taxonomy/tagging + * Ability to change the SQLCipher key + * Export to other formats (plaintext, json, sql etc) + + +## How to install + +### From source + + * Clone this repo or download the tarball from the releases page + * Ensure you have poetry installed + * Run `poetry install` to install dependencies + * Run `poetry run bouquin` to start the application. + +### From the releases page + + * Download the whl and run it + +### From PyPi + + * `pip install bouquin` + + +## How to run the tests + + * Clone the repo + * Ensure you have poetry installed + * Run `poetry install --with test` + * Run `poetry run pytest -vvv` diff --git a/bouquin/__init__.py b/bouquin/__init__.py new file mode 100644 index 0000000..c28a133 --- /dev/null +++ b/bouquin/__init__.py @@ -0,0 +1 @@ +from .main import main diff --git a/bouquin/__main__.py b/bouquin/__main__.py new file mode 100644 index 0000000..40e2b01 --- /dev/null +++ b/bouquin/__main__.py @@ -0,0 +1,4 @@ +from .main import main + +if __name__ == "__main__": + main() diff --git a/bouquin/db.py b/bouquin/db.py new file mode 100644 index 0000000..1ea60fa --- /dev/null +++ b/bouquin/db.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +from sqlcipher3 import dbapi2 as sqlite + + +@dataclass +class DBConfig: + path: Path + key: str + + +class DBManager: + def __init__(self, cfg: DBConfig): + self.cfg = cfg + self.conn: sqlite.Connection | None = None + + def connect(self) -> bool: + # Ensure parent dir exists + self.cfg.path.parent.mkdir(parents=True, exist_ok=True) + self.conn = sqlite.connect(str(self.cfg.path)) + cur = self.conn.cursor() + cur.execute(f"PRAGMA key = '{self.cfg.key}';") + cur.execute("PRAGMA cipher_compatibility = 4;") + cur.execute("PRAGMA journal_mode = WAL;") + self.conn.commit() + try: + self._integrity_ok() + except Exception: + self.conn.close() + self.conn = None + return False + self._ensure_schema() + return True + + def _integrity_ok(self) -> bool: + cur = self.conn.cursor() + cur.execute("PRAGMA cipher_integrity_check;") + rows = cur.fetchall() + + # OK + if not rows: + return + + # Not OK + details = "; ".join(str(r[0]) for r in rows if r and r[0] is not None) + raise sqlite.IntegrityError( + "SQLCipher integrity check failed" + + (f": {details}" if details else f" ({len(rows)} issue(s) reported)") + ) + + def _ensure_schema(self) -> None: + cur = self.conn.cursor() + cur.execute( + """ + CREATE TABLE IF NOT EXISTS entries ( + date TEXT PRIMARY KEY, -- ISO yyyy-MM-dd + content TEXT NOT NULL + ); + """ + ) + cur.execute("PRAGMA user_version = 1;") + self.conn.commit() + + def get_entry(self, date_iso: str) -> str: + cur = self.conn.cursor() + cur.execute("SELECT content FROM entries WHERE date = ?;", (date_iso,)) + row = cur.fetchone() + return row[0] if row else "" + + def upsert_entry(self, date_iso: str, content: str) -> None: + cur = self.conn.cursor() + cur.execute( + """ + INSERT INTO entries(date, content) VALUES(?, ?) + ON CONFLICT(date) DO UPDATE SET content = excluded.content; + """, + (date_iso, content), + ) + self.conn.commit() + + def dates_with_content(self) -> list[str]: + cur = self.conn.cursor() + cur.execute("SELECT date FROM entries WHERE TRIM(content) <> '';") + return [r[0] for r in cur.fetchall()] + + def close(self) -> None: + if self.conn is not None: + self.conn.close() + self.conn = None diff --git a/bouquin/highlighter.py b/bouquin/highlighter.py new file mode 100644 index 0000000..456dfa2 --- /dev/null +++ b/bouquin/highlighter.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import re +from PySide6.QtGui import QFont, QTextCharFormat, QSyntaxHighlighter, QColor + + +class MarkdownHighlighter(QSyntaxHighlighter): + ST_NORMAL = 0 + ST_CODE = 1 + + FENCE = re.compile(r"^```") + + def __init__(self, document): + super().__init__(document) + + base_size = document.defaultFont().pointSizeF() or 12.0 + + # Monospace for code + self.mono = QFont("Monospace") + self.mono.setStyleHint(QFont.TypeWriter) + + # Light, high-contrast scheme for code + self.col_bg = QColor("#eef2f6") # light code bg + self.col_fg = QColor("#1f2328") # dark text + + # Formats + self.fmt_h = [QTextCharFormat() for _ in range(6)] + for i, f in enumerate(self.fmt_h, start=1): + f.setFontWeight(QFont.Weight.Bold) + f.setFontPointSize(base_size + (7 - i)) + self.fmt_bold = QTextCharFormat() + self.fmt_bold.setFontWeight(QFont.Weight.Bold) + self.fmt_italic = QTextCharFormat() + self.fmt_italic.setFontItalic(True) + self.fmt_quote = QTextCharFormat() + self.fmt_quote.setForeground(QColor("#6a737d")) + self.fmt_link = QTextCharFormat() + self.fmt_link.setFontUnderline(True) + self.fmt_list = QTextCharFormat() + self.fmt_list.setFontWeight(QFont.Weight.DemiBold) + self.fmt_strike = QTextCharFormat() + self.fmt_strike.setFontStrikeOut(True) + + # Uniform code style + self.fmt_code = QTextCharFormat() + self.fmt_code.setFont(self.mono) + self.fmt_code.setFontPointSize(max(6.0, base_size - 1)) + self.fmt_code.setBackground(self.col_bg) + self.fmt_code.setForeground(self.col_fg) + + # Simple patterns + self.re_heading = re.compile(r"^(#{1,6}) +.*$") + self.re_bold = re.compile(r"\*\*(.+?)\*\*|__(.+?)__") + self.re_italic = re.compile(r"\*(?!\*)(.+?)\*|_(?!_)(.+?)_") + self.re_strike = re.compile(r"~~(.+?)~~") + self.re_inline_code = re.compile(r"`([^`]+)`") + self.re_link = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") + self.re_list = re.compile(r"^ *(?:[-*+] +|[0-9]+[.)] +)") + self.re_quote = re.compile(r"^> ?.*$") + + def highlightBlock(self, text: str) -> None: + prev = self.previousBlockState() + in_code = prev == self.ST_CODE + + if in_code: + # Entire line is code + self.setFormat(0, len(text), self.fmt_code) + if self.FENCE.match(text): + self.setCurrentBlockState(self.ST_NORMAL) + else: + self.setCurrentBlockState(self.ST_CODE) + return + + # Starting/ending a fenced block? + if self.FENCE.match(text): + self.setFormat(0, len(text), self.fmt_code) + self.setCurrentBlockState(self.ST_CODE) + return + + # --- Normal markdown styling --- + m = self.re_heading.match(text) + if m: + level = min(len(m.group(1)), 6) + self.setFormat(0, len(text), self.fmt_h[level - 1]) + self.setCurrentBlockState(self.ST_NORMAL) + return + + m = self.re_list.match(text) + if m: + self.setFormat(m.start(), m.end() - m.start(), self.fmt_list) + + if self.re_quote.match(text): + self.setFormat(0, len(text), self.fmt_quote) + + for m in self.re_inline_code.finditer(text): + self.setFormat(m.start(), m.end() - m.start(), self.fmt_code) + + for m in self.re_bold.finditer(text): + self.setFormat(m.start(), m.end() - m.start(), self.fmt_bold) + + for m in self.re_italic.finditer(text): + self.setFormat(m.start(), m.end() - m.start(), self.fmt_italic) + + for m in self.re_strike.finditer(text): + self.setFormat(m.start(), m.end() - m.start(), self.fmt_strike) + + for m in self.re_link.finditer(text): + start = m.start(1) - 1 + length = len(m.group(1)) + 2 + self.setFormat(start, length, self.fmt_link) + + self.setCurrentBlockState(self.ST_NORMAL) diff --git a/bouquin/key_prompt.py b/bouquin/key_prompt.py new file mode 100644 index 0000000..1fe8dee --- /dev/null +++ b/bouquin/key_prompt.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from PySide6.QtWidgets import ( + QDialog, + QVBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QDialogButtonBox, +) + + +class KeyPrompt(QDialog): + def __init__( + self, + parent=None, + title: str = "Unlock database", + message: str = "Enter SQLCipher key", + ): + super().__init__(parent) + self.setWindowTitle(title) + v = QVBoxLayout(self) + v.addWidget(QLabel(message)) + self.edit = QLineEdit() + self.edit.setEchoMode(QLineEdit.Password) + v.addWidget(self.edit) + toggle = QPushButton("Show") + toggle.setCheckable(True) + toggle.toggled.connect( + lambda c: self.edit.setEchoMode( + QLineEdit.Normal if c else QLineEdit.Password + ) + ) + v.addWidget(toggle) + bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + v.addWidget(bb) + + def key(self) -> str: + return self.edit.text() diff --git a/bouquin/main.py b/bouquin/main.py new file mode 100644 index 0000000..9beb4d9 --- /dev/null +++ b/bouquin/main.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import sys +from PySide6.QtWidgets import QApplication + +from .settings import APP_NAME, APP_ORG +from .main_window import MainWindow + + +def main(): + app = QApplication(sys.argv) + app.setApplicationName(APP_NAME) + app.setOrganizationName(APP_ORG) + win = MainWindow(); win.show() + sys.exit(app.exec()) diff --git a/bouquin/main_window.py b/bouquin/main_window.py new file mode 100644 index 0000000..394ccb9 --- /dev/null +++ b/bouquin/main_window.py @@ -0,0 +1,245 @@ +from __future__ import annotations + +import sys + +from PySide6.QtCore import QDate, QTimer, Qt +from PySide6.QtGui import QAction, QFont, QTextCharFormat +from PySide6.QtWidgets import ( + QDialog, + QCalendarWidget, + QMainWindow, + QMessageBox, + QPlainTextEdit, + QSplitter, + QVBoxLayout, + QWidget, + QSizePolicy, +) + +from .db import DBManager +from .settings import APP_NAME, load_db_config, save_db_config +from .key_prompt import KeyPrompt +from .highlighter import MarkdownHighlighter +from .settings_dialog import SettingsDialog + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle(APP_NAME) + self.setMinimumSize(1000, 650) + + self.cfg = load_db_config() + # Always prompt for the key (we never store it) + if not self._prompt_for_key_until_valid(): + sys.exit(1) + + # ---- UI: Left fixed panel (calendar) + right editor ----------------- + self.calendar = QCalendarWidget() + self.calendar.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.calendar.setGridVisible(True) + self.calendar.selectionChanged.connect(self._on_date_changed) + + left_panel = QWidget() + left_layout = QVBoxLayout(left_panel) + left_layout.setContentsMargins(8, 8, 8, 8) + left_layout.addWidget(self.calendar, alignment=Qt.AlignTop) + left_layout.addStretch(1) + left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16) + + self.editor = QPlainTextEdit() + tab_w = 4 * self.editor.fontMetrics().horizontalAdvance(" ") + self.editor.setTabStopDistance(tab_w) + self.highlighter = MarkdownHighlighter(self.editor.document()) + + split = QSplitter() + split.addWidget(left_panel) + split.addWidget(self.editor) + split.setStretchFactor(1, 1) # editor grows + + container = QWidget() + lay = QVBoxLayout(container) + lay.addWidget(split) + self.setCentralWidget(container) + + # Status bar for feedback + self.statusBar().showMessage("Ready", 800) + + # Menu bar (File) + mb = self.menuBar() + file_menu = mb.addMenu("&File") + act_save = QAction("&Save", self) + act_save.setShortcut("Ctrl+S") + act_save.triggered.connect(lambda: self._save_current(explicit=True)) + file_menu.addAction(act_save) + act_settings = QAction("&Settings", self) + act_settings.triggered.connect(self._open_settings) + file_menu.addAction(act_settings) + file_menu.addSeparator() + act_quit = QAction("&Quit", self) + act_quit.setShortcut("Ctrl+Q") + act_quit.triggered.connect(self.close) + file_menu.addAction(act_quit) + + # Navigate menu with next/previous day + nav_menu = mb.addMenu("&Navigate") + act_prev = QAction("Previous Day", self) + act_prev.setShortcut("Ctrl+P") + act_prev.setShortcutContext(Qt.ApplicationShortcut) + act_prev.triggered.connect(lambda: self._adjust_day(-1)) + nav_menu.addAction(act_prev) + self.addAction(act_prev) + + act_next = QAction("Next Day", self) + act_next.setShortcut("Ctrl+N") + act_next.setShortcutContext(Qt.ApplicationShortcut) + act_next.triggered.connect(lambda: self._adjust_day(1)) + nav_menu.addAction(act_next) + self.addAction(act_next) + + # Autosave + self._dirty = False + self._save_timer = QTimer(self) + self._save_timer.setSingleShot(True) + self._save_timer.timeout.connect(self._save_current) + self.editor.textChanged.connect(self._on_text_changed) + + # First load + mark dates with content + self._load_selected_date() + self._refresh_calendar_marks() + + # --- DB lifecycle + def _try_connect(self) -> bool: + try: + self.db = DBManager(self.cfg) + ok = self.db.connect() + except Exception as e: + if str(e) == "file is not a database": + error = "The key is probably incorrect." + else: + error = str(e) + QMessageBox.critical(self, "Database Error", error) + return False + return ok + + def _prompt_for_key_until_valid(self) -> bool: + while True: + dlg = KeyPrompt(self, message="Enter a key to unlock the notebook") + if dlg.exec() != QDialog.Accepted: + return False + self.cfg.key = dlg.key() + if self._try_connect(): + return True + + # --- Calendar marks to indicate text exists for htat day ----------------- + def _refresh_calendar_marks(self): + fmt_bold = QTextCharFormat() + fmt_bold.setFontWeight(QFont.Weight.Bold) + # Clear previous marks + for d in getattr(self, "_marked_dates", set()): + self.calendar.setDateTextFormat(d, QTextCharFormat()) + self._marked_dates = set() + try: + for date_iso in self.db.dates_with_content(): + qd = QDate.fromString(date_iso, "yyyy-MM-dd") + if qd.isValid(): + self.calendar.setDateTextFormat(qd, fmt_bold) + self._marked_dates.add(qd) + except Exception: + pass + + # --- UI handlers --------------------------------------------------------- + def _current_date_iso(self) -> str: + d = self.calendar.selectedDate() + return f"{d.year():04d}-{d.month():02d}-{d.day():02d}" + + def _load_selected_date(self): + date_iso = self._current_date_iso() + try: + text = self.db.get_entry(date_iso) + except Exception as e: + QMessageBox.critical(self, "Read Error", str(e)) + return + self.editor.blockSignals(True) + self.editor.setPlainText(text) + self.editor.blockSignals(False) + self._dirty = False + # track which date the editor currently represents + self._active_date_iso = date_iso + + def _on_text_changed(self): + self._dirty = True + self._save_timer.start(1200) # autosave after idle + + def _adjust_day(self, delta: int): + """Move selection by delta days (negative for previous).""" + d = self.calendar.selectedDate().addDays(delta) + self.calendar.setSelectedDate(d) + + def _on_date_changed(self): + """ + When the calendar selection changes, save the previous day's note if dirty, + so we don't lose that text, then load the newly selected day. + """ + # Stop pending autosave and persist current buffer if needed + try: + self._save_timer.stop() + except Exception: + pass + prev = getattr(self, "_active_date_iso", None) + if prev and self._dirty: + self._save_date(prev, explicit=False) + # Now load the newly selected date + self._load_selected_date() + + def _save_date(self, date_iso: str, explicit: bool = False): + """ + Save editor contents into the given date. Shows status on success. + explicit=True means user invoked Save: show feedback even if nothing changed. + """ + if not self._dirty and not explicit: + return + text = self.editor.toPlainText() + try: + self.db.upsert_entry(date_iso, text) + except Exception as e: + QMessageBox.critical(self, "Save Error", str(e)) + return + self._dirty = False + self._refresh_calendar_marks() + # Feedback in the status bar + from datetime import datetime as _dt + + self.statusBar().showMessage( + f"Saved {date_iso} at {_dt.now().strftime('%H:%M:%S')}", 2000 + ) + + def _save_current(self, explicit: bool = False): + # Delegate to _save_date for the currently selected date + self._save_date(self._current_date_iso(), explicit) + + def _open_settings(self): + dlg = SettingsDialog(self.cfg, self) + if dlg.exec() == QDialog.Accepted: + new_cfg = dlg.config + if new_cfg.path != self.cfg.path: + # Save the new path to the notebook + self.cfg.path = new_cfg.path + save_db_config(self.cfg) + self.db.close() + # Prompt again for the key for the new path + if not self._prompt_for_key_until_valid(): + QMessageBox.warning( + self, "Reopen failed", "Could not unlock database at new path." + ) + return + self._load_selected_date() + self._refresh_calendar_marks() + + def closeEvent(self, event): # noqa: N802 + try: + self._save_current() + self.db.close() + except Exception: + pass + super().closeEvent(event) diff --git a/bouquin/settings.py b/bouquin/settings.py new file mode 100644 index 0000000..508e12f --- /dev/null +++ b/bouquin/settings.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from pathlib import Path +from PySide6.QtCore import QSettings, QStandardPaths + +from .db import DBConfig + +APP_ORG = "Bouquin" +APP_NAME = "Bouquin" + + +def default_db_path() -> Path: + base = Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)) + return base / "notebook.db" + + +def get_settings() -> QSettings: + return QSettings(APP_ORG, APP_NAME) + + +def load_db_config() -> DBConfig: + s = get_settings() + path = Path(s.value("db/path", str(default_db_path()))) + return DBConfig(path=path, key="") + + +def save_db_config(cfg: DBConfig) -> None: + s = get_settings() + s.setValue("db/path", str(cfg.path)) diff --git a/bouquin/settings_dialog.py b/bouquin/settings_dialog.py new file mode 100644 index 0000000..790c4e0 --- /dev/null +++ b/bouquin/settings_dialog.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from pathlib import Path + +from PySide6.QtWidgets import ( + QDialog, + QFormLayout, + QHBoxLayout, + QVBoxLayout, + QWidget, + QLineEdit, + QPushButton, + QFileDialog, + QDialogButtonBox, + QSizePolicy, +) + +from .db import DBConfig +from .settings import save_db_config + + +class SettingsDialog(QDialog): + def __init__(self, cfg: DBConfig, parent=None): + super().__init__(parent) + self.setWindowTitle("Settings") + self._cfg = DBConfig(path=cfg.path, key="") + + form = QFormLayout() + form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) + self.setMinimumWidth(520) + self.setSizeGripEnabled(True) + + self.path_edit = QLineEdit(str(self._cfg.path)) + self.path_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + browse_btn = QPushButton("Browse…") + browse_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + browse_btn.clicked.connect(self._browse) + path_row = QWidget() + h = QHBoxLayout(path_row) + h.setContentsMargins(0, 0, 0, 0) + h.addWidget(self.path_edit, 1) + h.addWidget(browse_btn, 0) + h.setStretch(0, 1) + h.setStretch(1, 0) + form.addRow("Database path", path_row) + + bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) + bb.accepted.connect(self._save) + bb.rejected.connect(self.reject) + + v = QVBoxLayout(self) + v.addLayout(form) + v.addWidget(bb) + + def _browse(self): + p, _ = QFileDialog.getSaveFileName( + self, + "Choose database file", + self.path_edit.text(), + "DB Files (*.db);;All Files (*)", + ) + if p: + self.path_edit.setText(p) + + def _save(self): + self._cfg = DBConfig(path=Path(self.path_edit.text()), key="") + save_db_config(self._cfg) + self.accept() + + @property + def config(self) -> DBConfig: + return self._cfg diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..e1c4ed5 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,544 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.10.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, + {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, + {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, + {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, + {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, + {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, + {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, + {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, + {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, + {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, + {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, + {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, + {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, + {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, + {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, + {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, + {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, + {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, + {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, + {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, + {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, + {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, + {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, + {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, + {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, + {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, + {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, + {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, + {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, + {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, + {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, + {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, + {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, + {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, + {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, + {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, + {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, + {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, + {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, + {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, + {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, + {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, + {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, + {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, + {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, + {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, + {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, + {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, + {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, + {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, + {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, + {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, + {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, + {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, + {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, + {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyside6" +version = "6.10.0" +description = "Python bindings for the Qt cross-platform application and UI framework" +optional = false +python-versions = "<3.14,>=3.9" +files = [ + {file = "pyside6-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0"}, + {file = "pyside6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01"}, + {file = "pyside6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820"}, + {file = "pyside6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f"}, + {file = "pyside6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339"}, +] + +[package.dependencies] +PySide6_Addons = "6.10.0" +PySide6_Essentials = "6.10.0" +shiboken6 = "6.10.0" + +[[package]] +name = "pyside6-addons" +version = "6.10.0" +description = "Python bindings for the Qt cross-platform application and UI framework (Addons)" +optional = false +python-versions = "<3.14,>=3.9" +files = [ + {file = "pyside6_addons-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c"}, + {file = "pyside6_addons-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464"}, + {file = "pyside6_addons-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0"}, + {file = "pyside6_addons-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859"}, + {file = "pyside6_addons-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1"}, +] + +[package.dependencies] +PySide6_Essentials = "6.10.0" +shiboken6 = "6.10.0" + +[[package]] +name = "pyside6-essentials" +version = "6.10.0" +description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)" +optional = false +python-versions = "<3.14,>=3.9" +files = [ + {file = "pyside6_essentials-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a"}, + {file = "pyside6_essentials-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8"}, + {file = "pyside6_essentials-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998"}, + {file = "pyside6_essentials-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe"}, + {file = "pyside6_essentials-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1"}, +] + +[package.dependencies] +shiboken6 = "6.10.0" + +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, + {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-qt" +version = "4.5.0" +description = "pytest support for PyQt and PySide applications" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest_qt-4.5.0-py3-none-any.whl", hash = "sha256:ed21ea9b861247f7d18090a26bfbda8fb51d7a8a7b6f776157426ff2ccf26eff"}, + {file = "pytest_qt-4.5.0.tar.gz", hash = "sha256:51620e01c488f065d2036425cbc1cbcf8a6972295105fd285321eb47e66a319f"}, +] + +[package.dependencies] +pluggy = ">=1.1" +pytest = "*" +typing_extensions = "*" + +[package.extras] +dev = ["pre-commit", "tox"] +doc = ["sphinx", "sphinx_rtd_theme"] + +[[package]] +name = "shiboken6" +version = "6.10.0" +description = "Python/C++ bindings helper module" +optional = false +python-versions = "<3.14,>=3.9" +files = [ + {file = "shiboken6-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543"}, + {file = "shiboken6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234"}, + {file = "shiboken6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148"}, + {file = "shiboken6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717"}, + {file = "shiboken6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61"}, +] + +[[package]] +name = "sqlcipher3-wheels" +version = "0.5.5.post0" +description = "DB-API 2.0 interface for SQLCipher 3.x" +optional = false +python-versions = "*" +files = [ + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:836cff85673ab9bdfe0f3e2bc38aefddb5f3a4c0de397b92f83546bb94ea38aa"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3fde9076a8810d19044f65fdfeeee5a9d044176ce91adc2258c8b18cb945474"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ad3ccb27f3fc9260b1bcebfd33fc5af1c2a1bf6a50e8e1bf7991d492458b438"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94bb8ab8cf7ae3dc0d51dcb75bf242ae4bd2f18549bfc975fd696c181e9ea8ca"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df0bf0a169b480615ea2021e7266e1154990762216d1fd8105b93d1fee336f49"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79cc1af145345e9bd625c961e4efc8fc6c6eefcaec90fbcf1c6b981492c08031"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d8b9f1c6d283acc5a0da16574c0f7690ba5b14cb5935f3078ccf8404a530075"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:952a23069a149a192a5eb8a9e552772b38c012825238175bc810f445a3aa8000"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24f1a57a4aa18d9ecd38cfce69dd06e58cfb521151a8316e18183e603e7108f4"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6568c64adc55f9882ba36c11a446810bd5d4c03796aab8ecb9024f3bca9eb2cd"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:26c2b58d2f2a9dd23ad4c310fb6c0f0c82ca4f36a0d4177a70f0efeb332798ee"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46827ffc7e705c5ecdf23ec69f56dd55b20857dc3c3c4893e360de8a38b4e708"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4033bbe2f0342936736ce7b8b2626f532509315576d5376764b410deae181cad"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-win32.whl", hash = "sha256:bfb26dbba945860427bd3f82c132e6d2ef409baa062d315b952dd5a930b25870"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-win_amd64.whl", hash = "sha256:168270b8fb295314aa4ee9f74435ceee42207bd16fe908f646a829b3a9daedad"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-win_arm64.whl", hash = "sha256:1f1bb2c4c6defa812eb0238055a283cf3c2f400e956a57c25cf65cbdbac6783f"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:55d557376a90f14baf0f35e917f8644c3a8cf48897947fcd7ecf51d490dd689f"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f1739264a971901088fe1670befb8a8a707543186c8eecc58158ca287e309b2"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:920e19345b6c5335b61e9fbed2625f96dbc3b0269ab5120baeae2c9288f0be01"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6462734f6d0703f863f5968419d229de75bbf2a829f762bfb257b6df2355f977"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c397305b65612da76f254c692ff866571aa98fd3817ed0e40fce6d568d704966"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf3467fe834075b58215c50f9db7355ef86a73d256ac8ba5fffb8c946741a5dc"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a76d783c095a3c95185757c418e3bad3eab69cbf986970d422cce5431e84d7f5"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf8d78895ee0f04dc525942a1f40796fa7c3d7d7fb36c987f55c243ce34192d"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d583a10dbe9a1752968788c2d6438461ec7068608ceaa72e6468d80727c3152e"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8c3156b39bb8f24dfbe17a49126d8fa404b00c01d7aa84e64a2293db1dae1a38"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:15c3cf77b31973aa008174518fa439d8620a093964c2d9edcb8d23d543345839"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f5743db0f3492359c2ab3a56b6bed00ecba193f2c75c74e8e3d78a45f1eb7c95"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cc40a213a3633f19c96432304a16f0cff7c4aeca1a3d2042d4be36e576e64a70"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-win32.whl", hash = "sha256:433456ce962ae50887d6428d55bad46e5748a2cdd3d036180eb0bcdbe8bae9f9"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-win_amd64.whl", hash = "sha256:ca4332b1890cc4f80587be8bd529e20475bd3291e07f11115b1fc773947b264a"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-win_arm64.whl", hash = "sha256:a4634300cb2440baf17a78d6481d10902de4a2a6518f83a5ab2fe081e6b20b42"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8df43c11d767c6aac5cc300c1957356a9fd1b25f1946891003cf20a0146241"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:797653f08ecffcef2948dfd907fb20dab402d9efde6217c96befafc236e96c5b"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dca428fde0d1a522473f766465324d6539d324f219f4f7c159a3eb0d4f9983c5"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48e97922240a04b44637eabf39f86d243fe61fe7db1bd2ad219eb4053158f263"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8d3a366e52a6732b1ccff14f9ca77ecbee53abfce87c417bf05d4301484584f"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dce28a2431260251d7acf253ea1950983e48dfec64245126b39a770d5a88f507"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea64cce27152cae453c353038336bda0dc1f885e5e8e30b5cd28b8c9b498bbeb"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02d9e6120a496f083c525efc34408d4f2ca282da05bebcc967a0aa1e12a0d6ca"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:875cfc61bbf694b8327c2485e5ed40573e8b715f4e583502f12c51c8d5a92dd5"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0e9ed3ff9c00ba3888f8dbc0c7c84377ef66f21c5f4ac373fc690dcf5e9bd594"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:08ad6d767502429e497b6d03b5ae11e43e896d36f05ac8e60c12d8f124378bc1"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ecdefdcf7ab8cb14b3147a59af83e8e3e5e3bed46fc43ab86a657f5c306a83d2"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75ffe5677407bf20a32486eb6facfbb07a353ce7c9aecc9fefd4e9d3275605d7"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-win32.whl", hash = "sha256:97b6c6556b430b5c0dff53e8f709f90ba53294c2a3958a8c38f573c6dbf467d9"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-win_amd64.whl", hash = "sha256:248cae211991f1ffb3a885a1223e62abee70c6c208fc2224a8dbf73d4e825baa"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-win_arm64.whl", hash = "sha256:5a49fc3a859a53fd025dc2fa08410292d267018897fc63198de6c92860fa8be7"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f798e1591fa5ba14d9da08a54f18e7000fd74973cde12eb862a3928a69b7996"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:369011b8dc68741313a8b77bb68a70b76052390eaf819e4cd6e13d0acbea602d"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5658990462a728c1f4b472d23c1f7f577eb2bced5bbbf7c2b45158b8340484bd"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:907a166e4e563da67fe22c480244459512e32d3e00853b3f1e6fdb9da6aa2da6"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ba972405e9f16042e37cbcb4fef47248339c8410847390d41268bd45dc3f6ca"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b5f1b380fe3b869f701f9d2a8c09e9edfeec261573c8bb009a3336717260d65"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2978c9d964ad643b0bc61e19d8d608a515ff270e9a2f1f6c5aeb8ad56255def"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29457feb1516a2542aa7676e6d03bf913191690bf1ed6c82353782a380388508"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:021af568414741a45bfca41d682da64916a7873498a31d896cc34ad540939c6b"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7471d12eef489eea60cc3806bae0690f6d0733f7aea371a3ad5c5642f3bc04a9"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0f5faf683db9ade192a870e28b1eeeec2eb0aeca18e88fa52195a4639974c7cb"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:64fe6bac67d2b807b91102eef41d7f389e008ded80575ba597b454e05f9522e5"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:63ef1e23eb9729e79783e2ab4b19f64276c155ba0a85ba1eeb21e248c6ce0956"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-win32.whl", hash = "sha256:4eafde00138dd3753085b4f5eab0811247207b699de862589f886a94ad3628a4"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-win_amd64.whl", hash = "sha256:909864f275460646d0bf5475dc42e9c2cadd79cd40805ea32fe9a69300595301"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-win_arm64.whl", hash = "sha256:a831846cc6b01d7f99576efbf797b61a269dffa6885f530b6957573ce1a24f10"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:aaad03f3eb099401306fead806908c85b923064a9da7a99c33a72c3b7c9143bf"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74d4b4cde184d9e354152fd1867bcbaee468529865703ad863840a0ce4eb60cd"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac3ad3cf9e1d0f08e8d22a65115368f2b22b9e96403fa644e146e1221c65c454"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45c4d3dca4acdbc5543bb00aee1e0715db797aa2819db5b7ca3feed3ab3366ff"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf7026851ea60d63c1a88f62439da78b68bfbfec192c781255e3cfb34b6efc12"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7ae4a83678c41c2cdbf3c2b18fc46be32225260c7b4807087bdb43793ee90fa"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:678c67d60b35eced29777fe9398b6e6a6638156f143c80662a0c7c99ce115be7"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:9830af5aef2c17686d6e7c78c20b92c7b57c9d7921a03e4c549b48fe0e98c5c0"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:08651e17c868a6a22124b6ab71e939a5bb4737e0535f381ce35077dc8116c4b3"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:b58b4c944d7ce20cd7b426ae8834433b5b1152391960960b778b37803f0ffc1c"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2b38818468ddb0c8fc4b172031d65ced3be22ba82360c45909a0546b2857d3e4"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-win32.whl", hash = "sha256:91d1f2284d13b68f213d05b51cd6242f4cfa065d291af6f353f9cbedd28d8c0d"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:a5c724f95366ba7a2895147b0690609b6774384fa8621aa46a66cf332e4b612f"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e37b33263fad4accdba45c8566566d45fc01f47fd4afa3e265df9e0e3581d4f4"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f1e29495bc968e37352c315d23a38462d7e77fcfa1597d005d17ed93f9f3103"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad8a774f545eb5471587e0389fca4f855f36d58901c65547796d59fc13aee458"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a5a0251e4ceca127b26d18f0965b7f3c820a2dd2c51c25015c819300fd5859"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e40535f0f57e8b605e1cbce1399c96bcd5ab99e60992d2c7669c689d0cbe5"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e04e1dd62d019cde936d18fcd21361f6c4695e0e73fd6dc509c4ccd9446d26d"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:2df377e3d04f5c427c9f79ef95bdf0b982bde76c1dbd4441f83268f3f1993a53"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:5f3cb8db19e7462ccb2e34b56feaccb2aac675ad8f77e28f8222b3e7c47d1f92"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:b40860b3e6d6108473836a29d3600e1b335192637e16e9421b43b58147ced3c1"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:ca4fd9e8b669e81bb74479bde61ee475d7a6832d667e2ce80e6136ddd7a0fedd"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:771e74a22f48c40b6402d0ca1d569ced5a796e118d4472da388744b5aa0ebd3f"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-win32.whl", hash = "sha256:4589bfca18ecf787598262327f7329fe1f4fc2655c04899d84451562e2099a57"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:f646ab958a601bad8925a876f5aa68bdf0ec3584630143ed1ad8e9df4e447044"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dfb8106a05af1cb1eadeea996171b52c80f18851972e49ffe91539e4fc064b0f"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b8b9b77a898b721fc634858fc43552119d3d303485adc6f28f3e76f028d5ea04"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c65efab1a0ab14f75314039694ac35d3181a5c8cf43584bd537b36caf2a6ccf9"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b450eee7e201c48aae58e2d45ef5d309a19cd49952cfb58d546fefbeef0a100"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8f0997202d7628c4312f0398122bdc5ada7fa79939d248652af40d9da689ef8"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dae69bef7628236d426e408fb14a40f0027bac1658a06efd29549b26ba369372"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef48e874bcc3ebf623672ec99f9aaa7b8a4f62fb270e33dad6db3739ea111086"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9006dc1a73e2b2a53421aa72decbcff08cb109f67a20f7d15a64ab140e0a1d2"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:96c07a345740fa71c0d8fc5fa7ea182ee24f62ebbf33d4d10c8c72d866dc332d"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:85a78af6f6e782e0df36f93c9e7c2dd568204f60a2ea55025c21d1837dea95ec"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:19eadc55bf69f9e9799a808cdcfc6657cf30511cb32849235d555adfa048a99f"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:377e8ad3bb3c17c43f860b570fd15e048246ade92babc9b310f2c417500aca57"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f8e07aec529d6fa31516201c524b0cfac108a9a6044a148f236291aae7991195"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-win32.whl", hash = "sha256:703ab55b77b1c1ebb80eb0b27574a8eadf109739d252de7f646dc41cb82f1a65"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-win_amd64.whl", hash = "sha256:b4f4b2e019c6d1ad33d5fc3163d31d0f731a073a5a52cdfae7b85408548ce342"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a84e3b098a29b8c298b01291cf8bc850a507ca45507d43674a84a8d33b7595b2"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9905b580cfdbd6945e44d81332483deace167d33e956ffae5c4b27eddeb676e7"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0c403a7418631dc7185ef8053acc765101f4f64cc0bf50d1bc44ae7d40fc28e"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d63f89bf28de4ce82a7c324275ce733bf31eb29ec1121e48261af89b5b7f30b"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc6dc782f5be4883279079c79fa88578258a0fd24651f6d69b0f4be2716f7d7e"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743c177822c60e66c5c9579b4f384bd98e60fd4a2abe0eacdec0af4747d925bc"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec5eeb87220e4d2abf6faad1ecb3b3ee88c4d9caad6cf2ce4c0a73a91c4c7ad9"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9318b814363b4bc062e54852ea62f58b69e7da9e51211afd6c55e9170e1ae9a0"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e7a1f58a2614f2ad9fcb4822f6da56313cbb88309880512bf5d01bd3d9142b87"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ae9427ddde670f605c84f704c12d840628467cc0f0a8e9ce6489577eef6a0479"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:be75ae311433254af3c6fe6eb68bf80ac6ef547ec0cf4594f5b301a044682186"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:73e1438437bbe67d453e2908b90b17b357a992a9ac0011ad20af1ea7c2b4cd58"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:393c0a9af080c1c1a0801cca9448eff3633dafc1a7934fdce58a8d1c15d8bd2b"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-win32.whl", hash = "sha256:13a79fc8e9b69bf6d70e7fa5a53bd42fab83dc3e6e93da7fa82871ec40874e43"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-win_amd64.whl", hash = "sha256:b1648708e5bf599b4455bf330faa4777c3963669acb2f3fa25240123a01f8b2b"}, + {file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-win_arm64.whl", hash = "sha256:4c3dd2f54bdd518b90e28b07c31cdfe34c8bd182c5107a30a9c2ef9569cd6cf9"}, + {file = "sqlcipher3_wheels-0.5.5.post0.tar.gz", hash = "sha256:2c291ba05fa3e57c9b4d407d2751aa69266b5372468e7402daaa312b251aca7f"}, +] + +[[package]] +name = "tomli" +version = "2.3.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, + {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, + {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, + {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, + {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, + {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, + {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, + {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, + {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, + {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, + {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, + {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, + {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, + {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<3.14" +content-hash = "f5a670c96c370ce7d70dd76c7e2ebf98f7443e307b446779ea0c748db1019dd4" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1be51b7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "bouquin" +version = "0.1.0" +description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." +authors = ["Miguel Jacq "] +readme = "README.md" +license = "GPL-3.0-or-later" + +[tool.poetry.dependencies] +python = ">=3.9,<3.14" +pyside6 = ">=6.8.1,<7.0.0" +sqlcipher3-wheels = "^0.5.5.post0" + +[tool.poetry.scripts] +bouquin = "bouquin.__main__:main" + +[tool.poetry.group.test.dependencies] +pytest = "^8.4.2" +pytest-qt = "^4.5.0" +pytest-mock = "^3.15.1" +pytest-cov = "^7.0.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..a0d47bfedebbec745b4cd48f7edc6bf997c75686 GIT binary patch literal 55893 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 literal 0 HcmV?d00001 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7f51d8c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,56 @@ +import os +import pytest + +# Run Qt without a visible display (CI-safe) +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + + +@pytest.fixture +def fake_key_prompt_cls(): + """A KeyPrompt stand-in that immediately returns Accepted with a fixed key.""" + from PySide6.QtWidgets import QDialog + + class FakeKeyPrompt: + accepted_count = 0 + + def __init__(self, *a, **k): + self._key = "sekret" + + def exec(self): + FakeKeyPrompt.accepted_count += 1 + return QDialog.Accepted + + def key(self): + return self._key + + return FakeKeyPrompt + + +@pytest.fixture +def fake_db_cls(): + """In-memory DB fake that mimics the subset of DBManager used by the UI.""" + class FakeDB: + def __init__(self, cfg): + self.cfg = cfg + self.data = {} + self.connected_key = None + self.closed = False + + def connect(self): + # record the key that UI supplied + self.connected_key = self.cfg.key + return True + + def get_entry(self, date_iso: str) -> str: + return self.data.get(date_iso, "") + + def upsert_entry(self, date_iso: str, content: str) -> None: + self.data[date_iso] = content + + def dates_with_content(self) -> list[str]: + return [d for d, t in self.data.items() if t.strip()] + + def close(self) -> None: + self.closed = True + + return FakeDB diff --git a/tests/test_ui.py b/tests/test_ui.py new file mode 100644 index 0000000..5df04bc --- /dev/null +++ b/tests/test_ui.py @@ -0,0 +1,78 @@ +# tests/test_main_window.py +import pytest + + +@pytest.fixture +def patched_main_window(monkeypatch, qtbot, fake_db_cls, fake_key_prompt_cls): + """Construct MainWindow with faked DB + KeyPrompt so tests are deterministic.""" + mw_mod = pytest.importorskip("bouquin.main_window") + # Swap DBManager with in-memory fake + monkeypatch.setattr(mw_mod, "DBManager", fake_db_cls, raising=True) + # Make the unlock dialog auto-accept with a known key + monkeypatch.setattr(mw_mod, "KeyPrompt", fake_key_prompt_cls, raising=True) + + MainWindow = mw_mod.MainWindow + win = MainWindow() + qtbot.addWidget(win) + win.show() + return win, mw_mod, fake_db_cls, fake_key_prompt_cls + + +def test_always_prompts_for_key_and_uses_it(patched_main_window): + win, mw_mod, FakeDB, FakeKP = patched_main_window + # The fake DB instance is on win.db; it records the key provided by the UI flow + assert isinstance(win.db, FakeDB) + assert win.db.connected_key == "sekret" + assert FakeKP.accepted_count >= 1 # was prompted at startup + + +def test_manual_save_current_day(patched_main_window, qtbot): + win, *_ = patched_main_window + + # Type into the editor and save + win.editor.setPlainText("Test note") + win._save_current(explicit=True) # call directly to avoid waiting timers + + day = win._current_date_iso() + assert win.db.get_entry(day) == "Test note" + + +def test_switch_day_saves_previous(patched_main_window, qtbot): + from PySide6.QtCore import QDate + + win, *_ = patched_main_window + + # Write on Day 1 + d1 = win.calendar.selectedDate() + d1_iso = f"{d1.year():04d}-{d1.month():02d}-{d1.day():02d}" + win.editor.setPlainText("Notes day 1") + + # Trigger a day change (this path calls _on_date_changed via signal) + d2 = d1.addDays(1) + win.calendar.setSelectedDate(d2) + # After changing, previous day should be saved; editor now shows day 2 content (empty) + assert win.db.get_entry(d1_iso) == "Notes day 1" + assert win.editor.toPlainText() == "" + + +def test_calendar_marks_refresh(patched_main_window, qtbot): + from PySide6.QtCore import QDate + from PySide6.QtGui import QTextCharFormat, QFont + + win, *_ = patched_main_window + + # Put content on two dates and refresh marks + today = win.calendar.selectedDate() + win.db.upsert_entry(f"{today.year():04d}-{today.month():02d}-{today.day():02d}", "x") + another = today.addDays(2) + win.db.upsert_entry(f"{another.year():04d}-{another.month():02d}-{another.day():02d}", "y") + + win._refresh_calendar_marks() + + fmt_today = win.calendar.dateTextFormat(today) + fmt_other = win.calendar.dateTextFormat(another) + + # Both should be bold (DemiBold or Bold depending on platform); we just assert non-Normal + assert fmt_today.fontWeight() != QFont.Weight.Normal + assert fmt_other.fontWeight() != QFont.Weight.Normal +