From caaf17af1254d4ae0fe7932eb584b1c812f7d916 Mon Sep 17 00:00:00 2001 From: David Carmichael Date: Fri, 16 Aug 2024 15:08:51 +0100 Subject: [PATCH] feat: updated to newest flask-imp code. --- .github/workflows/python-publish.yml | 31 + .gitignore | 2 + LICENSE | 502 +--------- README.md | 26 +- app/__init__.py | 23 - app/blueprints/www/__init__.py | 10 - app/blueprints/www/config.toml | 25 - app/blueprints/www/routes/index.py | 8 - app/blueprints/www/static/css/water.css | 880 ------------------ .../www/static/img/quart-imp-logo.png | Bin 7987 -> 0 bytes app/blueprints/www/static/js/main.js | 1 - .../www/templates/www/extends/main.html | 24 - .../www/templates/www/includes/footer.html | 6 - .../www/templates/www/includes/header.html | 10 - app/blueprints/www/templates/www/index.html | 17 - app/default.config.toml | 81 -- app/extensions/__init__.py | 9 - app/models/__init__.py | 12 - app/models/example_user_table.py | 72 -- app/resources/cli/cli.py | 57 -- .../context_processors/context_processors.py | 14 - .../error_handlers/error_handlers.py | 44 - app/resources/filters/filters.py | 30 - app/resources/routes/routes.py | 7 - app/resources/static/favicon.ico | Bin 15086 -> 0 bytes app/resources/templates/errors/400.html | 11 - app/resources/templates/errors/401.html | 11 - app/resources/templates/errors/403.html | 11 - app/resources/templates/errors/404.html | 11 - app/resources/templates/errors/405.html | 11 - app/resources/templates/errors/500.html | 11 - app/resources/templates/index.html | 12 - pyproject.toml | 74 +- quart_imp/__init__.py | 8 - quart_imp/_cli/__init__.py | 74 -- quart_imp/_cli/blueprint.py | 129 --- quart_imp/_cli/filelib/__init__.py | 14 - quart_imp/_cli/filelib/all_files.py | 323 ------- quart_imp/_cli/filelib/app.py | 339 ------- quart_imp/_cli/filelib/main_js.py | 4 - quart_imp/_cli/helpers.py | 26 - quart_imp/_cli/init.py | 243 ----- quart_imp/blueprint.py | 543 ----------- quart_imp/helpers.py | 228 ----- quart_imp/imp.py | 791 ---------------- quart_imp/protocols.py | 38 - quart_imp/registeries.py | 33 - quart_imp/utilities.py | 167 ---- requirements.txt | 2 - requirements/build.in | 1 + requirements/build.txt | 24 + requirements/dev.in | 6 + requirements/dev.txt | 131 +++ requirements_docs.txt => requirements/docs.in | 1 + requirements/docs.txt | 28 + requirements/typing.in | 3 + requirements/typing.txt | 24 + requirements_dev.txt | 7 - src/quart_imp/__init__.py | 11 + src/quart_imp/_cli/__init__.py | 90 ++ src/quart_imp/_cli/blueprint.py | 165 ++++ src/quart_imp/_cli/filelib/__init__.py | 0 src/quart_imp/_cli/filelib/api_blueprint.py | 24 + .../quart_imp}/_cli/filelib/blueprint.py | 97 +- src/quart_imp/_cli/filelib/extensions.py | 6 + .../quart_imp}/_cli/filelib/favicon.py | 0 .../_cli/filelib/head_tag_generator.py | 6 +- src/quart_imp/_cli/filelib/init.py | 77 ++ src/quart_imp/_cli/filelib/main_js.py | 9 + .../quart_imp}/_cli/filelib/quart_imp_logo.py | 0 src/quart_imp/_cli/filelib/resources.py | 168 ++++ src/quart_imp/_cli/filelib/templates.py | 49 + .../quart_imp}/_cli/filelib/water_css.py | 0 src/quart_imp/_cli/helpers.py | 66 ++ src/quart_imp/_cli/init.py | 244 +++++ {quart_imp => src/quart_imp}/auth/__init__.py | 0 .../quart_imp}/auth/__legacy__.py | 3 +- .../quart_imp}/auth/__private_funcs__.py | 6 +- .../quart_imp}/auth/authenticate_password.py | 50 +- .../quart_imp}/auth/dataclasses.py | 0 .../quart_imp}/auth/encrypt_password.py | 0 .../auth/generate_alphanumeric_validator.py | 0 .../quart_imp}/auth/generate_csrf_token.py | 0 .../auth/generate_email_validator.py | 0 .../auth/generate_numeric_validator.py | 0 .../quart_imp}/auth/generate_password.py | 0 .../quart_imp}/auth/generate_private_key.py | 0 .../quart_imp}/auth/generate_salt.py | 0 .../quart_imp}/auth/is_email_address_valid.py | 0 .../quart_imp}/auth/is_username_valid.py | 0 src/quart_imp/config/__init__.py | 9 + src/quart_imp/config/imp_blueprint_config.py | 55 ++ src/quart_imp/config/imp_config.py | 14 + src/quart_imp/config/quart_config.py | 162 ++++ src/quart_imp/exceptions.py | 2 + src/quart_imp/imp.py | 294 ++++++ src/quart_imp/imp_blueprint.py | 167 ++++ src/quart_imp/protocols.py | 101 ++ .../quart_imp}/security/__init__.py | 0 .../quart_imp}/security/__private_funcs__.py | 2 +- .../quart_imp}/security/api_login_check.py | 13 +- .../quart_imp}/security/include_csrf.py | 10 +- .../quart_imp}/security/login_check.py | 53 +- .../security/pass_function_check.py | 48 +- .../quart_imp}/security/permission_check.py | 27 +- src/quart_imp/utilities.py | 73 ++ 106 files changed, 2265 insertions(+), 5066 deletions(-) create mode 100644 .github/workflows/python-publish.yml delete mode 100644 app/__init__.py delete mode 100644 app/blueprints/www/__init__.py delete mode 100644 app/blueprints/www/config.toml delete mode 100644 app/blueprints/www/routes/index.py delete mode 100644 app/blueprints/www/static/css/water.css delete mode 100644 app/blueprints/www/static/img/quart-imp-logo.png delete mode 100644 app/blueprints/www/static/js/main.js delete mode 100644 app/blueprints/www/templates/www/extends/main.html delete mode 100644 app/blueprints/www/templates/www/includes/footer.html delete mode 100644 app/blueprints/www/templates/www/includes/header.html delete mode 100644 app/blueprints/www/templates/www/index.html delete mode 100644 app/default.config.toml delete mode 100644 app/extensions/__init__.py delete mode 100644 app/models/__init__.py delete mode 100644 app/models/example_user_table.py delete mode 100644 app/resources/cli/cli.py delete mode 100644 app/resources/context_processors/context_processors.py delete mode 100644 app/resources/error_handlers/error_handlers.py delete mode 100644 app/resources/filters/filters.py delete mode 100644 app/resources/routes/routes.py delete mode 100644 app/resources/static/favicon.ico delete mode 100644 app/resources/templates/errors/400.html delete mode 100644 app/resources/templates/errors/401.html delete mode 100644 app/resources/templates/errors/403.html delete mode 100644 app/resources/templates/errors/404.html delete mode 100644 app/resources/templates/errors/405.html delete mode 100644 app/resources/templates/errors/500.html delete mode 100644 app/resources/templates/index.html delete mode 100644 quart_imp/__init__.py delete mode 100644 quart_imp/_cli/__init__.py delete mode 100644 quart_imp/_cli/blueprint.py delete mode 100644 quart_imp/_cli/filelib/__init__.py delete mode 100644 quart_imp/_cli/filelib/all_files.py delete mode 100644 quart_imp/_cli/filelib/app.py delete mode 100644 quart_imp/_cli/filelib/main_js.py delete mode 100644 quart_imp/_cli/helpers.py delete mode 100644 quart_imp/_cli/init.py delete mode 100644 quart_imp/blueprint.py delete mode 100644 quart_imp/helpers.py delete mode 100644 quart_imp/imp.py delete mode 100644 quart_imp/protocols.py delete mode 100644 quart_imp/registeries.py delete mode 100644 quart_imp/utilities.py delete mode 100644 requirements.txt create mode 100644 requirements/build.in create mode 100644 requirements/build.txt create mode 100644 requirements/dev.in create mode 100644 requirements/dev.txt rename requirements_docs.txt => requirements/docs.in (77%) create mode 100644 requirements/docs.txt create mode 100644 requirements/typing.in create mode 100644 requirements/typing.txt delete mode 100644 requirements_dev.txt create mode 100644 src/quart_imp/__init__.py create mode 100644 src/quart_imp/_cli/__init__.py create mode 100644 src/quart_imp/_cli/blueprint.py create mode 100644 src/quart_imp/_cli/filelib/__init__.py create mode 100644 src/quart_imp/_cli/filelib/api_blueprint.py rename {quart_imp => src/quart_imp}/_cli/filelib/blueprint.py (59%) create mode 100644 src/quart_imp/_cli/filelib/extensions.py rename {quart_imp => src/quart_imp}/_cli/filelib/favicon.py (100%) rename {quart_imp => src/quart_imp}/_cli/filelib/head_tag_generator.py (68%) create mode 100644 src/quart_imp/_cli/filelib/init.py create mode 100644 src/quart_imp/_cli/filelib/main_js.py rename {quart_imp => src/quart_imp}/_cli/filelib/quart_imp_logo.py (100%) create mode 100644 src/quart_imp/_cli/filelib/resources.py create mode 100644 src/quart_imp/_cli/filelib/templates.py rename {quart_imp => src/quart_imp}/_cli/filelib/water_css.py (100%) create mode 100644 src/quart_imp/_cli/helpers.py create mode 100644 src/quart_imp/_cli/init.py rename {quart_imp => src/quart_imp}/auth/__init__.py (100%) rename {quart_imp => src/quart_imp}/auth/__legacy__.py (99%) rename {quart_imp => src/quart_imp}/auth/__private_funcs__.py (84%) rename {quart_imp => src/quart_imp}/auth/authenticate_password.py (59%) rename {quart_imp => src/quart_imp}/auth/dataclasses.py (100%) rename {quart_imp => src/quart_imp}/auth/encrypt_password.py (100%) rename {quart_imp => src/quart_imp}/auth/generate_alphanumeric_validator.py (100%) rename {quart_imp => src/quart_imp}/auth/generate_csrf_token.py (100%) rename {quart_imp => src/quart_imp}/auth/generate_email_validator.py (100%) rename {quart_imp => src/quart_imp}/auth/generate_numeric_validator.py (100%) rename {quart_imp => src/quart_imp}/auth/generate_password.py (100%) rename {quart_imp => src/quart_imp}/auth/generate_private_key.py (100%) rename {quart_imp => src/quart_imp}/auth/generate_salt.py (100%) rename {quart_imp => src/quart_imp}/auth/is_email_address_valid.py (100%) rename {quart_imp => src/quart_imp}/auth/is_username_valid.py (100%) create mode 100644 src/quart_imp/config/__init__.py create mode 100644 src/quart_imp/config/imp_blueprint_config.py create mode 100644 src/quart_imp/config/imp_config.py create mode 100644 src/quart_imp/config/quart_config.py create mode 100644 src/quart_imp/exceptions.py create mode 100644 src/quart_imp/imp.py create mode 100644 src/quart_imp/imp_blueprint.py create mode 100644 src/quart_imp/protocols.py rename {quart_imp => src/quart_imp}/security/__init__.py (100%) rename {quart_imp => src/quart_imp}/security/__private_funcs__.py (92%) rename {quart_imp => src/quart_imp}/security/api_login_check.py (87%) rename {quart_imp => src/quart_imp}/security/include_csrf.py (89%) rename {quart_imp => src/quart_imp}/security/login_check.py (62%) rename {quart_imp => src/quart_imp}/security/pass_function_check.py (80%) rename {quart_imp => src/quart_imp}/security/permission_check.py (73%) create mode 100644 src/quart_imp/utilities.py diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..fd59aa5 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,31 @@ +name: Upload Python Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flit + - name: Build package + run: flit build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index b270892..4ea2fb8 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,5 @@ cython_debug/ /old_code/ /Dockerfile /src_dif/ +/_archive/ +/app/ diff --git a/LICENSE b/LICENSE index 069c736..cb6f2c6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,483 +1,19 @@ - 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 +Copyright 2024 David Carmichael + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index c18fec2..6b57eaa 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,9 @@ ## What is Quart-Imp? -Quart-Imp's main purpose is to help simplify the importing of blueprints, resources, and models. +Quart-Imp's main purpose is to help simplify the importing of blueprints, and resources. It has a few extra features built in to help with securing pages and password authentication. -## Note - -**Quart-Flask-Patch is required to use Quart-Imp.** - ## Generate a Quart app ```bash @@ -42,15 +38,9 @@ project/ `# app/extensions/__init__.py` ```python -import quart_flask_patch -from flask_sqlalchemy import SQLAlchemy - from quart_imp import Imp -_ = quart_flask_patch - imp = Imp() -db = SQLAlchemy() ``` `# app/__init__.py` @@ -58,25 +48,15 @@ db = SQLAlchemy() ```python from quart import Quart -from app.extensions import imp, db +from app.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_app_resources() 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/__init__.py b/app/__init__.py deleted file mode 100644 index 56a3beb..0000000 --- a/app/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 9a040ef..0000000 --- a/app/blueprints/www/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index e5d4da6..0000000 --- a/app/blueprints/www/config.toml +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 5789a23..0000000 --- a/app/blueprints/www/routes/index.py +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 877e139..0000000 --- a/app/blueprints/www/static/css/water.css +++ /dev/null @@ -1,880 +0,0 @@ -/** - * 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 deleted file mode 100644 index 1140eb4cc5c4f9538ea0a3c788db40a8fe08a663..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7987 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 diff --git a/app/blueprints/www/static/js/main.js b/app/blueprints/www/static/js/main.js deleted file mode 100644 index 695c8bd..0000000 --- a/app/blueprints/www/static/js/main.js +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 4c0f54a..0000000 --- a/app/blueprints/www/templates/www/extends/main.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - 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 deleted file mode 100644 index e0c54f7..0000000 --- a/app/blueprints/www/templates/www/includes/footer.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

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 deleted file mode 100644 index e3cd2e3..0000000 --- a/app/blueprints/www/templates/www/includes/header.html +++ /dev/null @@ -1,10 +0,0 @@ -
- 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 deleted file mode 100644 index 8554519..0000000 --- a/app/blueprints/www/templates/www/index.html +++ /dev/null @@ -1,17 +0,0 @@ -{% 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 deleted file mode 100644 index 91f7a8a..0000000 --- a/app/default.config.toml +++ /dev/null @@ -1,81 +0,0 @@ -# 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 deleted file mode 100644 index 5c1cf18..0000000 --- a/app/extensions/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 4736b83..0000000 --- a/app/models/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 853ca5f..0000000 --- a/app/models/example_user_table.py +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index cdbd9b9..0000000 --- a/app/resources/cli/cli.py +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index b1215af..0000000 --- a/app/resources/context_processors/context_processors.py +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index b31d620..0000000 --- a/app/resources/error_handlers/error_handlers.py +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index bbfb78f..0000000 --- a/app/resources/filters/filters.py +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 28700ac..0000000 --- a/app/resources/routes/routes.py +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index e734eba0d25de4264768b9e0d33d0caea8c763b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 k)YDerM3bNC#{tAj5ijjQEVYA3V} - - - - 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 deleted file mode 100644 index 81be123..0000000 --- a/app/resources/templates/errors/401.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - 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 deleted file mode 100644 index 1e0733c..0000000 --- a/app/resources/templates/errors/403.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - 403 Forbidden - - - -

Access forbidden!

- - diff --git a/app/resources/templates/errors/404.html b/app/resources/templates/errors/404.html deleted file mode 100644 index 6e6254b..0000000 --- a/app/resources/templates/errors/404.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - 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 deleted file mode 100644 index 3f1c549..0000000 --- a/app/resources/templates/errors/405.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - 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 deleted file mode 100644 index 2deb58e..0000000 --- a/app/resources/templates/errors/500.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - 500 Server Error! - - - -

There has been a server error!

- - diff --git a/app/resources/templates/index.html b/app/resources/templates/index.html deleted file mode 100644 index cd28e3f..0000000 --- a/app/resources/templates/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - 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 index 8d7616c..793c0cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,6 @@ -[build-system] -requires = ["flit_core >=3.2,<4"] -build-backend = "flit_core.buildapi" - [project] name = "quart-imp" +version = "1.0.0" description = 'A Quart auto importer that allows your Quart apps to grow big.' authors = [{ name = "David Carmichael", email = "david@uilix.com" }] readme = "README.md" @@ -11,9 +8,8 @@ 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+)', + 'License :: OSI Approved :: MIT License', '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', @@ -22,40 +18,56 @@ classifiers = [ 'Topic :: Software Development :: Libraries :: Python Modules', 'Natural Language :: English', ] -requires-python = ">=3.8" -dynamic = ["version"] +requires-python = ">=3.9" dependencies = [ - 'click', 'Quart', - 'quart-flask-patch', - 'Flask-SQLAlchemy', - 'toml', - 'more-itertools' ] +[project.urls] +Documentation = "https://cheesecake87.github.io/flask-imp/" +Source = "https://github.com/CheeseCake87/flask-imp" + [project.scripts] quart-imp = "quart_imp._cli:cli" -[project.urls] -Documentation = "https://cheesecake87.github.io/quart-imp/" -Source = "https://github.com/CheeseCake87/quart-imp" +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[tool.pyqwe] +install = "*:flit install --symlink" +build = "*:flit build" +publish = "*shell:export=FLIT_USERNAME=__token__ && flit publish" +docs = "*:flask --app docs compile" +docs-watch = "*:flask --app docs compile --watch" [tool.flit.sdist] exclude = [ - ".github/", - "_assets/", - "app/", - "dist/", - "docs/", - "docs_gen/", - "test_app/", - "test_docker/", - "tests/", - ".env", + ".github", + "_assets", + "app", + "instance", + "dist", + "docs", + "tests_docker", ".gitignore", - "docker-compose.yaml", - "Dockerfile", - "requirements_build.txt", - "requirements_dev.txt", - "requirements_docs.txt", + ".env", ] + +[tool.mypy] +python_version = "3.9" +files = ["src/quart_imp"] +show_error_codes = true +pretty = true +strict = true + +[tool.pyright] +pythonVersion = "3.9" +include = ["src/quart_imp"] +typeCheckingMode = "basic" + +[tool.ruff] +src = ["src"] +fix = true +show-fixes = true +output-format = "full" \ No newline at end of file diff --git a/quart_imp/__init__.py b/quart_imp/__init__.py deleted file mode 100644 index c68e126..0000000 --- a/quart_imp/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -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.2" - -__all__ = ["Auth", "PasswordGeneration", "Imp", "Blueprint"] diff --git a/quart_imp/_cli/__init__.py b/quart_imp/_cli/__init__.py deleted file mode 100644 index ba577ed..0000000 --- a/quart_imp/_cli/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 7bf62b9..0000000 --- a/quart_imp/_cli/blueprint.py +++ /dev/null @@ -1,129 +0,0 @@ -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 deleted file mode 100644 index f1f9b99..0000000 --- a/quart_imp/_cli/filelib/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 57ab3c2..0000000 --- a/quart_imp/_cli/filelib/all_files.py +++ /dev/null @@ -1,323 +0,0 @@ -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 deleted file mode 100644 index 86a99a5..0000000 --- a/quart_imp/_cli/filelib/app.py +++ /dev/null @@ -1,339 +0,0 @@ -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/main_js.py b/quart_imp/_cli/filelib/main_js.py deleted file mode 100644 index 536ab99..0000000 --- a/quart_imp/_cli/filelib/main_js.py +++ /dev/null @@ -1,4 +0,0 @@ -# format: main_js -main_js = """\ -console.log('This log is from the file {main_js}') -""" diff --git a/quart_imp/_cli/helpers.py b/quart_imp/_cli/helpers.py deleted file mode 100644 index 6126368..0000000 --- a/quart_imp/_cli/helpers.py +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 45d4c17..0000000 --- a/quart_imp/_cli/init.py +++ /dev/null @@ -1,243 +0,0 @@ -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/blueprint.py b/quart_imp/blueprint.py deleted file mode 100644 index 759c18f..0000000 --- a/quart_imp/blueprint.py +++ /dev/null @@ -1,543 +0,0 @@ -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 deleted file mode 100644 index 52f32e9..0000000 --- a/quart_imp/helpers.py +++ /dev/null @@ -1,228 +0,0 @@ -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 deleted file mode 100644 index a247fe8..0000000 --- a/quart_imp/imp.py +++ /dev/null @@ -1,791 +0,0 @@ -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 deleted file mode 100644 index 7ced492..0000000 --- a/quart_imp/protocols.py +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index aebbd32..0000000 --- a/quart_imp/registeries.py +++ /dev/null @@ -1,33 +0,0 @@ -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/utilities.py b/quart_imp/utilities.py deleted file mode 100644 index 77f3a6c..0000000 --- a/quart_imp/utilities.py +++ /dev/null @@ -1,167 +0,0 @@ -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 deleted file mode 100644 index 8ac7b61..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Quart -Flask-SQLAlchemy diff --git a/requirements/build.in b/requirements/build.in new file mode 100644 index 0000000..c5b71ce --- /dev/null +++ b/requirements/build.in @@ -0,0 +1 @@ +flit \ No newline at end of file diff --git a/requirements/build.txt b/requirements/build.txt new file mode 100644 index 0000000..f114cb6 --- /dev/null +++ b/requirements/build.txt @@ -0,0 +1,24 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile build.in +# +certifi==2024.7.4 + # via requests +charset-normalizer==3.3.2 + # via requests +docutils==0.21.2 + # via flit +flit==3.9.0 + # via -r build.in +flit-core==3.9.0 + # via flit +idna==3.7 + # via requests +requests==2.32.3 + # via flit +tomli-w==1.0.0 + # via flit +urllib3==2.2.2 + # via requests diff --git a/requirements/dev.in b/requirements/dev.in new file mode 100644 index 0000000..211affc --- /dev/null +++ b/requirements/dev.in @@ -0,0 +1,6 @@ +-r docs.txt +-r typing.txt +-r build.txt +quart +ruff +pyqwe \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..4387c14 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,131 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile dev.in +# +aiofiles==24.1.0 + # via quart +blinker==1.8.2 + # via + # flask + # quart +certifi==2024.7.4 + # via + # -r build.txt + # requests +charset-normalizer==3.3.2 + # via + # -r build.txt + # requests +click==8.1.7 + # via + # flask + # quart +docutils==0.21.2 + # via + # -r build.txt + # flit +exceptiongroup==1.2.2 + # via -r typing.txt +flask==3.0.3 + # via quart +flit==3.9.0 + # via -r build.txt +flit-core==3.9.0 + # via + # -r build.txt + # flit +h11==0.14.0 + # via + # hypercorn + # wsproto +h2==4.1.0 + # via hypercorn +hpack==4.0.0 + # via h2 +hypercorn==0.17.3 + # via quart +hyperframe==6.0.1 + # via h2 +idna==3.7 + # via + # -r build.txt + # requests +iniconfig==2.0.0 + # via + # -r typing.txt + # pytest +itsdangerous==2.2.0 + # via + # flask + # quart +jinja2==3.1.4 + # via + # flask + # quart +markupsafe==2.1.5 + # via + # jinja2 + # quart + # werkzeug +mistune==3.0.2 + # via -r docs.txt +mypy==1.11.1 + # via -r typing.txt +mypy-extensions==1.0.0 + # via + # -r typing.txt + # mypy +nodeenv==1.9.1 + # via + # -r typing.txt + # pyright +packaging==24.1 + # via + # -r typing.txt + # pytest +pluggy==1.5.0 + # via + # -r typing.txt + # pytest +priority==2.0.0 + # via hypercorn +pygments==2.18.0 + # via -r docs.txt +pyqwe==1.5.2 + # via -r dev.in +pyright==1.1.375 + # via -r typing.txt +pytest==8.3.2 + # via -r typing.txt +pytz==2024.1 + # via -r docs.txt +quart==0.19.6 + # via -r dev.in +requests==2.32.3 + # via + # -r build.txt + # flit +ruff==0.5.6 + # via -r dev.in +tomli==2.0.1 + # via -r typing.txt +tomli-w==1.0.0 + # via + # -r build.txt + # flit +typing-extensions==4.12.2 + # via + # -r typing.txt + # mypy +urllib3==2.2.2 + # via + # -r build.txt + # requests +werkzeug==3.0.3 + # via + # flask + # quart +wsproto==1.2.0 + # via hypercorn diff --git a/requirements_docs.txt b/requirements/docs.in similarity index 77% rename from requirements_docs.txt rename to requirements/docs.in index 1daf61f..7287cbc 100644 --- a/requirements_docs.txt +++ b/requirements/docs.in @@ -1,3 +1,4 @@ +flask mistune pygments pytz \ No newline at end of file diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 0000000..cc35f66 --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,28 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile docs.in +# +blinker==1.8.2 + # via flask +click==8.1.7 + # via flask +flask==3.0.3 + # via -r docs.in +itsdangerous==2.2.0 + # via flask +jinja2==3.1.4 + # via flask +markupsafe==2.1.5 + # via + # jinja2 + # werkzeug +mistune==3.0.2 + # via -r docs.in +pygments==2.18.0 + # via -r docs.in +pytz==2024.1 + # via -r docs.in +werkzeug==3.0.3 + # via flask diff --git a/requirements/typing.in b/requirements/typing.in new file mode 100644 index 0000000..198ab7f --- /dev/null +++ b/requirements/typing.in @@ -0,0 +1,3 @@ +mypy +pyright +pytest \ No newline at end of file diff --git a/requirements/typing.txt b/requirements/typing.txt new file mode 100644 index 0000000..641a479 --- /dev/null +++ b/requirements/typing.txt @@ -0,0 +1,24 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile typing.in +# +iniconfig==2.0.0 + # via pytest +mypy==1.11.1 + # via -r typing.in +mypy-extensions==1.0.0 + # via mypy +nodeenv==1.9.1 + # via pyright +packaging==24.1 + # via pytest +pluggy==1.5.0 + # via pytest +pyright==1.1.375 + # via -r typing.in +pytest==8.3.2 + # via -r typing.in +typing-extensions==4.12.2 + # via mypy diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 5ab82c6..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,7 +0,0 @@ --r requirements.txt -flit -ruff -pytest -pytest-cov -mypy -types-toml \ No newline at end of file diff --git a/src/quart_imp/__init__.py b/src/quart_imp/__init__.py new file mode 100644 index 0000000..4b30450 --- /dev/null +++ b/src/quart_imp/__init__.py @@ -0,0 +1,11 @@ +from .auth import Auth as Auth +from .auth import PasswordGeneration as PasswordGeneration +from .imp import Imp as Imp +from .imp_blueprint import ImpBlueprint + +__all__ = [ + "Auth", + "PasswordGeneration", + "Imp", + "ImpBlueprint", +] diff --git a/src/quart_imp/_cli/__init__.py b/src/quart_imp/_cli/__init__.py new file mode 100644 index 0000000..32ea59a --- /dev/null +++ b/src/quart_imp/_cli/__init__.py @@ -0,0 +1,90 @@ +import click + +from .blueprint import add_api_blueprint as _add_api_blueprint +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() -> None: + pass # Entry Point + + +@cli.command("blueprint", help="Create a quart-imp blueprint") +@click.option( + "-n", + "--name", + nargs=1, + default="new_blueprint", + prompt="Name", + help="The name of the blueprint to create.", +) +@click.option( + "-f", + "--folder", + nargs=1, + default=".", + prompt=(f"Folder {Sp.WARNING}(relative to CWD){Sp.END}"), + help="The folder to create the blueprint in, creation is relative to the current working directory.", +) +def add_blueprint(name: str, folder: str) -> None: + _add_blueprint(name=name, folder=folder) + + +@cli.command("api-blueprint", help="Create a quart-imp api blueprint") +@click.option( + "-n", + "--name", + nargs=1, + default="new_api_blueprint", + prompt="Name", + help="The name of the api blueprint to create.", +) +@click.option( + "-f", + "--folder", + nargs=1, + default=".", + prompt=(f"Folder {Sp.WARNING}(relative to CWD){Sp.END}"), + help="The folder to create the api blueprint in, creation is relative to the current working directory.", +) +def add_api_blueprint(name: str, folder: str) -> None: + _add_api_blueprint(name=name, folder=folder) + + +@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("-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." +) +@click.option("-f", "--full", is_flag=True, default=False, help="Create a full app.") +def init_new_app(name: str, full: bool, slim: bool, minimal: bool) -> None: + if not full and not slim and not minimal: + choice = click.prompt( + "What type of app would you like to create?", + default="minimal", + type=click.Choice(["minimal", "slim", "full"]), + ) + + 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 + + _init_app(set_name, full, slim, minimal) diff --git a/src/quart_imp/_cli/blueprint.py b/src/quart_imp/_cli/blueprint.py new file mode 100644 index 0000000..598eb7e --- /dev/null +++ b/src/quart_imp/_cli/blueprint.py @@ -0,0 +1,165 @@ +import typing as t +from pathlib import Path + +import click + +from .filelib.head_tag_generator import head_tag_generator +from .filelib.main_js import main_js +from .filelib.quart_imp_logo import quart_imp_logo +from .filelib.water_css import water_css +from .helpers import Sprinkles as Sp +from .helpers import build +from .helpers import to_snake_case + + +def add_api_blueprint( + name: str = "new_api_blueprint", + folder: str = ".", + _init_app: bool = False, + _cwd: t.Optional[Path] = None, + _url_prefix: t.Optional[str] = None, +) -> None: + from .filelib.api_blueprint import api_blueprint_init_py + from .filelib.api_blueprint import api_blueprint_routes_index_py + + click.echo(f"{Sp.OKGREEN}Creating API Blueprint: {name}") + + if _cwd: + cwd = _cwd + 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) + + if folder == ".": + root_folder = cwd / name + else: + root_folder = cwd / folder / name + + folders: t.Dict[str, Path] = { + "root": root_folder, + "routes": root_folder / "routes", + } + + files: t.Dict[str, t.Tuple[Path, t.Any]] = { + "root/__init__.py": ( + folders["root"] / "__init__.py", + api_blueprint_init_py( + url_prefix=name if not _url_prefix else _url_prefix, name=name + ), + ), + "routes/index.py": ( + folders["routes"] / "index.py", + api_blueprint_routes_index_py(), + ), + } + + build(folders, files, building="API Blueprint") + + +def add_blueprint( + name: str = "new_blueprint", + folder: str = ".", + _init_app: bool = False, + _cwd: t.Optional[Path] = None, + _url_prefix: t.Optional[str] = None, +) -> None: + from .filelib.blueprint import blueprint_init_py + from .filelib.blueprint import blueprint_routes_index_py + from .filelib.blueprint import blueprint_templates_index_html + from .filelib.blueprint import blueprint_init_app_templates_index_html + from .filelib.blueprint import blueprint_templates_extends_main_html + from .filelib.blueprint import blueprint_templates_includes_header_html + from .filelib.blueprint import blueprint_templates_includes_footer_html + + click.echo(f"{Sp.OKGREEN}Creating Blueprint: {name}") + + if _cwd: + cwd = _cwd + 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) + + if folder == ".": + root_folder = cwd / name + else: + root_folder = cwd / folder / name + + folders: t.Dict[str, Path] = { + "root": root_folder, + "routes": root_folder / "routes", + "static": root_folder / "static", + "static/img": root_folder / "static" / "img", + "static/css": root_folder / "static" / "css", + "static/js": root_folder / "static" / "js", + "templates": root_folder / "templates" / name, + "templates/extends": root_folder / "templates" / name / "extends", + "templates/includes": root_folder / "templates" / name / "includes", + } + + files: t.Dict[str, t.Tuple[Path, t.Any]] = { + "root/__init__.py": ( + folders["root"] / "__init__.py", + blueprint_init_py( + url_prefix=name if not _url_prefix else _url_prefix, name=name + ), + ), + "routes/index.py": ( + folders["routes"] / "index.py", + blueprint_routes_index_py(), + ), + "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(main_js_=folders["static"] / "main.js"), + ), + "templates/-/index.html": ( + folders["templates"] / "index.html", + blueprint_templates_index_html(root=folders["root"], blueprint_name=name) + if not _init_app + else blueprint_init_app_templates_index_html( + blueprint_name=name, + 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", + blueprint_templates_extends_main_html( + name=name, + head_tag=head_tag_generator(f"{name}.static"), + ), + ), + "templates/-/includes/header.html": ( + folders["templates/includes"] / "header.html", + blueprint_templates_includes_header_html( + header_html=folders["templates/includes"] / "header.html", + main_html=folders["templates/extends"] / "main.html", + static_url_endpoint=f"{name}.static", + ), + ), + "templates/-/includes/footer.html": ( + folders["templates/includes"] / "footer.html", + blueprint_templates_includes_footer_html( + footer_html=folders["templates/includes"] / "footer.html", + main_html=folders["templates/extends"] / "main.html", + ), + ), + } + + build(folders, files, building="Blueprint") diff --git a/src/quart_imp/_cli/filelib/__init__.py b/src/quart_imp/_cli/filelib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/quart_imp/_cli/filelib/api_blueprint.py b/src/quart_imp/_cli/filelib/api_blueprint.py new file mode 100644 index 0000000..fc8bba6 --- /dev/null +++ b/src/quart_imp/_cli/filelib/api_blueprint.py @@ -0,0 +1,24 @@ +def api_blueprint_init_py(url_prefix: str, name: str) -> str: + return f"""\ +from quart_imp import ImpBlueprint +from quart_imp.config import ImpBlueprintConfig + +bp = ImpBlueprint(__name__, ImpBlueprintConfig( + enabled=True, + url_prefix="/{url_prefix}", + init_session={{"{name}_session_loaded": True}}, +)) + +bp.import_resources("routes") +""" + + +def api_blueprint_routes_index_py() -> str: + return """\ +from .. import bp + + +@bp.route("/", methods=["GET"]) +async def index(): + return await {"message": "Hello, World!"} +""" diff --git a/quart_imp/_cli/filelib/blueprint.py b/src/quart_imp/_cli/filelib/blueprint.py similarity index 59% rename from quart_imp/_cli/filelib/blueprint.py rename to src/quart_imp/_cli/filelib/blueprint.py index 41a7c0e..9697f8b 100644 --- a/quart_imp/_cli/filelib/blueprint.py +++ b/src/quart_imp/_cli/filelib/blueprint.py @@ -1,53 +1,25 @@ -from dataclasses import dataclass +from pathlib import Path -@dataclass(frozen=True) -class BlueprintFileLib: - # Format to: NONE - init_py = """\ -from quart_imp import Blueprint +def blueprint_init_py(url_prefix: str, name: str) -> str: + return f"""\ +from quart_imp import ImpBlueprint +from quart_imp.config import ImpBlueprintConfig -bp = Blueprint(__name__) +bp = ImpBlueprint(__name__, ImpBlueprintConfig( + enabled=True, + url_prefix="/{url_prefix}", + static_folder="static", + template_folder="templates", + init_session={{"{name}_session_loaded": True}}, +)) 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 = """\ +def blueprint_routes_index_py() -> str: + return """\ from quart import render_template from .. import bp @@ -58,14 +30,15 @@ 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' %}} + +def blueprint_templates_index_html(blueprint_name: str, root: Path) -> str: + return f"""\ +{{% extends '{blueprint_name}/extends/main.html' %}} {{% block content %}}
-

Blueprint: {name}

+

Blueprint: {blueprint_name}

Here's your new blueprint.

Located here: {root}

Remember to double-check the config.toml file.

@@ -74,14 +47,21 @@ async def index(): {{% endblock %}} """ - # Format to: name, quart_imp_logo, index_html, extends_main_html, index_py, init_py - ia_templates_index_html = """\ + +def blueprint_init_app_templates_index_html( + blueprint_name: str, + index_html: Path, + extends_main_html: Path, + index_py: Path, + init_py: Path, +) -> str: + return f"""\ {{% extends 'www/extends/main.html' %}} {{% block content %}}
-

