commit 9687db5a964cf0c8a113bfe25ef56fa480ca96e7 Author: David Carmichael Date: Sun Feb 11 21:59:18 2024 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b270892 --- /dev/null +++ b/.gitignore @@ -0,0 +1,171 @@ +src/old +src/flask_bigapp.egg-info +src/dist +_tools/ +_tools/* + +.idea/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this from_file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea from_folder. +#.idea/ +/old_code/ +/Dockerfile +/src_dif/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..069c736 --- /dev/null +++ b/LICENSE @@ -0,0 +1,483 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + +Quart-Imp +========= + +A Quart auto importer that allows your Quart apps to grow big. + +Copyright (C) 2022 David Carmichael + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +USA diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d0d9e0 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +![](https://raw.githubusercontent.com/CheeseCake87/Quart-Imp/master/_assets/quart-Imp-Small.png) + +# Quart-Imp + +[![PyPI version](https://img.shields.io/pypi/v/quart-imp)](https://pypi.org/project/quart-imp/) +[![License](https://img.shields.io/badge/license-LGPL_v2-red.svg)](https://raw.githubusercontent.com/CheeseCake87/quart-imp/master/LICENSE) +![Downloads](https://static.pepy.tech/badge/quart-imp) +![black](https://img.shields.io/badge/code%20style-black-000000.svg) + +## What is Quart-Imp? + +Quart-Imp's main purpose is to help simplify the importing of blueprints, resources, and models. +It has a few extra features built in to help with securing pages and password authentication. diff --git a/_assets/quart-Imp-Large.png b/_assets/quart-Imp-Large.png new file mode 100644 index 0000000..3f3b27a Binary files /dev/null and b/_assets/quart-Imp-Large.png differ diff --git a/_assets/quart-Imp-Small.png b/_assets/quart-Imp-Small.png new file mode 100644 index 0000000..a0a105b Binary files /dev/null and b/_assets/quart-Imp-Small.png differ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..56a3beb --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,23 @@ +from quart import Quart + +from app.extensions import imp, db + + +def create_app(): + app = Quart(__name__, static_url_path="/") + + imp.init_app(app) + imp.import_app_resources( + files_to_import=["*"], + folders_to_import=["*"] + ) + imp.import_blueprints("blueprints") + imp.import_models("models") + + db.init_app(app) + + @app.before_serving + async def create_tables(): + db.create_all() + + return app diff --git a/app/blueprints/www/__init__.py b/app/blueprints/www/__init__.py new file mode 100644 index 0000000..9a040ef --- /dev/null +++ b/app/blueprints/www/__init__.py @@ -0,0 +1,10 @@ +from quart_imp import Blueprint + +bp = Blueprint(__name__) + +bp.import_resources("routes") + + +@bp.before_app_request +async def before_app_request(): + bp.init_session() diff --git a/app/blueprints/www/config.toml b/app/blueprints/www/config.toml new file mode 100644 index 0000000..e5d4da6 --- /dev/null +++ b/app/blueprints/www/config.toml @@ -0,0 +1,25 @@ +ENABLED = "yes" + +[SETTINGS] +URL_PREFIX = "/" +#SUBDOMAIN = "" +#URL_DEFAULTS = {} +STATIC_FOLDER = "static" +TEMPLATE_FOLDER = "templates" +STATIC_URL_PATH = "/static" +#ROOT_PATH = "" +#CLI_GROUP = "" + +[SESSION] +#www_session = "yes" + +# Set ENABLED to true to allow the blueprint +# to create a database bind, change settings accordingly. +[DATABASE_BIND] +ENABLED = false +DIALECT = "sqlite" +DATABASE_NAME = "www" +LOCATION = "" +PORT = "" +USERNAME = "" +PASSWORD = "" diff --git a/app/blueprints/www/routes/index.py b/app/blueprints/www/routes/index.py new file mode 100644 index 0000000..5789a23 --- /dev/null +++ b/app/blueprints/www/routes/index.py @@ -0,0 +1,8 @@ +from quart import render_template + +from .. import bp + + +@bp.route("/", methods=["GET"]) +async def index(): + return await render_template(bp.tmpl("index.html")) diff --git a/app/blueprints/www/static/css/water.css b/app/blueprints/www/static/css/water.css new file mode 100644 index 0000000..877e139 --- /dev/null +++ b/app/blueprints/www/static/css/water.css @@ -0,0 +1,880 @@ +/** + * Forced dark theme version + */ + +:root { + --background-body: #202b38; + --background: #161f27; + --background-alt: #1a242f; + --selection: #1c76c5; + --text-main: #dbdbdb; + --text-bright: #fff; + --text-muted: #a9b1ba; + --links: #41adff; + --focus: #0096bfab; + --border: #526980; + --code: #ffbe85; + --animation-duration: 0.1s; + --button-base: #0c151c; + --button-hover: #040a0f; + --scrollbar-thumb: var(--button-hover); + --scrollbar-thumb-hover: rgb(0, 0, 0); + --form-placeholder: #a9a9a9; + --form-text: #fff; + --variable: #d941e2; + --highlight: #efdb43; + --select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3 .org/1999/xlink' height='62.5' width='116.9' fill='%23efef ef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6, 7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115. 3,1.6Z'/%3E %3C/svg%3E"); +} + +html { + scrollbar-color: #040a0f #202b38; + scrollbar-color: var(--scrollbar-thumb) var(--background-body); + scrollbar-width: thin; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif; + line-height: 1.4; + max-width: 800px; + margin: 20px auto; + padding: 0 10px; + word-wrap: break-word; + color: #dbdbdb; + color: var(--text-main); + background: #202b38; + background: var(--background-body); + text-rendering: optimizeLegibility; +} + +button { + transition: background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +input { + transition: background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +textarea { + transition: background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +h1 { + font-size: 2.2em; + margin-top: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-bottom: 12px; + margin-top: 24px; +} + +h1 { + color: #fff; + color: var(--text-bright); +} + +h2 { + color: #fff; + color: var(--text-bright); +} + +h3 { + color: #fff; + color: var(--text-bright); +} + +h4 { + color: #fff; + color: var(--text-bright); +} + +h5 { + color: #fff; + color: var(--text-bright); +} + +h6 { + color: #fff; + color: var(--text-bright); +} + +strong { + color: #fff; + color: var(--text-bright); +} + +h1, +h2, +h3, +h4, +h5, +h6, +b, +strong, +th { + font-weight: 600; +} + +q::before { + content: none; +} + +q::after { + content: none; +} + +blockquote { + border-left: 4px solid #0096bfab; + border-left: 4px solid var(--focus); + margin: 1.5em 0; + padding: 0.5em 1em; + font-style: italic; +} + +q { + border-left: 4px solid #0096bfab; + border-left: 4px solid var(--focus); + margin: 1.5em 0; + padding: 0.5em 1em; + font-style: italic; +} + +blockquote > footer { + font-style: normal; + border: 0; +} + +blockquote cite { + font-style: normal; +} + +address { + font-style: normal; +} + +a[href^='mailto\:']::before { + content: '📧 '; +} + +a[href^='tel\:']::before { + content: '📞 '; +} + +a[href^='sms\:']::before { + content: '💬 '; +} + +mark { + background-color: #efdb43; + background-color: var(--highlight); + border-radius: 2px; + padding: 0 2px 0 2px; + color: #000; +} + +a > code, +a > strong { + color: inherit; +} + +button, +select, +input[type='submit'], +input[type='reset'], +input[type='button'], +input[type='checkbox'], +input[type='range'], +input[type='radio'] { + cursor: pointer; +} + +input, +select { + display: block; +} + +[type='checkbox'], +[type='radio'] { + display: initial; +} + +input { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +button { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +textarea { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +select { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +button { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='submit'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='reset'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='button'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +button:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='submit']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='reset']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='button']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='color'] { + min-height: 2rem; + padding: 8px; + cursor: pointer; +} + +input[type='checkbox'], +input[type='radio'] { + height: 1em; + width: 1em; +} + +input[type='radio'] { + border-radius: 100%; +} + +input { + vertical-align: top; +} + +label { + vertical-align: middle; + margin-bottom: 4px; + display: inline-block; +} + +input:not([type='checkbox']):not([type='radio']), +input[type='range'], +select, +button, +textarea { + -webkit-appearance: none; +} + +textarea { + display: block; + margin-right: 0; + box-sizing: border-box; + resize: vertical; +} + +textarea:not([cols]) { + width: 100%; +} + +textarea:not([rows]) { + min-height: 40px; + height: 140px; +} + +select { + background: #161f27 url("data:image/svg+xml;charset=utf-8,% 3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version= '1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w 3.org/1999/xlink' height='62.5' width='116.9' fill='%23efef ef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L5 8.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7. 4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60 .5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3, 1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat; + background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat; + padding-right: 35px; +} + +select::-ms-expand { + display: none; +} + +select[multiple] { + padding-right: 10px; + background-image: none; + overflow-y: auto; +} + +input:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +select:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +button:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +textarea:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +input[type='checkbox']:active, +input[type='radio']:active, +input[type='submit']:active, +input[type='reset']:active, +input[type='button']:active, +input[type='range']:active, +button:active { + transform: translateY(2px); +} + +input:disabled, +select:disabled, +button:disabled, +textarea:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +::-moz-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +:-ms-input-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +::-ms-input-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +::placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +fieldset { + border: 1px #0096bfab solid; + border: 1px var(--focus) solid; + border-radius: 6px; + margin: 0; + margin-bottom: 12px; + padding: 10px; +} + +legend { + font-size: 0.9em; + font-weight: 600; +} + +input[type='range'] { + margin: 10px 0; + padding: 10px 0; + background: transparent; +} + +input[type='range']:focus { + outline: none; +} + +input[type='range']::-webkit-slider-runnable-track { + width: 100%; + height: 9.5px; + -webkit-transition: 0.2s; + transition: 0.2s; + background: #161f27; + background: var(--background); + border-radius: 3px; +} + +input[type='range']::-webkit-slider-thumb { + box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); + -webkit-appearance: none; + margin-top: -7px; +} + +input[type='range']:focus::-webkit-slider-runnable-track { + background: #161f27; + background: var(--background); +} + +input[type='range']::-moz-range-track { + width: 100%; + height: 9.5px; + -moz-transition: 0.2s; + transition: 0.2s; + background: #161f27; + background: var(--background); + border-radius: 3px; +} + +input[type='range']::-moz-range-thumb { + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); +} + +input[type='range']::-ms-track { + width: 100%; + height: 9.5px; + background: transparent; + border-color: transparent; + border-width: 16px 0; + color: transparent; +} + +input[type='range']::-ms-fill-lower { + background: #161f27; + background: var(--background); + border: 0.2px solid #010101; + border-radius: 3px; + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; +} + +input[type='range']::-ms-fill-upper { + background: #161f27; + background: var(--background); + border: 0.2px solid #010101; + border-radius: 3px; + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; +} + +input[type='range']::-ms-thumb { + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + border: 1px solid #000; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); +} + +input[type='range']:focus::-ms-fill-lower { + background: #161f27; + background: var(--background); +} + +input[type='range']:focus::-ms-fill-upper { + background: #161f27; + background: var(--background); +} + +a { + text-decoration: none; + color: #41adff; + color: var(--links); +} + +a:hover { + text-decoration: underline; +} + +code { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +samp { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +time { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +pre > code { + padding: 10px; + display: block; + overflow-x: auto; +} + +var { + color: #d941e2; + color: var(--variable); + font-style: normal; + font-family: monospace; +} + +kbd { + background: #161f27; + background: var(--background); + border: 1px solid #526980; + border: 1px solid var(--border); + border-radius: 2px; + color: #dbdbdb; + color: var(--text-main); + padding: 2px 4px 2px 4px; +} + +img, +video { + max-width: 100%; + height: auto; +} + +hr { + border: none; + border-top: 1px solid #526980; + border-top: 1px solid var(--border); +} + +table { + border-collapse: collapse; + margin-bottom: 10px; + width: 100%; + table-layout: fixed; +} + +table caption { + text-align: left; +} + +td, +th { + padding: 6px; + text-align: left; + vertical-align: top; + word-wrap: break-word; +} + +thead { + border-bottom: 1px solid #526980; + border-bottom: 1px solid var(--border); +} + +tfoot { + border-top: 1px solid #526980; + border-top: 1px solid var(--border); +} + +tbody tr:nth-child(even) { + background-color: #161f27; + background-color: var(--background); +} + +tbody tr:nth-child(even) button { + background-color: #1a242f; + background-color: var(--background-alt); +} + +tbody tr:nth-child(even) button:hover { + background-color: #202b38; + background-color: var(--background-body); +} + +::-webkit-scrollbar { + height: 10px; + width: 10px; +} + +::-webkit-scrollbar-track { + background: #161f27; + background: var(--background); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb { + background: #040a0f; + background: var(--scrollbar-thumb); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgb(0, 0, 0); + background: var(--scrollbar-thumb-hover); +} + +::-moz-selection { + background-color: #1c76c5; + background-color: var(--selection); + color: #fff; + color: var(--text-bright); +} + +::selection { + background-color: #1c76c5; + background-color: var(--selection); + color: #fff; + color: var(--text-bright); +} + +details { + display: flex; + flex-direction: column; + align-items: flex-start; + background-color: #1a242f; + background-color: var(--background-alt); + padding: 10px 10px 0; + margin: 1em 0; + border-radius: 6px; + overflow: hidden; +} + +details[open] { + padding: 10px; +} + +details > :last-child { + margin-bottom: 0; +} + +details[open] summary { + margin-bottom: 10px; +} + +summary { + display: list-item; + background-color: #161f27; + background-color: var(--background); + padding: 10px; + margin: -10px -10px 0; + cursor: pointer; + outline: none; +} + +summary:hover, +summary:focus { + text-decoration: underline; +} + +details > :not(summary) { + margin-top: 0; +} + +summary::-webkit-details-marker { + color: #dbdbdb; + color: var(--text-main); +} + +dialog { + background-color: #1a242f; + background-color: var(--background-alt); + color: #dbdbdb; + color: var(--text-main); + border: none; + border-radius: 6px; + border-color: #526980; + border-color: var(--border); + padding: 10px 30px; +} + +dialog > header:first-child { + background-color: #161f27; + background-color: var(--background); + border-radius: 6px 6px 0 0; + margin: -10px -30px 10px; + padding: 10px; + text-align: center; +} + +dialog::-webkit-backdrop { + background: #0000009c; + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); +} + +dialog::backdrop { + background: #0000009c; + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); +} + +footer { + border-top: 1px solid #526980; + border-top: 1px solid var(--border); + padding-top: 10px; + color: #a9b1ba; + color: var(--text-muted); +} + +body > footer { + margin-top: 40px; +} + +@media print { + body, + pre, + code, + summary, + details, + button, + input, + textarea { + background-color: #fff; + } + + button, + input, + textarea { + border: 1px solid #000; + } + + body, + h1, + h2, + h3, + h4, + h5, + h6, + pre, + code, + button, + input, + textarea, + footer, + summary, + strong { + color: #000; + } + + summary::marker { + color: #000; + } + + summary::-webkit-details-marker { + color: #000; + } + + tbody tr:nth-child(even) { + background-color: #f2f2f2; + } + + a { + color: #00f; + text-decoration: underline; + } +} diff --git a/app/blueprints/www/static/img/quart-imp-logo.png b/app/blueprints/www/static/img/quart-imp-logo.png new file mode 100644 index 0000000..1140eb4 Binary files /dev/null and b/app/blueprints/www/static/img/quart-imp-logo.png differ diff --git a/app/blueprints/www/static/js/main.js b/app/blueprints/www/static/js/main.js new file mode 100644 index 0000000..695c8bd --- /dev/null +++ b/app/blueprints/www/static/js/main.js @@ -0,0 +1 @@ +console.log('This log is from the file /home/david/PycharmProjects/quart-imp/app/blueprints/www/static/main.js') diff --git a/app/blueprints/www/templates/www/extends/main.html b/app/blueprints/www/templates/www/extends/main.html new file mode 100644 index 0000000..4c0f54a --- /dev/null +++ b/app/blueprints/www/templates/www/extends/main.html @@ -0,0 +1,24 @@ + + + + + + + Quart-Imp + + + + + + + + + +{% include 'www/includes/header.html' %} +{% block content %}{% endblock %} +{% include 'www/includes/footer.html' %} + + + diff --git a/app/blueprints/www/templates/www/includes/footer.html b/app/blueprints/www/templates/www/includes/footer.html new file mode 100644 index 0000000..e0c54f7 --- /dev/null +++ b/app/blueprints/www/templates/www/includes/footer.html @@ -0,0 +1,6 @@ +
+
+

This is the footer, located here: /home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/includes/footer.html

+

It's being imported in the /home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/extends/main.html template.

+
+
diff --git a/app/blueprints/www/templates/www/includes/header.html b/app/blueprints/www/templates/www/includes/header.html new file mode 100644 index 0000000..e3cd2e3 --- /dev/null +++ b/app/blueprints/www/templates/www/includes/header.html @@ -0,0 +1,10 @@ +
+ quart-imp logo +

Quart-Imp

+
+
+

This is the header, located here: /home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/includes/header.html

+

It's being imported in the /home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/extends/main.html template.

+
diff --git a/app/blueprints/www/templates/www/index.html b/app/blueprints/www/templates/www/index.html new file mode 100644 index 0000000..8554519 --- /dev/null +++ b/app/blueprints/www/templates/www/index.html @@ -0,0 +1,17 @@ +{% extends 'www/extends/main.html' %} + +{% block content %} +
+
+

Blueprint: www

+

This is the index route of the included example blueprint.

+

+ This template page is located in /home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/index.html
+ it extends from /home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/extends/main.html
+ with its route defined in /home/david/PycharmProjects/quart-imp/app/blueprints/www/routes/index.py

+ It's being imported by bp.import_resources("routes") + in the /home/david/PycharmProjects/quart-imp/app/blueprints/www/__init__.py file. +

+
+
+{% endblock %} diff --git a/app/default.config.toml b/app/default.config.toml new file mode 100644 index 0000000..91f7a8a --- /dev/null +++ b/app/default.config.toml @@ -0,0 +1,81 @@ +# Quart-Imp Config File +# ------------------------ +# Updates the Quart app config with the variables below. +# If any variable below does not exist in the standard Quart env +# vars it is created and will be accessible using +# app.config. All key names defined below will be +# capitalised when imported. +[FLASK] +DEBUG = false +#PROPAGATE_EXCEPTIONS = true +TRAP_HTTP_EXCEPTIONS = false +#TRAP_BAD_REQUEST_ERRORS = true +SECRET_KEY = "86685ef98889e7db7da6df2dd7184f866f4ef34244fe2c52" +SESSION_COOKIE_NAME = "session" +#SESSION_COOKIE_DOMAIN = "domain-here.com" +#SESSION_COOKIE_PATH = "/" +SESSION_COOKIE_HTTPONLY = true +SESSION_COOKIE_SECURE = false +SESSION_COOKIE_SAMESITE = "Lax" +PERMANENT_SESSION_LIFETIME = 3600 # 1 hour +SESSION_REFRESH_EACH_REQUEST = true +USE_X_SENDFILE = false +#SEND_FILE_MAX_AGE_DEFAULT = 43200 +ERROR_404_HELP = true +#SERVER_NAME = "localhost:5000" +APPLICATION_ROOT = "/" +PREFERRED_URL_SCHEME = "http" +#MAX_CONTENT_LENGTH = 0 +#TEMPLATES_AUTO_RELOAD = true +EXPLAIN_TEMPLATE_LOADING = false +MAX_COOKIE_SIZE = 4093 + +# This will set the default session variables for the app. +# Anything here will be accessible using session["your_var_name"] +# or session.get("your_var_name") +[SESSION] +logged_in = false + +# These settings are specific to the Flask-SQLAlchemy extension. +# Anything here will be accessible using app.config +[SQLALCHEMY] +SQLALCHEMY_ECHO = false +SQLALCHEMY_TRACK_MODIFICATIONS = false +SQLALCHEMY_RECORD_QUERIES = false +# Below are extra settings that Quart-Imp uses but relates to Flask-SQLAlchemy. +# This sets the file extension for SQLite databases, and where to create the folder +# that the database will be stored in. true will create the folder on the same level as your +# app, false will create the folder in the app root. +SQLITE_DB_EXTENSION = ".sqlite" +SQLITE_STORE_IN_PARENT = false + +# [DATABASE.MAIN] is loaded as SQLALCHEMY_DATABASE_URI +# Dialets = mysql / postgresql / sqlite / oracle / mssql +# Uncomment below to generate the SQLALCHEMY_DATABASE_URI. +[DATABASE.MAIN] +ENABLED = true +DIALECT = "sqlite" +DATABASE_NAME = "database" +LOCATION = "" +PORT = "" +USERNAME = "" +PASSWORD = "" + +# Adding another database is as simple as adding a new section. +# [DATABASE.ANOTHER] will then be accessible using SQLALCHEMY_BINDS +# The bind key will be stored as a lowercase value, so "ANOTHER" will +# be accessible as "another" +# You can then use the bind key in the model as follows: +# class MyModel(db.Model): +# __bind_key__ = "another" +# ... + +# Uncomment below to generate and add to SQLALCHEMY_BINDS. +#[DATABASE.ANOTHER] +#ENABLED = true +#DIALECT = "sqlite" +#DATABASE_NAME = "another" +#LOCATION = "" +#PORT = "" +#USERNAME = "" +#PASSWORD = "" diff --git a/app/extensions/__init__.py b/app/extensions/__init__.py new file mode 100644 index 0000000..5c1cf18 --- /dev/null +++ b/app/extensions/__init__.py @@ -0,0 +1,9 @@ +import quart_flask_patch +from flask_sqlalchemy import SQLAlchemy + +from quart_imp import Imp + +_ = quart_flask_patch + +imp = Imp() +db = SQLAlchemy() diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..4736b83 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,12 @@ +from sqlalchemy import select, update, delete, insert +from sqlalchemy.orm import relationship + +from app.extensions import db + +__all__ = [ + "db", + "select", + "update", + "delete", + "insert", +] diff --git a/app/models/example_user_table.py b/app/models/example_user_table.py new file mode 100644 index 0000000..853ca5f --- /dev/null +++ b/app/models/example_user_table.py @@ -0,0 +1,72 @@ +from quart_imp.auth import authenticate_password +from quart_imp.auth import encrypt_password +from quart_imp.auth import generate_private_key +from quart_imp.auth import generate_salt +from . import * + + +class ExampleUserTable(db.Model): + user_id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(256), nullable=False) + password = db.Column(db.String(512), nullable=False) + salt = db.Column(db.String(4), nullable=False) + private_key = db.Column(db.String(256), nullable=False) + disabled = db.Column(db.Boolean) + + @classmethod + def login(cls, username, password: str) -> bool: + user = cls.get_by_username(username) + if user is None: + return False + return authenticate_password(password, user.password, user.salt) + + @classmethod + def get_by_id(cls, user_id: int): + return db.session.execute( + select(cls).filter_by(user_id=user_id).limit(1) + ).scalar_one_or_none() + + @classmethod + def get_by_username(cls, username: str): + return db.session.execute( + select(cls).filter_by(username=username).limit(1) + ).scalar_one_or_none() + + @classmethod + def create(cls, username, password, disabled): + salt = generate_salt() + salt_pepper_password = encrypt_password(password, salt) + private_key = generate_private_key(username) + + db.session.execute( + insert(cls).values( + username=username, + password=salt_pepper_password, + salt=salt, + private_key=private_key, + disabled=disabled, + ) + ) + db.session.commit() + + @classmethod + def update(cls, user_id: int, username, private_key, disabled): + db.session.execute( + update(cls).where( + cls.user_id == user_id + ).values( + username=username, + private_key=private_key, + disabled=disabled, + ) + ) + db.session.commit() + + @classmethod + def delete(cls, user_id: int): + db.session.execute( + delete(cls).where( + cls.user_id == user_id + ) + ) + db.session.commit() diff --git a/app/resources/cli/cli.py b/app/resources/cli/cli.py new file mode 100644 index 0000000..cdbd9b9 --- /dev/null +++ b/app/resources/cli/cli.py @@ -0,0 +1,57 @@ +from quart import current_app as app +from app.extensions import db +from app.models.example_user_table import ExampleUserTable + + +@app.cli.command("config") +async def create_tables(): + print(app.config) + + +@app.cli.command("create-tables") +async def create_tables(): + db.create_all() + + +@app.cli.command("get-example-user") +async def get_example_user(): + result = ExampleUserTable.get_by_id(1) + if not result: + print("User not found.") + return + print( + f""" + user_id: {result.user_id} + username: {result.username} + salt: {result.salt} + password: {result.password} + private_key: {result.private_key} + disabled: {result.disabled} + """ + ) + + +@app.cli.command("create-example-user") +async def add_example_user(): + ExampleUserTable.create( + username="admin", + password="password", + disabled=False, + ) + + +@app.cli.command("update-example-user") +async def update_example_user(): + ExampleUserTable.update( + user_id=1, + username="admin-updated", + private_key="private_key", + disabled=False, + ) + + +@app.cli.command("delete-example-user") +async def delete_example_user(): + ExampleUserTable.delete( + user_id=1, + ) diff --git a/app/resources/context_processors/context_processors.py b/app/resources/context_processors/context_processors.py new file mode 100644 index 0000000..b1215af --- /dev/null +++ b/app/resources/context_processors/context_processors.py @@ -0,0 +1,14 @@ +from quart import current_app as app + + +@app.context_processor +async def example__utility_processor(): + """ + Usage: + {{ format_price(100.33) }} -> $100.33 + """ + + async def example__format_price(amount, currency='$'): + return '{1}{0:.2f}'.format(amount, currency) + + return dict(format_price=example__format_price) diff --git a/app/resources/error_handlers/error_handlers.py b/app/resources/error_handlers/error_handlers.py new file mode 100644 index 0000000..b31d620 --- /dev/null +++ b/app/resources/error_handlers/error_handlers.py @@ -0,0 +1,44 @@ +from quart import current_app as app +from quart import render_template + + +@app.errorhandler(400) +async def error_400(error): + return await render_template( + "errors/400.html", + ), 400 + + +@app.errorhandler(401) +async def error_401(error): + return await render_template( + "errors/401.html", + ), 401 + + +@app.errorhandler(403) +async def error_403(error): + return await render_template( + "errors/403.html", + ), 403 + + +@app.errorhandler(404) +async def error_404(error): + return await render_template( + "errors/404.html", + ), 404 + + +@app.errorhandler(405) +async def error_405(error): + return await render_template( + "errors/405.html", + ), 405 + + +@app.errorhandler(500) +async def error_500(error): + return await render_template( + "errors/500.html", + ), 500 diff --git a/app/resources/filters/filters.py b/app/resources/filters/filters.py new file mode 100644 index 0000000..bbfb78f --- /dev/null +++ b/app/resources/filters/filters.py @@ -0,0 +1,30 @@ +from quart import current_app as app + + +@app.template_filter('example__num_to_month') +async def example__num_to_month(num: str) -> str: + """ + Usage: + {{ 1 | example__num_to_month }} -> January + """ + if isinstance(num, int): + num = str(num) + + months = { + "1": "January", + "2": "February", + "3": "March", + "4": "April", + "5": "May", + "6": "June", + "7": "July", + "8": "August", + "9": "September", + "10": "October", + "11": "November", + "12": "December", + } + + if num in months: + return months[num] + return "Month not found" diff --git a/app/resources/routes/routes.py b/app/resources/routes/routes.py new file mode 100644 index 0000000..28700ac --- /dev/null +++ b/app/resources/routes/routes.py @@ -0,0 +1,7 @@ +from quart import current_app as app +from quart import render_template + + +@app.route("/resources") +async def index(): + return await render_template("index.html") diff --git a/app/resources/static/favicon.ico b/app/resources/static/favicon.ico new file mode 100644 index 0000000..e734eba Binary files /dev/null and b/app/resources/static/favicon.ico differ diff --git a/app/resources/templates/errors/400.html b/app/resources/templates/errors/400.html new file mode 100644 index 0000000..b7820b2 --- /dev/null +++ b/app/resources/templates/errors/400.html @@ -0,0 +1,11 @@ + + + + + 400 Bad Request + + + +

It's not us, it's you.

+ + diff --git a/app/resources/templates/errors/401.html b/app/resources/templates/errors/401.html new file mode 100644 index 0000000..81be123 --- /dev/null +++ b/app/resources/templates/errors/401.html @@ -0,0 +1,11 @@ + + + + + 401 Unauthorized + + + +

You lack valid authentication credentials for the requested resource

+ + diff --git a/app/resources/templates/errors/403.html b/app/resources/templates/errors/403.html new file mode 100644 index 0000000..1e0733c --- /dev/null +++ b/app/resources/templates/errors/403.html @@ -0,0 +1,11 @@ + + + + + 403 Forbidden + + + +

Access forbidden!

+ + diff --git a/app/resources/templates/errors/404.html b/app/resources/templates/errors/404.html new file mode 100644 index 0000000..6e6254b --- /dev/null +++ b/app/resources/templates/errors/404.html @@ -0,0 +1,11 @@ + + + + + 404 Page Not Found + + + +

No route associated with the URL

+ + diff --git a/app/resources/templates/errors/405.html b/app/resources/templates/errors/405.html new file mode 100644 index 0000000..3f1c549 --- /dev/null +++ b/app/resources/templates/errors/405.html @@ -0,0 +1,11 @@ + + + + + 405 Method Not Allowed + + + +

Should of GET when you POST, or POST when you GET

+ + diff --git a/app/resources/templates/errors/500.html b/app/resources/templates/errors/500.html new file mode 100644 index 0000000..2deb58e --- /dev/null +++ b/app/resources/templates/errors/500.html @@ -0,0 +1,11 @@ + + + + + 500 Server Error! + + + +

There has been a server error!

+ + diff --git a/app/resources/templates/index.html b/app/resources/templates/index.html new file mode 100644 index 0000000..cd28e3f --- /dev/null +++ b/app/resources/templates/index.html @@ -0,0 +1,12 @@ + + + + + + + Quart-Imp Global Template + + + +

This is the example resources template file located in resources/templates/index.html

+ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8d7616c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "quart-imp" +description = 'A Quart auto importer that allows your Quart apps to grow big.' +authors = [{ name = "David Carmichael", email = "david@uilix.com" }] +readme = "README.md" +license = { file = "LICENSE" } +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', + 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Natural Language :: English', +] +requires-python = ">=3.8" +dynamic = ["version"] +dependencies = [ + 'click', + 'Quart', + 'quart-flask-patch', + 'Flask-SQLAlchemy', + 'toml', + 'more-itertools' +] + +[project.scripts] +quart-imp = "quart_imp._cli:cli" + +[project.urls] +Documentation = "https://cheesecake87.github.io/quart-imp/" +Source = "https://github.com/CheeseCake87/quart-imp" + +[tool.flit.sdist] +exclude = [ + ".github/", + "_assets/", + "app/", + "dist/", + "docs/", + "docs_gen/", + "test_app/", + "test_docker/", + "tests/", + ".env", + ".gitignore", + "docker-compose.yaml", + "Dockerfile", + "requirements_build.txt", + "requirements_dev.txt", + "requirements_docs.txt", +] diff --git a/quart_imp/__init__.py b/quart_imp/__init__.py new file mode 100644 index 0000000..8defdd3 --- /dev/null +++ b/quart_imp/__init__.py @@ -0,0 +1,8 @@ +from .auth import Auth as Auth +from .auth import PasswordGeneration as PasswordGeneration +from .blueprint import ImpBlueprint as Blueprint +from .imp import Imp as Imp + +__version__ = "0.1.0" + +__all__ = ["Auth", "PasswordGeneration", "Imp", "Blueprint"] diff --git a/quart_imp/_cli/__init__.py b/quart_imp/_cli/__init__.py new file mode 100644 index 0000000..ba577ed --- /dev/null +++ b/quart_imp/_cli/__init__.py @@ -0,0 +1,74 @@ +import click + +from .blueprint import add_blueprint as _add_blueprint +from .helpers import Sprinkles as Sp +from .init import init_app as _init_app + + +@click.group() +def cli(): + pass # Entry Point + + +@cli.command("blueprint", help="Create a quart-imp blueprint") +@click.option( + "-f", + "--folder", + nargs=1, + default="Current Working Directory", + prompt=( + f"\n{Sp.WARNING}(Creation is relative to the current working directory){Sp.END}\n" + f"Folder to create blueprint in" + ), + help="The from_folder to create the blueprint in, defaults to the current working directory", +) +@click.option( + "-n", + "--name", + nargs=1, + default="my_new_blueprint", + prompt="Name of the blueprint to create", + help="The name of the blueprint to create", +) +def add_blueprint(folder, name): + _add_blueprint(folder, name) + + +@cli.command("init", help="Create a new quart-imp app") +@click.option( + "-n", + "--name", + nargs=1, + default=None, + help="The name of the app folder that will be created", +) +@click.option("-f", "--full", is_flag=True, default=False, help="Create a full app") +@click.option("-s", "--slim", is_flag=True, default=False, help="Create a slim app") +@click.option( + "-m", "--minimal", is_flag=True, default=False, help="Create a minimal app" +) +def init_new_app(name, full, slim, minimal): + if not full and not slim and not minimal: + choice = click.prompt( + "What type of app would you like to create?", + default="full", + type=click.Choice(["full", "slim", "minimal"]), + ) + + if choice == "full": + full = True + elif choice == "slim": + slim = True + elif choice == "minimal": + minimal = True + + if name is None: + set_name = click.prompt("What would you like to call your app?", default="app") + + else: + set_name = name + + if minimal: + slim = True + + _init_app(set_name, full, slim, minimal) diff --git a/quart_imp/_cli/blueprint.py b/quart_imp/_cli/blueprint.py new file mode 100644 index 0000000..7bf62b9 --- /dev/null +++ b/quart_imp/_cli/blueprint.py @@ -0,0 +1,129 @@ +from pathlib import Path +from typing import Optional +import click + +from .filelib import BlueprintFileLib as BpFlib +from .filelib import quart_imp_logo +from .filelib.head_tag_generator import head_tag_generator +from .filelib.main_js import main_js +from .filelib.water_css import water_css +from .helpers import Sprinkles as Sp +from .helpers import to_snake_case + + +def add_blueprint(folder, name, _init_app: bool = False, _cwd: Optional[Path] = None): + click.echo(f"{Sp.OKGREEN}Creating Blueprint: {name}") + + if _cwd: + cwd = _cwd + + else: + if folder != "Current Working Directory": + cwd = Path(Path.cwd() / folder) + else: + cwd = Path.cwd() + + if not cwd.exists(): + click.echo(f"{Sp.FAIL}{folder} does not exist.{Sp.END}") + return + + name = to_snake_case(name) + + # Folders + folders = { + "root": cwd / name, + "routes": cwd / name / "routes", + "static": cwd / name / "static", + "static/img": cwd / name / "static" / "img", + "static/css": cwd / name / "static" / "css", + "static/js": cwd / name / "static" / "js", + "templates": cwd / name / "templates" / name, + "templates/extends": cwd / name / "templates" / name / "extends", + "templates/includes": cwd / name / "templates" / name / "includes", + } + + # Files + files = { + "root/__init__.py": (folders["root"] / "__init__.py", BpFlib.init_py), + "root/config.toml": ( + folders["root"] / "config.toml", + BpFlib.config_toml.format(name=name, url_prefix="" if _init_app else name), + ), + "routes/index.py": ( + folders["routes"] / "index.py", + BpFlib.routes_index_py.format(name=name), + ), + "static/img/quart-imp-logo.png": ( + folders["static/img"] / "quart-imp-logo.png", + quart_imp_logo, + ), + "static/water.css": (folders["static/css"] / "water.css", water_css), + "static/main.js": ( + folders["static/js"] / "main.js", + main_js.format(main_js=folders["static"] / "main.js"), + ), + "templates/-/index.html": ( + folders["templates"] / "index.html", + BpFlib.templates_index_html.format( + root=folders["root"], name=name, quart_imp_logo=quart_imp_logo + ) + if not _init_app + else BpFlib.ia_templates_index_html.format( + name=name, + quart_imp_logo=quart_imp_logo, + index_html=folders["templates"] / "index.html", + extends_main_html=folders["templates/extends"] / "main.html", + index_py=folders["routes"] / "index.py", + init_py=folders["root"] / "__init__.py", + ), + ), + "templates/-/extends/main.html": ( + folders["templates/extends"] / "main.html", + BpFlib.templates_extends_main_html.format( + name=name, + head_tag=head_tag_generator(f"{name}.static"), + ), + ), + "templates/-/includes/header.html": ( + folders["templates/includes"] / "header.html", + BpFlib.templates_includes_header_html.format( + header_html=folders["templates/includes"] / "header.html", + main_html=folders["templates/extends"] / "main.html", + static_path=f"{name}.static", + ), + ), + "templates/-/includes/footer.html": ( + folders["templates/includes"] / "footer.html", + BpFlib.templates_includes_footer_html.format( + footer_html=folders["templates/includes"] / "footer.html", + main_html=folders["templates/extends"] / "main.html", + ), + ), + } + + # Loop create folders + for folder, path in folders.items(): + if not path.exists(): + path.mkdir(parents=True) + click.echo(f"{Sp.OKGREEN}Blueprint folder: {folder}, created{Sp.END}") + else: + click.echo( + f"{Sp.WARNING}Blueprint folder already exists: {folder}, skipping{Sp.END}" + ) + + # Loop create files + for file, (path, content) in files.items(): + if not path.exists(): + if file == "static/img/quart-imp-logo.png": + path.write_bytes(bytes.fromhex(content)) + continue + + path.write_text(content, encoding="utf-8") + + click.echo(f"{Sp.OKGREEN}Blueprint file: {file}, created{Sp.END}") + else: + click.echo( + f"{Sp.WARNING}Blueprint file already exists: {file}, skipping{Sp.END}" + ) + + click.echo(f"{Sp.OKGREEN}Blueprint created: {folders['root']}{Sp.END}") diff --git a/quart_imp/_cli/filelib/__init__.py b/quart_imp/_cli/filelib/__init__.py new file mode 100644 index 0000000..f1f9b99 --- /dev/null +++ b/quart_imp/_cli/filelib/__init__.py @@ -0,0 +1,14 @@ +from .all_files import GlobalFileLib +from .app import AppFileLib +from .blueprint import BlueprintFileLib + +from .quart_imp_logo import quart_imp_logo +from .water_css import water_css + +__all__ = [ + "GlobalFileLib", + "AppFileLib", + "BlueprintFileLib", + "quart_imp_logo", + "water_css", +] diff --git a/quart_imp/_cli/filelib/all_files.py b/quart_imp/_cli/filelib/all_files.py new file mode 100644 index 0000000..57ab3c2 --- /dev/null +++ b/quart_imp/_cli/filelib/all_files.py @@ -0,0 +1,323 @@ +class GlobalFileLib: + # Format to: app_name + collections_cli_py = """\ +from quart import current_app as app +from {app_name}.extensions import db +from {app_name}.models.example_user_table import ExampleUserTable + + +@app.cli.command("config") +async def create_tables(): + print(app.config) + + +@app.cli.command("create-tables") +async def create_tables(): + db.create_all() + + +@app.cli.command("get-example-user") +async def get_example_user(): + result = ExampleUserTable.get_by_id(1) + if not result: + print("User not found.") + return + print( + f\"\"\" + user_id: {{result.user_id}} + username: {{result.username}} + salt: {{result.salt}} + password: {{result.password}} + private_key: {{result.private_key}} + disabled: {{result.disabled}} + \"\"\" + ) + + +@app.cli.command("create-example-user") +async def add_example_user(): + ExampleUserTable.create( + username="admin", + password="password", + disabled=False, + ) + + +@app.cli.command("update-example-user") +async def update_example_user(): + ExampleUserTable.update( + user_id=1, + username="admin-updated", + private_key="private_key", + disabled=False, + ) + + +@app.cli.command("delete-example-user") +async def delete_example_user(): + ExampleUserTable.delete( + user_id=1, + ) +""" + + slim_collections_cli_py = """\ +from quart import current_app as app + + +@app.cli.command("config") +async def create_tables(): + print(app.config) +""" + + # Format to: None + collections_context_processors_py = """\ +from quart import current_app as app + + +@app.context_processor +async def example__utility_processor(): + \""" + Usage: + {{ format_price(100.33) }} -> $100.33 + \""" + + async def example__format_price(amount, currency='$'): + return '{1}{0:.2f}'.format(amount, currency) + + return dict(format_price=example__format_price) +""" + + # Format to: None + collections_error_handlers_py = """\ +from quart import current_app as app +from quart import render_template + + +@app.errorhandler(400) +async def error_400(error): + return await render_template( + "errors/400.html", + ), 400 + + +@app.errorhandler(401) +async def error_401(error): + return await render_template( + "errors/401.html", + ), 401 + + +@app.errorhandler(403) +async def error_403(error): + return await render_template( + "errors/403.html", + ), 403 + + +@app.errorhandler(404) +async def error_404(error): + return await render_template( + "errors/404.html", + ), 404 + + +@app.errorhandler(405) +async def error_405(error): + return await render_template( + "errors/405.html", + ), 405 + + +@app.errorhandler(500) +async def error_500(error): + return await render_template( + "errors/500.html", + ), 500 +""" + + # Format to: None + collections_filters_py = """\ +from quart import current_app as app + + +@app.template_filter('example__num_to_month') +async def example__num_to_month(num: str) -> str: + \""" + Usage: + {{ 1 | example__num_to_month }} -> January + \""" + if isinstance(num, int): + num = str(num) + + months = { + "1": "January", + "2": "February", + "3": "March", + "4": "April", + "5": "May", + "6": "June", + "7": "July", + "8": "August", + "9": "September", + "10": "October", + "11": "November", + "12": "December", + } + + if num in months: + return months[num] + return "Month not found" +""" + + # Format to: None + collections_routes_py = """\ +from quart import current_app as app +from quart import render_template + + +@app.route("/resources") +async def index(): + head = Head(title="Quart Imp Global Template") + return await render_template("index.html") +""" + + minimal_collections_routes_py = """\ +from quart import current_app as app +from quart import render_template + + +@app.route("/") +async def index(): + return await render_template("index.html") +""" + + # Format to: None + templates_index_html = """\ + + + + + + + Quart-Imp Global Template + + + +

