feat: updated to newest flask-imp code.
This commit is contained in:
parent
8258bc1cbd
commit
caaf17af12
31
.github/workflows/python-publish.yml
vendored
Normal file
31
.github/workflows/python-publish.yml
vendored
Normal file
@ -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 }}
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -169,3 +169,5 @@ cython_debug/
|
||||
/old_code/
|
||||
/Dockerfile
|
||||
/src_dif/
|
||||
/_archive/
|
||||
/app/
|
||||
|
502
LICENSE
502
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.
|
26
README.md
26
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
|
||||
```
|
||||
|
@ -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
|
@ -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()
|
@ -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 = ""
|
@ -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"))
|
@ -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;
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 7.8 KiB |
@ -1 +0,0 @@
|
||||
console.log('This log is from the file /home/david/PycharmProjects/quart-imp/app/blueprints/www/static/main.js')
|
@ -1,24 +0,0 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="'width=device-width, initial-scale=1.0'">
|
||||
<title>Quart-Imp</title>
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" sizes="16x16 32x32" type="image/x-icon">
|
||||
<link rel="stylesheet" href="{{ url_for('www.static', filename='css/water.css') }}">
|
||||
<script defer src="{{ url_for('www.static', filename='js/main.js') }}"></script>
|
||||
|
||||
<script>
|
||||
// inline script
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% include 'www/includes/header.html' %}
|
||||
{% block content %}{% endblock %}
|
||||
{% include 'www/includes/footer.html' %}
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,6 +0,0 @@
|
||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 2rem; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<p>This is the footer, located here: <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/includes/footer.html</code></p>
|
||||
<p>It's being imported in the <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/extends/main.html</code> template.</p>
|
||||
</div>
|
||||
</div>
|
@ -1,10 +0,0 @@
|
||||
<div style="display: flex; flex-direction: row; align-items: center;
|
||||
justify-content: start; gap: 2rem; margin-bottom: 2rem;">
|
||||
<img style="border-radius: 50%"
|
||||
src="{{ url_for('www.static', filename='img/quart-imp-logo.png') }}" alt="quart-imp logo">
|
||||
<h1 style="font-size: 4rem;">Quart-Imp</h1>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<p>This is the header, located here: <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/includes/header.html</code></p>
|
||||
<p>It's being imported in the <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/extends/main.html</code> template.</p>
|
||||
</div>
|
@ -1,17 +0,0 @@
|
||||
{% extends 'www/extends/main.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 2rem; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<h2 style="margin: 0;">Blueprint: www</h2>
|
||||
<h3>This is the index route of the included example blueprint.</h3>
|
||||
<p style="margin-bottom: 0;">
|
||||
This template page is located in <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/index.html</code><br/>
|
||||
it extends from <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/templates/www/extends/main.html</code><br/>
|
||||
with its route defined in <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/routes/index.py</code><br/><br/>
|
||||
It's being imported by <code>bp.import_resources("routes")</code>
|
||||
in the <code>/home/david/PycharmProjects/quart-imp/app/blueprints/www/__init__.py</code> file.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -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 = ""
|
@ -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()
|
@ -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",
|
||||
]
|
@ -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()
|
@ -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,
|
||||
)
|
@ -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)
|
@ -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
|
@ -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"
|
@ -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")
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
@ -1,11 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>400 Bad Request</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>It's not us, it's you.</p>
|
||||
</body>
|
||||
</html>
|
@ -1,11 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>401 Unauthorized</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>You lack valid authentication credentials for the requested resource</p>
|
||||
</body>
|
||||
</html>
|
@ -1,11 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>Access forbidden!</p>
|
||||
</body>
|
||||
</html>
|
@ -1,11 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>404 Page Not Found</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>No route associated with the URL</p>
|
||||
</body>
|
||||
</html>
|
@ -1,11 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>405 Method Not Allowed</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>Should of GET when you POST, or POST when you GET</p>
|
||||
</body>
|
||||
</html>
|
@ -1,11 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>500 Server Error!</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>There has been a server error!</p>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="'width=device-width, initial-scale=1.0'">
|
||||
<title>Quart-Imp Global Template</title>
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" sizes="16x16 32x32" type="image/x-icon">
|
||||
</head>
|
||||
<body>
|
||||
<p>This is the example resources template file located in <code>resources/templates/index.html</code></p>
|
||||
</body>
|
@ -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"
|
@ -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"]
|
@ -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)
|
@ -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}")
|
@ -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",
|
||||
]
|
@ -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 = """\
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="'width=device-width, initial-scale=1.0'">
|
||||
<title>Quart-Imp Global Template</title>
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" sizes="16x16 32x32" type="image/x-icon">
|
||||
</head>
|
||||
<body>
|
||||
<p>This is the example resources template file located in <code>resources/templates/index.html</code></p>
|
||||
</body>
|
||||
"""
|
||||
|
||||
# Format to: head_tag, static_path, index_py, index_html, init_py
|
||||
minimal_templates_index_html = """\
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
{head_tag}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="display: flex; flex-direction: row; align-items: center;
|
||||
justify-content: start; gap: 2rem; margin-bottom: 2rem;">
|
||||
<img style="border-radius: 50%"
|
||||
src="{{{{ url_for('{static_path}', filename='img/quart-imp-logo.png') }}}}" alt="quart-imp logo">
|
||||
<h1 style="font-size: 4rem;">Quart-Imp</h1>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 2rem; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<p style="margin-bottom: 0;">
|
||||
This template page is located in <code>{index_html}</code><br/>
|
||||
with its route defined in <code>{index_py}</code><br/><br/>
|
||||
It's being imported by <code>app.import_app_resources()</code>
|
||||
in the <code>{init_py}</code> file.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
"""
|
||||
|
||||
templates_errors_400_html = """\
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>400 Bad Request</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>It's not us, it's you.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
templates_errors_401_html = """\
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>401 Unauthorized</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>You lack valid authentication credentials for the requested resource</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
templates_errors_403_html = """\
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>403 Forbidden</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>Access forbidden!</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
templates_errors_404_html = """\
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>404 Page Not Found</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>No route associated with the URL</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
templates_errors_405_html = """\
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>405 Method Not Allowed</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>Should of GET when you POST, or POST when you GET</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
templates_errors_500_html = """\
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>500 Server Error!</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>There has been a server error!</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
@ -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()
|
||||
"""
|
@ -1,4 +0,0 @@
|
||||
# format: main_js
|
||||
main_js = """\
|
||||
console.log('This log is from the file {main_js}')
|
||||
"""
|
@ -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"
|
@ -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(" ")
|
@ -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:`<br />`
|
||||
|
||||
`config.toml` must be in the same directory as the `__init__.py` file.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-- 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:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
**Example use:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
--- 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:`<br />`
|
||||
|
||||
--- __init__.py ---
|
||||
|
||||
.. code-block::
|
||||
|
||||
from quart_imp import Blueprint
|
||||
|
||||
bp = Blueprint(__name__)
|
||||
|
||||
bp.import_resources("user_routes")
|
||||
bp.import_resources("car_routes")
|
||||
...
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
--- 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:`<br />`
|
||||
|
||||
The endpoint my_blueprint.user_dashboard will be available at /my_blueprint/user-dashboard
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
Has the same import rules as the `Imp.import_blueprint()` method.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Must be setup in a Python package**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Example:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
--- Folder structure ---
|
||||
.. code-block::
|
||||
|
||||
app
|
||||
├── my_blueprint
|
||||
│ ├── ...
|
||||
│ ├── my_nested_blueprint
|
||||
│ │ ├── ...
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── config.toml
|
||||
│ ├── __init__.py
|
||||
│ └── config.toml
|
||||
└── ...
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
--- my_blueprint/__init__.py ---
|
||||
|
||||
.. code-block::
|
||||
|
||||
from quart_imp import Blueprint
|
||||
|
||||
bp = Blueprint(__name__)
|
||||
|
||||
bp.import_nested_blueprint("my_nested_blueprint")
|
||||
|
||||
...
|
||||
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
See `Imp.import_nested_blueprint()` for more information.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Example:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
--- 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:`<br />`
|
||||
|
||||
--- my_blueprint/__init__.py ---
|
||||
|
||||
.. code-block::
|
||||
|
||||
from quart_imp import Blueprint
|
||||
|
||||
bp = Blueprint(__name__)
|
||||
bp.import_nested_blueprints("nested_blueprints")
|
||||
...
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
All blueprints in the nested_blueprints folder will be imported and nested under my_blueprint.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
**Example usage:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
@bp.before_app_request
|
||||
def before_app_request():
|
||||
bp.init_session()
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
**Each model found will be added to the model registry.**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
See: `Imp.model()` for more information.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Example usage from files:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
# in my_blueprint/__init__.py
|
||||
bp.import_models("users.py")
|
||||
bp.import_models("cars.py")
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-- Folder structure --
|
||||
|
||||
.. code-block::
|
||||
|
||||
my_blueprint
|
||||
├── ...
|
||||
├── users.py
|
||||
├── cars.py
|
||||
├── config.toml
|
||||
└── __init__.py
|
||||
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Example usage from folders:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
# in my_blueprint/__init__.py
|
||||
bp.import_models("models")
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-- Folder structure --
|
||||
|
||||
.. code-block::
|
||||
|
||||
my_blueprint
|
||||
├── ...
|
||||
├── models
|
||||
│ ├── users.py
|
||||
│ └── cars.py
|
||||
├── config.toml
|
||||
└── __init__.py
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Example of model file:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-- users.py --
|
||||
|
||||
.. code-block::
|
||||
|
||||
from app.extensions import db
|
||||
|
||||
class User(db.Model):
|
||||
attribute = db.Column(db.String(255))
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
**Example usage:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
@bp.route("/")
|
||||
def index():
|
||||
return render_template(bp.tmpl("index.html"))
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-- Folder structure --
|
||||
|
||||
.. code-block::
|
||||
|
||||
my_blueprint
|
||||
├── ...
|
||||
├── templates
|
||||
│ └── my_blueprint
|
||||
│ └── index.html
|
||||
├── config.toml
|
||||
└── __init__.py
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
bp.tmpl("index.html") will return "my_blueprint/index.html"
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
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:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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)
|
@ -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
|
791
quart_imp/imp.py
791
quart_imp/imp.py
@ -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:`<br />`
|
||||
|
||||
If no `app_config_file` specified, an attempt to read `IMP_CONFIG` from the environment will be made.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
If `IMP_CONFIG` is not in the environment variables, an attempt to load `default.config.toml` will be made.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
`default.config.toml` will be created, and used if not found.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
This will import any resources that have been set to the Quart app. Routes, context processors, cli, etc.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Can only be called once.**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
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:`<br />`
|
||||
|
||||
**Small example of usage:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
imp.import_app_resources(folder="resources")
|
||||
# or
|
||||
imp.import_app_resources()
|
||||
# as the default folder is "resources"
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
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:`<br />`
|
||||
|
||||
---
|
||||
`resources` folder structure
|
||||
---
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
app
|
||||
├── resources
|
||||
│ ├── routes.py
|
||||
│ ├── app_fac.py
|
||||
│ ├── static
|
||||
│ │ └── css
|
||||
│ │ └── style.css
|
||||
│ └── templates
|
||||
│ └── index.html
|
||||
└── ...
|
||||
...
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
---
|
||||
`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:`<br />`
|
||||
|
||||
**How factories work**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
Factories are functions that are called when importing the app resources. Here's an example:
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
imp.import_app_resources(folder="resources", factories=["development_cli"])
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
["development_cli"] => development_cli(app) function will be called, and the current app will be passed in.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
--- `app_fac.py` file ---
|
||||
|
||||
.. code-block::
|
||||
|
||||
def development_cli(app):
|
||||
@app.cli.command("dev")
|
||||
def dev():
|
||||
print("dev cli command")
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Scoping imports**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
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:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
imp.import_app_resources(files_to_import=[None], folders_to_import=[None])
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
To scope the imports, set the files_to_import and or folders_to_import to a list of files and or folders.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
files_to_import=["cli.py", "routes.py"] => will only import the files `resources/cli.py`
|
||||
and `resources/routes.py`
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
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:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
imp.init_session()
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
**Must be setup in a Python package**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Example of a Quart-Imp Blueprint:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
Will look for a config.toml file in the blueprint folder.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
--- 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:`<br />`
|
||||
|
||||
--- __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:`<br />`
|
||||
|
||||
--- 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:`<br />`
|
||||
|
||||
**Example of a standard Quart Blueprint:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
--- Folder structure ---
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
app
|
||||
├── my_blueprint
|
||||
│ ├── ...
|
||||
│ └── __init__.py
|
||||
└── ...
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
--- __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:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
**Example folder structure:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
app
|
||||
├── blueprints
|
||||
│ ├── regular_blueprint
|
||||
│ │ ├── ...
|
||||
│ │ └── __init__.py
|
||||
│ └── quart_imp_blueprint
|
||||
│ ├── ...
|
||||
│ ├── config.toml
|
||||
│ └── __init__.py
|
||||
└── ...
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
See: `import_blueprint` for more information.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
**Each model found will be added to the model registry.**
|
||||
|
||||
See: `Imp.model()` for more information.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Example usage from files:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
imp.import_models("users.py")
|
||||
imp.import_models("cars.py")
|
||||
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-- Folder structure --
|
||||
|
||||
.. code-block::
|
||||
|
||||
app
|
||||
├── ...
|
||||
├── users.py
|
||||
├── cars.py
|
||||
├── default.config.toml
|
||||
└── __init__.py
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Example usage from folders:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
imp.import_models("models")
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-- Folder structure --
|
||||
|
||||
.. code-block::
|
||||
|
||||
app
|
||||
├── ...
|
||||
├── models
|
||||
│ ├── users.py
|
||||
│ └── cars.py
|
||||
├── default.config.toml
|
||||
└── __init__.py
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Example of model file:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-- users.py --
|
||||
|
||||
.. code-block::
|
||||
|
||||
from app.extensions import db
|
||||
|
||||
class User(db.Model):
|
||||
attribute = db.Column(db.String(255))
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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:`<br />`
|
||||
|
||||
This is used to omit the need to import the models from their locations.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**For example, this:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
from app.models.user import User
|
||||
from app.models.cars import Cars
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Can be replaced with:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
from app.extensions import imp
|
||||
|
||||
User = imp.model("User")
|
||||
Cars = imp.model("Cars")
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
imp.model("User") -> <class 'app.models.User'>
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
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:`<br />`
|
||||
|
||||
-----
|
||||
: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:`<br />`
|
||||
|
||||
**Example:**
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. code-block::
|
||||
|
||||
from app.extensions import imp
|
||||
|
||||
User = imp.model("User")
|
||||
|
||||
print(imp.model_meta(User))
|
||||
# or
|
||||
print(imp.model_meta("User"))
|
||||
|
||||
:raw-html:`<br />`
|
||||
Will output:
|
||||
|
||||
{"location": "app.models.user", "table_name": "user"}
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
**Advanced use case:**
|
||||
|
||||
`location` can be used to import a function from the model file using Pythons importlib.
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
Here's an example:
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
.. 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:`<br />`
|
||||
|
||||
`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:`<br />`
|
||||
|
||||
-----
|
||||
|
||||
: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__,
|
||||
}
|
@ -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]):
|
||||
...
|
@ -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})"
|
@ -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
|
@ -1,2 +0,0 @@
|
||||
Quart
|
||||
Flask-SQLAlchemy
|
1
requirements/build.in
Normal file
1
requirements/build.in
Normal file
@ -0,0 +1 @@
|
||||
flit
|
24
requirements/build.txt
Normal file
24
requirements/build.txt
Normal file
@ -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
|
6
requirements/dev.in
Normal file
6
requirements/dev.in
Normal file
@ -0,0 +1,6 @@
|
||||
-r docs.txt
|
||||
-r typing.txt
|
||||
-r build.txt
|
||||
quart
|
||||
ruff
|
||||
pyqwe
|
131
requirements/dev.txt
Normal file
131
requirements/dev.txt
Normal file
@ -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
|
@ -1,3 +1,4 @@
|
||||
flask
|
||||
mistune
|
||||
pygments
|
||||
pytz
|
28
requirements/docs.txt
Normal file
28
requirements/docs.txt
Normal file
@ -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
|
3
requirements/typing.in
Normal file
3
requirements/typing.in
Normal file
@ -0,0 +1,3 @@
|
||||
mypy
|
||||
pyright
|
||||
pytest
|
24
requirements/typing.txt
Normal file
24
requirements/typing.txt
Normal file
@ -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
|
@ -1,7 +0,0 @@
|
||||
-r requirements.txt
|
||||
flit
|
||||
ruff
|
||||
pytest
|
||||
pytest-cov
|
||||
mypy
|
||||
types-toml
|
11
src/quart_imp/__init__.py
Normal file
11
src/quart_imp/__init__.py
Normal file
@ -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",
|
||||
]
|
90
src/quart_imp/_cli/__init__.py
Normal file
90
src/quart_imp/_cli/__init__.py
Normal file
@ -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)
|
165
src/quart_imp/_cli/blueprint.py
Normal file
165
src/quart_imp/_cli/blueprint.py
Normal file
@ -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")
|
0
src/quart_imp/_cli/filelib/__init__.py
Normal file
0
src/quart_imp/_cli/filelib/__init__.py
Normal file
24
src/quart_imp/_cli/filelib/api_blueprint.py
Normal file
24
src/quart_imp/_cli/filelib/api_blueprint.py
Normal file
@ -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!"}
|
||||
"""
|
@ -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 %}}
|
||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 2rem; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<h2 style="margin: 0;">Blueprint: {name}</h2>
|
||||
<h2 style="margin: 0;">Blueprint: {blueprint_name}</h2>
|
||||
<h3>Here's your new blueprint.</h3>
|
||||
<p>Located here: <code>{root}</code></p>
|
||||
<p style="margin-bottom: 0;">Remember to double-check the config.toml file.</p>
|
||||
@ -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 %}}
|
||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 2rem; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<h2 style="margin: 0;">Blueprint: {name}</h2>
|
||||
<h2 style="margin: 0;">Blueprint: {blueprint_name}</h2>
|
||||
<h3>This is the index route of the included example blueprint.</h3>
|
||||
<p style="margin-bottom: 0;">
|
||||
This template page is located in <code>{index_html}</code><br/>
|
||||
@ -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"""\
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
@ -113,12 +94,15 @@ async def index():
|
||||
</html>
|
||||
"""
|
||||
|
||||
# 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"""\
|
||||
<div style="display: flex; flex-direction: row; align-items: center;
|
||||
justify-content: start; gap: 2rem; margin-bottom: 2rem;">
|
||||
<img style="border-radius: 50%"
|
||||
src="{{{{ url_for('{static_path}', filename='img/quart-imp-logo.png') }}}}" alt="quart-imp logo">
|
||||
src="{{{{ url_for('{static_url_endpoint}', filename='img/quart-imp-logo.png') }}}}" alt="quart-imp logo">
|
||||
<h1 style="font-size: 4rem;">Quart-Imp</h1>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
@ -127,8 +111,9 @@ async def index():
|
||||
</div>
|
||||
"""
|
||||
|
||||
# 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"""\
|
||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 2rem; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<p>This is the footer, located here: <code>{footer_html}</code></p>
|
6
src/quart_imp/_cli/filelib/extensions.py
Normal file
6
src/quart_imp/_cli/filelib/extensions.py
Normal file
@ -0,0 +1,6 @@
|
||||
def extensions_init() -> str:
|
||||
return """\
|
||||
from quart_imp import Imp
|
||||
|
||||
imp = Imp()
|
||||
"""
|
@ -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"<script defer src=\"{{{{ url_for('{static_endpoint}', "
|
||||
f"<script defer src=\"{{{{ url_for('{static_url_endpoint}', "
|
||||
f"filename='js/main.js') }}}}\"></script>"
|
||||
)
|
||||
if not no_js
|
||||
@ -21,7 +21,7 @@ def head_tag_generator(static_endpoint="static", no_js=False):
|
||||
<meta name="viewport" content="'width=device-width, initial-scale=1.0'">
|
||||
<title>Quart-Imp</title>
|
||||
{favicon}
|
||||
<link rel="stylesheet" href="{{{{ url_for('{static_endpoint}', filename='css/water.css') }}}}">
|
||||
<link rel="stylesheet" href="{{{{ url_for('{static_url_endpoint}', filename='css/water.css') }}}}">
|
||||
{js}
|
||||
|
||||
<script>
|
77
src/quart_imp/_cli/filelib/init.py
Normal file
77
src/quart_imp/_cli/filelib/init.py
Normal file
@ -0,0 +1,77 @@
|
||||
def init_full_py(app_name: str, secret_key: str) -> str:
|
||||
return f"""\
|
||||
from quart import Quart
|
||||
|
||||
from {app_name}.extensions import imp
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Quart(__name__, static_url_path="/")
|
||||
|
||||
QuartConfig(
|
||||
secret_key="{secret_key}",
|
||||
app_instance=app
|
||||
)
|
||||
|
||||
imp.init_app(app, ImpConfig(
|
||||
init_session={{"logged_in": False}},
|
||||
))
|
||||
|
||||
imp.import_app_resources()
|
||||
imp.import_blueprints("blueprints")
|
||||
imp.import_models("models")
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
return app
|
||||
"""
|
||||
|
||||
|
||||
def init_slim_py(app_name: str, secret_key: str) -> str:
|
||||
return f"""\
|
||||
from quart import Quart
|
||||
|
||||
from {app_name}.extensions import imp
|
||||
from quart_imp.config import ImpConfig, QuartConfig
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Quart(__name__, static_url_path="/")
|
||||
|
||||
QuartConfig(
|
||||
secret_key="{secret_key}",
|
||||
app_instance=app
|
||||
)
|
||||
|
||||
imp.init_app(app, ImpConfig())
|
||||
imp.import_app_resources()
|
||||
imp.import_blueprint("www")
|
||||
|
||||
return app
|
||||
"""
|
||||
|
||||
|
||||
def init_minimal_py(secret_key: str) -> str:
|
||||
return f"""\
|
||||
from quart import Quart
|
||||
|
||||
from quart_imp import Imp
|
||||
from quart_imp.config import ImpConfig, QuartConfig
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Quart(__name__, static_url_path="/")
|
||||
|
||||
QuartConfig(
|
||||
secret_key="{secret_key}",
|
||||
app_instance=app
|
||||
)
|
||||
|
||||
imp = Imp(app, ImpConfig())
|
||||
imp.import_app_resources()
|
||||
|
||||
return app
|
||||
"""
|
9
src/quart_imp/_cli/filelib/main_js.py
Normal file
9
src/quart_imp/_cli/filelib/main_js.py
Normal file
@ -0,0 +1,9 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main_js(
|
||||
main_js_: Path,
|
||||
) -> str:
|
||||
return f"""\
|
||||
console.log('This log is from the file {main_js_}')
|
||||
"""
|
168
src/quart_imp/_cli/filelib/resources.py
Normal file
168
src/quart_imp/_cli/filelib/resources.py
Normal file
@ -0,0 +1,168 @@
|
||||
def resources_cli_py() -> str:
|
||||
return """\
|
||||
from quart import current_app as app
|
||||
|
||||
|
||||
@app.cli.command("show-config")
|
||||
def show_config():
|
||||
print(app.config)
|
||||
"""
|
||||
|
||||
|
||||
def resources_context_processors_py() -> str:
|
||||
return """\
|
||||
from quart import current_app as app
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def example__utility_processor():
|
||||
\"""
|
||||
Usage:
|
||||
{{ example__format_price(100.33) }} -> $100.33
|
||||
\"""
|
||||
|
||||
def example__format_price(amount, currency='$'):
|
||||
return '{1}{0:.2f}'.format(amount, currency)
|
||||
|
||||
return dict(example__format_price=example__format_price)
|
||||
"""
|
||||
|
||||
|
||||
def resources_error_handlers_py() -> str:
|
||||
return """\
|
||||
from quart import current_app as app
|
||||
|
||||
from quart import render_template
|
||||
|
||||
|
||||
@app.errorhandler(400)
|
||||
async def bad_request(e):
|
||||
return render_template(
|
||||
"error.html",
|
||||
error_code=400,
|
||||
error_message="The request is invalid.",
|
||||
), 400
|
||||
|
||||
|
||||
@app.errorhandler(401)
|
||||
async def unauthorized(e):
|
||||
return render_template(
|
||||
"error.html",
|
||||
error_code=401,
|
||||
error_message="You are not authorized to access this page.",
|
||||
), 401
|
||||
|
||||
|
||||
@app.errorhandler(403)
|
||||
async def forbidden(e):
|
||||
return render_template(
|
||||
"error.html",
|
||||
error_code=403,
|
||||
error_message="You do not have permission to access this page.",
|
||||
), 403
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
async def page_not_found(e):
|
||||
return render_template(
|
||||
"error.html",
|
||||
error_code=404,
|
||||
error_message="The page you are looking for does not exist.",
|
||||
|
||||
), 404
|
||||
|
||||
|
||||
@app.errorhandler(405)
|
||||
async def method_not_allowed(e):
|
||||
return render_template(
|
||||
"error.html",
|
||||
error_code=405,
|
||||
error_message="The method is not allowed for the requested URL.",
|
||||
), 405
|
||||
|
||||
|
||||
@app.errorhandler(410)
|
||||
async def gone(e):
|
||||
return render_template(
|
||||
"error.html",
|
||||
error_code=410,
|
||||
error_message="This page is no longer available.",
|
||||
), 410
|
||||
|
||||
|
||||
@app.errorhandler(429)
|
||||
async def too_many_requests(e):
|
||||
return render_template(
|
||||
"error.html",
|
||||
error_code=429,
|
||||
error_message="You have made too many requests.",
|
||||
), 429
|
||||
|
||||
|
||||
@app.errorhandler(500)
|
||||
async def server_error(e):
|
||||
return render_template(
|
||||
"error.html",
|
||||
error_code=500,
|
||||
error_message="An internal server error has occurred.",
|
||||
), 500
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def resources_filters_py() -> str:
|
||||
return """\
|
||||
from quart import current_app as app
|
||||
|
||||
|
||||
@app.template_filter('example__num_to_month')
|
||||
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"
|
||||
"""
|
||||
|
||||
|
||||
def resources_routes_py() -> str:
|
||||
return """\
|
||||
from quart import current_app as app
|
||||
|
||||
|
||||
@app.route("/example--resources")
|
||||
async def example_route():
|
||||
return await "From the [app_root]/resources/routes/routes.py file"
|
||||
"""
|
||||
|
||||
|
||||
def resources_minimal_routes_py() -> str:
|
||||
return """\
|
||||
from quart import current_app as app
|
||||
from quart import render_template
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
"""
|
49
src/quart_imp/_cli/filelib/templates.py
Normal file
49
src/quart_imp/_cli/filelib/templates.py
Normal file
@ -0,0 +1,49 @@
|
||||
def templates_minimal_index_html(
|
||||
head_tag: str, static_path: str, index_py: str, index_html: str, init_py: str
|
||||
) -> str:
|
||||
return f"""\
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
{head_tag}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="display: flex; flex-direction: row; align-items: center;
|
||||
justify-content: start; gap: 2rem; margin-bottom: 2rem;">
|
||||
<img style="border-radius: 50%"
|
||||
src="{{{{ url_for('{static_path}', filename='img/quart-imp-logo.png') }}}}" alt="quart-imp logo">
|
||||
<h1 style="font-size: 4rem;">Quart-Imp</h1>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 2rem; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<p style="margin-bottom: 0;">
|
||||
This template page is located in <code>{index_html}</code><br/>
|
||||
with its route defined in <code>{index_py}</code><br/><br/>
|
||||
It's being imported by <code>app.import_app_resources()</code>
|
||||
in the <code>{init_py}</code> file.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def templates_error_html() -> str:
|
||||
return """\
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>{{ error_code }}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{ error_code }}</h1>
|
||||
<p>{{ error_message }}</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
66
src/quart_imp/_cli/helpers.py
Normal file
66
src/quart_imp/_cli/helpers.py
Normal file
@ -0,0 +1,66 @@
|
||||
import re
|
||||
import typing as t
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def to_snake_case(string: str) -> str:
|
||||
"""
|
||||
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"
|
||||
|
||||
|
||||
def build(
|
||||
folders: t.Dict[str, t.Any], files: t.Dict[str, t.Any], building: str = "App"
|
||||
) -> None:
|
||||
write_bytes: t.List[str] = [
|
||||
"resources/static/favicon.ico",
|
||||
"resources/static/img/quart-imp-logo.png",
|
||||
"static/img/quart-imp-logo.png",
|
||||
]
|
||||
|
||||
for folder, path in folders.items():
|
||||
if not path.exists():
|
||||
path.mkdir(parents=True)
|
||||
click.echo(
|
||||
f"{Sprinkles.OKGREEN}{building} folder: {folder}, created{Sprinkles.END}"
|
||||
)
|
||||
else:
|
||||
click.echo(
|
||||
f"{Sprinkles.WARNING}{building} folder already exists: {folder}, skipping{Sprinkles.END}"
|
||||
)
|
||||
|
||||
for file, (path, content) in files.items():
|
||||
if not path.exists():
|
||||
if file in write_bytes:
|
||||
path.write_bytes(bytes.fromhex(content))
|
||||
continue
|
||||
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
click.echo(
|
||||
f"{Sprinkles.OKGREEN}{building} file: {file}, created{Sprinkles.END}"
|
||||
)
|
||||
else:
|
||||
click.echo(
|
||||
f"{Sprinkles.WARNING}{building} file already exists: {file}, skipping{Sprinkles.END}"
|
||||
)
|
244
src/quart_imp/_cli/init.py
Normal file
244
src/quart_imp/_cli/init.py
Normal file
@ -0,0 +1,244 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from .blueprint import add_blueprint
|
||||
from .filelib.favicon import favicon
|
||||
from .filelib.head_tag_generator import head_tag_generator
|
||||
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
|
||||
|
||||
|
||||
def minimal_app(app_folder: Path) -> None:
|
||||
from .filelib.init import init_minimal_py
|
||||
from .filelib.templates import templates_minimal_index_html
|
||||
from .filelib.resources import resources_minimal_routes_py
|
||||
|
||||
# Folders
|
||||
folders = {
|
||||
"root": app_folder,
|
||||
"resources": app_folder / "resources",
|
||||
"resources/static": app_folder / "resources" / "static",
|
||||
"resources/static/css": app_folder / "resources" / "static" / "css",
|
||||
"resources/static/img": app_folder / "resources" / "static" / "img",
|
||||
"resources/templates": app_folder / "resources" / "templates",
|
||||
}
|
||||
|
||||
files = {
|
||||
"root/__init__.py": (
|
||||
folders["root"] / "__init__.py",
|
||||
init_minimal_py(secret_key=os.urandom(24).hex()),
|
||||
),
|
||||
"resources/static/favicon.ico": (
|
||||
folders["resources/static"] / "favicon.ico",
|
||||
favicon,
|
||||
),
|
||||
"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/templates/index.html": (
|
||||
folders["resources/templates"] / "index.html",
|
||||
templates_minimal_index_html(
|
||||
head_tag=head_tag_generator(
|
||||
no_js=True,
|
||||
),
|
||||
static_path="static",
|
||||
index_py=str(folders["resources"] / "index.py"),
|
||||
index_html=str(folders["resources/templates"] / "index.html"),
|
||||
init_py=str(folders["root"] / "__init__.py"),
|
||||
),
|
||||
),
|
||||
"resources/routes.py": (
|
||||
folders["resources"] / "routes.py",
|
||||
resources_minimal_routes_py(),
|
||||
),
|
||||
}
|
||||
|
||||
build(folders, files)
|
||||
|
||||
|
||||
def slim_app(app_folder: Path) -> None:
|
||||
from .filelib.init import init_slim_py
|
||||
from .filelib.extensions import extensions_init
|
||||
from .filelib.resources import resources_cli_py
|
||||
from .filelib.resources import resources_error_handlers_py
|
||||
from .filelib.templates import templates_error_html
|
||||
|
||||
app_name = app_folder.name
|
||||
|
||||
folders = {
|
||||
"root": app_folder,
|
||||
"extensions": app_folder / "extensions",
|
||||
"resources": app_folder / "resources",
|
||||
"resources/cli": app_folder / "resources" / "cli",
|
||||
"resources/error_handlers": app_folder / "resources" / "error_handlers",
|
||||
"resources/static": app_folder / "resources" / "static",
|
||||
"resources/static/css": app_folder / "resources" / "static" / "css",
|
||||
"resources/static/img": app_folder / "resources" / "static" / "img",
|
||||
"resources/templates": app_folder / "resources" / "templates",
|
||||
}
|
||||
|
||||
files = {
|
||||
"root/__init__.py": (
|
||||
folders["root"] / "__init__.py",
|
||||
init_slim_py(app_name=app_name, secret_key=os.urandom(24).hex()),
|
||||
),
|
||||
"extensions/__init__.py": (
|
||||
folders["extensions"] / "__init__.py",
|
||||
extensions_init(),
|
||||
),
|
||||
"resources/cli/cli.py": (
|
||||
folders["resources/cli"] / "cli.py",
|
||||
resources_cli_py(),
|
||||
),
|
||||
"resources/error_handlers/error_handlers.py": (
|
||||
folders["resources/error_handlers"] / "error_handlers.py",
|
||||
resources_error_handlers_py(),
|
||||
),
|
||||
"resources/static/favicon.ico": (
|
||||
folders["resources/static"] / "favicon.ico",
|
||||
favicon,
|
||||
),
|
||||
"resources/templates/error.html": (
|
||||
folders["resources/templates"] / "error.html",
|
||||
templates_error_html(),
|
||||
),
|
||||
}
|
||||
|
||||
build(folders, files)
|
||||
|
||||
add_blueprint(
|
||||
name="www",
|
||||
_init_app=True,
|
||||
_cwd=app_folder,
|
||||
_url_prefix="/",
|
||||
)
|
||||
|
||||
|
||||
def full_app(app_folder: Path) -> None:
|
||||
from .filelib.init import init_full_py
|
||||
from .filelib.extensions import extensions_init
|
||||
from .filelib.resources import resources_cli_py
|
||||
from .filelib.resources import resources_error_handlers_py
|
||||
from .filelib.resources import resources_context_processors_py
|
||||
from .filelib.resources import resources_filters_py
|
||||
from .filelib.resources import resources_routes_py
|
||||
|
||||
from .filelib.templates import templates_error_html
|
||||
|
||||
app_name = app_folder.name
|
||||
|
||||
folders = {
|
||||
"root": app_folder,
|
||||
"blueprints": app_folder / "blueprints",
|
||||
"extensions": app_folder / "extensions",
|
||||
"resources": app_folder / "resources",
|
||||
"resources/cli": app_folder / "resources" / "cli",
|
||||
"resources/context_processors": app_folder / "resources" / "context_processors",
|
||||
"resources/error_handlers": app_folder / "resources" / "error_handlers",
|
||||
"resources/filters": app_folder / "resources" / "filters",
|
||||
"resources/routes": app_folder / "resources" / "routes",
|
||||
"resources/static": app_folder / "resources" / "static",
|
||||
"resources/static/css": app_folder / "resources" / "static" / "css",
|
||||
"resources/static/img": app_folder / "resources" / "static" / "img",
|
||||
"resources/templates": app_folder / "resources" / "templates",
|
||||
}
|
||||
|
||||
files = {
|
||||
"root/__init__.py": (
|
||||
folders["root"] / "__init__.py",
|
||||
init_full_py(app_name=app_name, secret_key=os.urandom(24).hex()),
|
||||
),
|
||||
"extensions/__init__.py": (
|
||||
folders["extensions"] / "__init__.py",
|
||||
extensions_init(),
|
||||
),
|
||||
"resources/cli/cli.py": (
|
||||
folders["resources/cli"] / "cli.py",
|
||||
resources_cli_py(),
|
||||
),
|
||||
"resources/context_processors/context_processors.py": (
|
||||
folders["resources/context_processors"] / "context_processors.py",
|
||||
resources_context_processors_py(),
|
||||
),
|
||||
"resources/error_handlers/error_handlers.py": (
|
||||
folders["resources/error_handlers"] / "error_handlers.py",
|
||||
resources_error_handlers_py(),
|
||||
),
|
||||
"resources/filters/filters.py": (
|
||||
folders["resources/filters"] / "filters.py",
|
||||
resources_filters_py(),
|
||||
),
|
||||
"resources/routes/routes.py": (
|
||||
folders["resources/routes"] / "routes.py",
|
||||
resources_routes_py(),
|
||||
),
|
||||
"resources/static/favicon.ico": (
|
||||
folders["resources/static"] / "favicon.ico",
|
||||
favicon,
|
||||
),
|
||||
"resources/templates/error.html": (
|
||||
folders["resources/templates"] / "error.html",
|
||||
templates_error_html(),
|
||||
),
|
||||
}
|
||||
|
||||
build(folders, files)
|
||||
|
||||
add_blueprint(
|
||||
name="www",
|
||||
folder="blueprints",
|
||||
_init_app=True,
|
||||
_cwd=app_folder,
|
||||
_url_prefix="/",
|
||||
)
|
||||
|
||||
|
||||
def init_app(
|
||||
name: str,
|
||||
_full: bool = False,
|
||||
_slim: bool = False,
|
||||
_minimal: bool = False,
|
||||
) -> None:
|
||||
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)
|
||||
|
||||
if _minimal:
|
||||
minimal_app(app_folder)
|
||||
elif _slim:
|
||||
slim_app(app_folder)
|
||||
elif _full:
|
||||
full_app(app_folder)
|
||||
else:
|
||||
click.echo(f"{Sp.FAIL}No app type selected!{Sp.END}")
|
||||
click.echo(f"{Sp.FAIL}Use --minimal, --slim, or --full{Sp.END}")
|
||||
return
|
||||
|
||||
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(" ")
|
@ -16,7 +16,6 @@ from .is_username_valid import is_username_valid
|
||||
|
||||
|
||||
def auth_password(
|
||||
cls,
|
||||
input_password: str,
|
||||
database_password: str,
|
||||
database_salt: str,
|
||||
@ -24,7 +23,7 @@ def auth_password(
|
||||
pepper_length: int = 1,
|
||||
) -> bool:
|
||||
"""Legacy method, use authenticate_password instead"""
|
||||
return cls.authenticate_password(
|
||||
return authenticate_password(
|
||||
input_password, database_password, database_salt, encrypt, pepper_length
|
||||
)
|
||||
|
@ -2,16 +2,16 @@ import typing as t
|
||||
from hashlib import sha256, sha512
|
||||
|
||||
|
||||
def _pps(pepper_, pass_, salt_) -> str:
|
||||
def _pps(pepper_: str, pass_: str, salt_: str) -> str:
|
||||
return pepper_ + pass_ + salt_
|
||||
|
||||
|
||||
def _ppe(pepper_, pass_, salt_) -> str:
|
||||
def _ppe(pepper_: str, pass_: str, salt_: str) -> str:
|
||||
return pass_ + pepper_ + salt_
|
||||
|
||||
|
||||
def _guess_block(
|
||||
guesses: set,
|
||||
guesses: t.Set[str],
|
||||
input_password: str,
|
||||
database_password: str,
|
||||
database_salt: str,
|
@ -1,10 +1,7 @@
|
||||
import multiprocessing
|
||||
import typing as t
|
||||
from itertools import product
|
||||
from string import ascii_letters
|
||||
|
||||
from more_itertools import batched
|
||||
|
||||
from .__private_funcs__ import _guess_block
|
||||
|
||||
|
||||
@ -15,7 +12,6 @@ def authenticate_password(
|
||||
encryption_level: int = 512,
|
||||
pepper_length: int = 1,
|
||||
pepper_position: t.Literal["start", "end"] = "end",
|
||||
use_multiprocessing: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Takes the plain input password, the stored hashed password along with the stored salt
|
||||
@ -23,9 +19,6 @@ def authenticate_password(
|
||||
|
||||
:raw-html:`<br />`
|
||||
|
||||
NOTE: use_multiprocessing is not compatible with coroutine workers, e.g. eventlet/gevent
|
||||
commonly used with socketio.
|
||||
|
||||
.. Note::
|
||||
|
||||
You must know the length of the pepper used to hash the password.
|
||||
@ -53,40 +46,15 @@ def authenticate_password(
|
||||
|
||||
_guesses = {"".join(i) for i in product(ascii_letters, repeat=pepper_length)}
|
||||
|
||||
if not use_multiprocessing:
|
||||
for guess in _guesses:
|
||||
if _guess_block(
|
||||
{guess},
|
||||
input_password,
|
||||
database_password,
|
||||
database_salt,
|
||||
encryption_level,
|
||||
pepper_position,
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
thread_pool = multiprocessing.Pool(processes=pepper_length)
|
||||
threads = []
|
||||
|
||||
for batch in batched(_guesses, 1000):
|
||||
threads.append(
|
||||
thread_pool.apply_async(
|
||||
_guess_block,
|
||||
args=(
|
||||
batch,
|
||||
input_password,
|
||||
database_password,
|
||||
database_salt,
|
||||
encryption_level,
|
||||
pepper_position,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
for thread in threads:
|
||||
if thread.get():
|
||||
for guess in _guesses:
|
||||
if _guess_block(
|
||||
{guess},
|
||||
input_password,
|
||||
database_password,
|
||||
database_salt,
|
||||
encryption_level,
|
||||
pepper_position,
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
9
src/quart_imp/config/__init__.py
Normal file
9
src/quart_imp/config/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
from .imp_blueprint_config import ImpBlueprintConfig
|
||||
from .imp_config import ImpConfig
|
||||
from .quart_config import QuartConfig
|
||||
|
||||
__all__ = [
|
||||
"ImpConfig",
|
||||
"ImpBlueprintConfig",
|
||||
"QuartConfig",
|
||||
]
|
55
src/quart_imp/config/imp_blueprint_config.py
Normal file
55
src/quart_imp/config/imp_blueprint_config.py
Normal file
@ -0,0 +1,55 @@
|
||||
import typing as t
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImpBlueprintConfig:
|
||||
enabled: t.Optional[bool] = None
|
||||
url_prefix: t.Optional[str] = None
|
||||
subdomain: t.Optional[str] = None
|
||||
url_default: t.Optional[t.Dict[str, t.Any]] = None
|
||||
static_folder: t.Optional[str] = None
|
||||
template_folder: t.Optional[str] = None
|
||||
static_url_path: t.Optional[str] = None
|
||||
root_path: t.Optional[str] = None
|
||||
cli_group: t.Optional[str] = None
|
||||
|
||||
init_session: t.Optional[t.Dict[str, t.Any]] = None
|
||||
|
||||
_blueprint_attrs = {
|
||||
"url_prefix",
|
||||
"subdomain",
|
||||
"url_defaults",
|
||||
"static_folder",
|
||||
"template_folder",
|
||||
"static_url_path",
|
||||
"root_path",
|
||||
"cli_group",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
enabled: bool = False,
|
||||
url_prefix: t.Optional[str] = None,
|
||||
subdomain: t.Optional[str] = None,
|
||||
url_defaults: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
static_folder: t.Optional[str] = None,
|
||||
template_folder: t.Optional[str] = None,
|
||||
static_url_path: t.Optional[str] = None,
|
||||
root_path: t.Optional[str] = None,
|
||||
cli_group: t.Optional[str] = None,
|
||||
init_session: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
):
|
||||
self.enabled = enabled
|
||||
self.url_prefix = url_prefix
|
||||
self.subdomain = subdomain
|
||||
self.url_defaults = url_defaults
|
||||
self.static_folder = static_folder
|
||||
self.template_folder = template_folder
|
||||
self.static_url_path = static_url_path
|
||||
self.root_path = root_path
|
||||
self.cli_group = cli_group
|
||||
self.init_session = init_session
|
||||
|
||||
def quart_blueprint_args(self) -> t.Dict[str, t.Any]:
|
||||
return {k: getattr(self, k) for k in self._blueprint_attrs if getattr(self, k)}
|
14
src/quart_imp/config/imp_config.py
Normal file
14
src/quart_imp/config/imp_config.py
Normal file
@ -0,0 +1,14 @@
|
||||
import typing as t
|
||||
|
||||
|
||||
class ImpConfig:
|
||||
IMP_INIT_SESSION: t.Optional[t.Dict[str, t.Any]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
init_session: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
):
|
||||
if not init_session:
|
||||
self.IMP_INIT_SESSION = {}
|
||||
else:
|
||||
self.IMP_INIT_SESSION = init_session
|
162
src/quart_imp/config/quart_config.py
Normal file
162
src/quart_imp/config/quart_config.py
Normal file
@ -0,0 +1,162 @@
|
||||
import typing as t
|
||||
|
||||
from quart import Quart
|
||||
|
||||
|
||||
class QuartConfig:
|
||||
DEBUG: t.Optional[bool]
|
||||
PROPAGATE_EXCEPTIONS: t.Optional[bool]
|
||||
TRAP_HTTP_EXCEPTIONS: t.Optional[bool]
|
||||
TRAP_BAD_REQUEST_ERRORS: t.Optional[bool]
|
||||
SECRET_KEY: t.Optional[str]
|
||||
SESSION_COOKIE_NAME: t.Optional[str]
|
||||
SESSION_COOKIE_DOMAIN: t.Optional[str]
|
||||
SESSION_COOKIE_PATH: t.Optional[str]
|
||||
SESSION_COOKIE_HTTPONLY: t.Optional[bool]
|
||||
SESSION_COOKIE_SECURE: t.Optional[bool]
|
||||
SESSION_COOKIE_SAMESITE: t.Optional[t.Literal["Lax", "Strict"]]
|
||||
PERMANENT_SESSION_LIFETIME: t.Optional[int]
|
||||
SESSION_REFRESH_EACH_REQUEST: t.Optional[bool]
|
||||
USE_X_SENDFILE: t.Optional[bool]
|
||||
SEND_FILE_MAX_AGE_DEFAULT: t.Optional[int]
|
||||
ERROR_404_HELP: t.Optional[bool]
|
||||
SERVER_NAME: t.Optional[str]
|
||||
APPLICATION_ROOT: t.Optional[str]
|
||||
PREFERRED_URL_SCHEME: t.Optional[str]
|
||||
MAX_CONTENT_LENGTH: t.Optional[int]
|
||||
TEMPLATES_AUTO_RELOAD: t.Optional[bool]
|
||||
EXPLAIN_TEMPLATE_LOADING: t.Optional[bool]
|
||||
MAX_COOKIE_SIZE: t.Optional[int]
|
||||
|
||||
_additional: t.Dict[str, t.Any]
|
||||
|
||||
_quart_config_keys = {
|
||||
"DEBUG",
|
||||
"PROPAGATE_EXCEPTIONS",
|
||||
"TRAP_HTTP_EXCEPTIONS",
|
||||
"TRAP_BAD_REQUEST_ERRORS",
|
||||
"SECRET_KEY",
|
||||
"SESSION_COOKIE_NAME",
|
||||
"SESSION_COOKIE_DOMAIN",
|
||||
"SESSION_COOKIE_PATH",
|
||||
"SESSION_COOKIE_HTTPONLY",
|
||||
"SESSION_COOKIE_SECURE",
|
||||
"SESSION_COOKIE_SAMESITE",
|
||||
"PERMANENT_SESSION_LIFETIME",
|
||||
"SESSION_REFRESH_EACH_REQUEST",
|
||||
"USE_X_SENDFILE",
|
||||
"SEND_FILE_MAX_AGE_DEFAULT",
|
||||
"ERROR_404_HELP",
|
||||
"SERVER_NAME",
|
||||
"APPLICATION_ROOT",
|
||||
"PREFERRED_URL_SCHEME",
|
||||
"MAX_CONTENT_LENGTH",
|
||||
"TEMPLATES_AUTO_RELOAD",
|
||||
"EXPLAIN_TEMPLATE_LOADING",
|
||||
"MAX_COOKIE_SIZE",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
debug: t.Optional[bool] = None,
|
||||
propagate_exceptions: t.Optional[bool] = None,
|
||||
trap_http_exceptions: t.Optional[bool] = None,
|
||||
trap_bad_request_errors: t.Optional[bool] = None,
|
||||
secret_key: t.Optional[str] = None,
|
||||
session_cookie_name: t.Optional[str] = None,
|
||||
session_cookie_domain: t.Optional[str] = None,
|
||||
session_cookie_path: t.Optional[str] = None,
|
||||
session_cookie_httponly: t.Optional[bool] = None,
|
||||
session_cookie_secure: t.Optional[bool] = None,
|
||||
session_cookie_samesite: t.Optional[t.Literal["Lax", "Strict"]] = None,
|
||||
permanent_session_lifetime: t.Optional[int] = None,
|
||||
session_refresh_each_request: t.Optional[bool] = None,
|
||||
use_x_sendfile: t.Optional[bool] = None,
|
||||
send_file_max_age_default: t.Optional[int] = None,
|
||||
error_404_help: t.Optional[bool] = None,
|
||||
server_name: t.Optional[str] = None,
|
||||
application_root: t.Optional[str] = None,
|
||||
preferred_url_scheme: t.Optional[str] = None,
|
||||
max_content_length: t.Optional[int] = None,
|
||||
templates_auto_reload: t.Optional[bool] = None,
|
||||
explain_template_loading: t.Optional[bool] = None,
|
||||
max_cookie_size: t.Optional[int] = None,
|
||||
app_instance: t.Optional["Quart"] = None,
|
||||
additional: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Quart configuration class modeled after the Quart documentation.
|
||||
|
||||
Additional config values can be set by passing them as a dict or using the set_additional method.
|
||||
|
||||
All key arguments are converted to uppercase and added to the Quart app.config dictionary.
|
||||
e.g. session_cookie_name -> app.config["SESSION_COOKIE_NAME"]
|
||||
"""
|
||||
self.DEBUG = debug
|
||||
self.PROPAGATE_EXCEPTIONS = propagate_exceptions
|
||||
self.TRAP_HTTP_EXCEPTIONS = trap_http_exceptions
|
||||
self.TRAP_BAD_REQUEST_ERRORS = trap_bad_request_errors
|
||||
self.SECRET_KEY = secret_key
|
||||
self.SESSION_COOKIE_NAME = session_cookie_name
|
||||
self.SESSION_COOKIE_DOMAIN = session_cookie_domain
|
||||
self.SESSION_COOKIE_PATH = session_cookie_path
|
||||
self.SESSION_COOKIE_HTTPONLY = session_cookie_httponly
|
||||
self.SESSION_COOKIE_SECURE = session_cookie_secure
|
||||
self.SESSION_COOKIE_SAMESITE = session_cookie_samesite
|
||||
self.PERMANENT_SESSION_LIFETIME = permanent_session_lifetime
|
||||
self.SESSION_REFRESH_EACH_REQUEST = session_refresh_each_request
|
||||
self.USE_X_SENDFILE = use_x_sendfile
|
||||
self.SEND_FILE_MAX_AGE_DEFAULT = send_file_max_age_default
|
||||
self.ERROR_404_HELP = error_404_help
|
||||
self.SERVER_NAME = server_name
|
||||
self.APPLICATION_ROOT = application_root
|
||||
self.PREFERRED_URL_SCHEME = preferred_url_scheme
|
||||
self.MAX_CONTENT_LENGTH = max_content_length
|
||||
self.TEMPLATES_AUTO_RELOAD = templates_auto_reload
|
||||
self.EXPLAIN_TEMPLATE_LOADING = explain_template_loading
|
||||
self.MAX_COOKIE_SIZE = max_cookie_size
|
||||
self._additional = additional or {}
|
||||
|
||||
if app_instance is not None:
|
||||
self.init_app(app_instance)
|
||||
|
||||
def set_additional(self, _auto_uppercase: bool = True, **kwargs: t.Any) -> None:
|
||||
"""
|
||||
Set additional config values that are not part of the QuartConfig class.
|
||||
Keys are converted to uppercase.
|
||||
"""
|
||||
if kwargs:
|
||||
self._additional.update(
|
||||
{
|
||||
k.upper()
|
||||
if isinstance(k, str)
|
||||
else k
|
||||
if _auto_uppercase
|
||||
else k and k.upper() not in self._quart_config_keys: v
|
||||
for k, v in kwargs.items()
|
||||
}
|
||||
)
|
||||
|
||||
def init_app(self, app: Quart) -> None:
|
||||
if not isinstance(app, Quart):
|
||||
raise TypeError("The app that was passed in is not an instance of Quart")
|
||||
self.apply_config(app)
|
||||
|
||||
def apply_config(self, app: Quart) -> None:
|
||||
if not isinstance(app, Quart):
|
||||
raise TypeError("The app that was passed in is not an instance of Quart")
|
||||
|
||||
app.config.update(
|
||||
{
|
||||
**{k: v for k, v in self.as_dict().items() if v is not None},
|
||||
**{k.upper(): v for k, v in self._additional.items()},
|
||||
}
|
||||
)
|
||||
|
||||
def as_dict(self) -> t.Dict[str, t.Any]:
|
||||
return {
|
||||
**{
|
||||
k: getattr(self, k) for k in self._quart_config_keys if getattr(self, k)
|
||||
},
|
||||
**self._additional,
|
||||
}
|
2
src/quart_imp/exceptions.py
Normal file
2
src/quart_imp/exceptions.py
Normal file
@ -0,0 +1,2 @@
|
||||
class NoConfigProvided(Exception):
|
||||
pass
|
294
src/quart_imp/imp.py
Normal file
294
src/quart_imp/imp.py
Normal file
@ -0,0 +1,294 @@
|
||||
import asyncio
|
||||
import typing as t
|
||||
from importlib import import_module
|
||||
from inspect import getmembers
|
||||
from pathlib import Path
|
||||
|
||||
from quart import Quart, Blueprint, session
|
||||
|
||||
from .config import ImpConfig
|
||||
from .imp_blueprint import ImpBlueprint
|
||||
from .utilities import cast_to_import_str
|
||||
|
||||
|
||||
class Imp:
|
||||
app: Quart
|
||||
app_name: str
|
||||
app_path: Path
|
||||
app_instance_path: Path
|
||||
app_folder: Path
|
||||
app_resources_imported: bool = False
|
||||
|
||||
config: ImpConfig
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app: t.Optional[Quart] = None,
|
||||
config: t.Optional[ImpConfig] = None,
|
||||
) -> None:
|
||||
if app is not None:
|
||||
self.init_app(app, config)
|
||||
|
||||
def init_app(
|
||||
self,
|
||||
app: Quart,
|
||||
config: t.Optional[ImpConfig] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initializes the app with the quart-imp extension.
|
||||
|
||||
:param app: The quart app to initialize.
|
||||
:param config: The config to use
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if app is None:
|
||||
raise ImportError(
|
||||
"No app was passed in, do imp = Imp(quartapp) or app.init_app(quartapp)"
|
||||
)
|
||||
if not isinstance(app, Quart):
|
||||
raise TypeError("The app that was passed in is not an instance of Quart")
|
||||
|
||||
if "imp" in app.extensions:
|
||||
raise ImportError("The app has already been initialized with quart-imp.")
|
||||
|
||||
self.app = app
|
||||
self.app_name = app.name
|
||||
self.app_path = Path(self.app.root_path)
|
||||
self.app_instance_path = Path(self.app.instance_path)
|
||||
self.app_folder = self.app_path.parent
|
||||
self.app.extensions["imp"] = self
|
||||
|
||||
if config:
|
||||
self.config = config
|
||||
else:
|
||||
self.config = ImpConfig()
|
||||
|
||||
self._init_session()
|
||||
|
||||
self.app_instance_path.mkdir(exist_ok=True)
|
||||
|
||||
def import_app_resources(
|
||||
self,
|
||||
folder: str = "resources",
|
||||
factories: t.Optional[t.List[str]] = None,
|
||||
static_folder: str = "static",
|
||||
templates_folder: str = "templates",
|
||||
scope_import: t.Optional[t.Dict[str, t.Union[t.List[str], str]]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Imports the app resources from the given folder.
|
||||
|
||||
: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 scope_import: A dict of files to import e.g. {"folder_name": "*"}.
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# Check if the app resources have already been imported
|
||||
if self.app_resources_imported:
|
||||
raise ImportError("The app resources can only be imported once.")
|
||||
|
||||
self.app_resources_imported = True
|
||||
|
||||
# Set defaults
|
||||
if factories is None:
|
||||
factories = []
|
||||
if scope_import is None:
|
||||
scope_import = {"*": ["*"]}
|
||||
|
||||
# Build folders
|
||||
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"Resources collection must be a folder, value given: {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
|
||||
)
|
||||
|
||||
skip_folders = (
|
||||
"static",
|
||||
"templates",
|
||||
)
|
||||
|
||||
for item in resources_folder.iterdir():
|
||||
if item.name.startswith("__"):
|
||||
continue
|
||||
|
||||
if item.is_file() and item.suffix == ".py":
|
||||
if "*" in scope_import:
|
||||
if "*" in scope_import["*"]:
|
||||
self._import_resource_module(item, factories)
|
||||
else:
|
||||
if item.name in scope_import["*"]:
|
||||
self._import_resource_module(item, factories)
|
||||
|
||||
if "." in scope_import:
|
||||
if "*" in scope_import["."]:
|
||||
self._import_resource_module(item, factories)
|
||||
else:
|
||||
if item.name in scope_import["."]:
|
||||
self._import_resource_module(item, factories)
|
||||
|
||||
if item.is_dir():
|
||||
# skip the static and templates folders
|
||||
if item.name in skip_folders:
|
||||
continue
|
||||
|
||||
for py_file_in_item in item.glob("*.py"):
|
||||
if "*" in scope_import:
|
||||
if "*" in scope_import["*"]:
|
||||
self._import_resource_module(py_file_in_item, factories)
|
||||
else:
|
||||
if py_file_in_item.name in scope_import["*"]:
|
||||
self._import_resource_module(py_file_in_item, factories)
|
||||
|
||||
if item.name in scope_import:
|
||||
if "*" in scope_import[item.name]:
|
||||
self._import_resource_module(py_file_in_item, factories)
|
||||
else:
|
||||
if py_file_in_item.name in scope_import[item.name]:
|
||||
self._import_resource_module(py_file_in_item, factories)
|
||||
|
||||
def import_blueprint(self, blueprint: str) -> None:
|
||||
"""
|
||||
Import a blueprint from the given package.
|
||||
|
||||
:param blueprint: The blueprint (folder name) to import. Must be relative.
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if Path(blueprint).is_absolute():
|
||||
blueprint_path = Path(blueprint)
|
||||
else:
|
||||
blueprint_path = Path(self.app_path / blueprint)
|
||||
|
||||
if blueprint_path.exists() and blueprint_path.is_dir():
|
||||
module = import_module(cast_to_import_str(self.app_name, blueprint_path))
|
||||
for name, potential_blueprint in getmembers(module):
|
||||
if isinstance(potential_blueprint, ImpBlueprint):
|
||||
self._imp_blueprint_registration(potential_blueprint)
|
||||
continue
|
||||
|
||||
if isinstance(potential_blueprint, Blueprint):
|
||||
self._quart_blueprint_registration(potential_blueprint)
|
||||
|
||||
def import_blueprints(self, folder: str) -> None:
|
||||
"""
|
||||
Import all blueprints from the given folder.
|
||||
|
||||
:param folder: The folder to import from. Must be relative.
|
||||
:return: None
|
||||
"""
|
||||
|
||||
folder_path = Path(self.app_path / folder)
|
||||
|
||||
if not folder_path.exists():
|
||||
raise ImportError(f"Cannot find blueprints folder at {folder_path}")
|
||||
|
||||
if not folder_path.is_dir():
|
||||
raise ImportError(f"Blueprints must be a folder {folder_path}")
|
||||
|
||||
for potential_bp in folder_path.iterdir():
|
||||
self.import_blueprint(f"{potential_bp}")
|
||||
|
||||
async def _async_import_resource_module(
|
||||
self, module: Path, factories: t.List[str]
|
||||
) -> None:
|
||||
try:
|
||||
async with self.app.app_context():
|
||||
file_module = import_module(cast_to_import_str(self.app_name, module))
|
||||
|
||||
for instance_factory in factories:
|
||||
if hasattr(file_module, instance_factory):
|
||||
getattr(file_module, instance_factory)(self.app)
|
||||
|
||||
except ImportError as e:
|
||||
raise ImportError(f"Error when importing {module}: {e}")
|
||||
|
||||
def _import_resource_module(self, module: Path, factories: t.List[str]) -> None:
|
||||
asyncio.run(self._async_import_resource_module(module, factories))
|
||||
|
||||
def _imp_blueprint_registration(self, imp_blueprint: ImpBlueprint) -> None:
|
||||
if not imp_blueprint.config.enabled:
|
||||
self.app.logger.debug(
|
||||
f"Imp Blueprint [{imp_blueprint.bp_name}] is disabled."
|
||||
)
|
||||
return
|
||||
|
||||
for nested_blueprint in imp_blueprint.nested_blueprints:
|
||||
if isinstance(nested_blueprint, ImpBlueprint):
|
||||
self._nested_imp_blueprint_registration(imp_blueprint, nested_blueprint)
|
||||
|
||||
elif isinstance(nested_blueprint, Blueprint):
|
||||
self._nested_quart_blueprint_registration(
|
||||
nested_blueprint, nested_blueprint
|
||||
)
|
||||
|
||||
if imp_blueprint.config.init_session:
|
||||
self.config.IMP_INIT_SESSION.update(
|
||||
imp_blueprint.config.init_session
|
||||
) if isinstance(self.config.IMP_INIT_SESSION, dict) else None
|
||||
|
||||
self._quart_blueprint_registration(imp_blueprint)
|
||||
|
||||
def _nested_imp_blueprint_registration(
|
||||
self,
|
||||
parent: ImpBlueprint,
|
||||
child: ImpBlueprint,
|
||||
) -> None:
|
||||
if not parent.config.enabled:
|
||||
return
|
||||
|
||||
if not child.config.enabled:
|
||||
self.app.logger.debug(
|
||||
f"Imp Blueprint [{child.bp_name}] is disabled. Parent: [{parent.bp_name}]"
|
||||
)
|
||||
return
|
||||
|
||||
parent.register_blueprint(child)
|
||||
|
||||
for partial_model in child.models:
|
||||
partial_model(imp_instance=self)
|
||||
|
||||
if child.config.init_session:
|
||||
if self.config.IMP_INIT_SESSION is None:
|
||||
self.config.IMP_INIT_SESSION = {}
|
||||
|
||||
self.config.IMP_INIT_SESSION.update(child.config.init_session)
|
||||
|
||||
def _quart_blueprint_registration(self, blueprint: Blueprint) -> None:
|
||||
self.app.register_blueprint(blueprint)
|
||||
|
||||
@staticmethod
|
||||
def _nested_quart_blueprint_registration(
|
||||
parent: Blueprint,
|
||||
child: Blueprint,
|
||||
) -> None:
|
||||
parent.register_blueprint(blueprint=child)
|
||||
|
||||
def _init_session(self) -> None:
|
||||
"""
|
||||
:return: None
|
||||
"""
|
||||
if isinstance(self.config.IMP_INIT_SESSION, dict):
|
||||
_: t.Dict[str, t.Any] = self.config.IMP_INIT_SESSION
|
||||
|
||||
@self.app.before_request
|
||||
async def imp_before_request() -> None:
|
||||
session.update({k: v for k, v in _.items() if k not in session})
|
167
src/quart_imp/imp_blueprint.py
Normal file
167
src/quart_imp/imp_blueprint.py
Normal file
@ -0,0 +1,167 @@
|
||||
import typing as t
|
||||
from importlib import import_module
|
||||
from importlib.util import find_spec
|
||||
from inspect import getmembers
|
||||
from pathlib import Path
|
||||
|
||||
from quart import Blueprint
|
||||
|
||||
from .config import ImpBlueprintConfig
|
||||
from .exceptions import NoConfigProvided
|
||||
from .utilities import (
|
||||
cast_to_import_str,
|
||||
slug,
|
||||
)
|
||||
|
||||
ArgT = t.TypeVar("ArgT")
|
||||
ReturnT = t.TypeVar("ReturnT")
|
||||
|
||||
|
||||
class ImpBlueprint(Blueprint):
|
||||
"""
|
||||
A Class that extends the capabilities of the Quart Blueprint class.
|
||||
"""
|
||||
|
||||
config: ImpBlueprintConfig
|
||||
|
||||
location: Path
|
||||
bp_name: str
|
||||
package: str
|
||||
|
||||
models: t.Set[t.Any]
|
||||
nested_blueprints: t.Set[t.Union["ImpBlueprint", Blueprint]]
|
||||
|
||||
def __init__(self, dunder_name: str, config: ImpBlueprintConfig) -> None:
|
||||
"""
|
||||
Initializes the ImpBlueprint.
|
||||
|
||||
:param dunder_name: __name__
|
||||
:param config: The blueprint's config.
|
||||
"""
|
||||
|
||||
self.models = set()
|
||||
self.nested_blueprints = set()
|
||||
|
||||
self.package = dunder_name
|
||||
|
||||
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
|
||||
|
||||
if config is None:
|
||||
raise NoConfigProvided(f"No config was provided for {self.location}")
|
||||
|
||||
self.config = config
|
||||
|
||||
if not self.config.url_prefix:
|
||||
self.config.url_prefix = f"/{slug(self.bp_name)}"
|
||||
|
||||
super().__init__(
|
||||
self.bp_name, self.package, **self.config.quart_blueprint_args()
|
||||
)
|
||||
|
||||
def _prevent_if_disabled(self: "ImpBlueprint") -> bool:
|
||||
if not self.config.enabled:
|
||||
return True
|
||||
return False
|
||||
|
||||
def as_quart_blueprint(self) -> Blueprint:
|
||||
"""
|
||||
Returns the blueprint as a Quart Blueprint.
|
||||
|
||||
:return: Blueprint
|
||||
"""
|
||||
return self
|
||||
|
||||
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).
|
||||
|
||||
:param folder: Folder to look for resources in. Defaults to "routes". Must be relative.
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if self._prevent_if_disabled():
|
||||
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: t.Union[str, Path]) -> None:
|
||||
"""
|
||||
Imports the specified Quart-Imp Blueprint or a standard Quart Blueprint as a nested blueprint,
|
||||
under the current blueprint.
|
||||
|
||||
:param blueprint: The blueprint (folder name) to import. Must be relative.
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if self._prevent_if_disabled():
|
||||
return
|
||||
|
||||
if isinstance(blueprint, Path):
|
||||
potential_bp = blueprint
|
||||
else:
|
||||
if isinstance(blueprint, str):
|
||||
if Path(blueprint).is_absolute():
|
||||
potential_bp = Path(blueprint)
|
||||
else:
|
||||
potential_bp = Path(self.location / blueprint)
|
||||
else:
|
||||
raise ValueError("Blueprint must be a string or a Path object")
|
||||
|
||||
if potential_bp.exists() and potential_bp.is_dir():
|
||||
module = import_module(
|
||||
cast_to_import_str(self.package.split(".")[0], potential_bp)
|
||||
)
|
||||
for name, potential in getmembers(module):
|
||||
if isinstance(potential, ImpBlueprint):
|
||||
self.nested_blueprints.add(potential)
|
||||
continue
|
||||
|
||||
if isinstance(potential, Blueprint):
|
||||
self.nested_blueprints.add(potential)
|
||||
|
||||
def import_nested_blueprints(self, folder: str) -> None:
|
||||
"""
|
||||
Imports all blueprints in the given folder.
|
||||
|
||||
:param folder: Folder to look for nested blueprints in.
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if self._prevent_if_disabled():
|
||||
return
|
||||
|
||||
folder_path = Path(self.location / folder)
|
||||
|
||||
if not folder_path.exists() or not folder_path.is_dir():
|
||||
raise NotADirectoryError(f"{folder_path} is not a directory")
|
||||
|
||||
for potential_bp in folder_path.iterdir():
|
||||
self.import_nested_blueprint(blueprint=potential_bp)
|
||||
|
||||
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.
|
||||
|
||||
: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}"
|
101
src/quart_imp/protocols.py
Normal file
101
src/quart_imp/protocols.py
Normal file
@ -0,0 +1,101 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
from pathlib import Path
|
||||
|
||||
from quart import Quart, Blueprint
|
||||
|
||||
ImpBlueprintSelf = t.TypeVar("ImpBlueprintSelf", bound="ImpBlueprint")
|
||||
|
||||
|
||||
@t.runtime_checkable
|
||||
class ImpBlueprint(t.Protocol):
|
||||
config: "ImpBlueprintConfig"
|
||||
|
||||
location: Path
|
||||
bp_name: str
|
||||
package: str
|
||||
|
||||
models: t.Set[t.Any]
|
||||
nested_blueprints: t.Set[t.Union["ImpBlueprint", Blueprint]]
|
||||
|
||||
def _prevent_if_disabled(self: "ImpBlueprint") -> bool: ...
|
||||
|
||||
def as_quart_blueprint(self) -> Blueprint: ...
|
||||
|
||||
def import_resources(self, folder: str = "routes") -> None: ...
|
||||
|
||||
def import_nested_blueprint(self, blueprint: str) -> None: ...
|
||||
|
||||
def import_nested_blueprints(self, folder: str) -> None: ...
|
||||
|
||||
def tmpl(self, template: str) -> str: ...
|
||||
|
||||
|
||||
@t.runtime_checkable
|
||||
class Imp(t.Protocol):
|
||||
app: Quart
|
||||
config: t.Any
|
||||
app_instance_path: Path
|
||||
app_path: Path
|
||||
|
||||
|
||||
@t.runtime_checkable
|
||||
class QuartConfig(t.Protocol):
|
||||
DEBUG: t.Optional[bool]
|
||||
PROPAGATE_EXCEPTIONS: t.Optional[bool]
|
||||
TRAP_HTTP_EXCEPTIONS: t.Optional[bool]
|
||||
TRAP_BAD_REQUEST_ERRORS: t.Optional[bool]
|
||||
SECRET_KEY: t.Optional[str]
|
||||
SESSION_COOKIE_NAME: t.Optional[str]
|
||||
SESSION_COOKIE_DOMAIN: t.Optional[str]
|
||||
SESSION_COOKIE_PATH: t.Optional[str]
|
||||
SESSION_COOKIE_HTTPONLY: t.Optional[bool]
|
||||
SESSION_COOKIE_SECURE: t.Optional[bool]
|
||||
SESSION_COOKIE_SAMESITE: t.Optional[t.Literal["Lax", "Strict"]]
|
||||
PERMANENT_SESSION_LIFETIME: t.Optional[int]
|
||||
SESSION_REFRESH_EACH_REQUEST: t.Optional[bool]
|
||||
USE_X_SENDFILE: t.Optional[bool]
|
||||
SEND_FILE_MAX_AGE_DEFAULT: t.Optional[int]
|
||||
ERROR_404_HELP: t.Optional[bool]
|
||||
SERVER_NAME: t.Optional[str]
|
||||
APPLICATION_ROOT: t.Optional[str]
|
||||
PREFERRED_URL_SCHEME: t.Optional[str]
|
||||
MAX_CONTENT_LENGTH: t.Optional[int]
|
||||
TEMPLATES_AUTO_RELOAD: t.Optional[bool]
|
||||
EXPLAIN_TEMPLATE_LOADING: t.Optional[bool]
|
||||
MAX_COOKIE_SIZE: t.Optional[int]
|
||||
|
||||
_quart_config_keys: t.Set[str]
|
||||
|
||||
def apply_config(self, app: Quart) -> None: ...
|
||||
|
||||
def as_dict(self) -> t.Dict[str, t.Any]: ...
|
||||
|
||||
|
||||
@t.runtime_checkable
|
||||
class ImpConfig(t.Protocol):
|
||||
FLASK: QuartConfig
|
||||
|
||||
INIT_SESSION: t.Optional[t.Dict[str, t.Any]]
|
||||
|
||||
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: ...
|
||||
|
||||
|
||||
@t.runtime_checkable
|
||||
class ImpBlueprintConfig(t.Protocol):
|
||||
enabled: t.Optional[bool]
|
||||
url_prefix: t.Optional[str]
|
||||
subdomain: t.Optional[str]
|
||||
url_default: t.Optional[t.Dict[str, t.Any]]
|
||||
static_folder: t.Optional[str]
|
||||
template_folder: t.Optional[str]
|
||||
static_url_path: t.Optional[str]
|
||||
root_path: t.Optional[str]
|
||||
cli_group: t.Optional[str]
|
||||
|
||||
init_session: t.Optional[t.Dict[str, t.Any]]
|
||||
|
||||
_blueprint_attrs: t.Set[str]
|
||||
|
||||
def quart_blueprint_args(self) -> t.Dict[str, t.Any]: ...
|
@ -2,7 +2,7 @@ import typing as t
|
||||
|
||||
|
||||
def _check_against_values_allowed(
|
||||
session_value: t.Union[list, str, int, bool],
|
||||
session_value: t.Union[t.List[str], str, int, bool],
|
||||
values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool],
|
||||
) -> bool:
|
||||
"""
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user