Blueprint: {name}

+

Blueprint: {blueprint_name}

This is the index route of the included example blueprint.

This template page is located in {index_html}
@@ -95,8 +75,9 @@ async def index(): {{% endblock %}} """ - # Format to: head_tag - templates_extends_main_html = """\ + +def blueprint_templates_extends_main_html(name: str, head_tag: str) -> str: + return f"""\ @@ -113,12 +94,15 @@ async def index(): """ - # Format to: header_html, main_html - templates_includes_header_html = """\ + +def blueprint_templates_includes_header_html( + header_html: Path, main_html: Path, static_url_endpoint: str +) -> str: + return f"""\

quart-imp logo + src="{{{{ url_for('{static_url_endpoint}', filename='img/quart-imp-logo.png') }}}}" alt="quart-imp logo">

Quart-Imp

@@ -127,8 +111,9 @@ async def index():
""" - # Format to: footer_html, main_html - templates_includes_footer_html = """\ + +def blueprint_templates_includes_footer_html(footer_html: Path, main_html: Path) -> str: + return f"""\

This is the footer, located here: {footer_html}

diff --git a/src/quart_imp/_cli/filelib/extensions.py b/src/quart_imp/_cli/filelib/extensions.py new file mode 100644 index 0000000..5a71d3a --- /dev/null +++ b/src/quart_imp/_cli/filelib/extensions.py @@ -0,0 +1,6 @@ +def extensions_init() -> str: + return """\ +from quart_imp import Imp + +imp = Imp() +""" diff --git a/quart_imp/_cli/filelib/favicon.py b/src/quart_imp/_cli/filelib/favicon.py similarity index 100% rename from quart_imp/_cli/filelib/favicon.py rename to src/quart_imp/_cli/filelib/favicon.py diff --git a/quart_imp/_cli/filelib/head_tag_generator.py b/src/quart_imp/_cli/filelib/head_tag_generator.py similarity index 68% rename from quart_imp/_cli/filelib/head_tag_generator.py rename to src/quart_imp/_cli/filelib/head_tag_generator.py index abaa151..2cee472 100644 --- a/quart_imp/_cli/filelib/head_tag_generator.py +++ b/src/quart_imp/_cli/filelib/head_tag_generator.py @@ -1,9 +1,9 @@ -def head_tag_generator(static_endpoint="static", no_js=False): +def head_tag_generator(static_url_endpoint: str = "static", no_js: bool = False) -> str: """Generate the head tag for the HTML template files.""" js = ( ( - f"" ) if not no_js @@ -21,7 +21,7 @@ def head_tag_generator(static_endpoint="static", no_js=False): Quart-Imp {favicon} - + {js}