This is the example resources template file located in resources/templates/index.html

+ +""" + + # Format to: head_tag, static_path, index_py, index_html, init_py + minimal_templates_index_html = """\ + + + + +{head_tag} + + + +
+ quart-imp logo +

Quart-Imp

+
+
+
+

+ This template page is located in {index_html}
+ with its route defined in {index_py}

+ It's being imported by app.import_app_resources() + in the {init_py} file. +

+
+
+ + + +""" + + templates_errors_400_html = """\ + + + + + 400 Bad Request + + + +

It's not us, it's you.

+ + +""" + + templates_errors_401_html = """\ + + + + + 401 Unauthorized + + + +

You lack valid authentication credentials for the requested resource

+ + +""" + + templates_errors_403_html = """\ + + + + + 403 Forbidden + + + +

Access forbidden!

+ + +""" + + templates_errors_404_html = """\ + + + + + 404 Page Not Found + + + +

No route associated with the URL

+ + +""" + + templates_errors_405_html = """\ + + + + + 405 Method Not Allowed + + + +

Should of GET when you POST, or POST when you GET

+ + +""" + + templates_errors_500_html = """\ + + + + + 500 Server Error! + + + +

There has been a server error!

+ + +""" diff --git a/quart_imp/_cli/filelib/app.py b/quart_imp/_cli/filelib/app.py new file mode 100644 index 0000000..86a99a5 --- /dev/null +++ b/quart_imp/_cli/filelib/app.py @@ -0,0 +1,339 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class AppFileLib: + # Format to: secret_key + default_init_config_toml = """\ +# Quart-Imp Config File +# ------------------------ +# Updates the Quart app config with the variables below. +# If any variable below does not exist in the standard Quart env +# vars it is created and will be accessible using +# app.config. All key names defined below will be +# capitalised when imported. +[FLASK] +DEBUG = false +#PROPAGATE_EXCEPTIONS = true +TRAP_HTTP_EXCEPTIONS = false +#TRAP_BAD_REQUEST_ERRORS = true +SECRET_KEY = "{secret_key}" +SESSION_COOKIE_NAME = "session" +#SESSION_COOKIE_DOMAIN = "domain-here.com" +#SESSION_COOKIE_PATH = "/" +SESSION_COOKIE_HTTPONLY = true +SESSION_COOKIE_SECURE = false +SESSION_COOKIE_SAMESITE = "Lax" +PERMANENT_SESSION_LIFETIME = 3600 # 1 hour +SESSION_REFRESH_EACH_REQUEST = true +USE_X_SENDFILE = false +#SEND_FILE_MAX_AGE_DEFAULT = 43200 +ERROR_404_HELP = true +#SERVER_NAME = "localhost:5000" +APPLICATION_ROOT = "/" +PREFERRED_URL_SCHEME = "http" +#MAX_CONTENT_LENGTH = 0 +#TEMPLATES_AUTO_RELOAD = true +EXPLAIN_TEMPLATE_LOADING = false +MAX_COOKIE_SIZE = 4093 + +# This will set the default session variables for the app. +# Anything here will be accessible using session["your_var_name"] +# or session.get("your_var_name") +[SESSION] +logged_in = false + +# These settings are specific to the Flask-SQLAlchemy extension. +# Anything here will be accessible using app.config +[SQLALCHEMY] +SQLALCHEMY_ECHO = false +SQLALCHEMY_TRACK_MODIFICATIONS = false +SQLALCHEMY_RECORD_QUERIES = false +# Below are extra settings that Quart-Imp uses but relates to Flask-SQLAlchemy. +# This sets the file extension for SQLite databases, and where to create the folder +# that the database will be stored in. true will create the folder on the same level as your +# app, false will create the folder in the app root. +SQLITE_DB_EXTENSION = ".sqlite" +SQLITE_STORE_IN_PARENT = false + +# [DATABASE.MAIN] is loaded as SQLALCHEMY_DATABASE_URI +# Dialets = mysql / postgresql / sqlite / oracle / mssql +# Uncomment below to generate the SQLALCHEMY_DATABASE_URI. +[DATABASE.MAIN] +ENABLED = true +DIALECT = "sqlite" +DATABASE_NAME = "database" +LOCATION = "" +PORT = "" +USERNAME = "" +PASSWORD = "" + +# Adding another database is as simple as adding a new section. +# [DATABASE.ANOTHER] will then be accessible using SQLALCHEMY_BINDS +# The bind key will be stored as a lowercase value, so "ANOTHER" will +# be accessible as "another" +# You can then use the bind key in the model as follows: +# class MyModel(db.Model): +# __bind_key__ = "another" +# ... + +# Uncomment below to generate and add to SQLALCHEMY_BINDS. +#[DATABASE.ANOTHER] +#ENABLED = true +#DIALECT = "sqlite" +#DATABASE_NAME = "another" +#LOCATION = "" +#PORT = "" +#USERNAME = "" +#PASSWORD = "" +""" + + # Format to: secret_key + default_config_toml = """\ +# Quart-Imp Config File +# ------------------------ +# Updates the Quart app config with the variables below. +# If any variable below does not exist in the standard Quart env +# vars it is created and will be accessible using +# app.config. All key names defined below will be +# capitalised when imported. +[FLASK] +DEBUG = false +#PROPAGATE_EXCEPTIONS = true +TRAP_HTTP_EXCEPTIONS = false +#TRAP_BAD_REQUEST_ERRORS = true +SECRET_KEY = "{secret_key}" +SESSION_COOKIE_NAME = "session" +#SESSION_COOKIE_DOMAIN = "domain-here.com" +#SESSION_COOKIE_PATH = "/" +SESSION_COOKIE_HTTPONLY = true +SESSION_COOKIE_SECURE = false +SESSION_COOKIE_SAMESITE = "Lax" +PERMANENT_SESSION_LIFETIME = 3600 # 1 hour +SESSION_REFRESH_EACH_REQUEST = true +USE_X_SENDFILE = false +#SEND_FILE_MAX_AGE_DEFAULT = 43200 +ERROR_404_HELP = true +#SERVER_NAME = "localhost:5000" +APPLICATION_ROOT = "/" +PREFERRED_URL_SCHEME = "http" +#MAX_CONTENT_LENGTH = 0 +#TEMPLATES_AUTO_RELOAD = true +EXPLAIN_TEMPLATE_LOADING = false +MAX_COOKIE_SIZE = 4093 + +# This will set the default session variables for the app. +# Anything here will be accessible using session["your_var_name"] +# or session.get("your_var_name") +[SESSION] +#logged_in = false + +# These settings are specific to the Flask-SQLAlchemy extension. +# Anything here will be accessible using app.config +[SQLALCHEMY] +SQLALCHEMY_ECHO = false +SQLALCHEMY_TRACK_MODIFICATIONS = false +SQLALCHEMY_RECORD_QUERIES = false +# Below are extra settings that Quart-Imp uses but relates to Flask-SQLAlchemy. +# This sets the file extension for SQLite databases, and where to create the folder +# that the database will be stored in. true will create the folder on the same level as your +# app, false will create the folder in the app root. +SQLITE_DB_EXTENSION = ".sqlite" +SQLITE_STORE_IN_PARENT = false + +# [DATABASE.MAIN] is loaded as SQLALCHEMY_DATABASE_URI +# Dialets = mysql / postgresql / sqlite / oracle / mssql +# Uncomment below to generate the SQLALCHEMY_DATABASE_URI. +#[DATABASE.MAIN] +#ENABLED = true +#DIALECT = "sqlite" +#DATABASE_NAME = "database" +#LOCATION = "" +#PORT = "" +#USERNAME = "" +#PASSWORD = "" + +# Adding another database is as simple as adding a new section. +# [DATABASE.ANOTHER] will then be accessible using SQLALCHEMY_BINDS +# The bind key will be stored as a lowercase value, so "ANOTHER" will +# be accessible as "another" +# You can then use the bind key in the model as follows: +# class MyModel(db.Model): +# __bind_key__ = "another" +# ... + +# Uncomment below to generate and add to SQLALCHEMY_BINDS. +#[DATABASE.ANOTHER] +#ENABLED = true +#DIALECT = "sqlite" +#DATABASE_NAME = "another" +#LOCATION = "" +#PORT = "" +#USERNAME = "" +#PASSWORD = "" +""" + + # Format to: app_name + init_py = """\ +from quart import Quart +from {app_name}.extensions import imp, db + + +def create_app(): + app = Quart(__name__, static_url_path="/") + imp.init_app(app) + imp.import_app_resources( + files_to_import=["*"], + folders_to_import=["*"] + ) + imp.import_blueprints("blueprints") + imp.import_models("models") + db.init_app(app) + + return app +""" + + slim_init_py = """\ +from quart import Quart +from {app_name}.extensions import imp + + +def create_app(): + app = Quart(__name__, static_url_path="/") + imp.init_app(app) + imp.import_app_resources( + files_to_import=["*"], + folders_to_import=["*"] + ) + imp.import_blueprint("www") + + return app +""" + + minimal_init_py = """\ +from quart import Quart +from {app_name}.extensions import imp + + +def create_app(): + app = Quart(__name__, static_url_path="/") + imp.init_app(app) + imp.import_app_resources( + files_to_import=["*"], + folders_to_import=["*"] + ) + + return app +""" + + extensions_init_py = """\ +import quart_flask_patch +from quart_imp import Imp +from flask_sqlalchemy import SQLAlchemy + +_ = quart_flask_patch + +imp = Imp() +db = SQLAlchemy() +""" + + slim_extensions_init_py = """\ +import quart_flask_patch +from quart_imp import Imp + +_ = quart_flask_patch + +imp = Imp() +""" + + # Format to: app_name + models_init_py = """\ +from sqlalchemy import select, update, delete, insert +from sqlalchemy.orm import relationship + +from {app_name}.extensions import db + +__all__ = [ + "db", + "select", + "update", + "delete", + "insert", +] +""" + + # Format to: None + models_example_user_table_py = """\ +from quart_imp.auth import authenticate_password +from quart_imp.auth import encrypt_password +from quart_imp.auth import generate_private_key +from quart_imp.auth import generate_salt +from . import * + + +class ExampleUserTable(db.Model): + user_id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(256), nullable=False) + password = db.Column(db.String(512), nullable=False) + salt = db.Column(db.String(4), nullable=False) + private_key = db.Column(db.String(256), nullable=False) + disabled = db.Column(db.Boolean) + + @classmethod + def login(cls, username, password: str) -> bool: + user = cls.get_by_username(username) + if user is None: + return False + return authenticate_password(password, user.password, user.salt) + + @classmethod + def get_by_id(cls, user_id: int): + return db.session.execute( + select(cls).filter_by(user_id=user_id).limit(1) + ).scalar_one_or_none() + + @classmethod + def get_by_username(cls, username: str): + return db.session.execute( + select(cls).filter_by(username=username).limit(1) + ).scalar_one_or_none() + + @classmethod + def create(cls, username, password, disabled): + salt = generate_salt() + salt_pepper_password = encrypt_password(password, salt) + private_key = generate_private_key(username) + + db.session.execute( + insert(cls).values( + username=username, + password=salt_pepper_password, + salt=salt, + private_key=private_key, + disabled=disabled, + ) + ) + db.session.commit() + + @classmethod + def update(cls, user_id: int, username, private_key, disabled): + db.session.execute( + update(cls).where( + cls.user_id == user_id + ).values( + username=username, + private_key=private_key, + disabled=disabled, + ) + ) + db.session.commit() + + @classmethod + def delete(cls, user_id: int): + db.session.execute( + delete(cls).where( + cls.user_id == user_id + ) + ) + db.session.commit() +""" diff --git a/quart_imp/_cli/filelib/blueprint.py b/quart_imp/_cli/filelib/blueprint.py new file mode 100644 index 0000000..41a7c0e --- /dev/null +++ b/quart_imp/_cli/filelib/blueprint.py @@ -0,0 +1,138 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class BlueprintFileLib: + # Format to: NONE + init_py = """\ +from quart_imp import Blueprint + +bp = Blueprint(__name__) + +bp.import_resources("routes") + + +@bp.before_app_request +async def before_app_request(): + bp.init_session() +""" + + # Format to: name, url_prefix + config_toml = """\ +ENABLED = "yes" + +[SETTINGS] +URL_PREFIX = "/{url_prefix}" +#SUBDOMAIN = "" +#URL_DEFAULTS = {{}} +STATIC_FOLDER = "static" +TEMPLATE_FOLDER = "templates" +STATIC_URL_PATH = "/static" +#ROOT_PATH = "" +#CLI_GROUP = "" + +[SESSION] +#{name}_session = "yes" + +# Set ENABLED to true to allow the blueprint +# to create a database bind, change settings accordingly. +[DATABASE_BIND] +ENABLED = false +DIALECT = "sqlite" +DATABASE_NAME = "{name}" +LOCATION = "" +PORT = "" +USERNAME = "" +PASSWORD = "" +""" + + # Format to: Name + routes_index_py = """\ +from quart import render_template + +from .. import bp + + +@bp.route("/", methods=["GET"]) +async def index(): + return await render_template(bp.tmpl("index.html")) +""" + + # Format to: root, name, quart_imp_logo + templates_index_html = """\ +{{% extends '{name}/extends/main.html' %}} + +{{% block content %}} +
+
+

Blueprint: {name}

+

Here's your new blueprint.

+

Located here: {root}

+

Remember to double-check the config.toml file.

+
+
+{{% endblock %}} +""" + + # Format to: name, quart_imp_logo, index_html, extends_main_html, index_py, init_py + ia_templates_index_html = """\ +{{% extends 'www/extends/main.html' %}} + +{{% block content %}} +
+
+

Blueprint: {name}

+

This is the index route of the included example blueprint.

+

+ This template page is located in {index_html}
+ it extends from {extends_main_html}
+ with its route defined in {index_py}

+ It's being imported by bp.import_resources("routes") + in the {init_py} file. +

+
+
+{{% endblock %}} +""" + + # Format to: head_tag + templates_extends_main_html = """\ + + + + + {head_tag} + + + +{{% include '{name}/includes/header.html' %}} +{{% block content %}}{{% endblock %}} +{{% include '{name}/includes/footer.html' %}} + + + +""" + + # Format to: header_html, main_html + templates_includes_header_html = """\ +
+ quart-imp logo +

Quart-Imp

+
+
+

This is the header, located here: {header_html}

+

It's being imported in the {main_html} template.

+
+""" + + # Format to: footer_html, main_html + templates_includes_footer_html = """\ +
+
+

This is the footer, located here: {footer_html}

+

It's being imported in the {main_html} template.

+
+
+""" diff --git a/quart_imp/_cli/filelib/favicon.py b/quart_imp/_cli/filelib/favicon.py new file mode 100644 index 0000000..1b0aba8 --- /dev/null +++ b/quart_imp/_cli/filelib/favicon.py @@ -0,0 +1,505 @@ +favicon = ( + "0000010003003030000001002000a8250000360000002020000001002000" + "a8100000de25000010100000010020006804000086360000280000003000" + "000060000000010020000000000000240000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000039393902393939113939392d3939394c393939773939" + "399b393939b2393939bd393939bb393939ae393939943939396c39393943" + "393939263939390c39393901000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000003939390e3939393a39393977393939bc3939" + "39e1393939f2393939fa393939fd393939ff393939ff393939ff393939ff" + "393939fd393939f8393939ee393939db393939ad393939673939392d3939" + "390700000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000393939123939395c393939b03939" + "39ec393939fe393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939fc393939e13939399b39393947393939080000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000393939063939394c3939" + "39b7393939f6393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ed" + "393939a03939393439393902000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000003939" + "39183939398d393939ec393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939dd3939396c3939390a0000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "00000000000039393933393939b4393939fa393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939f2393939933939391d00000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000039393941393939cf393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939fd393939b339393926" + "000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000039393947393939d9" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939fe393939be393939290000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "39393939393939d6393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff38383bff3838" + "3bff393939ff393938ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393938ff393938ff393939ff393939ff393939ff393939b73939" + "391e00000000000000000000000000000000000000000000000000000000" + "000000000000000039393921393939bb393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3737" + "3fff33334dff2e2f5aff2e2e5bff313152ff363643ff393939ff393939ff" + "393939ff393939ff393939ff39393aff323357ff373744ff393938ff3939" + "39ff393939ff393939fe3939399239393909000000000000000000000000" + "000000000000000000000000000000003939390b3939399b393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff363643ff32324fff353545ff37373fff37373eff353546ff" + "2e2f5aff353545ff393938ff393939ff393939ff393939ff38383eff2326" + "91ff252889ff373742ff393938ff393939ff393939ff393939f33939396c" + "393939020000000000000000000000000000000000000000000000003939" + "395c393939f2393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff37373fff32334dff38383cff393938ff" + "393939ff393939ff393938ff353644ff313153ff39393aff393939ff3939" + "39ff393938ff363646ff1f23a0ff171dbcff262985ff373742ff393938ff" + "393939ff393939ff393939dd393939340000000000000000000000000000" + "0000000000003939391f393939c8393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff313250ff" + "363643ff393938ff393939ff393939ff393939ff393939ff393939ff3131" + "51ff363641ff393939ff393939ff393938ff34354eff1c21abff171dbdff" + "181ebaff282b7eff38383dff393939ff393939ff393939ff3939399f3939" + "39080000000000000000000000000000000039393970393939fc393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff38383bff2f2f58ff37373eff393939ff393939ff393939ff3939" + "39ff393939ff393938ff33334bff353545ff393938ff393939ff393937ff" + "323356ff191fb4ff171dbcff171dbdff191fb5ff2d2f6aff393939ff3939" + "39ff393939ff393939ed3939394800000000000000000000000039393919" + "393939cb393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff38383eff313152ff38383bff3939" + "39ff393939ff393939ff393939ff393939ff393938ff353545ff343548ff" + "393938ff393939ff393937ff2f3160ff181eb8ff171dbcff171dbcff171d" + "bdff1d22a8ff34354fff393938ff393939ff393939ff3939399a39393906" + "000000000000000039393950393939f5393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393938ff343448ff28296cff2a2b65ff363642ff3334" + "4bff353547ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393938ff363642ff33334bff393938ff393939ff3a3937ff2d2f6aff181d" + "bbff171dbcff171dbcff171dbcff171dbcff252888ff38383dff393939ff" + "393939ff393939df3939392b0000000039393906393939a0393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff373740ff2628" + "71ff26286fff2e2f5aff343449ff39393aff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff363642ff33344aff393938ff3939" + "39ff393938ff292c77ff171dbdff171dbcff171dbcff171dbcff171dbdff" + "1a1fb2ff30315eff393938ff393939ff393939fc39393964000000003939" + "391f393939d3393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff363642ff38383eff39393aff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393938ff3233" + "4eff343448ff393938ff393939ff38383cff252889ff171dbeff171dbcff" + "171dbcff171dbcff171dbcff171dbeff23278eff38383eff393939ff3939" + "39ff393939ad3939390c39393951393939ef393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393938ff373740ff2f3057ff37373eff393939ff393938ff373743ff" + "1f249eff171dbdff171dbcff171dbcff171dbcff171dbcff171dbdff1a1f" + "b1ff31325bff393938ff393939ff393939db3939392639393988393939fc" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff38383bff2f3057ff32334cff393938ff" + "393939ff393938ff34354dff1b20adff171dbdff171dbcff171dbcff171d" + "bcff171dbcff171dbcff171dbdff272983ff39393aff393939ff393939ee" + "39393943393939b6393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff38383cff2e2f58ff" + "303154ff39393aff393939ff393939ff393937ff2f3060ff191eb7ff171d" + "bcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbeff1f23a0ff" + "373742ff393938ff393939f83939396c393939d9393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393938ff393938ff3939" + "38ff393939ff393939ff393939ff393939ff393939ff393939ff393938ff" + "373740ff2f2f59ff313250ff39393aff393939ff393939ff393938ff3434" + "47ff212490ff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbcff171dbdff1b20afff333451ff393938ff393939fd393939943939" + "39f0393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393938ff3e3e" + "3cff393a42ff323248ff363642ff38383cff393939ff393939ff393939ff" + "393939ff393939ff353546ff2e2f59ff34344aff393939ff393939ff3939" + "38ff39393aff313152ff22247fff191eafff171dbdff171dbcff171dbcff" + "171dbcff171dbcff171dbcff171dbcff171dbcff191eb7ff2e3065ff3939" + "37ff393939ff393939ae393939fc393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3838" + "38ff41413eff616059ff9e9c90ff9e9fafff4e4f7eff2c2d66ff33334bff" + "393939ff393938ff393939ff37373fff313250ff303154ff373740ff3939" + "38ff393938ff393939ff363642ff2b2c63ff1f2186ff1b1e95ff181db7ff" + "171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbcff2b2d72ff393937ff393939ff393939bb393939fe393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff383838ff565553ffa5a498ff707090ff9695a6ffbabad2ff" + "9b9bb2ff6d6d72ff39393cff38383bff363642ff32334eff303153ff3535" + "46ff39393aff393939ff37373fff33344aff2b2c62ff22247dff1c1f8cff" + "1c1f8dff1a1ea1ff171dbbff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbcff171dbcff171dbdff292c77ff3a3937ff393939ff" + "393939bd393939f4393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff373737ff646462ffe2e2deff" + "b8b7bcff3f4088ff1a1f90ff313373ff545469ff353553ff2e2f58ff2e2f" + "5aff32324fff343449ff33334cff2f3056ff2a2b65ff232578ff1e2186ff" + "1c1f8dff1c1f8eff1c1f8dff1c1f91ff181eb0ff171dbcff171dbcff171d" + "bcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "2a2c74ff393937ff393939ff393939b2393939e0393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff3a3a3aff858489ff9494a7ff232689ff0d1190ff14188aff1f21" + "82ff22247dff232579ff232579ff21247eff1f2284ff1e2088ff1d1f8bff" + "1c1f8eff1c1f8eff1c1f8dff1c1f8dff1c1f8dff1c1f8cff1b1e9cff171d" + "baff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbcff171dbcff181eb9ff2d2f6aff393937ff393939fe3939399b3939" + "39c0393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff37373dff2c2d71ff272982ff1115" + "8aff0d1290ff14188fff1c1f8dff1c1f8eff1c1f8eff1c1f8eff1c1f8eff" + "1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f" + "8dff1c1f8fff191eadff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbcff171dbcff171dbcff171dbcff171dbdff1a1fb2ff323357ff3939" + "38ff393939f93939397739393995393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393938ff3232" + "4fff1d248aff192292ff191c8cff1d2077ff1d2086ff1c1f8dff1c1f8dff" + "1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f" + "8dff1c1f8dff1c1f8dff1c1f8dff1a1e9cff171dbaff171dbcff171dbcff" + "171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171d" + "bdff1d22a5ff363646ff393938ff393939f23939394c39393960393939f3" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff3a3937ff2f3055ff1c238eff1b2491ff1d1f8bff2e2f59ff" + "242678ff1c1f8eff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f" + "8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f91ff181eafff" + "171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbcff171dbeff23278fff38383cff393939ff393939e1" + "3939392d39393929393939db393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff383a3bff363d41ff343848ff1b368fff" + "1541a8ff232a7cff353546ff28296cff1c1f8eff1c1f8dff1c1f8dff1c1f" + "8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff" + "1c1f8dff1a1ea3ff171dbbff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbcff171dbcff171dbcff171dbcff191eb6ff2e3065ff" + "393938ff393939ff393939bb393939113939390b393939b0393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff363c40ff" + "25506bff20587cff146097ff1075b9ff2d4861ff393838ff292b68ff1c1f" + "8eff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff" + "1c1f8dff1c1f8dff1c1f8dff1b1f9aff181db7ff171dbcff171dbcff171d" + "bcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbeff212598ff373742ff393938ff393939fe39393976000000000000" + "000039393963393939f9393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff353d42ff2e4b5eff265a7bff226288ff363f" + "44ff39383aff282a6bff1c1f8eff1c1f8dff1c1f8dff1c1f8dff1c1f8dff" + "1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1b1e97ff181db3ff171d" + "bcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbcff171dbcff171dbcff191eb7ff2d2f69ff393938ff393939ff3939" + "39ea39393937000000000000000039393925393939db393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393838ff3640" + "46ff2f4c5eff373d41ff393938ff38383dff252675ff1c1f8eff1c1f8dff" + "1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1b1f" + "94ff181db1ff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbcff171dbcff171dbcff171dbcff171dbcff171dbdff222596ff3737" + "41ff393938ff393939ff393939af3939390e000000000000000039393903" + "39393987393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393938ff3a3837ff393939ff393938ff363642ff" + "212380ff1c1f8eff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f" + "8dff1c1f8dff1b1f95ff181eafff171dbcff171dbcff171dbcff171dbcff" + "171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171d" + "bdff1b20afff31325aff393938ff393939ff393939f53939395b00000000" + "0000000000000000000000003939392f393939db393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393938ff32334eff1e2188ff1c1f8dff1c1f8dff1c1f8dff1c1f" + "8dff1c1f8dff1c1f8dff1c1f8dff1b1f9aff181db2ff171dbcff171dbcff" + "171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbcff181eb8ff2b2d74ff39393aff393939ff393939ff" + "393939b73939391200000000000000000000000000000000393939013939" + "3977393939fb393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff2a2b65ff1c1f8dff1c1f" + "8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f91ff1a1ea3ff181db7ff" + "171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbcff171dbcff171dbcff171dbbff262985ff38383fff" + "393938ff393939ff393939eb3939394c0000000000000000000000000000" + "0000000000000000000039393917393939b7393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3737" + "41ff22247cff1c1f8eff1c1f8dff1c1f8dff1c1f8dff1c1f8fff1b1e9bff" + "181eb0ff171dbbff171dbcff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbdff181eb8ff" + "262985ff373744ff393938ff393939ff393939fb3939398d393939060000" + "00000000000000000000000000000000000000000000000000003939393b" + "393939d9393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393938ff2f3057ff1d208aff1c1f8dff1c1f8dff1c1f8eff" + "1b1f98ff191eabff171db9ff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbdff181eb7ff282a7eff373840ff393938ff393939ff393939ff3939" + "39b439393918000000000000000000000000000000000000000000000000" + "00000000000000003939390139393959393939ea393939ff393939ff3939" + "39ff393939ff393939ff393939ff393938ff38383cff262771ff1c1f8fff" + "1b1f93ff1a1e9eff191eadff181db8ff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbcff171dbdff171dbdff1c21abff2c2e6fff38383fff393938ff3939" + "39ff393939ff393939d23939393400000000000000000000000000000000" + "000000000000000000000000000000000000000000000000393939053939" + "396b393939ee393939ff393939ff393939ff393939ff39393aff363646ff" + "2f3060ff21248fff1a1eaaff181db5ff171dbbff171dbdff171dbcff171d" + "bcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbcff171dbcff171dbdff171dbdff1a1fb2ff24278cff333453ff3939" + "39ff393938ff393939ff393939ff393939da393939450000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000003939390639393968393939e8393939ff393939ff" + "393939ff39393aff2e3067ff1f23a0ff181eb8ff171dbeff171dbeff171d" + "bdff171dbdff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbcff171dbdff171dbdff171dbeff181dbaff1d21a9ff262986ff3132" + "5bff38383dff393938ff393939ff393939ff393939ff393939d239393944" + "393939010000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000039393904" + "39393957393939d9393939ff393939ff393939ff39393bff34354eff2d2f" + "69ff262984ff20249dff1b20afff191fb5ff181eb9ff171dbcff171dbeff" + "171dbeff171dbdff181ebaff191eb6ff1b20afff20249eff262985ff2d2f" + "6aff34354eff39393aff393938ff393939ff393939ff393939ff393939fd" + "393939bb3939393700000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000393939023939393a393939b7393939fa3939" + "39ff393939ff393938ff393937ff39393aff373743ff333450ff303160ff" + "2c2e6dff2a2c77ff282b7cff282b7cff292c78ff2c2e70ff2f3162ff3334" + "51ff373744ff39393bff393937ff393938ff393939ff393939ff393939ff" + "393939ff393939f13939399b393939210000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "00003939391639393973393939da393939fe393939ff393939ff393939ff" + "393938ff393938ff393938ff393937ff393937ff393937ff393937ff3939" + "37ff393937ff393938ff393938ff393938ff393939ff393939ff393939ff" + "393939ff393939ff393939fc393939c6393939583939390a000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000393939023939392d39393987" + "393939db393939fa393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939f5393939cb3939396f3939391e0000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "0000000000000000393939053939392439393963393939b0393939dc3939" + "39f3393939fe393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939fc393939ee393939d4393939a0393939503939" + "391939393902000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "00003939390a393939293939396039393995393939bf393939df393939f4" + "393939fe393939fc393939f0393939d9393939b639393988393939503939" + "391f39393906000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000fffff81fffff0000ffff8001ffff0000fffc0000" + "3fff0000fff000000fff0000ffc0000007ff0000ff80000001ff0000ff00" + "000000ff0000fe000000007f0000fc000000003f0000f8000000001f0000" + "f0000000001f0000f0000000000f0000e000000000070000e00000000007" + "0000c000000000030000c000000000030000800000000003000080000000" + "000100008000000000010000000000000001000000000000000100000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000010000000000000001" + "0000800000000001000080000000000100008000000000030000c0000000" + "00030000c000000000030000c000000000070000e000000000070000f000" + "0000000f0000f0000000000f0000f8000000001f0000fc000000003f0000" + "fe000000007f0000ff00000000ff0000ff80000001ff0000ffc0000003ff" + "0000fff000000fff0000fff800003fff0000ffff0000ffff0000ffffe007" + "ffff00002800000020000000400000000100200000000000001000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000393939103939" + "394139393975393939a0393939c4393939d4393939d3393939bf39393999" + "3939396e393939383939390b000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000003939391f3939" + "3970393939be393939ec393939fd393939ff393939ff393939ff393939ff" + "393939ff393939ff393939fc393939e6393939b339393960393939160000" + "000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000003939390d3939" + "3966393939d3393939fd393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39fb393939c5393939533939390700000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000003939" + "391f3939399d393939f8393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939f1393939853939391300000000" + "000000000000000000000000000000000000000000000000000000000000" + "000039393929393939bc393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939fc" + "393939a63939391a00000000000000000000000000000000000000000000" + "00000000000039393922393939c0393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff38383cff38383dff393939ff393939ff393939ff393939ff393939ff" + "393938ff393939ff393939fe393939a83939391400000000000000000000" + "0000000000000000000039393910393939a5393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff38383bff353546ff32324eff32324fff33334bff38383cff393939ff" + "393939ff38383dff2f3162ff373741ff393938ff393939fc393939853939" + "39060000000000000000000000000000000039393972393939fa393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff39393aff353546ff363741ff39393aff39393aff343547ff" + "343448ff393939ff393938ff363745ff1f23a1ff262983ff38383fff3939" + "38ff393939f13939395400000000000000000000000039393929393939db" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393938ff363642ff343449ff393938ff393939ff" + "393939ff39393aff33334bff38393bff393938ff34354dff1b20aeff171d" + "bbff292b7aff39393bff393939ff393939c4393939170000000000000000" + "39393982393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff39393aff38383bff393939ff353545ff353644ff" + "393938ff393939ff393939ff393939ff343547ff38383dff393937ff3233" + "56ff191fb5ff171dbdff191fb4ff2f3160ff393938ff393939fb39393960" + "000000003939391d393939cd393939ff393939ff393939ff393939ff3939" + "39ff393939ff393939ff393939ff393938ff363643ff282a6bff303154ff" + "343448ff38383cff393939ff393939ff393939ff393939ff353546ff3737" + "3eff3a3a37ff2f315fff181eb9ff171dbcff171dbeff20249cff363744ff" + "393938ff393939b13939390a39393956393939f5393939ff393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff38383bff" + "32324eff343547ff38383cff393939ff393939ff393939ff393939ff3939" + "39ff33344aff38383dff3a3937ff2b2e6fff171dbcff171dbcff171dbcff" + "181dbaff2c2e6eff393938ff393939e53939393839393995393939ff3939" + "39ff393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393938ff393938ff393939ff393939ff393939ff3939" + "39ff393938ff363643ff33334cff393939ff393939ff262986ff171dbeff" + "171dbcff171dbcff171dbeff1f249eff373743ff393938fc3939396e3939" + "39cb393939ff393939ff393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff393939ff393939ff393939ff3939" + "39ff393939ff393938ff363642ff303152ff37373eff393938ff38383fff" + "20249bff171dbeff171dbcff171dbcff171dbcff191eb6ff31325aff3939" + "37ff39393999393939eb393939ff393939ff393939ff393939ff393939ff" + "393939ff393939ff393939ff383838ff373737ff36363aff38383aff3939" + "39ff393939ff393939ff393939ff353545ff313251ff37373fff393938ff" + "38383cff2a2c69ff191eb3ff171dbdff171dbcff171dbcff171dbcff171d" + "bcff2b2d72ff393938ff393939bf393939fb393939ff393939ff393939ff" + "393939ff393939ff393939ff393939ff3b3b3aff504f4cff81807eff5c5c" + "7cff333456ff38383bff393939ff37383eff33344aff33344aff38383bff" + "393939ff343547ff27286eff1b1f9bff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbeff262985ff393939ff393939d3393939fd393939ff" + "393939ff393939ff393939ff393939ff393939ff383838ff797873ff9b9a" + "a4ff8181a8ff7b7ca5ff565662ff353542ff333449ff33334cff353545ff" + "353644ff303153ff282a6aff202282ff1c1f8fff191eabff171dbdff171d" + "bcff171dbcff171dbcff171dbcff161dbeff24288bff393939ff393939d4" + "393939ef393939ff393939ff393939ff393939ff393939ff393939ff3838" + "37ff626262ff9d9daeff24288cff151986ff2b2d77ff262870ff262870ff" + "262871ff242678ff202381ff1d208aff1c1f8eff1c1f8dff1b1f97ff171d" + "b8ff171dbcff171dbcff171dbcff171dbcff171dbcff171dbeff262983ff" + "393939ff393939c3393939d2393939ff393939ff393939ff393939ff3939" + "39ff393939ff393938ff313252ff272a86ff14188aff15198cff1b1e8eff" + "1c1f8dff1c1f8eff1c1f8eff1c1f8eff1c1f8eff1c1f8dff1c1f8dff1c1f" + "8dff191ea8ff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff" + "171dbcff2c2e6fff393937ff393939a0393939a0393939ff393939ff3939" + "39ff393939ff393939ff393939ff3a3937ff2a2c63ff192394ff20237eff" + "252772ff1c1f8eff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f" + "8dff1c1f8cff1b1f97ff181db7ff171dbcff171dbcff171dbcff171dbcff" + "171dbcff171dbdff191fb4ff313357ff393937fd39393975393939603939" + "39f8393939ff393939ff393939ff393939ff393939ff334149ff26436cff" + "144ba5ff2a3767ff2d2d5dff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f" + "8dff1c1f8dff1c1f8dff1c1f90ff191eadff171dbcff171dbcff171dbcff" + "171dbcff171dbcff171dbcff171dbeff212597ff373840ff393938eb3939" + "394139393925393939d6393939ff393939ff393939ff393939ff393939ff" + "32414aff265471ff20628bff344249ff2d2e5bff1c1f8dff1c1f8dff1c1f" + "8dff1c1f8dff1c1f8dff1c1f8dff1c1f8eff1a1ea6ff171dbbff171dbcff" + "171dbcff171dbcff171dbcff171dbcff171dbcff181eb8ff2e3065ff3939" + "37ff393939bc3939390f3939390239393991393939ff393939ff393939ff" + "393939ff393939ff393938ff364047ff354047ff393938ff2a2b66ff1c1f" + "8eff1c1f8dff1c1f8dff1c1f8dff1c1f8dff1c1f8eff1a1ea2ff171dbaff" + "171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171dbeff2226" + "93ff373840ff393939fe3939396f000000000000000039393936393939e5" + "393939ff393939ff393939ff393939ff393939ff393938ff393938ff3838" + "3cff252774ff1c1f8eff1c1f8dff1c1f8dff1c1f8cff1c1f90ff1a1ea5ff" + "171dbaff171dbcff171dbcff171dbcff171dbcff171dbcff171dbcff171d" + "bdff1c21abff323354ff393938ff393939d2393939200000000000000000" + "3939390339393986393939fe393939ff393939ff393939ff393939ff3939" + "39ff393938ff343448ff1f2285ff1c1f8dff1c1f8dff1c1f8dff1b1f98ff" + "191eaeff171dbbff171dbcff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbdff1a20b0ff2e3065ff393938ff393939f83939396700000000" + "0000000000000000000000003939391a393939bc393939ff393939ff3939" + "39ff393939ff393939ff393938ff2b2d61ff1c1f8cff1c1f8dff1b1f94ff" + "191ea6ff181db7ff171dbdff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbeff1b20acff2f3063ff393939ff393939ff3939399d" + "3939390d0000000000000000000000000000000000000000393939343939" + "39d4393939ff393939ff393939ff393939ff363643ff23257fff1a1e9dff" + "191ea9ff181db6ff171dbcff171dbcff171dbcff171dbcff171dbcff171d" + "bcff171dbcff171dbeff181dbaff212599ff313258ff393939ff393939ff" + "393939be3939392000000000000000000000000000000000000000000000" + "0000000000003939393e393939d2393939ff393938ff373743ff282b7dff" + "1b20adff171dbcff161dbfff171dbeff171dbdff171dbdff171dbdff171d" + "beff171dbeff171dbdff191fb5ff20249aff2c2e6dff373743ff393938ff" + "393939ff393939be3939392a000000000000000000000000000000000000" + "00000000000000000000000000000000000039393932393939bd393939fd" + "39393aff353649ff2f3062ff282b7dff222694ff1e23a2ff1c21aaff1b20" + "adff1c21a9ff1f23a0ff23278fff2a2d74ff313259ff373740ff393938ff" + "393938ff393939fa393939a5393939210000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "3939391939393984393939e6393938ff393937ff393938ff38383dff3737" + "44ff353648ff353649ff353648ff373742ff39393bff393938ff393937ff" + "393939ff393939fe393939db393939703939391000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000393939033939393539393992393939d73939" + "39f8393939ff393938ff393938ff393938ff393938ff393938ff393939ff" + "393939ff393939f5393939ce393939823939392839393901000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000003939" + "39033939392539393960393939a0393939d1393939ef393939fd393939fb" + "393939eb393939cb39393995393939563939391c39393901000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "fff81fffffc003ffff0000fffc00003ff800001ff000000fe0000007e000" + "0007c0000003800000038000000180000001000000010000000000000000" + "000000000000000000000000000000000000000180000001800000018000" + "0003c0000003c0000007e0000007f000000ff800001ffc00003ffe0000ff" + "ff8001fffff00fff28000000100000002000000001002000000000000004" + "000000000000000000000000000000000000000000000000000000000000" + "39393904393939373939398e393939cb393939e8393939e6393939c83939" + "398739393931393939020000000000000000000000000000000000000000" + "393939103939397f393939e5393939fe393939ff393939ff393939ff3939" + "39ff393939fe393939e1393939743939390c000000000000000000000000" + "39393912393939a0393939fc393939ff393939ff393939ff393939ff3838" + "3bff38383cff393939ff393939ff393939fa393938933939390c00000000" + "3939390339393985393939fd393939ff393939ff393939ff393939ff3838" + "3dff363642ff353544ff38383dff373743ff2d2f6aff38383ffa39393874" + "38393b013939393f393939e8393939ff393939ff393939ff39393aff3738" + "3dff363643ff39393aff38383bff373740ff34354dff1b20acff2a2d73ff" + "393939df393939313939399b393939ff393939ff393939ff393939ff3737" + "3eff32334eff37373fff393939ff38383bff37373fff313357ff181eb7ff" + "1b20adff333450fe3a3a3686393939db393939ff393939ff393939ff3838" + "38ff373737ff393939ff393939ff393939ff363643ff38383dff2c2e6aff" + "171dbbff171dbdff282b7aff393937c7393939f9393939ff393939ff3939" + "39ff474746ff54545cff3b3b43ff38383aff363642ff363643ff31324fff" + "1f2399ff171dbdff161dbeff212595ff38383de6393939fa393939ff3939" + "39ff3b3b3bff70707dff525496ff353668ff2c2d5eff2b2c63ff252772ff" + "1e218cff181db5ff171dbcff161dbeff20249aff37383fe7393939df3939" + "39ff393939ff39393bff2d3274ff1c2084ff1c1f8aff1c1f8cff1c1f8dff" + "1c1f8eff1a1ea2ff171dbcff171dbcff161dbeff252889ff39393acb3939" + "39a1393939ff393939ff353d42ff244a76ff2a3565ff1d2089ff1c1f8dff" + "1c1f8cff1b1f97ff181db6ff171dbcff171dbcff181eb8ff2f3061ff3a3a" + "358d39393947393939ec393939ff393a3aff373f41ff2f3158ff1c1f8cff" + "1c1f8dff1b1f97ff181db2ff171dbcff171dbcff171dbeff23278eff3838" + "3fe43a3937383939390639393990393939ff393939ff393939ff27296dff" + "1b1e93ff1a1ea1ff181db6ff171dbdff171dbdff171dbdff212598ff3535" + "4afd3a39378038393b020000000039393917393939ae393938fe33344fff" + "1e2298ff181db4ff171dbdff171dbeff181ebaff1c21aaff272a7eff3636" + "46fd3a3937a039393911000000000000000000000000393939173939388f" + "373742ed31325aff2b2e6fff282b7aff2a2c75ff2f3062ff353649ff3939" + "39e939393784393939110000000000000000000000000000000000000000" + "39393a073a3937463a3a36a23a3a36df3a3937fb3a3937fa3a3a36dc3939" + "379b3939393f39393905000000000000000000000000f81f0000f00f0000" + "c00300008003000080010000000000000000000000000000000000000000" + "0000000000008001000080010000c0030000e0070000f81f0000" +) diff --git a/quart_imp/_cli/filelib/head_tag_generator.py b/quart_imp/_cli/filelib/head_tag_generator.py new file mode 100644 index 0000000..abaa151 --- /dev/null +++ b/quart_imp/_cli/filelib/head_tag_generator.py @@ -0,0 +1,30 @@ +def head_tag_generator(static_endpoint="static", no_js=False): + """Generate the head tag for the HTML template files.""" + + js = ( + ( + f"" + ) + if not no_js + else "" + ) + + favicon = ( + '' + ) + + return f"""\ + + + Quart-Imp + {favicon} + + {js} + + +""" diff --git a/quart_imp/_cli/filelib/main_js.py b/quart_imp/_cli/filelib/main_js.py new file mode 100644 index 0000000..536ab99 --- /dev/null +++ b/quart_imp/_cli/filelib/main_js.py @@ -0,0 +1,4 @@ +# format: main_js +main_js = """\ +console.log('This log is from the file {main_js}') +""" diff --git a/quart_imp/_cli/filelib/quart_imp_logo.py b/quart_imp/_cli/filelib/quart_imp_logo.py new file mode 100644 index 0000000..ea6f75e --- /dev/null +++ b/quart_imp/_cli/filelib/quart_imp_logo.py @@ -0,0 +1,328 @@ +quart_imp_logo = ( + "89504e470d0a1a0a0000000d494844520000012c0000012c080600000079" + "7d8e75000000017352474200aece1ce90000200049444154789ced9d799c" + "5cd575e77fd55d5dbd2fea458d76210909499600631601c21146c1303041" + "333193c4c423dbca8c27d8f39971ecfc11e3c4b19de5831dcf4c1cc6c61f" + "02820ffed8a34f300a984590a06096c6181b8424848496969084a4def7ee" + "ea5ae68f77ef7ba7ababaaababdf52b7eaf7fde71c9dbef5eaaabbeabe73" + "ce3bf7dcd0a64d9b408a9a90d05728f911615ba9e4c5c2b65cc956616b49" + "91001056b26186390c0a3da6648fb069bd5bd83a953c216cc7943c206cc7" + "954cce300762301d1d1d0080b280e741082139c3058b10620ce199879002" + "a559c91b84edfa34b68d42aff3744699491732360bfd92395c7b58c97784" + "ed15a1bf9ac6d63b87f72301420f8b10620c5cb00821c6c090b070592ff4" + "ffa0e4adc276b592e5fe4ca760d161ee75c2765d9a7171a1bfa1e4b3c2b6" + "5bc9fd2ecd8b78003d2c428831d0c30a96a54afe6761fb3d25d7f93c9762" + "477aa29b5224007c53c97785eda74a3e226ca75c9e179905f4b00821c6c0" + "058b10620c0c09bd45df106e17b62f08fd96947124786428aec3c46f08db" + "1e257f286c3f177ac283391105bf28841063a087e51e1125b70bdb97955c" + "e3ef5488cbc81bfbad2912000e0bfd7b4aee14b6a807732a49e86111428c" + "810b1621c4181812e64785929f15b67b955ce2f35c48f0c890ff0125ef15" + "b66f2bf9b0b04d7a3aa322851e1621c418e861e5cecd42bf5fc9d5414c84" + "1881f4b4b5d7f527c2f645255ff0673ac5013d2c42883170c12284180343" + "c2f42c14faff52f2ae2026428a0a9942785ec95dc2a6ebf6cef8331df3a0" + "87450831067a58537f075f52f29bc216541f74521a48cffd3625ff42d8fe" + "5ec918083d2c42883970c1228418432987843a01fa98b05d15c4440851e8" + "f4c3df099bee407bb7b01df1673a85073d2c428831949a87b55de8df57b2" + "6892ea3509ab775c7bccd9a6d6127372b50d4aaf4b3807c834c62d5b7ddc" + "b155a99f57259d5e74e54a8f24b2f7a78b9659f7c078c8b9174e087dbccc" + "6aad3e56e6d806cac35324000c842dbd3becd82e84ad2d9c236525759fd5" + "5eff6f844d3f1c7a18254649fde5092166c3058b10620cc51c12562af90f" + "c2b6238889e443ad08bd96442700004ba34ee3cac5d17100c0fcc909e735" + "f1e04b759c90d1997fb5cbef312242c733912a00c0692501e08348c4d63b" + "23d6c720160ab93c0bdfa915fa434aca0363bf28f4091429f4b00821c6c0" + "058b10620cc51612b609fd2925af096222999081890ef5d68c8fdbb63563" + "2300a6867a21247d999b29c8d077f5d8f014994a4c3d513c555963db8e54" + "5a41eac16ac7d65f5e0e0391298e0d42bf43c92e1fe7e20bf4b00821c650" + "2c1ed67225f7085be0dd409b546dd39523ceddffcad1215baf8ff1f427af" + "09ab87002b8407a6f55bfa1d7ff7ac4adabf53e394e5bd5563e5b9c7cca8" + "fb9291c42b4ade226c9dfe4dc53b8cf84b104208c0058b1062102687841b" + "85fe9c920b829808e0d44d6d1decb76d570c0f0000ca98342f48e4c38c45" + "d1b1291200b60ef60000deadaeb76dafd759fa07154ead5701a2d321af09" + "db27957cc7e7b9b80a3d2c42883198e8617d5cc9ddc2d614c444249b8706" + "0100978c8fda367a5666a313f61b47066c9bd63bab9cc2f397ea1b6dfd68" + "a553715f00c888e32525ef4c6333067a58841063e08245083186d0a64d9b" + "829e432edc24f49f2be9f69e5ad76950755817479daaf565134e55fbca09" + "2bc1db3c59b47b554b8e532a547cae719e6d2bb004fd98d06f57f2c52026" + "321b3a3a3a00d0c322841844a127dd75fb8ca784ade03d2bcda0da9fb64f" + "ec599b149d3dcfa99cfcbcf20adbb640ed936b11fbe51a550750ee292c7c" + "968e5b7b41ff483c7c3950db000078b6c179363414dcde45f9fdd1dfab9b" + "85adc3c7b9cc1a7a58841063e08245083186420c09659b8c6794ac4937d0" + "442645e7cba5aa958c64d896ceb8b232ebcfd4907442c226d5d153da1832" + "160ef26fb141d56e5d32eefcbd9f6d6c01e06cb00610c45f4f7faf9e15b6" + "cd4aeef7792e39410f8b10620c85e461b52bf98cb035a61b6832c7c41d75" + "d5b0d56a66a65eecba3b7abff0cefa61256dcb4547c026751457b3b857d7" + "24b31fcb45fca34afc9db7f59e07006c1873dad93c3eaf150030ec7f3b1b" + "f93dd3df3f79a8f0391fe792157a58841063e08245083186a043c24aa1ff" + "b3928b8398885f24445877a4deaacfb9a2bf37efebc585dea34e58ee11b6" + "6a714f6a53c1651313f505c32ad109f51e7574dbae9676db76225239ed35" + "1ea3bf7fb2b9806e3810f8960c7a5884106308dac3ba5fe80575ba8d1f74" + "aa0af84b079df625d58978a6e17931263cba532a517f5624ea5b5452be55" + "24e7c3f4ba02a14e25e5b75f386bdb5e68b212f1afd4d5a77d8d87c8efa3" + "fe9e067e10313d2c42883170c1228418431021e176a17f3e80f72f18e22a" + "5c3b2edcfdf5a227bc57c8aaaff32a51df1572ee5d3a4c9ccf30311064a7" + "da5bfaadb3509b6293b6ed99a666004e7d9e0fe8efe92bc2b6d3bfb777a0" + "87450831062e58841063f03324d4470f7ddfc7f73482e362bbceda217534" + "58d2df104c86173a3cec1161a20e0fdb4498c84336fce39a612755d0a80e" + "c7d835afc5b6c94df51ef20f429747881df1e3cd017a58841083f0dac3aa" + "10fa634ad6a51b58ca4c88cdaea7abacdaac74ad67fc467a5de794b7d52b" + "eee40b95b7d5c80dd6be72e9a875a4dcdde2f7fe58739bad7be86dd50afd" + "c742d79d8127e131f4b00821c6c0058b10620c5e87845f11fa551947119b" + "932a015f0821613aa2a2136a67c8daea532fee7b8bd476ec4a9f1f1a9422" + "2bc4c6e9bbc5fef947557818f73611ff31a1ebeff9df78f986003d2c4288" + "4178e561e912863ff7e8fa45cb854aab9dc868b9f3a7a999a12369d00cc9" + "9639ea23b540a4ec5b93ee6ee826d391ded67feab3fc909f34b7da368ffd" + "5dfd3d7f5cd83c2975a087450831062e58841063f02a24d4fd73aa3cba7e" + "d1a25df753a2fafdd2a181f4830b101d089e1155f2832251bf4425e52b98" + "94f78cb5aa4eebe6b05306f94283a7e7b9e8eff9ff15b69bd30d9c2bf4b0" + "0821c6e0a68775a7d03d595d4b893355d5b66e9287950e99943fac3e724b" + "4537fa0656ca7bc2e641a7d6e14285e56dedabf6f44ce24f085daf07bbd3" + "0dcc177a58841063e082450831063742429dd9bbcf856b11455f859330d5" + "3559855e8f950b3a103ca1aae401a05d25e52f62bd96abc823dceeecbb00" + "00e80a2fb26d672b2aa6bdc645bea3e4d3c236e7cdd1f4b00821c6e08687" + "f539252f71e15a240d172aada7c6cb478767186926baaffcb8287f58aabc" + "2d36097487b06afaf7a9de2edbf683f90b0000516ff61cae52f273c2f6c0" + "5c2f4a0f8b10620c5cb00821c6906f485829f4afb9311192992eb521ba58" + "4342cd80084d8ea953aa2f1689781e3536775a27c76dfd93037d008027d5" + "b1611e71afd01f51723cddc05ca087450831867c3daccf087d891b132199" + "e9a988043d05df1955ded65138e50f2bb90fd155ae5227f11c11bb2ade13" + "ba4b2c16ba5e377e94efc5e86111428c810b1621c418661b12ea05ee2b59" + "471157190e5b7fa66899131e4512a551153e2113f149ebffbf32e4fcdf19" + "1ece9d3bfa7b6cfd78bb5509ef516dd657957c50d866b5f39d1e1621c418" + "66eb61dda1e4eaaca388270c88866c6dd1d2f0b024dadbd29e1600ac5289" + "78963ce44f432c6aeb5b06ad56467b1a9bbc782b5dfd7ebbb03d399b0bd0" + "c322841803172c428831cc3624fc2f9eccc230aa2bacf063fb15e76ddbf5" + "4b4e00004e0f39472b3d7e602100e057676be10643153224ccbb58d87826" + "d254c4af1215f1e50c0ff3e6ba21abfafdedda3adb763eecfad10f9f173a" + "4342424871c2058b10620cb9f87acb84fe49af2662121f5b300200d87374" + "9e6ddbb5df0a056fbfb4cfb67d75f33b00809f1d721eaafef49d96bcdf77" + "acbc7ce64125c6b80a0f3bc5169e15223c0c313c9c15baffd8d641e773fc" + "58739bdb6f73abd0f5c5bbd20d4c851e1621c41872f1b0e446672e70005e" + "3e5597f1678fbce5dc8de2eae6fe1fd71eb16d1786d6dbfa8b271a66f5be" + "1365fcf567625824e2cf888fe962f689cf8b35a343b6beb4cefa9c9e8a54" + "661a3e5b6433f9df53f2fbb9bc90df0042883170c1228418432e21e1ef7b" + "3e8b02a3b5dad9aa70f7fa830080f5ad4edd53a3f28c07269cd7ecefb2fa" + "083d72c009f9f61eb3b637fcce1a67dc5d1b4ed8fa8b272e9bd5bc62de6c" + "482d3a7a42ce7d5807316d0c0df3e6e641ab6fd643aded5e5cfe0f956448" + "4808292eb279581b945cebc7440a89cf6d7ccbd6f777590d557f76c42961" + "a8afb00e34fd9dd5076cdb0d8bc700003515bfb66d4f1ebb7cdab593c9fc" + "bd243ea09f3d1f2a6fab56fcf66a92b3ea6852f25c3c6e95f12c9c74ce41" + "75f110d6ab9494ebcca14c83e96111428c810b1621c418b2858477fa368b" + "02e370cf225b7ff6f8c28ce3de7fe31a5bbfff13bf040044ce383fffc385" + "9dd35e73faa053c3b566c43ab6eb7db1d1345bb0126677cd59a37f632745" + "227eb5fa3d7293f4ecd8343c68eb8fcfcb7fc746067e57e8dfca34881e16" + "21c418b27958b766f95951f3dc89cc5e956432eeacf77b9f5d0000889e77" + "f6b4adbfd64ac4779d768e4e3adae1242b37c4adfd5a2d134e7d4447b375" + "e74a77efa787953f51380f3b4e87acbfd1b2642ca8e918c98631a7fa7d4f" + "a3f3106ad89d1d18b7099d1e1621c47cb86011428c2135246c16fad57e4e" + "a4909888e7b68e5f3c3a62ebd1fee9ad5f0ebe5e3d456662e1f8a8b86615" + "00e078cdf42ea53571566bbb41bfda31d024eed78daccd9a91f284f33bba" + "4c7cf65fadab77e3f257095daf43bda983e86111428c21d5c3ba51e8ec16" + "370317abb2042fae99cec36a8a46a7d948fe9c09391ff13a963acc8a8da3" + "ce67df250f4bae377a1dda9d3a881e1621c418b86011428c213524bc2e90" + "59184a636c72e6412e5c5357103579f07ea58cfc6dea4dd2ec509a1b0ba3" + "63b6de16b3ead9badc3b0e4caf430c090921e692ba245e1fc82c0c453e08" + "4fb7f2dff5df4e030076fd6071ced78c85a65fa94525db2b12bcfb7b856e" + "fad722feaad5dc599013978cab1d1dee24df812ceb103d2c42883170c122" + "841843185317add935192f717a2b9c638fe647c7a7fd3ce750505410978d" + "59eef5eade1edb56cb03547de38c28075a056e8ece855513d667f635f742" + "42bd0ec9b529916a20849082860b1621c418c20056887f4fdf0f423272a6" + "a6c6d6d38584e990cf9dca541fac642462db226a13ee47cf9eb46df12aeb" + "7d9269b6eb1077191147a90da8fb3937466767d984b5795f262ee6f83c5b" + "7fd0e5da7414a0874508318830808f043d09533959ed78586b0707000055" + "696aa59200cac79dcae0a4aa582f8f5a1e5622e6b49f49282faa4cb49c29" + "577a3ce224f993ee5515930c9cd31e56d64efb24a21e1acd17c7807de8ce" + "3160726d3a0ae476f233c98332b140a5120a57d88b1621247718127a40b6" + "c58a10923f6100ab829e84a9c44482f640432300e0ea7367320db7e94708" + "0847d0ac42c2504cf4b90ad5657815109a7012fbc970e671c41dc6d5df77" + "809d49736241ccf59070dada440fcb45665aac3e686db7162b42485e8401" + "2c0f7a12c540674ded8c4df0bb375e8eeeb77e6dffbb79541d9b54261e08" + "2732dfc1a77862c437ce4b0f8b09f88cb4cb8eb8e281d41c589e6aa087e5" + "22bb56accefab3f73b4ff8381b428a0f3e2574996c8b1600acbd69350ebd" + "7804bfffd98fe1c8b73bfd991421454218c0d2a02751ecfcf99d2b503fcf" + "392977f3e5cb30d4d787d5f7fe2e0edff71412621375683273b94388c77c" + "05c29878b832a482927a26dfa7d11c777db3f8b4b58921a1c7a42e560030" + "316a15828e9def0f624a84184b18c0fca027612a2175e74d66e94cf9cddd" + "c7f19dcf5e39c516557b08abdb9b3059df8832f1faf221ab623e51e3942d" + "247485bbb8d34f2a7d4224ecc7caad087f54b4a319128f97c7943d2eaea3" + "df3b2ce650a912ff95e20140a5eadb5d256daaaabf5ad88abd2b6a97f6b0" + "987c9f4683fbc5d0eda906e6b07c60187508df7aaffd6f59a172c51f00fb" + "6eb8d4ff491162205cb0e64836ef0a00beb5eb85d95fb32232f320424a90" + "308096a027612a332d569a6383c358d9508781e824bac7ad70b0b5aa128d" + "910abc76cb55a8971ba35316abb2d161948d8d0000fa6a9c8e8eaf2d590e" + "606a8856af5cf24691b86f1f77aae39b26adf71e1049fe0fabaa000067ab" + "9c0dd84373d8585da17e27753127012b8f2ed3f679a266a759b5e6a930e0" + "d087211d8a8b02e04a03e6ed07f5eea7039a530d4cba7bccd7efda8a7a95" + "476a8c546065431d5636d4a13162d9ea7b2e647dbdcc651152ea84916615" + "23ee32343989f9d595536cfb3fb62ce7d727aaad96331fd63a1e96f68286" + "c4b8aec854efacaaaa0ae3c2c32a579e40abf06ee6abfd89d7f774d9b6c9" + "32eb3e7652340c3ca52a97a365d9ef71fa61409f48f6f7cdb0af4cfb2af5" + "c22b6b51fb2cdbd4030a39d7742d7cfca657dceb17ccb55d5d91e0c10397" + "696b1373583eb0b2c1f19266b350a572e9f000deab6bcc79bc5cac082906" + "b860f9c05c1629428843185cb43ce3aee3475cbfe6a5c303336effc984ae" + "bf3a5fe984a75adfdfe0786efaa4e9a5634ed7d3ad17ce0100ce8a4dadef" + "d75a9ee3f01cbb9fea94f5a0b88ed64f88b054878e2d22a45da41e582c19" + "1db16d7e848cbde284ee8b44d57b08a59b80afc8b2713fdf4ba61a9874f7" + "082f162b424a9d308086a02751ead4bc781000307ad3fa9cc6df75fc48de" + "5e562ef4a8e47d8f48e26b0f6cb9f064aeefed06000c849d1be1bb6adca0" + "073de7b5efd22de6a575e9215e24ca4496ab6d500b448f7c373a92c95d73" + "4362e7404309973894bbef5d4e3b9995e1a047ec5ab13aad97b5e1cd9369" + "465b1c9be5c24548a9c105cb436a5e3c38e509e14cd863df3c896383c300" + "322f5e5e7b59841422a14d9b3695ae0feb11db1f7c62ca42d55e33f5be70" + "7e34f7361cc70687b37a5c412e5a3a105a2292f36b8606010003a262ff80" + "08d7e4c66c3fa911ad79568f5837838b479c2ab6f23984724de2b5cb92ae" + "b758318698a8d1fbcb85cb5dbd764747470860d2dd53da6bc2d316abd9b2" + "b2a1cece71a583c97d524ad0c372993ffbc973a82e2fcfb850cde45d551f" + "fc276c7c77279213499c59f70738f3d14f03b03cad9d3bb6655ca00a253c" + "d45e974cce5f32ec7832a75599c2e15ac70395ed6efca45a785deb9467b8" + "7c74d8b6e53a2b79d75f9f88295be97dad46cb9dcffcdf2c70b72f283d2c" + "0fd8fee0131917abf3a3b1ac8b5538368c6b9fde8acb3a1f40623081e444" + "120bdffa31aebcffb629e3322d4cf4b44829c005cb25b63ff844c69fe592" + "b3ba7aefed0080e4e8d43d876555c047bf772b5636d4d9ef916dd1e2c245" + "8a99d0a64d9b06c4bf599395273ad19ec9bbcac6752f6e46321a4162c86a" + "f192189c5a311cef4e201903defada73387ae62cdedcb10d831515d874f6" + "838cd72c941011989ad0fe880abdda45add4be46ab85b4acc00f8a26d19a" + "e7cafe5e00c0bcc9dc8f575b9eb4c2cc523c70b547b42dfadfed8bdcb8a4" + "9d4be8e8e86800e861b94665797ebfcaeb5fb976cabf53172b4d52352d08" + "d537607d97b54da663e1928cd7a5a7458a112e582eb0fdc127b0b8d6da63" + "b77e62a6e3541d6ee8b81a884f7dcc5fd630fd4f52deead8c229ed5d4e36" + "b5e064137b3092d2208ca9bb0cc81cd812d98a246ab12eb605ef86f7661d" + "bbf9d797230955ab142f47323ef3bd63e3976e01bebf07873ef3c758fbb3" + "c70000af37b702000e35347ab21dc60de453c07daa266bbee8707a655f0f" + "00a0abb2cab6bdddd8040088f9fc04b15ff4eedadb669d81b05685b10070" + "a93a2424d3ac06d54f726f02543cf487b3f73dcb8369a75ad0c3f28875b1" + "2d199f0c6e7e7b2392f1340594e1cc798fe484131646b6dfe3d63409318a" + "30805ef16f761fcd83950d75d812d93acdbe25b215e87c0f482481441992" + "d10a24a215b667958c974df3ac92f1dcea779e9f7fd1dc271e2017c406e6" + "7f559ecc95fd7db6ed66d5cee6cd794eb8db1df1f7700e7dfb3858ef3c8b" + "ea5647ae5da3bc42008888763603aaedcce2126c39d35deeba87df9b6aa0" + "87e502c70687b1379ae1749ce5d6115ec978797aaf4afd2c3597954a64b5" + "f3a79acdfe44428a092e587e132f07e265e917a85859d68c62921d8f4989" + "1306d023febd2aa889142dc70f01004215934846539292693cabe4c4cce1" + "c3f7befc47ae4daf10d0075b74343be19fdea0bc599c2af46ebd95ca3e5c" + "37ad4d926fe85ab1175b9d03d36f100778d4c5ad3bcea87858505b223db2" + "fadc4fba3324f49d156b6dd5ca594df7ac742e2b530d16008cffd2c9932c" + "bde21a77e74888218401643f188fccc8ce1ddbf0ad5d2f606ff485b4c977" + "00488e3b55c0c90c650ca1da10922399efc6071edd836383c33877f8c0dc" + "275de01c519ba3fb459b9a6b5587537950ecaf9bac2a79bf3750cb3ef6ba" + "fc01003ede6d7d9d86279d9b8fd395beb8393fc3716ef95c32d5400fcb25" + "466299934f72b19a62cfe071cd44292c5884a4830b964b9c1bcd2123ae13" + "eee9c892709f783b8efd3fda0300d8f3777f91e70c09319f308053414fa2" + "18d8b9639bd54da1e105dcd4b502c9452bd38e0b452611c22412f17284aa" + "27108f5a214fa86602a11a207e6eaa5b3d793281f7fef2510056f9c48787" + "de4128a0fe514120ebb574a25b1f7e010037765b09ef575b5a6ddb4ca753" + "bbcd8478bf975bda0000cd6abf2700b4c78afb64e884aaee3f1d717df3fa" + "b4b5a930f77218ccb1c161a0ed38b6742e059249201e025081c45855daf1" + "d9b6e4c4ce2530b46823a2cded188bc7b173c7368f664d8819840174063d" + "8962c1f6b200ec5d780a5bce38dd1412c39953afe1666b7f5aacd77a6c9f" + "1877ca1b8efdd7fbd03d3e81fff399dbbd9ab631e844f75e5152b0599514" + "dc284a0b7ea1bc1cc07f6f6b4cf5acdfd3e2cc71edf9d3008ab70be98588" + "75331e77dff3ef4c353087e5323b776cb34fbcd9bbc8e95755d6389876bc" + "5eacb45eb9ae1b88591fecb7bef61c0060203a6d0f2821250943420fd8b9" + "631b565eb705dbffc79f61ef522b0cbf71ff7a84aac791cc101a4a3ab77e" + "193debb6e2d8e0307efca54f63529c4a43482913067034e8491423c75edb" + "8bbf7def003e75df8f2cc306e7e49beb5fb916a1f2e945a2af6c7ac379bd" + "3a74824c4786793afc932161baf0d0efd0b04fd42475a94e9ced93c5b9b7" + "aab3b27ae641f9316d6da287e52123bd5df6a2a3735b2b1beaf0ea0daf67" + "7ccdffbc6d13c6c6c6d0d6d696710c21a54a1800ab107d402f5ceb6efe77" + "e839d589f3470eda8bd8ce1ddb90482450565686baba3ad4d5b11b43ae4c" + "2acf4926daa587757d8f5502f152abf3f384cf6521c7aaac6eb4c5ea61bd" + "5735739a234fa6ad4df4b07ce6dd7f79dad665c857e673c8428889f05b42" + "0831863080e3e2df23422f953d9ba40898141eea2b223cfc2db519f9f241" + "a77ce437aa5fbc5f9cf1b953aa1f8c89eea29d95ae87847a1d3a9efa037a" + "5884106308c3695d0d00fb847e9dcf7321c415e4debed7d4a9429f107bfb" + "cea9267c67ab3c7b1c3f850fdcdf63173887ab9c00cc839d927a1d9a56fb" + "430f8b10620c5cb00821c6905ad6f09ad0191212e319521ba6df6e724eb0" + "bb7ca01f007041248bbd3cb0b54f6d881e1189eadab8d9e717ffa6d6d35a" + "c1d732fd801e1621c418b86011428c2135247c55e85ff173228478496775" + "8dad2f1eb5ba5f5ca28e120380433e1c1d7636e284a0978c0d6719599874" + "5738f3eff4f6c92743424288f9a47a58bf10ba2eaf98fdb12e841430fb54" + "a5bb3e920b708e15f3f2b8b00b61a7e2fd12cfdec53bdeac75bc500f7aa7" + "ca72ae97320da287450831062e58841063480d09e559f6bf52f25a9fe642" + "882fe8daac1e91385e3061f5aa3aede1769d6ef74f46f68571553ff6a6b7" + "b5576f0abd37d3207a58841063c8d6c0ef1925e96119c282898969b60f2b" + "8b6fe3ad5b7c50e3943ab48f7bef615d089bd92fb3a3d63a7e6ec2db4ead" + "4fcf3c841e1621c420b86011428c219b8fba5bc96ffa3111921fe549a722" + "e6b2fee9b9ca0bf32f9a36b62ae1b4191a551b73bddcfc5ba85c1049f74b" + "87d21f74ebeafb85cd49ba4f9439e597affbb00b00c0e3b90ca287450831" + "866c1ed67e250f09db5a0fe742f2608dd80f5797a665c96f5f703a6dea96" + "2693c29b8aaa3be9b0b8fb1f68b092ac7d863e86cf1579b86a3831fd605b" + "b71917ef37a44a05ea0bb4cdcc2feae7d9faa8b7273ae972867773194c0f" + "8b10620c5cb00821c6904b61c84f84ce047c81501bb7f68aae191a98619c" + "1372ec6bb4dcfca3a26239698f73f69e6e541d394f8b3aa50f7c3ab02128" + "c2490fb6f366a1b7c24af8175a4838a0366877f893680780476733981e16" + "21c41872f1b0e40af80d25b9d005cce5fd7d00a69635ccc47c55cdfd7e9a" + "3d6123e5e23176730b0060536f8f6dd349e2fe224ac4cbdf9ddf1fe83ef5" + "fb5ce6f3fbcec4f38d56effb496fcb5c2685fed3d9bc900b0f21c418b860" + "11428c219790f0a4d0f72879ab077321b3e0d59656cfaead0325dd991300" + "aeeab3aae8ffadb5cdb3f7f51b59f13fe66dadd134faca0b6723f4fbd54e" + "8ae01dd1fbde439e157ad76c5e480f8b10620cb35de61f50921e56092013" + "f13a09db32e9e44b7b0c4fc057080f6bd8e77d7e85b08b4057fa3fd9d4e2" + "f75b3f94ef0be96111428c810b1621c418661b123ea5e4fbc266e2894564" + "9674abcea5ede363b6cdf4903022eab0867cfebff495077f7ade734dd603" + "947effe67254c9a7b28eca023d2c42883170c1228418c36c4342fd58e53b" + "c2f62397e6420a983115362c50db7b8a81ba98f3c473d8e70322fa03aac3" + "3a226aaedeaca9f5fbedbfab64decdc7e86111428c21df655e6e88feba92" + "4be6381752c0e8cea495a20d8de9d4c59cd62e677d6e9f33a43cd6249c4d" + "c62178d7e26650b58df9d93c6787844f0d754e0bfd91b95e8c1e1621c418" + "b86011428c21df90501e31fc574afe708e732105cca04a4a37c426671869" + "0e4dd1a8adbfe75f874d00800eac87450d94dbdd47e322dcdcd56cd55c8d" + "f8bcc91bc0b7853ee72736f4b00821c6e0c6b355bd91f14f848dd5ef4586" + "3e7075543c8e6f541ba1070cab78d79d462b92ced3f5a8ff9e070060b0dc" + "f9ddb9ed613dd33cdfd64f8a43637d4257b5e7bdd1391df4b00821c6c005" + "8b10620c6e84843a0bfba7c2f6840bd725054867ad531dbd7c6c1400b0af" + "a231a8e9e4c582092bf7db5f11097826c0a0e8c3b5283a966564eebc5e6f" + "1d24f186ff95ec92af2ae9ea531a7a58841063707343d36ea1ff8b9237bb" + "787d5200748abbf66f755d00004412c127af67c3b2911100c0719f4b19d2" + "e1d69ec2c335ceffe559d18bdf67fe55e8bb338e9a0385ffe92248ab5bca" + "000005d049444154841005172c42883178d5e3e21e25f7095b9547ef457c" + "44f605d9af428fcb06fa6ddbafe635fb3ca3dc6812876734aa6afd7395be" + "d7264d632edd3ecf469c0ddbff4f6c6acebb774bfee80af63ff6fa8de861" + "11428cc12b0feb8892df14b6bff6e8bd48407445acb280a649a73c60d5c8" + "3000e0686d5ddad7f88dde4d77657faf6d3ba692ed3eb557c9ca401e8d03" + "7b2a2ccff0d1d676dba68f610b08fd3d3f9275940bd0c322841803172c42" + "883178dd58fabb42dfa6e4551ebf27f199f745f8b76e681000b06678c8b6" + "1d0eb0dee90af54040064cef075b013e855c93ee7d6127ec7ea8f5220081" + "b48a91bc29f4ef661ce532f4b00821c6e0b58725f711ddade46f84ad706e" + "75c415dead6f00005c3c3a62dbaeefe9b6f5830dd6bec37e975bd2c86afb" + "2bfbfb6cbd69d26ad2f76fad4eab9544b009ea29f484b3ff1eb467f550db" + "02db3618dc21ac2342ffb4d07debea480f8b10620c5cb00821c6e0e7698e" + "ba46e38bc2f6b08fef4f7ce484486cf7886e9797ab704d0635e7aaac4d10" + "7da2ddcb880a7b62227c0bab4ea1b5e2a8b18b54ab9865234e92ff7ca553" + "01fe629b55ab3451a09bb2c7c5ff4f1fc5150d39737d58d55a0518064abe" + "2474cf6baed251987f454208494310e765ef14fa0d42ffbccff3203e3128" + "aab97fd16a9dde324feced5baa1a01ae1f74f624d6abfd7edaab029cc35c" + "65d3bb6eb51f706fdb4569dfcf24f6a91631af8a3290804b1734ffa864e0" + "115141fc3608212417b86011428c2168dff91ea17f44c96b829808f1973e" + "5187d567f78437ab37bcdb3cdf5050ffff5f0afd9e8ca37c861e1621c418" + "b86011428c21e8907042e8772af92b615bece35c0821c06925b709db44ba" + "8141400f8b10620c417b5892734ade266c2f2b5950d948428a8c01a1ebef" + "df87414c6426e86111428c810b1621c4180a2924d4ec17fa1d4a3e276c35" + "3ece8590626654c93b846d7fba8185023d2c42883114a28725d149f73b85" + "ed492579302b21b3674ce8ba74e1e574030b117a58841063e08245083186" + "420f09352f08fd934aee16b6261fe7428889e8666332bdf2521013990bf4" + "b00821c6608a8725d177858f0b9b2e7b580042884656abebc8e49d2026e2" + "16f4b00821c6c0058b10620c2686841ae9da5ea7e41e615bede35c082924" + "f4115cb7085b6700f3701d7a5884106330d9c392742a298f0dfbb99257fb" + "3b154202e10da1ff7b25cf0731112fa187450831062e5884106328969050" + "d325f41b95bc5fd878ba342926fe51e85f14fab8df13f10b7a5884106328" + "360f4ba24ffad8216caf09fdef95acf5673a84cc8911a1ff77251f0a6222" + "41420f8b10620c5cb00821c650cc21613aa40bfd8a928f09db553ece8590" + "5cd0070bdf2d6c47d20d2c05e86111428c810b1621c4184a2d249468b7fa" + "3a61fb5325bf2e6c3cec82f885ae9ffa96b07d47c9499fe75290d0c32284" + "1843297b589a98d0ff5ac97f12b61f2ab9c59fe9901263afd0bfa064c926" + "d567821e1621c418b86011428c8121617aa44b7e9392db84ed3e2557f933" + "1d52241c15ba7ec0f344101331157a58841063a087953bf24ef8b4929f15" + "b67b955ceccf744881735ae87fa5a4dc6911f5712e45033d2c42883170c1" + "2284180343c2fcd0eefc03c2b653c9cf09db97956472beb891c9f4ef29f9" + "b0b0156d0750bfa187450831067a58eea13b9cfe40d8b4077687b07d41e8" + "bfad246f1c854d42e8cf2b29bdeb27338c252ec32f0a21c418b86011428c" + "8121a1b7e8f0e09f854dea4b959489fa4f29b9ceab4991ac1c12fa2e2565" + "fdd4291fe74252a087450831067a58c1a2efd6df1036ad6f10b63b95bc55" + "d8ae56b2dcf559152771a1bfa1e4b3c2b65bc9fdfe4c87e4033d2c428831" + "70c12284180343c2c2657f1a5df6fa6e56f20661bb51c94dc27699d08bf1" + "946b7d22f23e61eb10facb2912007a3d9d11f10c7a58841063a087652eda" + "4b9055d64fa619276f4a2b94dc286c17a7480058ae64abb0b5a448c0f9fc" + "d4679b288021a1eb1efa3dc2a6f56e613ba9e471613ba1e43bc2a67fce0a" + "f312801e1621c418b8601142082184104248c9f2ff01ac2d3c02891e3dae" + "0000000049454e44ae426082" +) diff --git a/quart_imp/_cli/filelib/water_css.py b/quart_imp/_cli/filelib/water_css.py new file mode 100644 index 0000000..bf28193 --- /dev/null +++ b/quart_imp/_cli/filelib/water_css.py @@ -0,0 +1,902 @@ +water_css = """\ +/** + * Forced dark theme version + */ + +:root { + --background-body: #202b38; + --background: #161f27; + --background-alt: #1a242f; + --selection: #1c76c5; + --text-main: #dbdbdb; + --text-bright: #fff; + --text-muted: #a9b1ba; + --links: #41adff; + --focus: #0096bfab; + --border: #526980; + --code: #ffbe85; + --animation-duration: 0.1s; + --button-base: #0c151c; + --button-hover: #040a0f; + --scrollbar-thumb: var(--button-hover); + --scrollbar-thumb-hover: rgb(0, 0, 0); + --form-placeholder: #a9a9a9; + --form-text: #fff; + --variable: #d941e2; + --highlight: #efdb43; + --select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml \ + version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1'\ + xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3\ + .org/1999/xlink' height='62.5' width='116.9' fill='%23efef\ + ef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 \ + L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,\ + 7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5\ + 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.\ + 3,1.6Z'/%3E %3C/svg%3E"); +} + +html { + scrollbar-color: #040a0f #202b38; + scrollbar-color: var(--scrollbar-thumb) var(--background-body); + scrollbar-width: thin; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', \ + 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', \ + 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', \ + 'Noto Color Emoji', sans-serif; + line-height: 1.4; + max-width: 800px; + margin: 20px auto; + padding: 0 10px; + word-wrap: break-word; + color: #dbdbdb; + color: var(--text-main); + background: #202b38; + background: var(--background-body); + text-rendering: optimizeLegibility; +} + +button { + transition: background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +input { + transition: background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +textarea { + transition: background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +h1 { + font-size: 2.2em; + margin-top: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-bottom: 12px; + margin-top: 24px; +} + +h1 { + color: #fff; + color: var(--text-bright); +} + +h2 { + color: #fff; + color: var(--text-bright); +} + +h3 { + color: #fff; + color: var(--text-bright); +} + +h4 { + color: #fff; + color: var(--text-bright); +} + +h5 { + color: #fff; + color: var(--text-bright); +} + +h6 { + color: #fff; + color: var(--text-bright); +} + +strong { + color: #fff; + color: var(--text-bright); +} + +h1, +h2, +h3, +h4, +h5, +h6, +b, +strong, +th { + font-weight: 600; +} + +q::before { + content: none; +} + +q::after { + content: none; +} + +blockquote { + border-left: 4px solid #0096bfab; + border-left: 4px solid var(--focus); + margin: 1.5em 0; + padding: 0.5em 1em; + font-style: italic; +} + +q { + border-left: 4px solid #0096bfab; + border-left: 4px solid var(--focus); + margin: 1.5em 0; + padding: 0.5em 1em; + font-style: italic; +} + +blockquote > footer { + font-style: normal; + border: 0; +} + +blockquote cite { + font-style: normal; +} + +address { + font-style: normal; +} + +a[href^='mailto\\:']::before { + content: '📧 '; +} + +a[href^='tel\\:']::before { + content: '📞 '; +} + +a[href^='sms\\:']::before { + content: '💬 '; +} + +mark { + background-color: #efdb43; + background-color: var(--highlight); + border-radius: 2px; + padding: 0 2px 0 2px; + color: #000; +} + +a > code, +a > strong { + color: inherit; +} + +button, +select, +input[type='submit'], +input[type='reset'], +input[type='button'], +input[type='checkbox'], +input[type='range'], +input[type='radio'] { + cursor: pointer; +} + +input, +select { + display: block; +} + +[type='checkbox'], +[type='radio'] { + display: initial; +} + +input { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +button { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +textarea { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +select { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +button { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='submit'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='reset'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='button'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +button:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='submit']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='reset']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='button']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='color'] { + min-height: 2rem; + padding: 8px; + cursor: pointer; +} + +input[type='checkbox'], +input[type='radio'] { + height: 1em; + width: 1em; +} + +input[type='radio'] { + border-radius: 100%; +} + +input { + vertical-align: top; +} + +label { + vertical-align: middle; + margin-bottom: 4px; + display: inline-block; +} + +input:not([type='checkbox']):not([type='radio']), +input[type='range'], +select, +button, +textarea { + -webkit-appearance: none; +} + +textarea { + display: block; + margin-right: 0; + box-sizing: border-box; + resize: vertical; +} + +textarea:not([cols]) { + width: 100%; +} + +textarea:not([rows]) { + min-height: 40px; + height: 140px; +} + +select { + background: #161f27 url("data:image/svg+xml;charset=utf-8,%\ + 3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version=\ + '1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w\ + 3.org/1999/xlink' height='62.5' width='116.9' fill='%23efef\ + ef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L5\ + 8.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.\ + 4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60\ + .5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,\ + 1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat; + background: var(--background) var(--select-arrow) calc(100% - \ + 12px) 50% / 12px no-repeat; + padding-right: 35px; +} + +select::-ms-expand { + display: none; +} + +select[multiple] { + padding-right: 10px; + background-image: none; + overflow-y: auto; +} + +input:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +select:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +button:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +textarea:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +input[type='checkbox']:active, +input[type='radio']:active, +input[type='submit']:active, +input[type='reset']:active, +input[type='button']:active, +input[type='range']:active, +button:active { + transform: translateY(2px); +} + +input:disabled, +select:disabled, +button:disabled, +textarea:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +::-moz-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +:-ms-input-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +::-ms-input-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +::placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +fieldset { + border: 1px #0096bfab solid; + border: 1px var(--focus) solid; + border-radius: 6px; + margin: 0; + margin-bottom: 12px; + padding: 10px; +} + +legend { + font-size: 0.9em; + font-weight: 600; +} + +input[type='range'] { + margin: 10px 0; + padding: 10px 0; + background: transparent; +} + +input[type='range']:focus { + outline: none; +} + +input[type='range']::-webkit-slider-runnable-track { + width: 100%; + height: 9.5px; + -webkit-transition: 0.2s; + transition: 0.2s; + background: #161f27; + background: var(--background); + border-radius: 3px; +} + +input[type='range']::-webkit-slider-thumb { + box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); + -webkit-appearance: none; + margin-top: -7px; +} + +input[type='range']:focus::-webkit-slider-runnable-track { + background: #161f27; + background: var(--background); +} + +input[type='range']::-moz-range-track { + width: 100%; + height: 9.5px; + -moz-transition: 0.2s; + transition: 0.2s; + background: #161f27; + background: var(--background); + border-radius: 3px; +} + +input[type='range']::-moz-range-thumb { + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); +} + +input[type='range']::-ms-track { + width: 100%; + height: 9.5px; + background: transparent; + border-color: transparent; + border-width: 16px 0; + color: transparent; +} + +input[type='range']::-ms-fill-lower { + background: #161f27; + background: var(--background); + border: 0.2px solid #010101; + border-radius: 3px; + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; +} + +input[type='range']::-ms-fill-upper { + background: #161f27; + background: var(--background); + border: 0.2px solid #010101; + border-radius: 3px; + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; +} + +input[type='range']::-ms-thumb { + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + border: 1px solid #000; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); +} + +input[type='range']:focus::-ms-fill-lower { + background: #161f27; + background: var(--background); +} + +input[type='range']:focus::-ms-fill-upper { + background: #161f27; + background: var(--background); +} + +a { + text-decoration: none; + color: #41adff; + color: var(--links); +} + +a:hover { + text-decoration: underline; +} + +code { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +samp { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +time { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +pre > code { + padding: 10px; + display: block; + overflow-x: auto; +} + +var { + color: #d941e2; + color: var(--variable); + font-style: normal; + font-family: monospace; +} + +kbd { + background: #161f27; + background: var(--background); + border: 1px solid #526980; + border: 1px solid var(--border); + border-radius: 2px; + color: #dbdbdb; + color: var(--text-main); + padding: 2px 4px 2px 4px; +} + +img, +video { + max-width: 100%; + height: auto; +} + +hr { + border: none; + border-top: 1px solid #526980; + border-top: 1px solid var(--border); +} + +table { + border-collapse: collapse; + margin-bottom: 10px; + width: 100%; + table-layout: fixed; +} + +table caption { + text-align: left; +} + +td, +th { + padding: 6px; + text-align: left; + vertical-align: top; + word-wrap: break-word; +} + +thead { + border-bottom: 1px solid #526980; + border-bottom: 1px solid var(--border); +} + +tfoot { + border-top: 1px solid #526980; + border-top: 1px solid var(--border); +} + +tbody tr:nth-child(even) { + background-color: #161f27; + background-color: var(--background); +} + +tbody tr:nth-child(even) button { + background-color: #1a242f; + background-color: var(--background-alt); +} + +tbody tr:nth-child(even) button:hover { + background-color: #202b38; + background-color: var(--background-body); +} + +::-webkit-scrollbar { + height: 10px; + width: 10px; +} + +::-webkit-scrollbar-track { + background: #161f27; + background: var(--background); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb { + background: #040a0f; + background: var(--scrollbar-thumb); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgb(0, 0, 0); + background: var(--scrollbar-thumb-hover); +} + +::-moz-selection { + background-color: #1c76c5; + background-color: var(--selection); + color: #fff; + color: var(--text-bright); +} + +::selection { + background-color: #1c76c5; + background-color: var(--selection); + color: #fff; + color: var(--text-bright); +} + +details { + display: flex; + flex-direction: column; + align-items: flex-start; + background-color: #1a242f; + background-color: var(--background-alt); + padding: 10px 10px 0; + margin: 1em 0; + border-radius: 6px; + overflow: hidden; +} + +details[open] { + padding: 10px; +} + +details > :last-child { + margin-bottom: 0; +} + +details[open] summary { + margin-bottom: 10px; +} + +summary { + display: list-item; + background-color: #161f27; + background-color: var(--background); + padding: 10px; + margin: -10px -10px 0; + cursor: pointer; + outline: none; +} + +summary:hover, +summary:focus { + text-decoration: underline; +} + +details > :not(summary) { + margin-top: 0; +} + +summary::-webkit-details-marker { + color: #dbdbdb; + color: var(--text-main); +} + +dialog { + background-color: #1a242f; + background-color: var(--background-alt); + color: #dbdbdb; + color: var(--text-main); + border: none; + border-radius: 6px; + border-color: #526980; + border-color: var(--border); + padding: 10px 30px; +} + +dialog > header:first-child { + background-color: #161f27; + background-color: var(--background); + border-radius: 6px 6px 0 0; + margin: -10px -30px 10px; + padding: 10px; + text-align: center; +} + +dialog::-webkit-backdrop { + background: #0000009c; + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); +} + +dialog::backdrop { + background: #0000009c; + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); +} + +footer { + border-top: 1px solid #526980; + border-top: 1px solid var(--border); + padding-top: 10px; + color: #a9b1ba; + color: var(--text-muted); +} + +body > footer { + margin-top: 40px; +} + +@media print { + body, + pre, + code, + summary, + details, + button, + input, + textarea { + background-color: #fff; + } + + button, + input, + textarea { + border: 1px solid #000; + } + + body, + h1, + h2, + h3, + h4, + h5, + h6, + pre, + code, + button, + input, + textarea, + footer, + summary, + strong { + color: #000; + } + + summary::marker { + color: #000; + } + + summary::-webkit-details-marker { + color: #000; + } + + tbody tr:nth-child(even) { + background-color: #f2f2f2; + } + + a { + color: #00f; + text-decoration: underline; + } +} +""" diff --git a/quart_imp/_cli/helpers.py b/quart_imp/_cli/helpers.py new file mode 100644 index 0000000..6126368 --- /dev/null +++ b/quart_imp/_cli/helpers.py @@ -0,0 +1,26 @@ +import re + + +def to_snake_case(string): + """ + Thank you openai + """ + # Replace any non-alphanumeric characters with underscores + string = re.sub(r"[^a-zA-Z0-9]", "_", string) + # Remove any consecutive underscores + string = re.sub(r"_{2,}", "_", string) + # Convert the string to lowercase + string = string.lower() + return string + + +class Sprinkles: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + END = "\033[0m" diff --git a/quart_imp/_cli/init.py b/quart_imp/_cli/init.py new file mode 100644 index 0000000..45d4c17 --- /dev/null +++ b/quart_imp/_cli/init.py @@ -0,0 +1,243 @@ +import os +from pathlib import Path + +import click + +from .blueprint import add_blueprint +from .filelib.all_files import GlobalFileLib +from .filelib.app import AppFileLib +from .filelib.favicon import favicon +from .filelib.quart_imp_logo import quart_imp_logo +from .filelib.head_tag_generator import head_tag_generator +from .filelib.water_css import water_css +from .helpers import Sprinkles as Sp + + +def init_app(name, _full: bool = False, _slim: bool = False, _minimal: bool = False): + click.echo(f"{Sp.OKGREEN}Creating App: {name}") + + cwd = Path.cwd() + + app_folder = cwd / name + + if app_folder.exists(): + click.echo(f"{Sp.FAIL}{name} folder already exists!{Sp.END}") + click.confirm("Are you sure you want to continue?", abort=True) + + # Folders + folders = { + "root": app_folder, + "extensions": app_folder / "extensions", + "resources": app_folder / "resources", + "resources/static": app_folder / "resources" / "static", + "resources/templates": app_folder / "resources" / "templates", + } + + if _minimal: + folders.update( + { + "resources/static/css": app_folder / "resources" / "static" / "css", + "resources/static/img": app_folder / "resources" / "static" / "img", + } + ) + + if not _minimal: + folders.update( + { + "resources/cli": app_folder / "resources" / "cli", + "resources/error_handlers": app_folder / "resources" / "error_handlers", + "resources/templates/errors": app_folder + / "resources" + / "templates" + / "errors", + } + ) + + if not _slim: + folders.update( + { + "models": app_folder / "models", + "blueprints": app_folder / "blueprints", + "resources/context_processors": app_folder + / "resources" + / "context_processors", + "resources/filters": app_folder / "resources" / "filters", + "resources/routes": app_folder / "resources" / "routes", + } + ) + + # Files + files = { + "root/default.config.toml": ( + folders["root"] / "default.config.toml", + AppFileLib.default_init_config_toml.format(secret_key=os.urandom(24).hex()) + if not _slim + else AppFileLib.default_config_toml.format(secret_key=os.urandom(24).hex()), + ), + "root/__init__.py": ( + folders["root"] / "__init__.py", + AppFileLib.init_py.format(app_name=name) + if not _slim + else AppFileLib.slim_init_py.format(app_name=name) + if not _minimal + else AppFileLib.minimal_init_py.format(app_name=name), + ), + "resources/static/favicon.ico": ( + folders["resources/static"] / "favicon.ico", + favicon, + ), + "extensions/__init__.py": ( + folders["extensions"] / "__init__.py", + AppFileLib.extensions_init_py + if not _slim + else AppFileLib.slim_extensions_init_py, + ), + } + + if _minimal: + files.update( + { + "resources/templates/index.html": ( + folders["resources/templates"] / "index.html", + GlobalFileLib.minimal_templates_index_html.format( + head_tag=head_tag_generator( + no_js=True, + ), + static_path="static", + index_py=folders["resources"] / "index.py", + index_html=folders["resources/templates"] / "index.html", + init_py=folders["root"] / "__init__.py", + ), + ), + "resources/static/css/main.css": ( + folders["resources/static/css"] / "water.css", + water_css, + ), + "resources/static/img/quart-imp-logo.png": ( + folders["resources/static/img"] / "quart-imp-logo.png", + quart_imp_logo, + ), + "resources/routes.py": ( + folders["resources"] / "routes.py", + GlobalFileLib.minimal_collections_routes_py, + ), + } + ) + + if not _minimal: + files.update( + { + "resources/cli/cli.py": ( + folders["resources/cli"] / "cli.py", + GlobalFileLib.collections_cli_py.format(app_name=name) + if not _slim + else GlobalFileLib.slim_collections_cli_py, + ), + "resources/error_handlers/error_handlers.py": ( + folders["resources/error_handlers"] / "error_handlers.py", + GlobalFileLib.collections_error_handlers_py, + ), + "resources/templates/errors/400.html": ( + folders["resources/templates/errors"] / "400.html", + GlobalFileLib.templates_errors_400_html, + ), + "resources/templates/errors/401.html": ( + folders["resources/templates/errors"] / "401.html", + GlobalFileLib.templates_errors_401_html, + ), + "resources/templates/errors/403.html": ( + folders["resources/templates/errors"] / "403.html", + GlobalFileLib.templates_errors_403_html, + ), + "resources/templates/errors/404.html": ( + folders["resources/templates/errors"] / "404.html", + GlobalFileLib.templates_errors_404_html, + ), + "resources/templates/errors/405.html": ( + folders["resources/templates/errors"] / "405.html", + GlobalFileLib.templates_errors_405_html, + ), + "resources/templates/errors/500.html": ( + folders["resources/templates/errors"] / "500.html", + GlobalFileLib.templates_errors_500_html, + ), + } + ) + + if not _slim: + files.update( + { + "models/__init__.py": ( + folders["models"] / "__init__.py", + AppFileLib.models_init_py.format(app_name=name), + ), + "models/example_user_table.py": ( + folders["models"] / "example_user_table.py", + AppFileLib.models_example_user_table_py, + ), + "resources/context_processors/context_processors.py": ( + folders["resources/context_processors"] / "context_processors.py", + GlobalFileLib.collections_context_processors_py, + ), + "resources/filters/filters.py": ( + folders["resources/filters"] / "filters.py", + GlobalFileLib.collections_filters_py, + ), + "resources/routes/routes.py": ( + folders["resources/routes"] / "routes.py", + GlobalFileLib.collections_routes_py, + ), + "resources/templates/index.html": ( + folders["resources/templates"] / "index.html", + GlobalFileLib.templates_index_html, + ), + } + ) + + # Loop create folders + for folder, path in folders.items(): + if not path.exists(): + path.mkdir(parents=True) + click.echo(f"{Sp.OKGREEN}App folder: {folder}, created{Sp.END}") + else: + click.echo( + f"{Sp.WARNING}App folder already exists: {folder}, skipping{Sp.END}" + ) + + # Loop create files + for file, (path, content) in files.items(): + if not path.exists(): + if ( + file == "resources/static/favicon.ico" + or file == "resources/static/img/quart-imp-logo.png" + ): + path.write_bytes(bytes.fromhex(content)) + continue + + path.write_text(content, encoding="utf-8") + + click.echo(f"{Sp.OKGREEN}App file: {file}, created{Sp.END}") + else: + click.echo(f"{Sp.WARNING}App file already exists: {file}, skipping{Sp.END}") + + if not _minimal: + add_blueprint( + f"{name}/blueprints", + "www", + _init_app=True, + _cwd=folders["blueprints"] if not _slim else folders["root"], + ) + + click.echo(" ") + click.echo(f"{Sp.OKBLUE}==================={Sp.END}") + click.echo(f"{Sp.OKBLUE}Quart app deployed!{Sp.END}") + click.echo(f"{Sp.OKBLUE}==================={Sp.END}") + click.echo(" ") + if name == "app": + click.echo(f"{Sp.OKBLUE}Your app has the default name of 'app'{Sp.END}") + click.echo(f"{Sp.OKBLUE}Quart will automatically look for this!{Sp.END}") + click.echo(f"{Sp.OKBLUE}Run: quart run{Sp.END}") + else: + click.echo(f"{Sp.OKBLUE}Your app has the name of '{name}'{Sp.END}") + click.echo(f"{Sp.OKBLUE}Run: quart --app {name} run{Sp.END}") + click.echo(" ") diff --git a/quart_imp/auth/__init__.py b/quart_imp/auth/__init__.py new file mode 100644 index 0000000..4d88c29 --- /dev/null +++ b/quart_imp/auth/__init__.py @@ -0,0 +1,29 @@ +from .__legacy__ import Auth +from .authenticate_password import authenticate_password +from .dataclasses import PasswordGeneration +from .encrypt_password import encrypt_password +from .generate_alphanumeric_validator import generate_alphanumeric_validator +from .generate_csrf_token import generate_csrf_token +from .generate_email_validator import generate_email_validator +from .generate_numeric_validator import generate_numeric_validator +from .generate_password import generate_password +from .generate_private_key import generate_private_key +from .generate_salt import generate_salt +from .is_email_address_valid import is_email_address_valid +from .is_username_valid import is_username_valid + +__all__ = [ + "PasswordGeneration", + "is_email_address_valid", + "is_username_valid", + "generate_csrf_token", + "generate_private_key", + "generate_numeric_validator", + "generate_alphanumeric_validator", + "generate_email_validator", + "generate_salt", + "encrypt_password", + "authenticate_password", + "generate_password", + "Auth", +] diff --git a/quart_imp/auth/__legacy__.py b/quart_imp/auth/__legacy__.py new file mode 100644 index 0000000..c702e70 --- /dev/null +++ b/quart_imp/auth/__legacy__.py @@ -0,0 +1,178 @@ +import typing as t +from random import choice +from string import ascii_letters + +from .authenticate_password import authenticate_password +from .encrypt_password import encrypt_password +from .generate_alphanumeric_validator import generate_alphanumeric_validator +from .generate_csrf_token import generate_csrf_token +from .generate_email_validator import generate_email_validator +from .generate_numeric_validator import generate_numeric_validator +from .generate_password import generate_password +from .generate_private_key import generate_private_key +from .generate_salt import generate_salt +from .is_email_address_valid import is_email_address_valid +from .is_username_valid import is_username_valid + + +def auth_password( + cls, + input_password: str, + database_password: str, + database_salt: str, + encrypt: int = 512, + pepper_length: int = 1, +) -> bool: + """Legacy method, use authenticate_password instead""" + return cls.authenticate_password( + input_password, database_password, database_salt, encrypt, pepper_length + ) + + +def hash_password( + password: str, salt: str, encrypt: int = 512, pepper_length: int = 1 +) -> str: + """Legacy method, use encrypt_password instead""" + return encrypt_password(password, salt, encrypt, pepper_length) + + +def sha_password( + password: str, salt: str, encrypt: int = 512, pepper_length: int = 1 +) -> str: + """Legacy method, use encrypt_password instead""" + return hash_password(password, salt, encrypt, pepper_length) + + +def generate_pepper(password: str, length: int = 1) -> str: + """Legacy method, stop using this""" + return "".join(choice(ascii_letters) for _ in range(length)) + password + + +def generate_form_token() -> str: + """Legacy method, use generate_csrf_token instead""" + return generate_csrf_token() + + +class Auth: + @classmethod + def is_email_address_valid(cls, email_address: str) -> bool: + """Legacy class method, use from quart_imp.auth import is_email_address_valid instead""" + return is_email_address_valid(email_address) + + @classmethod + def is_username_valid( + cls, + username: str, + allowed: t.Optional[t.List[t.Literal["all", "dot", "dash", "under"]]] = None, + ) -> bool: + """Legacy class method, use from quart_imp.auth import is_username_valid instead""" + return is_username_valid(username, allowed) + + @classmethod + def generate_csrf_token(cls) -> str: + """Legacy class method, use from quart_imp.auth import generate_csrf_token instead""" + return generate_csrf_token() + + @classmethod + def generate_private_key(cls, hook: t.Optional[str]) -> str: + """Legacy class method, use from quart_imp.auth import generate_private_key instead""" + return generate_private_key(hook) + + @classmethod + def generate_numeric_validator(cls, length: int) -> int: + """Legacy class method, use from quart_imp.auth import generate_numeric_validator instead""" + return generate_numeric_validator(length) + + @classmethod + def generate_alphanumeric_validator(cls, length: int) -> str: + """Legacy class method, use from quart_imp.auth import generate_alphanumeric_validator instead""" + return generate_alphanumeric_validator(length) + + @classmethod + def generate_email_validator(cls) -> str: + """Legacy class method, use from quart_imp.auth import generate_private_key instead""" + return generate_email_validator() + + @classmethod + def generate_salt(cls, length: int = 4) -> str: + """Legacy class method, use from quart_imp.auth import generate_salt instead""" + return generate_salt(length) + + @classmethod + def encrypt_password( + cls, + password: str, + salt: str, + encryption_level: int = 512, + pepper_length: int = 1, + pepper_position: t.Literal["start", "end"] = "end", + ) -> str: + """Legacy class method, use from quart_imp.auth import encrypt_password instead""" + return encrypt_password( + password, salt, encryption_level, pepper_length, pepper_position + ) + + @classmethod + def authenticate_password( + cls, + input_password: str, + database_password: str, + database_salt: str, + encryption_level: int = 512, + pepper_length: int = 1, + pepper_position: t.Literal["start", "end"] = "end", + ) -> bool: + """Legacy class method, use from quart_imp.auth import authenticate_password instead""" + return authenticate_password( + input_password, + database_password, + database_salt, + encryption_level, + pepper_length, + pepper_position, + ) + + @classmethod + def generate_password(cls, style: str = "mixed", length: int = 3) -> str: + """Legacy class method, use from quart_imp.auth import generate_password instead""" + return generate_password(style, length) + + # LEGACY METHODS + + @classmethod + def auth_password( + cls, + input_password: str, + database_password: str, + database_salt: str, + encrypt: int = 512, + pepper_length: int = 1, + ) -> bool: + """Legacy class method, use from quart_imp.auth import authenticate_password instead""" + return authenticate_password( + input_password, database_password, database_salt, encrypt, pepper_length + ) + + @classmethod + def hash_password( + cls, password: str, salt: str, encrypt: int = 512, pepper_length: int = 1 + ) -> str: + """Legacy class method, use from quart_imp.auth import encrypt_password instead""" + return encrypt_password(password, salt, encrypt, pepper_length) + + @classmethod + def sha_password( + cls, password: str, salt: str, encrypt: int = 512, pepper_length: int = 1 + ) -> str: + """Legacy class method, use from quart_imp.auth import encrypt_password instead""" + return encrypt_password(password, salt, encrypt, pepper_length) + + @classmethod + def generate_pepper(cls, password: str, length: int = 1) -> str: + """Legacy class method, stop using this""" + return "".join(choice(ascii_letters) for _ in range(length)) + password + + @classmethod + def generate_form_token(cls) -> str: + """Legacy class method, use from quart_imp.auth import generate_csrf_token instead""" + return generate_csrf_token() diff --git a/quart_imp/auth/__private_funcs__.py b/quart_imp/auth/__private_funcs__.py new file mode 100644 index 0000000..c63edb4 --- /dev/null +++ b/quart_imp/auth/__private_funcs__.py @@ -0,0 +1,33 @@ +import typing as t +from hashlib import sha256, sha512 + + +def _pps(pepper_, pass_, salt_) -> str: + return pepper_ + pass_ + salt_ + + +def _ppe(pepper_, pass_, salt_) -> str: + return pass_ + pepper_ + salt_ + + +def _guess_block( + guesses: set, + input_password: str, + database_password: str, + database_salt: str, + encryption_level: int = 512, + pepper_position: t.Literal["start", "end"] = "end", +) -> bool: + for guess in guesses: + _sha = sha512() if encryption_level == 512 else sha256() + _sha.update( + ( + _pps(guess, input_password, database_salt) + if pepper_position == "start" + else _ppe(guess, input_password, database_salt) + ).encode("utf-8") + ) + if _sha.hexdigest() == database_password: + return True + + return False diff --git a/quart_imp/auth/authenticate_password.py b/quart_imp/auth/authenticate_password.py new file mode 100644 index 0000000..732723a --- /dev/null +++ b/quart_imp/auth/authenticate_password.py @@ -0,0 +1,92 @@ +import multiprocessing +import typing as t +from itertools import product +from string import ascii_letters + +from more_itertools import batched + +from .__private_funcs__ import _guess_block + + +def authenticate_password( + input_password: str, + database_password: str, + database_salt: str, + encryption_level: int = 512, + pepper_length: int = 1, + pepper_position: t.Literal["start", "end"] = "end", + use_multiprocessing: bool = False, +) -> bool: + """ + Takes the plain input password, the stored hashed password along with the stored salt + and will try every possible combination of pepper values to find a match. + + :raw-html:`
` + + NOTE: use_multiprocessing is not compatible with coroutine workers, e.g. eventlet/gevent + commonly used with socketio. + + .. Note:: + + You must know the length of the pepper used to hash the password. + + You must know the position of the pepper used to hash the password. + + You must know the encryption level used to hash the password. + + :raw-html:`
` + + ----- + + :param input_password: str - plain password + :param database_password: str - hashed password from database + :param database_salt: str - salt from database + :param encryption_level: int - encryption used to generate database password + :param pepper_length: int - length of pepper used to generate database password + :param pepper_position: str - "start" or "end" - position of pepper used to generate database password + :param use_multiprocessing: bool - use multiprocessing to speed up the process (not compatible with eventlet/gevent) + :return: bool - True if match, False if not + """ + + if pepper_length > 3: + pepper_length = 3 + + _guesses = {"".join(i) for i in product(ascii_letters, repeat=pepper_length)} + + if not use_multiprocessing: + for guess in _guesses: + if _guess_block( + {guess}, + input_password, + database_password, + database_salt, + encryption_level, + pepper_position, + ): + return True + + return False + + thread_pool = multiprocessing.Pool(processes=pepper_length) + threads = [] + + for batch in batched(_guesses, 1000): + threads.append( + thread_pool.apply_async( + _guess_block, + args=( + batch, + input_password, + database_password, + database_salt, + encryption_level, + pepper_position, + ), + ) + ) + + for thread in threads: + if thread.get(): + return True + + return False diff --git a/quart_imp/auth/dataclasses.py b/quart_imp/auth/dataclasses.py new file mode 100644 index 0000000..2c9ace0 --- /dev/null +++ b/quart_imp/auth/dataclasses.py @@ -0,0 +1,767 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class PasswordGeneration: + """ + This is a bank of words used to generate random passwords. + """ + + animals = [ + "Canidae", + "Felidae", + "Cat", + "Cattle", + "Dog", + "Donkey", + "Goat", + "Horse", + "Pig", + "Rabbit", + "Aardvark", + "Aardwolf", + "Albatross", + "Alligator", + "Alpaca", + "Amphibian", + "Anaconda", + "Angelfish", + "Anglerfish", + "Ant", + "Anteater", + "Antelope", + "Antlion", + "Ape", + "Aphid", + "Armadillo", + "Asp", + "Baboon", + "Badger", + "Bandicoot", + "Barnacle", + "Barracuda", + "Basilisk", + "Bass", + "Bat", + "Bear", + "Beaver", + "Bedbug", + "Bee", + "Beetle", + "Bird", + "Bison", + "Blackbird", + "Boa", + "Boar", + "Bobcat", + "Bobolink", + "Bonobo", + "Bovid", + "Bug", + "Butterfly", + "Buzzard", + "Camel", + "Canid", + "Capybara", + "Cardinal", + "Caribou", + "Carp", + "Cat", + "Catshark", + "Caterpillar", + "Catfish", + "Cattle", + "Centipede", + "Cephalopod", + "Chameleon", + "Cheetah", + "Chickadee", + "Chicken", + "Chimpanzee", + "Chinchilla", + "Chipmunk", + "Clam", + "Clownfish", + "Cobra", + "Cockroach", + "Cod", + "Condor", + "Constrictor", + "Coral", + "Cougar", + "Cow", + "Coyote", + "Crab", + "Crane", + "Crawdad", + "Crayfish", + "Cricket", + "Crocodile", + "Crow", + "Cuckoo", + "Cicada", + "Damselfly", + "Deer", + "Dingo", + "Dinosaur", + "Dog", + "Dolphin", + "Donkey", + "Dormouse", + "Dove", + "Dragonfly", + "Dragon", + "Duck", + "Eagle", + "Earthworm", + "Earwig", + "Echidna", + "Eel", + "Egret", + "Elephant", + "Elk", + "Emu", + "Ermine", + "Falcon", + "Ferret", + "Finch", + "Firefly", + "Fish", + "Flamingo", + "Flea", + "Fly", + "Flyingfish", + "Fowl", + "Fox", + "Frog", + "Gamefowl", + "Galliform", + "Gazelle", + "Gecko", + "Gerbil", + "Gibbon", + "Giraffe", + "Goat", + "Goldfish", + "Goose", + "Gopher", + "Gorilla", + "Grasshopper", + "Grouse", + "Guan", + "Guanaco", + "Guineafowl", + "Gull", + "Guppy", + "Haddock", + "Halibut", + "Hamster", + "Hare", + "Harrier", + "Hawk", + "Hedgehog", + "Heron", + "Herring", + "Hippopotamus", + "Hookworm", + "Hornet", + "Horse", + "Hoverfly", + "Hummingbird", + "Hyena", + "Iguana", + "Impala", + "Jackal", + "Jaguar", + "Jay", + "Jellyfish", + "Junglefowl", + "Kangaroo", + "Kingfisher", + "Kite", + "Kiwi", + "Koala", + "Koi", + "Krill", + "Ladybug", + "Lamprey", + "Landfowl", + "Lark", + "Leech", + "Lemming", + "Lemur", + "Leopard", + "Leopon", + "Limpet", + "Lion", + "Lizard", + "Llama", + "Lobster", + "Locust", + "Loon", + "Louse", + "Lungfish", + "Lynx", + "Macaw", + "Mackerel", + "Magpie", + "Mammal", + "Manatee", + "Mandrill", + "Marlin", + "Marmoset", + "Marmot", + "Marsupial", + "Marten", + "Mastodon", + "Meadowlark", + "Meerkat", + "Mink", + "Minnow", + "Mite", + "Mockingbird", + "Mole", + "Mollusk", + "Mongoose", + "Monkey", + "Moose", + "Mosquito", + "Moth", + "Mouse", + "Mule", + "Muskox", + "Narwhal", + "Newt", + "Nightingale", + "Ocelot", + "Octopus", + "Opossum", + "Orangutan", + "Orca", + "Ostrich", + "Otter", + "Owl", + "Ox", + "Panda", + "Panther", + "Parakeet", + "Parrot", + "Parrotfish", + "Partridge", + "Peacock", + "Peafowl", + "Pelican", + "Penguin", + "Perch", + "Pheasant", + "Pig", + "Pigeon", + "Pike", + "Pinniped", + "Piranha", + "Planarian", + "Platypus", + "Pony", + "Porcupine", + "Porpoise", + "Possum", + "Prawn", + "Primate", + "Ptarmigan", + "Puffin", + "Puma", + "Python", + "Quail", + "Quelea", + "Quokka", + "Rabbit", + "Raccoon", + "Rat", + "Rattlesnake", + "Raven", + "Reindeer", + "Reptile", + "Rhinoceros", + "Roadrunner", + "Rodent", + "Rook", + "Rooster", + "Roundworm", + "Sailfish", + "Salamander", + "Salmon", + "Sawfish", + "Scallop", + "Scorpion", + "Seahorse", + "Shark", + "Sheep", + "Shrew", + "Shrimp", + "Silkworm", + "Silverfish", + "Skink", + "Skunk", + "Sloth", + "Slug", + "Smelt", + "Snail", + "Snake", + "Snipe", + "Sole", + "Sparrow", + "Spider", + "Spoonbill", + "Squid", + "Squirrel", + "Starfish", + "Stingray", + "Stoat", + "Stork", + "Sturgeon", + "Swallow", + "Swan", + "Swift", + "Swordfish", + "Swordtail", + "Tahr", + "Takin", + "Tapir", + "Tarantula", + "Tarsier", + "Termite", + "Tern", + "Thrush", + "Tick", + "Tiger", + "Tiglon", + "Toad", + "Tortoise", + "Toucan", + "Trout", + "Tuna", + "Turkey", + "Turtle", + "Tyrannosaurus", + "Urial", + "Vicuna", + "Viper", + "Vole", + "Vulture", + "Wallaby", + "Walrus", + "Wasp", + "Warbler", + "Weasel", + "Whale", + "Whippet", + "Whitefish", + "Wildcat", + "Wildebeest", + "Wildfowl", + "Wolf", + "Wolverine", + "Wombat", + "Woodpecker", + "Worm", + "Wren", + "Xerinae", + "Yak", + "Zebra", + "Alpaca", + "Cat", + "Cattle", + "Chicken", + "Dog", + "Donkey", + "Ferret", + "Gayal", + "Goldfish", + "Guppy", + "Horse", + "Koi", + "Llama", + "Sheep", + "Yak", + ] + + colors = [ + "DarkViolet", + "MediumVioletRed", + "Rose", + "Avocado", + "Greenish", + "Blood", + "Sangria", + "Pastel", + "Night", + "Celeste", + "Ocean", + "Cloudy", + "Battleship", + "Oak", + "BlanchedAlmond", + "Gold", + "Slate", + "DarkGray", + "MidnightBlue", + "PeachPuff", + "Dark", + "Chartreuse", + "Bashful", + "PaleVioletRed", + "DarkTurquoise", + "Grapefruit", + "Sun", + "Eggplant", + "Golden", + "Cyan", + "Sand", + "LightYellow", + "Cobalt", + "Tron", + "Ruby", + "Mustard", + "AntiqueWhite", + "Western", + "Deep-Sea", + "Iron", + "LimeGreen", + "Orange", + "DarkCyan", + "Velvet", + "Clover", + "Butterfly", + "Jasmine", + "Fire", + "DarkSlateGray", + "Heliotrope", + "Scarlet", + "Medium", + "Unbleached", + "Dimorphotheca", + "Cornsilk", + "GoldenRod", + "Beer", + "Canary", + "DeepPink", + "Sunrise", + "SlateGray", + "Burnt", + "Algae", + "Granite", + "Baby", + "Cream", + "LightBlue", + "Tan", + "Yellow", + "Burgundy", + "Cherry", + "Papaya", + "Lapis", + "Robin", + "Mango", + "Blush", + "Blueberry", + "Roman", + "Bisque", + "Iceberg", + "Rosy", + "Teal", + "SeaShell", + "Copper", + "Pea", + "Jeans", + "Watermelon", + "Grayish", + "Flamingo", + "Rich", + "Navy", + "Raspberry", + "Lime", + "Halloween", + "RosyBrown", + "Tangerine", + "Sea", + "Wood", + "MediumOrchid", + "Shamrock", + "Chameleon", + "Glacial", + "BlueViolet", + "Deep", + "FloralWhite", + "Fall", + "Black", + "Marble", + "Hazel", + "Hot", + "DarkSalmon", + "LavenderBlush", + "Organic", + "Violet", + "MintCream", + "Slime", + "DarkSlateBlue", + "DodgerBlue", + "MediumSpringGreen", + "Bee", + "Jade", + "Sage", + "Egg", + "Neon", + "WhiteSmoke", + "Grape", + "LightCyan", + "Acid", + "Day", + "Earth", + "Olive", + "Balloon", + "Pine", + "Rice", + "OliveDrab", + "Tulip", + "Corn", + "Rosy-Finch", + "Dirty", + "Coffee", + "Vampire", + "Pig", + "Jellyfish", + "Salmon", + "Vermilion", + "Camouflage", + "IndianRed", + "Mint", + "Viola", + "Venom", + "Cookie", + "HoneyDew", + "MediumSeaGreen", + "DarkMagenta", + "Magic", + "DarkGoldenRod", + "Lipstick", + "Tomato", + "Lavender", + "LightSkyBlue", + "Midday", + "Seafoam", + "CornflowerBlue", + "GhostWhite", + "Carbon", + "PapayaWhip", + "Wheat", + "Harvest", + "SteelBlue", + "Gulf", + "Mauve", + "Champagne", + "DarkOliveGreen", + "PaleGoldenRod", + "Oil", + "Clematis", + "Deer", + "Purple", + "LightGray", + "Parchment", + "PaleTurquoise", + "Northern", + "MistyRose", + "Tea", + "Ginger", + "New", + "AliceBlue", + "Jungle", + "SlateBlue", + "Khaki", + "RebeccaPurple", + "Pale", + "Water", + "School", + "Sepia", + "Wisteria", + "LightPink", + "Stoplight", + "Seaweed", + "DimGray", + "Mocha", + "LightGoldenRodYellow", + "Donut", + "Basket", + "Dusty", + "Construction", + "Metallic", + "Chestnut", + "Light", + "Fuchsia", + "SeaGreen", + "Plum", + "RoyalBlue", + "BurlyWood", + "Azure", + "Very", + "Aztech", + "Gray", + "DarkSeaGreen", + "LemonChiffon", + "FireBrick", + "Dull", + "Brown", + "Ash", + "Denim", + "Dull-Sea", + "Sapphire", + "Carnation", + "Antique", + "Dragon", + "PaleGreen", + "LightSteelBlue", + "Cinnamon", + "Heavenly", + "Sonic", + "Coral", + "SkyBlue", + "Jet", + "Thistle", + "Beetle", + "Blonde", + "Red", + "MediumSlateBlue", + "HotPink", + "MediumBlue", + "DarkKhaki", + "Carrot", + "DeepSkyBlue", + "Taupe", + "Aquamarine", + "Pistachio", + "DarkOrange", + "Camel", + "Gainsboro", + "DarkRed", + "Linen", + "Kelly", + "Off", + "Macaw", + "Bullet", + "MediumPurple", + "Brass", + "Cardboard", + "Sienna", + "Midnight", + "Electric", + "CadetBlue", + "Chilli", + "Columbia", + "Vanilla", + "Puce", + "Snow", + "Bean", + "Cadillac", + "LightCoral", + "Soft", + "MediumTurquoise", + "Bold", + "NavajoWhite", + "Cantaloupe", + "Blue", + "Maroon", + "LightSlateGray", + "Cotton", + "Iguana", + "Chrome", + "DarkOrchid", + "Indigo", + "Moccasin", + "Orchid", + "Nebula", + "Milk", + "Fern", + "GreenYellow", + "Ferrari", + "Pearl", + "Bakers", + "Bright", + "Emerald", + "Beige", + "Army", + "Alien", + "Periwinkle", + "SpringGreen", + "Rubber", + "Chocolate", + "Charcoal", + "Tiger", + "Nardo", + "Rogue", + "Aqua", + "Lilac", + "PowderBlue", + "OrangeRed", + "SaddleBrown", + "DarkBlue", + "Hummingbird", + "White", + "Saffron", + "Old", + "LightSalmon", + "LightSeaGreen", + "OldLace", + "Cranberry", + "Zombie", + "Crocus", + "Windows", + "Frog", + "Peru", + "DarkGreen", + "Ivory", + "Love", + "Pink", + "Sky", + "Mahogany", + "French", + "SandyBrown", + "Dollar", + "Dinosaur", + "Sedona", + "ForestGreen", + "Mist", + "Smokey", + "Crystal", + "Iridium", + "Banana", + "Desert", + "LightGreen", + "Sandstone", + "Silver", + "Valentine", + "Silk", + "Green", + "Parrot", + "Macaroni", + "Caramel", + "Pumpkin", + "Indian", + "Crimson", + "Tiffany", + "Gunmetal", + "Salad", + "Platinum", + "MediumAquaMarine", + "Bronze", + "Lava", + "Peach", + "Tyrian", + "Rust", + "Petra", + "Lovely", + "Aloe", + "Blossom", + "Rat", + "Shocking", + "LawnGreen", + "YellowGreen", + "Turquoise", + ] diff --git a/quart_imp/auth/encrypt_password.py b/quart_imp/auth/encrypt_password.py new file mode 100644 index 0000000..3b05b8a --- /dev/null +++ b/quart_imp/auth/encrypt_password.py @@ -0,0 +1,66 @@ +import typing as t +from hashlib import sha256, sha512 +from random import choice +from string import ascii_letters + +from .__private_funcs__ import _pps, _ppe + + +def encrypt_password( + password: str, + salt: str, + encryption_level: int = 512, + pepper_length: int = 1, + pepper_position: t.Literal["start", "end"] = "end", +) -> str: + """ + Takes the plain password, applies a pepper, salts it, then produces a digested sha512 or sha256 if specified. + + :raw-html:`
` + + Can set the encryption level to 256 or 512, defaults to 512. + + Can set the pepper length, defaults to 1. Max is 3. + + Can set the pepper position, "start" or "end", defaults to "end". + + :raw-html:`
` + + For use in password hashing. + + :raw-html:`
` + + .. Note:: + + You must inform the authenticate_password function of the pepper length used to hash the password. + + You must inform the authenticate_password function of the position of the pepper used to hash the password. + + You must inform the authenticate_password function of the encryption level used to hash the password. + + :raw-html:`
` + + ----- + + :param password: str - plain password + :param salt: str - salt + :param encryption_level: int - 256 or 512 - defaults to 512 + :param pepper_length: int - length of pepper + :param pepper_position: str - "start" or "end" - defaults to "end" + :return str: hash: + """ + + if pepper_length > 3: + pepper_length = 3 + + _sha = sha512() if encryption_level == 512 else sha256() + _pepper = "".join(choice(ascii_letters) for _ in range(pepper_length)) + + _sha.update( + ( + _pps(_pepper, password, salt) + if pepper_position == "start" + else _ppe(_pepper, password, salt) + ).encode("utf-8") + ) + return _sha.hexdigest() diff --git a/quart_imp/auth/generate_alphanumeric_validator.py b/quart_imp/auth/generate_alphanumeric_validator.py new file mode 100644 index 0000000..3f8d0b0 --- /dev/null +++ b/quart_imp/auth/generate_alphanumeric_validator.py @@ -0,0 +1,21 @@ +from random import choice +from string import ascii_uppercase, digits + + +def generate_alphanumeric_validator(length: int) -> str: + """ + Generates (length) of alphanumeric. + + :raw-html:`
` + + For use in MFA email, or unique filename generation. + + :raw-html:`
` + + ----- + :param length: int - length of alphanumeric to generate + :return: str - Example return of "F5R6" if length is 4 + """ + + _alpha_numeric = ascii_uppercase + digits + return "".join([choice(_alpha_numeric) for _ in range(length)]) diff --git a/quart_imp/auth/generate_csrf_token.py b/quart_imp/auth/generate_csrf_token.py new file mode 100644 index 0000000..c02b253 --- /dev/null +++ b/quart_imp/auth/generate_csrf_token.py @@ -0,0 +1,21 @@ +from datetime import datetime +from hashlib import sha1 + + +def generate_csrf_token() -> str: + """ + Generates a SHA1 using the current date and time. + + :raw-html:`
` + + For use in Cross-Site Request Forgery. + + :raw-html:`
` + + ----- + + :return: str - sha1 + """ + sha = sha1() + sha.update(str(datetime.now()).encode("utf-8")) + return sha.hexdigest() diff --git a/quart_imp/auth/generate_email_validator.py b/quart_imp/auth/generate_email_validator.py new file mode 100644 index 0000000..11be048 --- /dev/null +++ b/quart_imp/auth/generate_email_validator.py @@ -0,0 +1,20 @@ +from .generate_alphanumeric_validator import generate_alphanumeric_validator + + +def generate_email_validator() -> str: + """ + Uses generate_alphanumeric_validator with a length of 8 to + generate a random alphanumeric value for the specific use of + validating accounts via email. + + :raw-html:`
` + + See `generate_alphanumeric_validator` for more information. + + :raw-html:`
` + + ----- + + :return: str - alphanumeric of length 8 + """ + return str(generate_alphanumeric_validator(length=8)) diff --git a/quart_imp/auth/generate_numeric_validator.py b/quart_imp/auth/generate_numeric_validator.py new file mode 100644 index 0000000..f6eae53 --- /dev/null +++ b/quart_imp/auth/generate_numeric_validator.py @@ -0,0 +1,24 @@ +from random import randrange + + +def generate_numeric_validator(length: int) -> int: + """ + Generates random choice between 1 * (length) and 9 * (length). + + :raw-html:`
` + + If the length is 4, it will generate a number between 1111 and 9999. + + :raw-html:`
` + + For use in MFA email, or unique filename generation. + + :raw-html:`
` + + ----- + :param length: int - length of number to generate + :return: int - Example return of number between 1111 and 9999 if length is 4 + """ + start = int("1" * length) + end = int("9" * length) + return randrange(start, end) diff --git a/quart_imp/auth/generate_password.py b/quart_imp/auth/generate_password.py new file mode 100644 index 0000000..5082763 --- /dev/null +++ b/quart_imp/auth/generate_password.py @@ -0,0 +1,56 @@ +from random import choice + +from .dataclasses import PasswordGeneration +from .generate_numeric_validator import generate_numeric_validator + + +def generate_password(style: str = "mixed", length: int = 3) -> str: + """ + Generates a plain text password based on choice of style and length. + 2 random numbers are appended to the end of every generated password. + + :raw-html:`
` + + style options: "animals", "colors", "mixed" - defaults to "mixed" + + :raw-html:`
` + + **Example use:** + + .. code-block:: + + generate_password(style="animals", length=3) + + :raw-html:`
` + + **Output:** + + Cat-Goat-Pig12 + + :raw-html:`
` + + ----- + + :param style: str - "animals", "colors", "mixed" - defaults to "mixed" + :param length: int - how many words are chosen - defaults to 3 + :return: str - a generated plain text password + """ + if style == "animals": + return "-".join( + [choice(PasswordGeneration.animals) for _ in range(length)] + ) + str(generate_numeric_validator(length=2)) + + if style == "colors": + return "-".join( + [choice(PasswordGeneration.colors) for _ in range(length)] + ) + str(generate_numeric_validator(length=2)) + + if style == "mixed": + return "-".join( + [ + choice([*PasswordGeneration.animals, *PasswordGeneration.colors]) + for _ in range(length) + ] + ) + str(generate_numeric_validator(length=2)) + + raise ValueError(f"Invalid style passed in {style}") diff --git a/quart_imp/auth/generate_private_key.py b/quart_imp/auth/generate_private_key.py new file mode 100644 index 0000000..b93ccbc --- /dev/null +++ b/quart_imp/auth/generate_private_key.py @@ -0,0 +1,30 @@ +import typing as t +from datetime import datetime +from hashlib import sha256 +from random import randrange + + +def generate_private_key(hook: t.Optional[str]) -> str: + """ + Generates a sha256 private key from a passed in hook value. + + :raw-html:`
` + + If no hook is passed in, it will generate a hook using datetime.now() and a + random number between 1 and 1000. + + :raw-html:`
` + + ----- + + :param hook: str - hook value to generate private key from + :return: str - sha256 + """ + + if hook is None: + _range = randrange(1, 1000) + hook = f"{datetime.now()}-{_range}" + + sha = sha256() + sha.update(hook.encode("utf-8")) + return sha.hexdigest() diff --git a/quart_imp/auth/generate_salt.py b/quart_imp/auth/generate_salt.py new file mode 100644 index 0000000..e73c679 --- /dev/null +++ b/quart_imp/auth/generate_salt.py @@ -0,0 +1,23 @@ +from random import choice +from string import punctuation + + +def generate_salt(length: int = 4) -> str: + """ + Generates a string of (length) characters of punctuation. + + :raw-html:`
` + + The Default length is 4. + + :raw-html:`
` + + For use in password salting + + :raw-html:`
` + + ----- + + :return: str - salt of (length) + """ + return "".join(choice(punctuation) for _ in range(length)) diff --git a/quart_imp/auth/is_email_address_valid.py b/quart_imp/auth/is_email_address_valid.py new file mode 100644 index 0000000..2c5b1af --- /dev/null +++ b/quart_imp/auth/is_email_address_valid.py @@ -0,0 +1,41 @@ +import re + + +def is_email_address_valid(email_address: str) -> bool: + """ + Checks if email_address is a valid email address. + + Is not completely RFC 5322 compliant, but it is good enough for most use cases. + + :raw-html:`
` + + Here are examples of mistakes that it will not catch: + + :raw-html:`
` + + Valid but fails: + + - email@[123.123.123.123] is VALID => PASSED : False + - “email”@example.com is VALID => PASSED : False + - very.unusual.“@”.unusual.com@example.com is VALID => PASSED : False + - very.“(),:;<>[]”.VERY.“very@\\ "very”.unusual@strange.example.com is VALID => PASSED : False + + Invalid but passes: + + - email@example.com (Joe Smith) is INVALID => PASSED : True + - email@111.222.333.44444 is INVALID => PASSED : True + + + :raw-html:`
` + + ----- + + :param email_address: str + :return: bool + """ + pattern = re.compile( + r"[a-z\d!#$%&'*+?^_`{|}~-]+(?:\.[a-z\d!#$%&'*+?^_`" + r"{|}~-]+)*@(?:[a-z\d](?:[a-z\d-]*[a-z\d])?\.)+[a-z\d](?:[a-z\d-]*[a-z\d])?", + re.IGNORECASE, + ) + return bool(pattern.match(email_address)) diff --git a/quart_imp/auth/is_username_valid.py b/quart_imp/auth/is_username_valid.py new file mode 100644 index 0000000..7c02094 --- /dev/null +++ b/quart_imp/auth/is_username_valid.py @@ -0,0 +1,92 @@ +import re +import typing as t + + +def is_username_valid( + username: str, + allowed: t.Optional[t.List[t.Literal["all", "dot", "dash", "under"]]] = None, +) -> bool: + """ + Checks if a username is valid. + + :raw-html:`
` + + Valid usernames can only include letters, + numbers, ., -, and _ but cannot begin or end with + the last three mentioned. + + :raw-html:`
` + + + **Example use:** + + :raw-html:`
` + + .. code-block:: + + is_username_valid("username", allowed=["all"]) + + :raw-html:`
` + + **Output:** + + .. code-block:: + + username : WILL PASS : True + user.name : WILL PASS : True + user-name : WILL PASS : True + user_name : WILL PASS : True + _user_name : WILL PASS : False + + + :raw-html:`
` + + .. code-block:: + + is_username_valid("username", allowed=["dot", "dash"]) + + :raw-html:`
` + + **Output:** + + .. code-block:: + + username : WILL PASS : True + user.name : WILL PASS : True + user-name : WILL PASS : True + user-name.name : WILL PASS : True + user_name : WILL PASS : False + _user_name : WILL PASS : False + .user.name : WILL PASS : False + + :raw-html:`
` + + ----- + + :param username: str + :param allowed: list - ["all", "dot", "dash", "under"] - defaults to ["all"] + :return bool: + """ + + if not username[0].isalnum() or not username[-1].isalnum(): + return False + + if allowed is None: + allowed = ["all"] + + if "all" in allowed: + return bool(re.match(r"^[a-zA-Z0-9._-]+$", username)) + + if "under" not in allowed: + if "_" in username: + return False + + if "dot" not in allowed: + if "." in username: + return False + + if "dash" not in allowed: + if "-" in username: + return False + + return True diff --git a/quart_imp/blueprint.py b/quart_imp/blueprint.py new file mode 100644 index 0000000..759c18f --- /dev/null +++ b/quart_imp/blueprint.py @@ -0,0 +1,543 @@ +import logging +from functools import partial +from importlib import import_module +from importlib.util import find_spec +from inspect import getmembers +from pathlib import Path + +from quart import Blueprint +from quart import session + +from .helpers import _init_bp_config, _build_database_uri +from .utilities import cast_to_import_str + + +class ImpBlueprint(Blueprint): + """ + A Class that extends the capabilities of the Flask Blueprint class. + """ + + enabled: bool = False + location: Path + bp_name: str + package: str + + session: dict + settings: dict + database_bind: dict + + __model_imports__: list + __nested_blueprint_imports__: list + + def __init__(self, dunder_name: str, config_file: str = "config.toml") -> None: + """ + Creates a new ImpBlueprint instance. + + :raw-html:`
` + + `config.toml` must be in the same directory as the `__init__.py` file. + + :raw-html:`
` + + -- config.toml -- + .. code-block:: + + ENABLED = "yes" + + [SETTINGS] + URL_PREFIX = "" + #SUBDOMAIN = "" + #URL_DEFAULTS = { } + #STATIC_FOLDER = "" + TEMPLATE_FOLDER = "" + #STATIC_URL_PATH = "" + #ROOT_PATH = "" + #CLI_GROUP = "" + + [SESSION] + var = "" + + [DATABASE_BIND] + ENABLED = false + #DIALECT = "sqlite" + #DATABASE_NAME = "" + #LOCATION = "" + #PORT = "" + #USERNAME = "" + #PASSWORD = "" + + :raw-html:`
` + + ----- + + :param dunder_name: __name__ + :param config_file: Must be in the same directory as the blueprint, defaults to "config.toml" + """ + self.package = dunder_name + self.__model_imports__ = [] + self.__nested_blueprint_imports__ = [] + + spec = find_spec(self.package) + + if spec is None: + raise ImportError(f"Cannot find origin of {self.package}") + + self.location = Path(f"{spec.origin}").parent + self.bp_name = self.location.name + + ( + self.enabled, + self.session, + self.settings, + self.database_bind, + ) = _init_bp_config( + self.bp_name, + self.location / config_file, + ) + + if self.enabled: + super().__init__(self.bp_name, self.package, **self.settings) + + def import_resources(self, folder: str = "routes") -> None: + """ + Will import all the resources (cli, routes, filters, context_processors...) from the given folder. + Given folder must be relative to the blueprint (in the same folder as the __init__.py file). + + :raw-html:`
` + + **Example use:** + + :raw-html:`
` + + --- Folder structure --- + + .. code-block:: + + my_blueprint + ├── user_routes + │ ├── user_dashboard.py + │ └── user_settings.py + ├── car_routes + │ ├── car_dashboard.py + │ └── car_settings.py + ├── __init__.py + └── config.toml + + + :raw-html:`
` + + --- __init__.py --- + + .. code-block:: + + from quart_imp import Blueprint + + bp = Blueprint(__name__) + + bp.import_resources("user_routes") + bp.import_resources("car_routes") + ... + + :raw-html:`
` + + --- user_dashboard.py --- + + .. code-block:: + + from quart import render_template + + from .. import bp + + @bp.route("/user-dashboard") + def user_dashboard(): + return render_template(bp.tmpl("user_dashboard.html")) + + :raw-html:`
` + + The endpoint my_blueprint.user_dashboard will be available at /my_blueprint/user-dashboard + + :raw-html:`
` + + ----- + + :param folder: Folder to look for resources in. Defaults to "routes". Must be relative. + """ + if not self.enabled: + return + + resource_path = self.location / folder + if not resource_path.exists(): + raise NotADirectoryError(f"{resource_path} is not a directory") + + resources = resource_path.glob("*.py") + for resource in resources: + try: + import_module(f"{self.package}.{folder}.{resource.stem}") + except ImportError as e: + raise ImportError( + f"Error when importing {self.package}.{resource}: {e}" + ) + + def import_nested_blueprint(self, blueprint: str) -> None: + """ + Imports the specified Flask-Imp Blueprint or a standard Flask Blueprint as a nested blueprint, + under the current blueprint. + + :raw-html:`
` + + Has the same import rules as the `Imp.import_blueprint()` method. + + :raw-html:`
` + + **Must be setup in a Python package** + + :raw-html:`
` + + **Example:** + + :raw-html:`
` + + --- Folder structure --- + .. code-block:: + + app + ├── my_blueprint + │ ├── ... + │ ├── my_nested_blueprint + │ │ ├── ... + │ │ ├── __init__.py + │ │ └── config.toml + │ ├── __init__.py + │ └── config.toml + └── ... + + :raw-html:`
` + + --- my_blueprint/__init__.py --- + + .. code-block:: + + from quart_imp import Blueprint + + bp = Blueprint(__name__) + + bp.import_nested_blueprint("my_nested_blueprint") + + ... + + + :raw-html:`
` + + ----- + + :param blueprint: The blueprint (folder name) to import. Must be relative. + :return: None + """ + if not self.enabled: + return + + self.__nested_blueprint_imports__.append( + partial(self._partial_nested_blueprint_import, blueprint=blueprint) + ) + + def import_nested_blueprints(self, folder: str) -> None: + """ + Imports all blueprints in the given folder. + + .. Note:: + Folder has no requirement to be a Python package. + + :raw-html:`
` + + See `Imp.import_nested_blueprint()` for more information. + + :raw-html:`
` + + **Example:** + + :raw-html:`
` + + --- Folder structure --- + + .. code-block:: + + app + ├── my_blueprint + │ ├── ... + │ ├── nested_blueprints + │ │ ├── my_nested_blueprint_1 + │ │ │ ├── ... + │ │ │ ├── __init__.py + │ │ │ └── config.toml + │ │ ├── my_nested_blueprint_2 + │ │ │ ├── ... + │ │ │ ├── __init__.py + │ │ │ └── config.toml + │ │ └── my_nested_blueprint_3 + │ │ ├── ... + │ │ ├── __init__.py + │ │ └── config.toml + │ ├── __init__.py + │ └── config.toml + └── ... + + :raw-html:`
` + + --- my_blueprint/__init__.py --- + + .. code-block:: + + from quart_imp import Blueprint + + bp = Blueprint(__name__) + bp.import_nested_blueprints("nested_blueprints") + ... + + :raw-html:`
` + + All blueprints in the nested_blueprints folder will be imported and nested under my_blueprint. + + :raw-html:`
` + + ----- + + :param folder: Folder to look for nested blueprints in. + Must be relative. + """ + if not self.enabled: + return + + folder_path = Path(self.location / folder) + + for potential_bp in folder_path.iterdir(): + self.import_nested_blueprint(potential_bp.as_posix()) + + def init_session(self) -> None: + """ + Similar to the `Imp.init_session()` method, + but scoped to the current blueprint's config.toml session values. + + :raw-html:`
` + + **Example usage:** + + :raw-html:`
` + + .. code-block:: + + @bp.before_app_request + def before_app_request(): + bp.init_session() + + :raw-html:`
` + + ----- + + :return: None + + """ + if not self.enabled: + return + + for key in self.session: + if key not in session: + session.update(self.session) + break + + def import_models(self, file_or_folder: str) -> None: + """ + Same actions as `Imp.import_models()`, but scoped to the current blueprint's package. + + :raw-html:`
` + + **Each model found will be added to the model registry.** + + :raw-html:`
` + + See: `Imp.model()` for more information. + + :raw-html:`
` + + **Example usage from files:** + + :raw-html:`
` + + .. code-block:: + + # in my_blueprint/__init__.py + bp.import_models("users.py") + bp.import_models("cars.py") + + :raw-html:`
` + + -- Folder structure -- + + .. code-block:: + + my_blueprint + ├── ... + ├── users.py + ├── cars.py + ├── config.toml + └── __init__.py + + + :raw-html:`
` + + **Example usage from folders:** + + :raw-html:`
` + + .. code-block:: + + # in my_blueprint/__init__.py + bp.import_models("models") + + :raw-html:`
` + + -- Folder structure -- + + .. code-block:: + + my_blueprint + ├── ... + ├── models + │ ├── users.py + │ └── cars.py + ├── config.toml + └── __init__.py + + :raw-html:`
` + + **Example of model file:** + + :raw-html:`
` + + -- users.py -- + + .. code-block:: + + from app.extensions import db + + class User(db.Model): + attribute = db.Column(db.String(255)) + + :raw-html:`
` + + ----- + + :param file_or_folder: The file or folder to import from. Must be relative. + :return: None + """ + if not self.enabled: + return + + self.__model_imports__.append( + partial(self._partial_models_import, file_or_folder=file_or_folder) + ) + + def tmpl(self, template: str) -> str: + """ + Pushes the blueprint name to the template name. + This saves time in having to type out the blueprint name when rendering a + template file from the blueprint's template folder. + + :raw-html:`
` + + **Example usage:** + + :raw-html:`
` + + .. code-block:: + + @bp.route("/") + def index(): + return render_template(bp.tmpl("index.html")) + + :raw-html:`
` + + -- Folder structure -- + + .. code-block:: + + my_blueprint + ├── ... + ├── templates + │ └── my_blueprint + │ └── index.html + ├── config.toml + └── __init__.py + + :raw-html:`
` + + bp.tmpl("index.html") will return "my_blueprint/index.html" + + :raw-html:`
` + + This use case is a common workaround in Flask to allow for multiple templates with the same name, + but in different registered template folders. + + :raw-html:`
` + + ----- + + :param template: The template name to push the blueprint name to. + :return: str - The template name with the blueprint name pushed to it. + """ + return f"{self.name}/{template}" + + def _setup_imp_blueprint(self, imp_instance) -> None: + """ + Sets up the ImpBlueprint instance. This is a private method and should not be called directly. + """ + bind_enabled = self.database_bind.get("ENABLED", False) + + app_instance = imp_instance.app + + if bind_enabled: + database_uri = _build_database_uri(self.database_bind, app_instance) + + if database_uri: + if self.name in app_instance.config.get("SQLALCHEMY_BINDS", {}): + raise ValueError( + f"Blueprint {self.name} already has a database bind set" + ) + + app_instance.config["SQLALCHEMY_BINDS"].update( + {self.name: database_uri} + ) + + for partial_models_import in self.__model_imports__: + partial_models_import(imp_instance=imp_instance) + + for partial_nested_blueprint_import in self.__nested_blueprint_imports__: + partial_nested_blueprint_import(imp_instance=imp_instance) + + def _partial_models_import( + self, + file_or_folder: str, + imp_instance, + ) -> None: + file_or_folder_path = Path(self.location / file_or_folder) + imp_instance.import_models(file_or_folder_path.as_posix()) + + def _partial_nested_blueprint_import(self, blueprint: str, imp_instance) -> None: + if Path(blueprint).is_absolute(): + potential_bp = Path(blueprint) + else: + potential_bp = Path(self.location / blueprint) + + if potential_bp.exists() and potential_bp.is_dir(): + module = import_module( + cast_to_import_str(self.package.split(".")[0], potential_bp) + ) + for name, value in getmembers(module): + if isinstance(value, Blueprint) or isinstance(value, ImpBlueprint): + if hasattr(value, "_setup_imp_blueprint"): + if getattr(value, "enabled", False): + value._setup_imp_blueprint(imp_instance) + self.register_blueprint(value) + else: + logging.debug(f"Blueprint {name} is disabled") + else: + self.register_blueprint(value) diff --git a/quart_imp/helpers.py b/quart_imp/helpers.py new file mode 100644 index 0000000..52f32e9 --- /dev/null +++ b/quart_imp/helpers.py @@ -0,0 +1,228 @@ +import logging +import os +import typing as t +from pathlib import Path + +from quart import Quart +from toml import load as toml_load + +from ._cli.filelib import AppFileLib + +from .utilities import cast_to_bool, process_dict + + +def _build_database_uri( + database_config_value: dict, app_instance: Quart +) -> t.Optional[str]: + """ + Puts together the correct database URI depending on the type specified. + + Fails if type is not supported. + """ + + app_root = Path(app_instance.root_path) + db_dialect = database_config_value.get("DIALECT", "None") + db_name = database_config_value.get("DATABASE_NAME", "database") + db_location = database_config_value.get("LOCATION", "instance") + db_port = str(database_config_value.get("PORT", "None")) + db_username = database_config_value.get("USERNAME", "None") + db_password = database_config_value.get("PASSWORD", "None") + + allowed_dialects = ("postgresql", "mysql", "oracle", "sqlite", "mssql") + + if db_dialect == "None": + raise ValueError( + """\ +Database dialect was not specified, must be: postgresql / mysql / oracle / sqlite / mssql +Example: + +[DATABASE.MAIN] +ENABLED = true +DIALECT = "sqlite" +DATABASE_NAME = "database" +LOCATION = "instance" +PORT = "" +USERNAME = "database" +PASSWORD = "password" + +This will create a sqlite file called +database.sqlite in a folder called instance. + +You can change the file extension by setting the environment variable IMP_SQLITE_DB_EXTENSION""" + ) + + if not db_location: + db_location = "instance" + + if "sqlite" in db_dialect: + set_db_extension = app_instance.config.get("SQLITE_DB_EXTENSION", ".sqlite") + store_db_in_parent = cast_to_bool( + app_instance.config.get("SQLITE_STORE_IN_PARENT", False) + ) + + if store_db_in_parent: + db_location_path = Path(app_root.parent / db_location) + else: + db_location_path = Path(app_root / db_location) + + db_location_path.mkdir(parents=True, exist_ok=True) + db_location_file_path = db_location_path / f"{db_name}{set_db_extension}" + return f"{db_dialect}:///{db_location_file_path}" + + for dialect in allowed_dialects: + if dialect in db_dialect: + return f"{db_dialect}://{db_username}:{db_password}@{db_location}:{db_port}/{db_name}" + + raise ValueError( + """\ +Database dialect is unknown, must be: postgresql / mysql / oracle / sqlite / mssql + +Example: + +[DATABASE.MAIN] +ENABLED = true +DIALECT = "sqlite" +DATABASE_NAME = "database" +LOCATION = "instance" +PORT = "" +USERNAME = "database" +PASSWORD = "password" + +This will create a sqlite file called +database.sqlite in a folder called instance. + +You can change the file extension by setting the environment variable IMP_SQLITE_DB_EXTENSION""" + ) + + +def _init_app_config( + config_file_path: Path, ignore_missing_env_variables: bool, app +) -> dict: + """ + Processes the values from the configuration from_file. + """ + if not config_file_path.exists(): + logging.critical( + "Config file was not found, creating default.config.toml to use" + ) + + config_file_path.write_text( + AppFileLib.default_config_toml.format(secret_key=os.urandom(24).hex()) + ) + + config_suffix = (".toml", ".tml") + + if config_file_path.suffix not in config_suffix: + raise TypeError( + "Config from_file must be one of the following types: .toml / .tml" + ) + + config = process_dict(toml_load(config_file_path)) + + quart_config = process_dict( + config.get("FLASK"), + key_case_switch="upper", + ignore_missing_env_variables=ignore_missing_env_variables, + ) + session_config = process_dict( + config.get("SESSION"), + key_case_switch="ignore", + ignore_missing_env_variables=ignore_missing_env_variables, + ) + sqlalchemy_config = process_dict( + config.get("SQLALCHEMY"), + key_case_switch="upper", + ignore_missing_env_variables=ignore_missing_env_variables, + ) + database_config = process_dict( + config.get("DATABASE"), + key_case_switch="upper", + ignore_missing_env_variables=ignore_missing_env_variables, + crawl=True, + ) + + if quart_config is not None and isinstance(quart_config, dict): + for quart_config_key, quart_config_value in quart_config.items(): + app.config.update({quart_config_key: quart_config_value}) + + if sqlalchemy_config is not None and isinstance(sqlalchemy_config, dict): + for sqlalchemy_config_key, sqlalchemy_config_value in sqlalchemy_config.items(): + app.config.update({sqlalchemy_config_key: sqlalchemy_config_value}) + + if database_config is not None and isinstance(database_config, dict): + app.config["SQLALCHEMY_BINDS"] = dict() + for database_config_key, database_config_values in database_config.items(): + if database_config_values.get("ENABLED", False): + database_uri = _build_database_uri(database_config_values, app) + if database_uri: + if database_config_key == "MAIN": + app.config["SQLALCHEMY_DATABASE_URI"] = database_uri + continue + + app.config["SQLALCHEMY_BINDS"].update( + {str(database_config_key).lower(): database_uri} + ) + + return { + "FLASK": {**quart_config, **sqlalchemy_config}, + "SESSION": session_config, + "DATABASE": database_config, + } + + +def _init_bp_config(blueprint_name: str, config_file_path: Path) -> tuple: + """ + Attempts to load and process the blueprint configuration file. + """ + + if not config_file_path.exists(): + raise FileNotFoundError( + f"{blueprint_name} Blueprint config {config_file_path.name} was not found" + ) + + config_suffix = (".toml", ".tml") + + if config_file_path.suffix not in config_suffix: + raise TypeError( + "Blueprint Config must be one of the following types: .toml / .tml" + ) + + config = process_dict(toml_load(config_file_path), key_case_switch="upper") + enabled = cast_to_bool(config.get("ENABLED", False)) + + if not enabled: + return enabled, {}, {}, {} + + session = process_dict(config.get("SESSION", {}), key_case_switch="ignore") + settings = process_dict(config.get("SETTINGS", {}), key_case_switch="lower") + database_bind = process_dict( + config.get("DATABASE_BIND", {}), key_case_switch="upper" + ) + + kwargs = {} + + valid_settings = ( + "url_prefix", + "subdomain", + "url_defaults", + "static_folder", + "template_folder", + "static_url_path", + "root_path", + ) + + for setting in valid_settings: + if setting == "url_prefix": + kwargs.update( + { + "url_prefix": settings.get("url_prefix") + if settings.get("url_prefix") != "" + else f"/{blueprint_name}" + } + ) + continue + if setting in settings: + if settings.get(setting, False): + kwargs.update({setting: settings.get(setting)}) + + return enabled, session, settings, database_bind diff --git a/quart_imp/imp.py b/quart_imp/imp.py new file mode 100644 index 0000000..a247fe8 --- /dev/null +++ b/quart_imp/imp.py @@ -0,0 +1,791 @@ +import asyncio +import logging +import os +from functools import partial +from importlib import import_module +from inspect import getmembers +from inspect import isclass +from pathlib import Path +from typing import Dict, Union, Optional, List + +from flask_sqlalchemy.model import DefaultMeta +from quart import Blueprint, session + +from .helpers import _init_app_config +from .protocols import Quart, ImpBlueprint +from .registeries import ModelRegistry +from .utilities import cast_to_import_str + + +class Imp: + app: Quart + app_name: str + app_path: Path + app_folder: Path + app_resources_imported: bool = False + + __model_registry__: ModelRegistry + + config_path: Path + config: Dict + + def __init__( + self, + app: Optional[Quart] = None, + app_config_file: Optional[str] = None, + ignore_missing_env_variables: bool = False, + ) -> None: + if app is not None: + self.init_app(app, app_config_file, ignore_missing_env_variables) + + def init_app( + self, + app: Quart, + app_config_file: Optional[str] = os.environ.get("IMP_CONFIG"), + ignore_missing_env_variables: bool = False, + ) -> None: + """ + Initializes the quart app to work with quart-imp. + + :raw-html:`
` + + If no `app_config_file` specified, an attempt to read `IMP_CONFIG` from the environment will be made. + + :raw-html:`
` + + If `IMP_CONFIG` is not in the environment variables, an attempt to load `default.config.toml` will be made. + + :raw-html:`
` + + `default.config.toml` will be created, and used if not found. + + :raw-html:`
` + + ----- + + :param app: The quart app to initialize. + :param app_config_file: The config file to use. + :param ignore_missing_env_variables: Will ignore missing environment variables in the config if set to True. + :return: None + """ + + if app is None: + raise ImportError( + "No app was passed in, do ba = Imp(quartapp) or app.initapp(quartapp)" + ) + if not isinstance(app, Quart): + raise TypeError("The app that was passed in is not an instance of Quart") + + if app_config_file is None: + app_config_file = "default.config.toml" + + self.app = app + + if "imp" in self.app.extensions: + raise ImportError("The app has already been initialized with quart-imp.") + + self.app_name = app.name + self.app_path = Path(self.app.root_path) + self.app_folder = self.app_path.parent + self.config_path = self.app_path / app_config_file + + self.config = _init_app_config( + self.config_path, ignore_missing_env_variables, self.app + ) + + self.__model_registry__ = ModelRegistry() + self.app.extensions["imp"] = self + + def import_app_resources( + self, + folder: str = "resources", + factories: Optional[List] = None, + static_folder: str = "static", + templates_folder: str = "templates", + files_to_import: Optional[List] = None, + folders_to_import: Optional[List] = None, + ) -> None: + """ + Import standard app resources from the specified folder. + + :raw-html:`
` + + This will import any resources that have been set to the Quart app. Routes, context processors, cli, etc. + + :raw-html:`
` + + **Can only be called once.** + + :raw-html:`
` + + If no static and or template folder is found, the static and or template folder will be set to None + in the Quart app config. + + :raw-html:`
` + + **Small example of usage:** + + :raw-html:`
` + + .. code-block:: text + + imp.import_app_resources(folder="resources") + # or + imp.import_app_resources() + # as the default folder is "resources" + + :raw-html:`
` + + This will import all files in the resources folder, and set the Quart app static and template folders to + `resources/static` and `resources/templates` respectively. + + :raw-html:`
` + + --- + `resources` folder structure + --- + + .. code-block:: text + + app + ├── resources + │ ├── routes.py + │ ├── app_fac.py + │ ├── static + │ │ └── css + │ │ └── style.css + │ └── templates + │ └── index.html + └── ... + ... + + :raw-html:`
` + + --- + `routes.py` file + --- + + .. code-block:: + + from quart import current_app as app + from quart import render_template + + @app.route("/") + def index(): + return render_template("index.html") + + :raw-html:`
` + + **How factories work** + + :raw-html:`
` + + Factories are functions that are called when importing the app resources. Here's an example: + + :raw-html:`
` + + .. code-block:: + + imp.import_app_resources(folder="resources", factories=["development_cli"]) + + :raw-html:`
` + + ["development_cli"] => development_cli(app) function will be called, and the current app will be passed in. + + :raw-html:`
` + + --- `app_fac.py` file --- + + .. code-block:: + + def development_cli(app): + @app.cli.command("dev") + def dev(): + print("dev cli command") + + :raw-html:`
` + + **Scoping imports** + + :raw-html:`
` + + By default, all files and folders will be imported. To disable this, set files_to_import and or + folders_to_import to [None]. + + :raw-html:`
` + + .. code-block:: + + imp.import_app_resources(files_to_import=[None], folders_to_import=[None]) + + :raw-html:`
` + + To scope the imports, set the files_to_import and or folders_to_import to a list of files and or folders. + + :raw-html:`
` + + files_to_import=["cli.py", "routes.py"] => will only import the files `resources/cli.py` + and `resources/routes.py` + + :raw-html:`
` + + folders_to_import=["template_filters", "context_processors"] => will import all files in the folders + `resources/template_filters/*.py` and `resources/context_processors/*.py` + + :raw-html:`
` + + ----- + + :param folder: The folder to import from, must be relative. + :param factories: A list of function names to call with the app instance. + :param static_folder: The name of the static folder (if not found will be set to None) + :param templates_folder: The name of the templates folder (if not found will be set to None) + :param files_to_import: A list of files to import e.g. ["cli.py", "routes.py"] set to ["*"] to import all. + :param folders_to_import: A list of folders to import e.g. ["cli", "routes"] set to ["*"] to import all. + :return: None + """ + + async def run_async( + imp: "Imp", + folder: str = "resources", + factories: Optional[List] = None, + static_folder: str = "static", + templates_folder: str = "templates", + files_to_import: Optional[List] = None, + folders_to_import: Optional[List] = None, + ): + async with imp.app.app_context(): + if factories is None: + factories = [] + + if files_to_import is None: + files_to_import = ["*"] + + if folders_to_import is None: + folders_to_import = ["*"] + + if self.app_resources_imported: + raise ImportError("The app resources can only be imported once.") + + self.app_resources_imported = True + + def process_module(import_location: str) -> tuple: + def gm(mf): + return getmembers(mf) + + module_file = import_module(import_location) + quart_instance = ( + True + if [name for name, value in gm(module_file) if isinstance(value, Quart)] + else False + ) + + return module_file, quart_instance + + resources_folder = self.app_path / folder + app_static_folder = resources_folder / static_folder + app_templates_folder = resources_folder / templates_folder + + if not resources_folder.exists(): + raise ImportError( + f"Cannot find resources collection folder at {resources_folder}" + ) + + if not resources_folder.is_dir(): + raise ImportError(f"Global collection must be a folder {resources_folder}") + + self.app.static_folder = ( + app_static_folder.as_posix() if app_static_folder.exists() else None + ) + self.app.template_folder = ( + app_templates_folder.as_posix() if app_templates_folder.exists() else None + ) + + import_all_files = True if "*" in files_to_import else False + import_all_folders = True if "*" in folders_to_import else False + + skip_folders = ( + "static", + "templates", + ) + + for item in resources_folder.iterdir(): + # iter over files and folders in the resources folder + if item.is_file() and item.suffix == ".py": + # only pull in python files + if not import_all_files: + # if import_all_files is False, only import the files in the list + if item.name not in files_to_import: + continue + + file_module = import_module(cast_to_import_str(self.app_name, item)) + + for instance_factory in factories: + if hasattr(file_module, instance_factory): + getattr(file_module, instance_factory)(self.app) + + if item.is_dir(): + # item is a folder + + if item.name in skip_folders: + # skip the static and templates folders + continue + + if not import_all_folders: + # if import_all_folders is False, only import the folders in the list + if item.name not in folders_to_import: + continue + + for py_file in item.glob("*.py"): + dir_module = import_module( + f"{cast_to_import_str(self.app_name, item)}.{py_file.stem}" + ) + + for instance_factory in factories: + if hasattr(dir_module, instance_factory): + getattr(dir_module, instance_factory)(self.app) + + pfunc = partial( + run_async, + self, + folder=folder, + factories=factories, + static_folder=static_folder, + templates_folder=templates_folder, + files_to_import=files_to_import, + folders_to_import=folders_to_import, + ) + + asyncio.run(pfunc()) + + def init_session(self) -> None: + """ + Initialize the session variables found in the config. Commonly used in `app.before_request`. + + :raw-html:`
` + + .. code-block:: + + @app.before_request + def before_request(): + imp.init_session() + + :raw-html:`
` + + ----- + + :return: None + """ + if self.config.get("SESSION"): + for key, value in self.config.get("SESSION", {}).items(): + if key not in session: + session[key] = value + + def import_blueprint(self, blueprint: str) -> None: + """ + Import a specified Quart-Imp or standard Quart Blueprint. + + :raw-html:`
` + + **Must be setup in a Python package** + + :raw-html:`
` + + **Example of a Quart-Imp Blueprint:** + + :raw-html:`
` + + Will look for a config.toml file in the blueprint folder. + + :raw-html:`
` + + --- Folder structure --- + .. code-block:: text + + app + ├── my_blueprint + │ ├── routes + │ │ └── index.py + │ ├── static + │ │ └── css + │ │ └── style.css + │ ├── templates + │ │ └── my_blueprint + │ │ └── index.html + │ ├── __init__.py + │ └── config.toml + └── ... + + :raw-html:`
` + + --- __init__.py --- + + .. code-block:: + + from quart_imp import Blueprint + + bp = Blueprint(__name__) + + bp.import_resources("routes") + + + @bp.beforeapp_request + def beforeapp_request(): + bp.init_session() + + + :raw-html:`
` + + --- config.toml --- + + .. code-block:: + + enabled = "yes" + + [settings] + url_prefix = "/my-blueprint" + #subdomain = "" + #url_defaults = { } + #static_folder = "static" + #template_folder = "templates" + #static_url_path = "/my-blueprint/static" + #root_path = "" + #cli_group = "" + + [session] + session_values_used_by_blueprint = "will be set by bp.init_session()" + + :raw-html:`
` + + **Example of a standard Quart Blueprint:** + + :raw-html:`
` + + --- Folder structure --- + + .. code-block:: text + + app + ├── my_blueprint + │ ├── ... + │ └── __init__.py + └── ... + + :raw-html:`
` + + --- __init__.py --- + + .. code-block:: + + from quart import Blueprint + + bp = Blueprint("my_blueprint", __name__, url_prefix="/my-blueprint") + + + @bp.route("/") + def index(): + return "regular_blueprint" + + :raw-html:`
` + + ----- + + :param blueprint: The blueprint (folder name) to import. Must be relative. + :return: None + """ + if Path(blueprint).is_absolute(): + potential_bp = Path(blueprint) + else: + potential_bp = Path(self.app_path / blueprint) + + if potential_bp.exists() and potential_bp.is_dir(): + try: + module = import_module(cast_to_import_str(self.app_name, potential_bp)) + for name, value in getmembers(module): + if isinstance(value, Blueprint) or isinstance(value, ImpBlueprint): + if hasattr(value, "_setup_imp_blueprint"): + if getattr(value, "enabled", False): + value._setup_imp_blueprint(self) + self.app.register_blueprint(value) + else: + logging.debug(f"Blueprint {name} is disabled") + else: + self.app.register_blueprint(value) + + except Exception as e: + raise ImportError(f"Error when importing {potential_bp.name}: {e}") + + def import_blueprints(self, folder: str) -> None: + """ + Imports all the blueprints in the given folder. + + :raw-html:`
` + + **Example folder structure:** + + :raw-html:`
` + + .. code-block:: text + + app + ├── blueprints + │ ├── regular_blueprint + │ │ ├── ... + │ │ └── __init__.py + │ └── quart_imp_blueprint + │ ├── ... + │ ├── config.toml + │ └── __init__.py + └── ... + + :raw-html:`
` + + See: `import_blueprint` for more information. + + :raw-html:`
` + + ----- + + :param folder: The folder to import from. Must be relative. + """ + + folder_path = Path(self.app_path / folder) + + for potential_bp in folder_path.iterdir(): + self.import_blueprint(potential_bp.as_posix()) + + def import_models(self, file_or_folder: str) -> None: + """ + Imports all the models from the given file or folder. + + + :raw-html:`
` + + **Each model found will be added to the model registry.** + + See: `Imp.model()` for more information. + + :raw-html:`
` + + **Example usage from files:** + + :raw-html:`
` + + .. code-block:: + + imp.import_models("users.py") + imp.import_models("cars.py") + + + :raw-html:`
` + + -- Folder structure -- + + .. code-block:: + + app + ├── ... + ├── users.py + ├── cars.py + ├── default.config.toml + └── __init__.py + + :raw-html:`
` + + **Example usage from folders:** + + :raw-html:`
` + + .. code-block:: + + imp.import_models("models") + + :raw-html:`
` + + -- Folder structure -- + + .. code-block:: + + app + ├── ... + ├── models + │ ├── users.py + │ └── cars.py + ├── default.config.toml + └── __init__.py + + :raw-html:`
` + + **Example of model file:** + + :raw-html:`
` + + -- users.py -- + + .. code-block:: + + from app.extensions import db + + class User(db.Model): + attribute = db.Column(db.String(255)) + + :raw-html:`
` + + ----- + + :param file_or_folder: The file or folder to import from. Must be relative. + :return: None + """ + + def model_processor(path: Path): + """ + Picks apart the model from_file and builds a registry of the models found. + """ + import_string = cast_to_import_str(self.app_name, path) + try: + model_module = import_module(import_string) + for name, value in getmembers(model_module, isclass): + if hasattr(value, "__tablename__"): + self.__model_registry__.add(name, value) + + except ImportError as e: + raise ImportError(f"Error when importing {import_string}: {e}") + + if Path(file_or_folder).is_absolute(): + file_or_folder_path = Path(file_or_folder) + else: + file_or_folder_path = Path(self.app_path / file_or_folder) + + if file_or_folder_path.is_file() and file_or_folder_path.suffix == ".py": + model_processor(file_or_folder_path) + + elif file_or_folder_path.is_dir(): + for model_file in [ + _ for _ in file_or_folder_path.iterdir() if "__" not in _.name + ]: + model_processor(model_file) + + def model(self, class_: str) -> DefaultMeta: + """ + Returns the model class for the given ORM class name. + + :raw-html:`
` + + This is used to omit the need to import the models from their locations. + + :raw-html:`
` + + **For example, this:** + + :raw-html:`
` + + .. code-block:: + + from app.models.user import User + from app.models.cars import Cars + + :raw-html:`
` + + **Can be replaced with:** + + :raw-html:`
` + + .. code-block:: + + from app.extensions import imp + + User = imp.model("User") + Cars = imp.model("Cars") + + :raw-html:`
` + + imp.model("User") -> + + :raw-html:`
` + + Although this method is convenient, you lose out on an IDE's ability of attribute and method + suggestions due to the type being unknown. + + :raw-html:`
` + + ----- + :param class_: The class name of the model to return. + :return: The model class [DefaultMeta]. + """ + return self.__model_registry__.class_(class_) + + def model_meta(self, class_: Union[str, DefaultMeta]) -> dict: + """ + Returns meta information for the given ORM class name + + :raw-html:`
` + + **Example:** + + :raw-html:`
` + + .. code-block:: + + from app.extensions import imp + + User = imp.model("User") + + print(imp.model_meta(User)) + # or + print(imp.model_meta("User")) + + :raw-html:`
` + Will output: + + {"location": "app.models.user", "table_name": "user"} + + :raw-html:`
` + + **Advanced use case:** + + `location` can be used to import a function from the model file using Pythons importlib. + + :raw-html:`
` + + Here's an example: + + :raw-html:`
` + + .. code-block:: + + from app.extensions import imp + + + users_meta = imp.model_meta("User") + users_module = import_module(users_meta["location"]) + users_module.some_function() + + :raw-html:`
` + + `table_name` is the snake_case version of the class name, pulled from `__table_name__`, which can be useful + if you'd like to use the table name in a raw query in a route. + + :raw-html:`
` + + ----- + + :param class_: The class name of the model to return [Class Instance | Name of class as String]. + :return: dict of meta-information. + """ + + def check_for_table_name(model_): + if not hasattr(model_, "__tablename__"): + raise AttributeError(f"{model_} is not a valid model") + + if isinstance(class_, str): + model = self.__model_registry__.class_(class_) + check_for_table_name(model) + return { + "location": model.__module__, + "table_name": model.__tablename__, + } + + return { + "location": class_.__module__, + "table_name": class_.__tablename__, + } diff --git a/quart_imp/protocols.py b/quart_imp/protocols.py new file mode 100644 index 0000000..7ced492 --- /dev/null +++ b/quart_imp/protocols.py @@ -0,0 +1,38 @@ +from typing import Protocol, runtime_checkable, Union, Any, Optional + + +@runtime_checkable +class Blueprint(Protocol): + root_path: str + + +@runtime_checkable +class ImpBlueprint(Protocol): + app_path: str + app_config: dict + + settings: dict + + def register_blueprint(self, blueprint: Blueprint): + ... + + def _register(self, app: "Quart", options: dict) -> None: + ... + + def _setup_imp_blueprint(self, imp_instance) -> None: + ... + + +@runtime_checkable +class Quart(Protocol): + name: str + root_path: str + extensions: dict + config: dict + static_folder: Optional[str] + template_folder: Optional[str] + + app_context: Any + + def register_blueprint(self, blueprint: Union[Blueprint, ImpBlueprint]): + ... diff --git a/quart_imp/registeries.py b/quart_imp/registeries.py new file mode 100644 index 0000000..aebbd32 --- /dev/null +++ b/quart_imp/registeries.py @@ -0,0 +1,33 @@ +import typing as t + +from flask_sqlalchemy.model import DefaultMeta + + +class ModelRegistry: + """ + A registry for SQLAlchemy models. + This is used to store all imported SQLAlchemy models in a central location. + Accessible via Imp.__model_registry__ + """ + + registry: t.Dict[str, t.Any] + + def __init__(self): + self.registry = dict() + + def assert_exists(self, class_name: str): + if class_name not in self.registry: + raise KeyError( + f"Model {class_name} not found in model registry \n" + f"Available models: {', '.join(self.registry.keys())}" + ) + + def add(self, ref: str, model: t.Any): + self.registry[ref] = model + + def class_(self, class_name: str) -> DefaultMeta: + self.assert_exists(class_name) + return self.registry[class_name] + + def __repr__(self): + return f"ModelRegistry({self.registry})" diff --git a/quart_imp/security/__init__.py b/quart_imp/security/__init__.py new file mode 100644 index 0000000..1aa28ef --- /dev/null +++ b/quart_imp/security/__init__.py @@ -0,0 +1,13 @@ +from .api_login_check import api_login_check +from .include_csrf import include_csrf +from .login_check import login_check +from .pass_function_check import pass_function_check +from .permission_check import permission_check + +__all__ = [ + "api_login_check", + "include_csrf", + "login_check", + "pass_function_check", + "permission_check", +] diff --git a/quart_imp/security/__private_funcs__.py b/quart_imp/security/__private_funcs__.py new file mode 100644 index 0000000..4f24c2a --- /dev/null +++ b/quart_imp/security/__private_funcs__.py @@ -0,0 +1,25 @@ +import typing as t + + +def _check_against_values_allowed( + session_value: t.Union[list, str, int, bool], + values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool], +) -> bool: + """ + Checks if the session value matches the values allowed. Used by login_check and permission_check. + """ + if isinstance(values_allowed, list): + if isinstance(session_value, list): + for value in session_value: + if value in values_allowed: + return True + return False + + if session_value in values_allowed: + return True + return False + + if session_value == values_allowed: + return True + + return False diff --git a/quart_imp/security/api_login_check.py b/quart_imp/security/api_login_check.py new file mode 100644 index 0000000..798c172 --- /dev/null +++ b/quart_imp/security/api_login_check.py @@ -0,0 +1,66 @@ +import typing as t +from functools import wraps + +from quart import session + +from .__private_funcs__ import _check_against_values_allowed + + +def api_login_check( + session_key: str, + values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool], + fail_json: t.Optional[t.Dict[str, t.Any]] = None, +): + """ + A decorator that is used to secure API routes that return JSON responses. + + :raw-html:`
` + + **Example of a route that requires a user to be logged in:** + + :raw-html:`
` + + .. code-block:: + + @bp.route("/api/resource", methods=["GET"]) + @api_login_check('logged_in', True) + def api_page(): + ... + + :raw-html:`
` + + **You can also supply your own failed return JSON:** + + :raw-html:`
` + + .. code-block:: + + @bp.route("/api/resource", methods=["GET"]) + @api_login_check('logged_in', True, fail_json={"error": "You are not logged in."}) + def api_page(): + ... + + :raw-html:`
` + + ----- + + :param session_key: The session key to check for. + :param values_allowed: A list of or singular value(s) that the session key must contain. + :param fail_json: JSON that is returned on failure. {"error": "You are not logged in."} by default. + :return: The decorated function, or a JSON response. + """ + + def api_login_check_wrapper(func): + @wraps(func) + def inner(*args, **kwargs): + skey = session.get(session_key) + if skey: + if _check_against_values_allowed(skey, values_allowed): + return func(*args, **kwargs) + else: + if fail_json: + return fail_json or {"error": "You are not logged in."} + + return inner + + return api_login_check_wrapper diff --git a/quart_imp/security/include_csrf.py b/quart_imp/security/include_csrf.py new file mode 100644 index 0000000..f84e17d --- /dev/null +++ b/quart_imp/security/include_csrf.py @@ -0,0 +1,76 @@ +from functools import wraps + +from quart import abort +from quart import request +from quart import session + +from quart_imp.auth import generate_csrf_token + + +def include_csrf( + session_key: str = "csrf", form_key: str = "csrf", abort_code: int = 401 +): + """ + A decorator that handles CSRF protection. + + :raw-html:`
` + + On a **GET** request, a CSRF token is generated and stored in the session key + specified by the session_key parameter. + + On a **POST** request, the form_key specified is checked against the session_key + specified. + + :raw-html:`
` + + | If they match, the request is allowed to continue. + | If no match, the response will be abort(abort_code), default 401. + + :raw-html:`
` + + .. code-block:: + + @bp.route("/admin", methods=["GET", "POST"]) + @include_csrf(session_key="csrf", form_key="csrf") + def admin_page(): + ... + # You must pass in the CSRF token from the session into the template. + # Then add to the form. + return render_template("admin.html", csrf=session.get("csrf")) + + :raw-html:`
` + + ----- + + :param session_key: The session key to store the CSRF token in. + :param form_key: The form key to check against the session key. + :param abort_code: The abort code to use if the CSRF check fails. + :return: The decorated function, or abort(abort_code). + """ + + def include_csrf_wrapper(func): + @wraps(func) + def inner(*args, **kwargs): + if request.method == "GET": + session[session_key] = generate_csrf_token() + + return func(*args, **kwargs) + + if request.method == "POST": + _session_key = session.get(session_key) + _form_key = request.form.get(form_key) + + if _form_key is None: + return abort(abort_code) + + if _session_key is None: + return abort(abort_code) + + if _session_key != _form_key: + return abort(abort_code) + + return func(*args, **kwargs) + + return inner + + return include_csrf_wrapper diff --git a/quart_imp/security/login_check.py b/quart_imp/security/login_check.py new file mode 100644 index 0000000..1543af8 --- /dev/null +++ b/quart_imp/security/login_check.py @@ -0,0 +1,118 @@ +import typing as t +from functools import partial +from functools import wraps + +from quart import abort +from quart import flash +from quart import redirect +from quart import session +from quart import url_for + +from .__private_funcs__ import _check_against_values_allowed + + +def login_check( + session_key: str, + values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool], + fail_endpoint: t.Optional[str] = None, + pass_endpoint: t.Optional[str] = None, + endpoint_kwargs: t.Optional[t.Dict[str, t.Union[str, int]]] = None, + message: t.Optional[str] = None, + message_category: str = "message", +): + """ + A decorator that checks if the specified session key exists and contains the specified value. + + :raw-html:`
` + + **Example of a route that requires a user to be logged in:** + + :raw-html:`
` + + .. code-block:: + + @bp.route("/admin", methods=["GET"]) + @login_check('logged_in', True, fail_endpoint='blueprint.login_page', message="Login needed") + def admin_page(): + ... + + :raw-html:`
` + + **Example of a route that if the user is already logged in, redirects to the specified endpoint:** + + :raw-html:`
` + + .. code-block:: + + @bp.route("/login-page", methods=["GET"]) + @login_check('logged_in', True, pass_endpoint='blueprint.admin_page', message="Already logged in") + def login_page(): + ... + + :raw-html:`
` + + ----- + + :param session_key: The session key to check for. + :param values_allowed: A list of or singular value(s) that the session key must contain. + :param fail_endpoint: The endpoint to redirect to if the session key does not exist or + match the pass_value. + :param pass_endpoint: The endpoint to redirect to if the session key passes. + Used to redirect away from login pages, if already logged in. + :param endpoint_kwargs: A dictionary of keyword arguments to pass to the redirect endpoint. + :param message: If a message is specified, a flash message is shown. + :param message_category: The category of the flash message. + :return: The decorated function, or abort(403). + """ + + def login_check_wrapper(func): + @wraps(func) + def inner(*args, **kwargs): + skey = session.get(session_key) + + def setup_flash(_message, _message_category): + if _message: + partial_flash = partial(flash, _message) + if _message_category: + partial_flash(_message_category) + else: + partial_flash() + + if skey is None: + if fail_endpoint: + setup_flash(message, message_category) + + if endpoint_kwargs: + return redirect(url_for(fail_endpoint, **endpoint_kwargs)) + + return redirect(url_for(fail_endpoint)) + + return func(*args, **kwargs) + + if skey is not None: + if _check_against_values_allowed(skey, values_allowed): + if pass_endpoint: + setup_flash(message, message_category) + + if endpoint_kwargs: + return redirect(url_for(pass_endpoint, **endpoint_kwargs)) + + return redirect(url_for(pass_endpoint)) + + return func(*args, **kwargs) + + if fail_endpoint: + setup_flash(message, message_category) + + if endpoint_kwargs: + return redirect(url_for(fail_endpoint, **endpoint_kwargs)) + + return redirect(url_for(fail_endpoint)) + + return func(*args, **kwargs) + + return abort(403) + + return inner + + return login_check_wrapper diff --git a/quart_imp/security/pass_function_check.py b/quart_imp/security/pass_function_check.py new file mode 100644 index 0000000..47b95ab --- /dev/null +++ b/quart_imp/security/pass_function_check.py @@ -0,0 +1,207 @@ +import typing as t +from functools import partial +from functools import wraps + +from quart import abort +from quart import flash +from quart import redirect +from quart import url_for + + +def pass_function_check( + function: t.Callable, + predefined_args: t.Optional[t.Dict] = None, + fail_endpoint: t.Optional[str] = None, + pass_endpoint: t.Optional[str] = None, + endpoint_kwargs: t.Optional[t.Dict[str, t.Union[str, int]]] = None, + message: t.Optional[str] = None, + message_category: str = "message", + fail_on_missing_kwargs: bool = False, + with_app_context: bool = False, +): + """ + A decorator that takes the result of a function and checks if it is True or False. + + URL variables from `@route` will be read by this decorator. + To use URL variables in your passed in function, + make sure your functions argument(s) name(s) match the name(s) of the URL variable(s). + + :raw-html:`
` + + **Example:** + + :raw-html:`
` + + .. code-block:: + + def check_if_number(value): + if isinstance(value, int): + return True + return False + + @bp.route("/admin-page/", methods=["GET"]) + @login_check('logged_in', True, 'blueprint.login_page') # can be mixed with login_check + @pass_function_check( + check_if_number, + predefined_args=None, + fail_endpoint='www.index', + message="Failed message" + ) + def admin_page(): + ... + + @bp.route("/admin-page/", methods=["GET"]) + @login_check('logged_in', True, 'blueprint.login_page') # can be mixed with login_check + @pass_function_check( + check_if_number, + predefined_args={'value': 10}, + fail_endpoint='www.index', + message="Failed message" + ) + def admin_page_overwrite(): + ... + + :raw-html:`
` + + **Advanced use case:** + + Here's an example of accessing quart.session from within the passed in function. including the + `with_app_context` parameter, the function will be called with `app_context()`. + + :raw-html:`
` + + .. code-block:: + + from quart import current_app + from quart import session + + ... + + def check_if_number(number=1, session_=None): + if session_: + print(session_) + try: + int(number) + return True + except ValueError: + return False + + @bp.route("/pass-func-check-with-url-var/", methods=["GET"]) + @pass_function_check( + check_if_number, + predefined_args={'number': 10, 'session_': session}, + fail_endpoint="www.index", + with_app_context=True + ) + def admin_page_overwrite_with_session(): + ... + + :raw-html:`
` + + If you pass in a predefined arg that has the same key name as a session variable that exists, the value + of that predefined arg will be replaced with the session variable value. + + :raw-html:`
` + + .. code-block:: + + session['car'] = 'Toyota' + ... + def check_function(car): + if car == 'Toyota': + return True + return False + ... + @bp.route("/pass-func-check-with-url-var/", methods=["GET"]) + @pass_function_check( + check_function, + predefined_args={'car': session}, + ... + + This will pass, as pass_function_check will replace the value of the predefined arg 'car' with the value + of the session variable 'car'. + + :raw-html:`
` + + ----- + + :param function: The function to call (this will be passed the url variables of the route. + :param predefined_args: A dictionary of predefined arguments to pass to the function. Any keys that match any URL + variables will overwrite the URL variable specified in @route. + :param fail_endpoint: The endpoint to redirect to if the + session key does not exist or does not contain the + specified values. + :param pass_endpoint: The endpoint to redirect to if the function check passes. + :param endpoint_kwargs: A dictionary of keyword arguments to pass to the redirect endpoint. + :param message: If a message is specified, a flash message is shown. + :param message_category: The category of the flash message. + :param fail_on_missing_kwargs: If any of the required arguments for the passed in function are missing + from the url variables, force function result to False. + :param with_app_context: If True, the passed in function will be called with app_context(). + :return: The decorated function, or abort(403). + """ + import inspect + from quart import current_app + from quart.sessions import SessionMixin + + def pass_function_wrapper(func): + @wraps(func) + def inner(*args, **kwargs): + def setup_flash(_message, _message_category): + if _message: + partial_flash = partial(flash, _message) + if _message_category: + partial_flash(_message_category) + else: + partial_flash() + + function_args = dict(inspect.signature(function).parameters) + passed_in_kwargs = {k: v for k, v in kwargs.items() if k in function_args} + + if predefined_args: + passed_in_kwargs.update(predefined_args) + + for key, value in passed_in_kwargs.items(): + if isinstance(value, SessionMixin): + if with_app_context: + with current_app.app_context(): + if key in value: + passed_in_kwargs[key] = value.get(key) + + try: + if with_app_context: + with current_app.app_context(): + func_result = True if function(**passed_in_kwargs) else False + else: + func_result = True if function(**passed_in_kwargs) else False + + except TypeError: + if fail_on_missing_kwargs: + func_result = False + else: + return func(*args, **kwargs) + + if func_result: + if pass_endpoint: + setup_flash(message, message_category) + + if endpoint_kwargs: + return redirect(url_for(pass_endpoint, **endpoint_kwargs)) + + return redirect(url_for(pass_endpoint)) + + return func(*args, **kwargs) + + if fail_endpoint: + setup_flash(message, message_category) + + if endpoint_kwargs: + return redirect(url_for(fail_endpoint, **endpoint_kwargs)) + + return redirect(url_for(fail_endpoint)) + + return abort(403) + + return inner + + return pass_function_wrapper diff --git a/quart_imp/security/permission_check.py b/quart_imp/security/permission_check.py new file mode 100644 index 0000000..9044de6 --- /dev/null +++ b/quart_imp/security/permission_check.py @@ -0,0 +1,83 @@ +import typing as t +from functools import partial +from functools import wraps + +from quart import abort +from quart import flash +from quart import redirect +from quart import session +from quart import url_for + +from .__private_funcs__ import _check_against_values_allowed + + +def permission_check( + session_key: str, + values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool], + fail_endpoint: t.Optional[str] = None, + endpoint_kwargs: t.Optional[t.Dict[str, t.Union[str, int]]] = None, + message: t.Optional[str] = None, + message_category: str = "message", +): + """ + A decorator that checks if the specified session key exists and its value(s) match the specified value(s). + + :raw-html:`
` + + **Example:** + + :raw-html:`
` + + .. code-block:: + + @bp.route("/admin-page", methods=["GET"]) + @login_check('logged_in', True, 'blueprint.login_page') # can be mixed with login_check + @permission_check('permissions', ['admin'], fail_endpoint='www.index', message="Failed message") + def admin_page(): + ... + + :raw-html:`
` + + ----- + + :param session_key: The session key to check for. + :param values_allowed: A list of or singular value(s) that the session key must contain. + :param fail_endpoint: The endpoint to redirect to if the + session key does not exist or does not contain the + specified values. + :param endpoint_kwargs: A dictionary of keyword arguments to pass to the redirect endpoint. + :param message: If a message is specified, a flash message is shown. + :param message_category: The category of the flash message. + :return: The decorated function, or abort(403). + """ + + def permission_check_wrapper(func): + @wraps(func) + def inner(*args, **kwargs): + skey = session.get(session_key) + + def setup_flash(_message, _message_category): + if _message: + partial_flash = partial(flash, _message) + if _message_category: + partial_flash(_message_category) + else: + partial_flash() + + if skey: + if _check_against_values_allowed(skey, values_allowed): + return func(*args, **kwargs) + + setup_flash(message, message_category) + + if fail_endpoint: + if endpoint_kwargs: + return redirect(url_for(fail_endpoint, **endpoint_kwargs)) + + return redirect(url_for(fail_endpoint)) + + return abort(403) + + return inner + + return permission_check_wrapper diff --git a/quart_imp/utilities.py b/quart_imp/utilities.py new file mode 100644 index 0000000..77f3a6c --- /dev/null +++ b/quart_imp/utilities.py @@ -0,0 +1,167 @@ +import functools +import logging +import os +import re +import sys +import typing as t +from pathlib import Path + + +class Sprinkles: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + END = "\033[0m" + + +def deprecated(message: str): + def func_wrapper(func): + @functools.wraps(func) + def proc_function(*args, **kwargs): + logging.critical( + f"{Sprinkles.FAIL}Function deprecated: {message}{Sprinkles.END}" + ) + return func(*args, **kwargs) + + return proc_function + + return func_wrapper + + +def if_env_replace( + env_value: t.Optional[t.Any], ignore_missing_env_variables: bool = False +) -> t.Any: + """ + Looks for the replacement pattern to swap out values in the config file with environment variables. + """ + pattern = re.compile(r"<(.*?)>") + + if isinstance(env_value, str): + if re.match(pattern, env_value): + env_var = re.findall(pattern, env_value)[0] + + if env_var: + if os.environ.get(env_var): + return parse_config_env_var(os.environ.get(env_var)) + + if ignore_missing_env_variables: + return None + + raise ValueError(f"Environment variable {env_value} not found") + + return env_value + + +def process_dict( + this_dict: t.Optional[dict], + key_case_switch: str = "upper", + ignore_missing_env_variables: bool = False, + crawl: bool = False, +) -> dict: + """ + Used to process the config from_file dictionary and replace environment variables. Turns all keys to upper case. + """ + + if this_dict is None: + return {} + + return_dict = {} + for key, value in this_dict.items(): + if key_case_switch == "ignore": + cs_key = key + else: + cs_key = key.upper() if key_case_switch == "upper" else key.lower() + + if crawl: + if isinstance(value, dict): + return_dict[cs_key] = process_dict( + value, key_case_switch, ignore_missing_env_variables, crawl + ) + continue + + return_dict[cs_key] = if_env_replace(value, ignore_missing_env_variables) + + return return_dict + + +def cast_to_import_str(app_name: str, folder_path: Path) -> str: + """ + Takes the folder path and converts it to a string that can be imported + """ + folder_parts = folder_path.parts + parts = folder_parts[folder_parts.index(app_name) :] + if sys.version_info.major == 3: + if sys.version_info.minor < 9: + return ".".join(parts).replace(".py", "") + return ".".join(parts).removesuffix(".py") + raise NotImplementedError("Python version not supported") + + +def snake(value: str) -> str: + """ + Switches name of the class CamelCase to snake_case + """ + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", value) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() + + +def class_field(class_: str, field: str) -> str: + """ + Switches name of the class CamelCase to snake_case and tacks on the field name + + Used for SQLAlchemy foreign key assignments + + INFO ::: This function may not produce the correct information if you are using __tablename__ in your class + """ + return f"{snake(class_)}.{field}" + + +def cast_to_bool(value: t.Union[str, bool, None]) -> bool: + """ + Casts an array of truly string values to a boolean. Used for config files. + """ + if value is None: + return False + if isinstance(value, bool): + return value + if isinstance(value, str): + true_str = ("true", "yes", "y", "1") + false_str = ("false", "no", "n", "0") + + if value.lower() in true_str: + return True + elif value.lower() in false_str: + return False + else: + raise TypeError(f"Cannot cast {value} to bool") + else: + raise TypeError(f"Cannot cast {value} to bool") + + +def parse_config_env_var(value: t.Optional[str]) -> t.Optional[t.Union[bool, str, int]]: + """ + Casts value to a boolean, string, or int if possible. If not, returns none. + """ + if value == "None": + return None + + if isinstance(value, str): + true_str = ("true", "yes", "y", "1") + false_str = ("false", "no", "n", "0") + + if value.lower() in true_str: + return True + elif value.lower() in false_str: + return False + else: + try: + return int(value) + except ValueError: + return value + + return None diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8ac7b61 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Quart +Flask-SQLAlchemy diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..5ab82c6 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,7 @@ +-r requirements.txt +flit +ruff +pytest +pytest-cov +mypy +types-toml \ No newline at end of file diff --git a/requirements_docs.txt b/requirements_docs.txt new file mode 100644 index 0000000..1daf61f --- /dev/null +++ b/requirements_docs.txt @@ -0,0 +1,3 @@ +mistune +pygments +pytz \ No newline at end of file