diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 0000000..3e4c634 --- /dev/null +++ b/docs/__init__.py @@ -0,0 +1,60 @@ +from pathlib import Path +from time import sleep + +import click +from flask import Flask + +from .config import Config +from ._ssg import compiler + +cwd = Path(__file__).parent + + +def create_app(): + app = Flask(__name__) + app.template_folder = "_templates" + + doc_path = Path(cwd / Config.latest) + markdown_path = Path(cwd / "_md" / Config.latest) + + @app.cli.command("compile") + @click.option("--watch", is_flag=True, help="Watch for file changes") + def compile_site(watch): + if watch: + watching_files = {} + + def change_loop(): + change = False + updated = [] + for file in markdown_path.glob("**/*.md"): + if file not in watching_files: + watching_files[file] = file.stat().st_mtime + updated.append(file) + change = True + else: + if file.stat().st_mtime > watching_files[file]: + watching_files[file] = file.stat().st_mtime + updated.append(file) + change = True + + if change: + print("Update detected, recompiling...") + for file in updated: + print(f" - {file}") + + compiler(doc_path, markdown_path) + + print("Watching for changes...") + + while True: + change_loop() + sleep(1) + + else: + compiler(doc_path, markdown_path) + + @app.route("/") + def index(): + return "To use run the following command: flask --app gdocs compile" + + return app diff --git a/docs/_md/v1/CLI Commands-quart-imp blueprint.md b/docs/_md/v1/CLI Commands-quart-imp blueprint.md new file mode 100644 index 0000000..a1b465d --- /dev/null +++ b/docs/_md/v1/CLI Commands-quart-imp blueprint.md @@ -0,0 +1,101 @@ +``` +Menu = CLI Commands/quart-imp blueprint +Title = Generate a Quart-Imp Blueprint +``` + +Quart-Imp has its own type of blueprint. It comes with some methods to auto import routes, and nested blueprints etc... +see [ImpBlueprint / Introduction](impblueprint-introduction.html) for more information. + +You have the option to generate a regular template rendering blueprint, or a API blueprint that returns a JSON response. + +```bash +quart-imp blueprint --help +``` +or +```bash +quart-imp api-blueprint --help +``` + +To generate a Quart-Imp blueprint, run the following command: + +```bash +quart-imp blueprint +``` +or +```bash +quart-imp api-blueprint +``` + +After running this command, you will be prompted to enter the location of where you want to create your blueprint: + +```text +~ $ quart-imp blueprint +(Creation is relative to the current working directory) +Folder to create blueprint in [Current Working Directory]: +``` + +As detailed in the prompt, the creation of the blueprint is relative to the current working directory. So to create a +blueprint in the folder `app/blueprints`, you would enter `app/blueprints` in the prompt. + +```text +~ $ quart-imp blueprint +(Creation is relative to the current working directory) +Folder to create blueprint in [Current Working Directory]: app/blueprints +``` + +You will then be prompted to enter a name for your blueprint: + +```text +~ $ quart-imp blueprint +... +Name of the blueprint to create [my_new_blueprint]: +``` + +The default name is 'my_new_blueprint', we will change this to 'admin' + +```text +~ $ quart-imp blueprint +... +Name of the blueprint to create [my_new_blueprint]: admin +``` + +After creating your blueprint, the folder structure will look like this: + +```text +app/ +├── blueprints +│ └── admin +│ ├── routes +│ │ └── index.py +│ │ +│ ├── static +│ │ ├── css +│ │ │ └── water.css +│ │ ├── img +│ │ │ └── quart-imp-logo.png +│ │ └── js +│ │ └── main.js +│ │ +│ ├── templates +│ │ └── www +│ │ ├── extends +│ │ │ └── main.html +│ │ ├── includes +│ │ │ ├── footer.html +│ │ │ └── header.html +│ │ └── index.html +│ │ +│ └── __init__.py +│ +... +``` + +This is a self-contained blueprint, so it has its own static, templates and routes folders. +You can now navigate '/admin' + +You can streamline this process by specifying the name of the blueprint, the folder to +create it in and the configuration to use, like so: + +```bash +quart-imp blueprint -n admin -f app/blueprints +``` \ No newline at end of file diff --git a/docs/_md/v1/CLI Commands-quart-imp init.md b/docs/_md/v1/CLI Commands-quart-imp init.md new file mode 100644 index 0000000..dd2653b --- /dev/null +++ b/docs/_md/v1/CLI Commands-quart-imp init.md @@ -0,0 +1,215 @@ +``` +Menu = CLI Commands/quart-imp init +Title = Initialising a Quart-Imp Project +``` + +Quart-Imp has a cli command that deploys a new ready-to-go project. +This project is structured in a way to give you the best idea of +how to use Quart-Imp. + +```bash +quart-imp init --help +``` + +## Create a new project + +Make sure you are in the virtual environment, and at the root of your +project folder, then run the following command: + +```bash +quart-imp init +``` + +After running this command, you will be prompted to choose what type of +app you want to deploy: + +```text +~ $ quart-imp init +What type of app would you like to create? (minimal, slim, full) [minimal]: +``` + +See below for the differences between the app types. + +After this, you will be prompted to enter a name for your app: + +```text +~ $ quart-imp init +... +What would you like to call your app? [app]: +``` + +'app' is the default name, so if you just press enter, your app will be +called 'app'. You will then see this output: + +```text +~ FILES CREATED WILL LOOP OUT HERE ~ + +=================== +Quart app deployed! +=================== + +Your app has the default name of 'app' +Quart will automatically look for this! +Run: quart run --debug + +``` + +If you called your app something other than 'app', like 'new' for example, you will see: + +```text +~ FILES CREATED WILL LOOP OUT HERE ~ + +=================== +Quart app deployed! +=================== + +Your app has the name of 'new' +Run: quart --app new run --debug + +``` + +As you can see from the output, it gives you instructions on how to start your app, +depending on the name you gave it. + +You should see a new folder that has been given the name you specified in +the `quart-imp init` command. + +### Additional options + +You can also specify a name for your app in the command itself, like so: + +```bash +quart-imp init -n my_app +``` + +This will create a new app called 'my_app'. +The default will be a minimal app, this has no blueprints. + +You can also deploy a slim app, that will have one blueprint, like so: + +```bash +quart-imp init -n my_app --slim +``` + +You can also deploy a full app that is setup for multiple blueprints, like so: + +```bash +quart-imp init -n my_app --full +``` + +## init Folder structures + +### Minimal app (default) + +`quart-imp init --minimal`: + +```text +app/ +├── resources +│ ├── static +│ │ ├── css +│ │ │ └── water.css +│ │ ├── img +│ │ │ └── quart-imp-logo.png +│ │ └── favicon.ico +│ ├── templates +│ │ └── index.html +│ └── routes.py +│ +└── __init__.py +``` + +### Slim app + +`quart-imp init --slim`: + +```text +app/ +├── extensions +│ └── __init__.py +│ +├── resources +│ ├── cli +│ │ └── cli.py +│ ├── error_handlers +│ │ └── error_handlers.py +│ ├── static +│ │ ├── css +│ │ │ └── water.css +│ │ ├── img +│ │ │ └── quart-imp-logo.png +│ │ └── favicon.ico +│ └── templates +│ └── error.html +│ +├── www +│ ├── __init__.py +│ ├── routes +│ │ └── index.py +│ ├── static +│ │ ├── css +│ │ │ └── water.css +│ │ ├── img +│ │ │ └── quart-imp-logo.png +│ │ └── js +│ │ └── main.js +│ └── templates +│ └── www +│ ├── extends +│ │ └── main.html +│ ├── includes +│ │ ├── footer.html +│ │ └── header.html +│ └── index.html +│ +└── __init__.py +``` + +### Full app + +`quart-imp init --full`: + +```text +app/ +├── blueprints +│ └── www +│ ├── __init__.py +│ ├── routes +│ │ └── index.py +│ ├── static +│ │ ├── css +│ │ │ └── water.css +│ │ ├── img +│ │ │ └── quart-imp-logo.png +│ │ └── js +│ │ └── main.js +│ └── templates +│ └── www +│ ├── extends +│ │ └── main.html +│ ├── includes +│ │ ├── footer.html +│ │ └── header.html +│ └── index.html +│ +├── extensions +│ └── __init__.py +│ +├── resources +│ ├── cli +│ │ └── cli.py +│ ├── context_processors +│ │ └── context_processors.py +│ ├── error_handlers +│ │ └── error_handlers.py +│ ├── filters +│ │ └── filters.py +│ ├── routes +│ │ └── routes.py +│ ├── static +│ │ └── favicon.ico +│ └── templates +│ └── error.html +│ +└── __init__.py +``` diff --git a/docs/_md/v1/Imp-Introduction.md b/docs/_md/v1/Imp-Introduction.md new file mode 100644 index 0000000..46144f6 --- /dev/null +++ b/docs/_md/v1/Imp-Introduction.md @@ -0,0 +1,84 @@ +``` +Menu = Imp/Introduction +Title = Quart-Imp Introduction +``` + +Quart-Imp is a Quart extension that provides auto import methods for various Quart resources. It will import +blueprints, and other resources. It uses the importlib module to achieve this. + +Quart-Imp favors the application factory pattern as a project structure, and is opinionated towards using +Blueprints. However, you can use Quart-Imp without using Blueprints. + +Here's an example of a standard Quart-Imp project structure: + +```text +app/ +├── blueprints/ +│ ├── admin/... +│ ├── api/... +│ └── www/... +├── resources/ +│ ├── filters/... +│ ├── context_processors/... +│ ├── static/... +│ └── templates/... +└── __init__.py +``` + +Here's an example of the `app/__init__.py` file: + +```python +from quart import Quart +from quart_sqlalchemy import SQLAlchemy +from quart_imp import Imp +from quart_imp.config import QuartConfig, ImpConfig + +db = SQLAlchemy() +imp = Imp() + + +def create_app(): + app = Quart(__name__) + QuartConfig( + secret_key="super_secret_key", + app_instance=app, + ) + + imp.init_app(app, config=ImpConfig( + init_session={"logged_in": False}, + )) + imp.import_app_resources("resources") + imp.import_blueprints("blueprints") + + db.init_app(app) + + return app +``` + +The Quart configuration can be loaded from any standard Quart configuration method, or from the `QuartConfig` class +shown above. + +This class contains the standard Quart configuration options found in the Quart documentation. + +The `ImpConfig` class is used to configure the `Imp` instance. + +The `init_session` option of the `ImpConfig` class is used to set the initial session variables for the Quart app. +This happens before the request is processed. + +`ImpConfig` also has the ability to set `SQLALCHEMY_DATABASE_URI` and `SQLALCHEMY_BINDS` + +For more information about the configuration setting see +[quart_imp_config-impconfig.md](quart_imp_config-impconfig.html). + +`import_app_resources` will walk one level deep into the `resources` folder, and import +all `.py` files as modules. +It will also check for the existence of a `static` and `templates` folder, and register them with the Quart app. + +There is a couple of options for `import_app_resources` to control what +is imported, see: [Imp / import_app_resources](imp-import_app_resources.html) + +`import_blueprints` expects a folder that contains many Blueprint as Python packages. +It will check each blueprint folder's `__init__.py` file for an instance of a Quart Blueprint or a +Quart-Imp Blueprint. That instant will then be registered with the Quart app. + +See more about how importing blueprints work here: [ImpBlueprint / Introduction](impblueprint-introduction.html) diff --git a/docs/_md/v1/Imp-import_app_resources.md b/docs/_md/v1/Imp-import_app_resources.md new file mode 100644 index 0000000..3b52423 --- /dev/null +++ b/docs/_md/v1/Imp-import_app_resources.md @@ -0,0 +1,106 @@ +``` +Menu = Imp/import_app_resources +Title = Imp.import_app_resources +``` + +```python +import_app_resources( + 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. + +This will import any resources that have been set to the Quart app. + +Routes, context processors, cli, etc. + +**Can only be called once.** + +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. + +#### Small example of usage: + +```python +imp.import_app_resources(folder="resources") +# or +imp.import_app_resources() +# as the default folder is "resources" +``` + +Folder Structure: `resources` + +```text +app +├── resources +│ ├── routes.py +│ ├── app_fac.py +│ ├── static +│ │ └── css +│ │ └── style.css +│ └── templates +│ └── index.html +└── ... +... +``` + +File: `routes.py` + +```python +from quart import current_app as app +from quart import render_template + + +@app.route("/") +async def index(): + return await render_template("index.html") +``` + +#### How factories work + +Factories are functions that are called when importing the app resources. Here's an example: + +```python +imp.import_app_resources( + folder="resources", + factories=["development_cli"] +) +``` + +`["development_cli"]` => `development_cli(app)` function will be called, and the current app will be passed in. + +File: `app_fac.py` + +```python +def development_cli(app): + @app.cli.command("dev") + def dev(): + print("dev cli command") +``` + +#### Scoping imports + +By default, all files and folders will be imported. + +To disable this, set `files_to_import` and or +`folders_to_import` to `[None]`. + +```python +imp.import_app_resources(scope_import=[None], folders_to_import=[None]) +``` + +To scope the imports, set the `files_to_import` and or `folders_to_import` to a list of files and or folders. + +`files_to_import=["cli.py", "routes.py"]` => will only import the files `resources/cli.py` +and `resources/routes.py` + +`folders_to_import=["template_filters", "context_processors"]` => will import all files in the folders +`resources/template_filters/*.py` and `resources/context_processors/*.py` diff --git a/docs/_md/v1/Imp-import_blueprint.md b/docs/_md/v1/Imp-import_blueprint.md new file mode 100644 index 0000000..b3d165b --- /dev/null +++ b/docs/_md/v1/Imp-import_blueprint.md @@ -0,0 +1,121 @@ +``` +Menu = Imp/import_blueprint +Title = Imp.import_blueprint +``` + +```python +import_blueprint(self, blueprint: str) -> None +``` + +--- + +Import a specified Quart-Imp or standard Quart Blueprint relative to the Quart app root. + + +```text +app +├── my_blueprint +│ ├── ... +│ └── __init__.py +├── ... +└── __init__.py +``` + +File: `app/__init__.py` + +```python +from quart import Quart + +from quart_imp import Imp + +imp = Imp() + + +def create_app(): + app = Quart(__name__) + imp.init_app(app) + + imp.import_blueprint("my_blueprint") + + return app +``` + +Quart-Imp Blueprints have the ability to auto import resources, and initialize session variables. + +For more information on how Quart-Imp Blueprints work, see the [ImpBlueprint / Introduction](impblueprint-introduction.html) + +##### Example of 'my_blueprint' as a Quart-Imp Blueprint: + +```text +app +├── my_blueprint +│ ├── routes +│ │ └── index.py +│ ├── static +│ │ └── css +│ │ └── style.css +│ ├── templates +│ │ └── my_blueprint +│ │ └── index.html +│ ├── __init__.py +│ └── config.toml +└── ... +``` + +File: `__init__.py` + +```python +from quart_imp import ImpBlueprint +from quart_imp.config import ImpBlueprintConfig + +bp = ImpBlueprint( + __name__, + ImpBlueprintConfig( + enabled=True, + url_prefix="/my-blueprint", + static_folder="static", + template_folder="templates", + static_url_path="/static/my_blueprint", + init_session={"my_blueprint": "session_value"}, + ), +) + +bp.import_resources("routes") +``` + +File: `routes / index.py` + +```python +from .. import bp + + +@bp.route("/") +async def index(): + return "regular_blueprint" +``` + +##### Example of 'my_blueprint' as a standard Quart Blueprint: + +```text +app +├── my_blueprint +│ ├── ... +│ └── __init__.py +└── ... +``` + +File: `__init__.py` + +```python +from quart import Blueprint + +bp = Blueprint("my_blueprint", __name__, url_prefix="/my-blueprint") + + +@bp.route("/") +async def index(): + return "regular_blueprint" +``` + +Both of the above examples will work with `imp.import_blueprint("my_blueprint")`, they will be registered +with the Quart app, and will be accessible via `url_for("my_blueprint.index")`. \ No newline at end of file diff --git a/docs/_md/v1/Imp-import_blueprints.md b/docs/_md/v1/Imp-import_blueprints.md new file mode 100644 index 0000000..333da2e --- /dev/null +++ b/docs/_md/v1/Imp-import_blueprints.md @@ -0,0 +1,50 @@ +``` +Menu = Imp/import_blueprints +Title = Imp.import_blueprints +``` + +```python +import_blueprints(self, folder: str) -> None +``` + +--- + +Import all Quart-Imp or standard Quart Blueprints from a specified folder relative to the Quart app root. + +```text +app/ +├── blueprints/ +│ ├── admin/ +│ │ ├── ... +│ │ └── __init__.py +│ ├── www/ +│ │ ├── ... +│ │ └── __init__.py +│ └── api/ +│ ├── ... +│ └── __init__.py +├── ... +└── __init__.py +``` + +File: `app/__init__.py` + +```python +from quart import Quart + +from quart_imp import Imp + +imp = Imp() + + +def create_app(): + app = Quart(__name__) + imp.init_app(app) + + imp.import_blueprints("blueprints") + + return app +``` + +This will import all Blueprints from the `blueprints` folder using the `Imp.import_blueprint` method. +See [Imp / import_blueprint](imp-import_blueprint.html) for more information. \ No newline at end of file diff --git a/docs/_md/v1/Imp-init_app-init.md b/docs/_md/v1/Imp-init_app-init.md new file mode 100644 index 0000000..7c334c0 --- /dev/null +++ b/docs/_md/v1/Imp-init_app-init.md @@ -0,0 +1,22 @@ +``` +Menu = Imp/init_app, __init__ +Title = Imp.init_app, __init__ +``` + +```python +def init_app( + app: Quart, + config: ImpConfig +) -> None: +# -or- +Imp( + app: Quart, + config: ImpConfig +) +``` + +--- + +Initializes the quart app to work with quart-imp. + +See [quart_imp_config-impconfig.md](quart_imp_config-impconfig.html) for more information on the `ImpConfig` class. diff --git a/docs/_md/v1/Imp-init_session.md b/docs/_md/v1/Imp-init_session.md new file mode 100644 index 0000000..6c672c6 --- /dev/null +++ b/docs/_md/v1/Imp-init_session.md @@ -0,0 +1,48 @@ +``` +Menu = Imp/init_session +Title = Imp.init_session +``` + +```python +init_session() -> None +``` + +--- + +Initialize the session variables found in the config. Commonly used in `app.before_request`. + +```python +@app.before_request +async def before_request(): + imp._init_session() +``` + +File: `config.toml` + +```toml +... +[SESSION] +logged_in = false +... +``` + +`logged_in` is now available in the session. + +```python +@app.route('/get-session-value') +async def login(): + print(session['logged_in']) + return "Check Terminal" +``` + +`Output: False` + +Can also be used to reset the values in the session. Here's an example: + +```python +@app.route('/logout') +async def logout(): + session.clear() + imp._init_session() + return redirect(url_for('index')) +``` \ No newline at end of file diff --git a/docs/_md/v1/ImpBlueprint-Introduction.md b/docs/_md/v1/ImpBlueprint-Introduction.md new file mode 100644 index 0000000..7c8247c --- /dev/null +++ b/docs/_md/v1/ImpBlueprint-Introduction.md @@ -0,0 +1,68 @@ +``` +Menu = ImpBlueprint/Introduction +Title = Quart-Imp Blueprint Introduction +``` + +The Quart-Imp Blueprint inherits from the Quart Blueprint class, then adds some additional methods to allow for auto +importing of resources and other nested blueprints. + +The Quart-Imp Blueprint requires you to provide the `ImpBlueprintConfig` class as the second argument to the Blueprint. + +Here's an example of a Quart-Imp Blueprint structure: + +```text +www/ +├── nested_blueprints/ +│ ├── blueprint_one/ +│ │ ├── ... +│ │ └── __init__.py +│ └── blueprint_two/ +│ ├── ... +│ └── __init__.py +├── standalone_nested_blueprint/ +│ ├── ... +│ └── __init__.py +├── routes/ +│ └── index.py +├── static/ +│ └── ... +├── templates/ +│ └── www/ +│ └── index.html +└── __init__.py +``` + +File: `__init__.py` + +```python +from quart_imp import ImpBlueprint +from quart_imp.config import ImpBlueprintConfig + +bp = ImpBlueprint(__name__, ImpBlueprintConfig( + enabled=True, + url_prefix="/www", + static_folder="static", + template_folder="templates", + init_session={"logged_in": False}, +)) + +bp.import_resources("routes") +bp.import_nested_blueprints("nested_blueprints") +bp.import_nested_blueprint("standalone_nested_blueprint") +``` + +The `ImpBlueprintConfig` class is used to configure the Blueprint. It provides a little more flexibility than the +standard Quart Blueprint configuration, like the ability to enable or disable the Blueprint. + +`ImpBlueprintConfig`'s `init_session` works the same as `ImpConfig`'s `init_session`, this will add the session data to +the Quart app's session object on initialization of the Quart app. + +To see more about configuration see: [quart_imp.config / ImpBlueprintConfig](quart_imp_config-impblueprintconfig.html) + +`import_resources` method will walk one level deep into the `routes` folder, and import all `.py` files as modules. +For more information see: [ImpBlueprint / import_resources](impblueprint-import_resources.html) + +`import_nested_blueprints` will do the same as `imp.import_blueprints`, but will register the blueprints found as +nested to the current blueprint. For example `www.blueprint_one.index` + +`import_nested_blueprint` behaves the same as `import_nested_blueprints`, but will only import a single blueprint. diff --git a/docs/_md/v1/ImpBlueprint-import_nested_blueprint.md b/docs/_md/v1/ImpBlueprint-import_nested_blueprint.md new file mode 100644 index 0000000..0a69bbc --- /dev/null +++ b/docs/_md/v1/ImpBlueprint-import_nested_blueprint.md @@ -0,0 +1,78 @@ +``` +Menu = ImpBlueprint/import_nested_blueprint +Title = ImpBlueprint.import_nested_blueprint +``` + +```python +import_nested_blueprint(self, blueprint: str) -> None +``` + +--- + +Import a specified Quart-Imp or standard Quart Blueprint relative to the Blueprint root. + +Works the same as [Imp / import_blueprint](imp-import_blueprint.html) but relative to the Blueprint root. + +Blueprints that are imported this way will be scoped to the parent Blueprint that imported them. + +`url_for('my_blueprint.my_nested_blueprint.index')` + +```text +my_blueprint/ +├── routes/... +├── static/... +├── templates/... +│ +├── my_nested_blueprint/ +│ ├── routes/ +│ │ └── index.py +│ ├── static/... +│ ├── templates/... +│ ├── __init__.py +│ +├── __init__.py +``` + +File: `my_blueprint/__init__.py` + +```python +from quart_imp import ImpBlueprint +from quart_imp.config import ImpBlueprintConfig + +bp = ImpBlueprint(__name__, ImpBlueprintConfig( + enabled=True, + static_folder="static", + template_folder="templates", +)) + +bp.import_resources("routes") +bp.import_nested_blueprint("my_nested_blueprint") +``` + +File: `my_blueprint/my_nested_blueprint/__init__.py` + +```python +from quart_imp import ImpBlueprint +from quart_imp.config import ImpBlueprintConfig + +bp = ImpBlueprint(__name__, ImpBlueprintConfig( + enabled=True, + static_folder="static", + template_folder="templates", +)) + +bp.import_resources("routes") +``` + +File: `my_blueprint/my_nested_blueprint/routes/index.py` + +```python +from quart import render_template + +from .. import bp + + +@bp.route("/") +async def index(): + return await render_template(bp.tmpl("index.html")) +``` \ No newline at end of file diff --git a/docs/_md/v1/ImpBlueprint-import_nested_blueprints.md b/docs/_md/v1/ImpBlueprint-import_nested_blueprints.md new file mode 100644 index 0000000..276938d --- /dev/null +++ b/docs/_md/v1/ImpBlueprint-import_nested_blueprints.md @@ -0,0 +1,60 @@ +``` +Menu = ImpBlueprint/import_nested_blueprints +Title = ImpBlueprint.import_nested_blueprints +``` + +```python +import_nested_blueprints(self, folder: str) -> None +``` + +--- + +Will import all the Blueprints from the given folder relative to the Blueprint's root directory. + +Uses [Blueprint / import_nested_blueprint](blueprint-import_nested_blueprint.html) to import blueprints from +the specified folder. + +Blueprints that are imported this way will be scoped to the parent Blueprint that imported them. + +`url_for('my_blueprint.nested_bp_one.index')` + +`url_for('my_blueprint.nested_bp_two.index')` + +`url_for('my_blueprint.nested_bp_three.index')` + +```text +my_blueprint/ +├── routes/... +├── static/... +├── templates/... +│ +├── nested_blueprints/ +│ │ +│ ├── nested_bp_one/ +│ │ ├── ... +│ │ ├── __init__.py +│ ├── nested_bp_two/ +│ │ ├── ... +│ │ ├── __init__.py +│ └── nested_bp_three/ +│ ├── ... +│ ├── __init__.py +│ +├── __init__.py +``` + +File: `my_blueprint/__init__.py` + +```python +from quart_imp import ImpBlueprint +from quart_imp.config import ImpBlueprintConfig + +bp = ImpBlueprint(__name__, ImpBlueprintConfig( + enabled=True, + static_folder="static", + template_folder="templates", +)) + +bp.import_resources("routes") +bp.import_nested_blueprints("nested_blueprints") +``` \ No newline at end of file diff --git a/docs/_md/v1/ImpBlueprint-import_resources.md b/docs/_md/v1/ImpBlueprint-import_resources.md new file mode 100644 index 0000000..65487c7 --- /dev/null +++ b/docs/_md/v1/ImpBlueprint-import_resources.md @@ -0,0 +1,57 @@ +``` +Menu = ImpBlueprint/import_resources +Title = ImpBlueprint.import_resources +``` + +```python +import_resources(folder: str = "routes") -> None +``` + +--- + +Will import all the resources (cli, routes, filters, context_processors...) from the given folder relative to the +Blueprint's root directory. + +```text +my_blueprint +├── user_routes +│ ├── user_dashboard.py +│ └── user_settings.py +├── car_routes +│ ├── car_dashboard.py +│ └── car_settings.py +├── static/... +├── templates/ +│ └── my_blueprint/ +│ ├── user_dashboard.html +│ └── ... +├── __init__.py +``` + +File: `my_blueprint/__init__.py` + +```python +from quart_imp import ImpBlueprint +from quart_imp.config import ImpBlueprintConfig + +bp = ImpBlueprint(__name__, ImpBlueprintConfig( + enabled=True, + static_folder="static", + template_folder="templates", +)) + +bp.import_resources("user_routes") +bp.import_resources("car_routes") +``` + +File: `my_blueprint/user_routes/user_dashboard.py` + +```python +from quart import render_template + +from .. import bp + +@bp.route("/user-dashboard") +async def user_dashboard(): + return await render_template(bp.tmpl("user_dashboard.html")) +``` diff --git a/docs/_md/v1/ImpBlueprint-init.md b/docs/_md/v1/ImpBlueprint-init.md new file mode 100644 index 0000000..d5cb266 --- /dev/null +++ b/docs/_md/v1/ImpBlueprint-init.md @@ -0,0 +1,17 @@ +``` +Menu = ImpBlueprint/__init__ +Title = Quart-Imp Blueprint __init__ +``` + +```python +ImpBlueprint(dunder_name: str, config: ImpBlueprintConfig) -> None +``` + +--- + +Initializes the Quart-Imp Blueprint. + +`dunder_name` should always be set to `__name__` + +`config` is an instance of `ImpBlueprintConfig` that will be used to load the Blueprint's configuration. +See [quart_imp.config / ImpBlueprintConfig](quart_imp_config-impblueprintconfig.html) for more information. diff --git a/docs/_md/v1/ImpBlueprint-tmpl.md b/docs/_md/v1/ImpBlueprint-tmpl.md new file mode 100644 index 0000000..d3e94d6 --- /dev/null +++ b/docs/_md/v1/ImpBlueprint-tmpl.md @@ -0,0 +1,44 @@ +``` +Menu = ImpBlueprint/tmpl +Title = ImpBlueprint.tmpl +``` + +```python +tmpl(template: str) -> str +``` + +--- + +Scopes the template lookup to the name of the blueprint (this takes from the `__name__` attribute of the Blueprint). + +Due to the way Quart templating works, and to avoid template name collisions. +It is standard practice to place the name of the Blueprint in the template path, +then to place any templates under that folder. + +```text +my_blueprint/ +├── routes/ +│ └── index.py +├── static/... +│ +├── templates/ +│ └── my_blueprint/ +│ └── index.html +│ +├── __init__.py +``` + +File: `my_blueprint/routes/index.py` + +```python +from quart import render_template + +from .. import bp + + +@bp.route("/") +async def index(): + return await render_template(bp.tmpl("index.html")) +``` + +`bp.tmpl("index.html")` will output `"my_blueprint/index.html"`. diff --git a/docs/_md/v1/__index__.md b/docs/_md/v1/__index__.md new file mode 100644 index 0000000..7f496f5 --- /dev/null +++ b/docs/_md/v1/__index__.md @@ -0,0 +1,121 @@ +# Welcome to the Quart-Imp Documentation + +## What is Quart-Imp? + +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. + +## Install Quart-Imp + +```bash +pip install quart-imp +``` + +## Getting Started + +To get started right away, you can use the CLI commands to create a new Quart-Imp project. + +```bash +quart-imp init +``` + +### Minimal Quart-Imp Setup + +Run the following command to create a minimal Quart-Imp project. + +```bash +quart-imp init -n app --minimal +``` + +See [CLI Commands / quart-imp init](cli_commands-quart-imp_init.html) for more information. + +### The minimal structure + +#### Folder Structure + +```text +app/ +├── resources/ +│ ├── static/... +│ ├── templates/ +│ │ └── index.html +│ └── index.py +└── __init__.py +``` + +File: `app/__init__.py` + +```python +from quart import Quart + +from quart_imp import Imp +from quart_imp.config import QuartConfig, ImpConfig + +imp = Imp() + + +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() + # Takes argument 'folder' default folder is 'resources' + + return app +``` + +File: `app/resources/routes.py` + +```python +from quart import current_app as app +from quart import render_template + + +@app.route("/") +async def index(): + return await render_template("index.html") +``` + +File: `app/resources/templates/index.html` + +```html + + + + + Quart-Imp + + +

Quart-Imp

+ + +``` + +--- + +Setting up a virtual environment is recommended. + +**Linux / Darwin** + +```bash +python3 -m venv venv +``` + +```bash +source venv/bin/activate +``` + +**Windows** + +```bash +python -m venv venv +``` + +```text +.\venv\Scripts\activate +``` \ No newline at end of file diff --git a/docs/_md/v1/__menu__.md b/docs/_md/v1/__menu__.md new file mode 100644 index 0000000..2aea2cd --- /dev/null +++ b/docs/_md/v1/__menu__.md @@ -0,0 +1,43 @@ +- CLI Commands + - quart-imp init + - quart-imp blueprint +- Imp + - Introduction + - init_app, __init__ + - init_session + - import_app_resources + - import_blueprint + - import_blueprints +- ImpBlueprint + - Introduction + - __init__ + - init_session + - import_resources + - import_nested_blueprint + - import_nested_blueprints + - tmpl +- quart_imp.config + - QuartConfig + - ImpConfig + - ImpBlueprintConfig + - DatabaseConfig + - SQLDatabaseConfig + - SQLiteDatabaseConfig +- quart_imp.security + - login_check + - permission_check + - pass_function_check + - api_login_check + - include_csrf +- quart_imp.auth + - encrypt_password + - authenticate_password + - generate_password + - generate_salt + - generate_csrf_token + - generate_private_key + - generate_email_validator + - generate_numeric_validator + - generate_alphanumeric_validator + - is_email_address_valid + - is_username_valid diff --git a/docs/_md/v1/quart_imp_auth-authenticate_password.md b/docs/_md/v1/quart_imp_auth-authenticate_password.md new file mode 100644 index 0000000..3efdadc --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-authenticate_password.md @@ -0,0 +1,55 @@ +``` +Menu = quart_imp.auth/authenticate_password +Title = authenticate_password - quart_imp.auth +``` + +```python +from quart_imp.auth import authenticate_password +``` + +```python +authenticate_password( + input_password: str, + database_password: str, + database_salt: str, + encryption_level: int = 512, + pepper_length: int = 1, + pepper_position: t.Literal["start", "end"] = "end" +) -> bool +``` + +--- + +For use in password hashing. + +To be used alongside the [quart_imp.auth / encrypt_password](quart_imp_auth-encrypt_password.html) function. + +Takes the plain input password, the stored hashed password along with the stored salt +and will try every possible combination of pepper values to find a match. + +**Note:** + +- You must know the pepper length used to hash the password. +- You must know the position of the pepper used to hash the password. +- You must know the encryption level used to hash the password. + +#### Authentication Scenario: + +``` +Plain password: "password" +Generated salt: "^%$*" (randomly generated) +Generated pepper (length 1): "A" (randomly generated) +Pepper position: "end" +``` + +```python +input_password = "password" +database_password = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0..." # pulled from database +database_salt = "^%$*" # pulled from database + +authenticate_password( + input_password, + database_password, + database_salt +) # >>> True +``` diff --git a/docs/_md/v1/quart_imp_auth-encrypt_password.md b/docs/_md/v1/quart_imp_auth-encrypt_password.md new file mode 100644 index 0000000..56d76c4 --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-encrypt_password.md @@ -0,0 +1,53 @@ +``` +Menu = quart_imp.auth/encrypt_password +Title = encrypt_password - quart_imp.auth +``` + +```python +from quart_imp.auth import encrypt_password +``` + +```python +encrypt_password( + password: str, + salt: str, + encryption_level: int = 512, + pepper_length: int = 1, + pepper_position: t.Literal["start", "end"] = "end" +) -> str +``` + +--- + +For use in password hashing. + +To be used alongside the [quart_imp.auth / authenticate_password](quart_imp_auth-authenticate_password.html) function. + +Takes the plain password, applies a pepper, salts it, then produces a digested sha512 or sha256 if specified. + +Can set the encryption level to 256 or 512, defaults to 512. + +Can set the pepper length, defaults to 1. Max is 3. + +Can set the pepper position, "start" or "end", defaults to "end". + +**Note:** + +- You must inform the authenticate_password function of the pepper length used to hash the password. +- You must inform the authenticate_password function of the position of the pepper used to hash the password. +- You must inform the authenticate_password function of the encryption level used to hash the password. + +#### Encryption Scenario: + +``` +Plain password: "password" +Generated salt: "^%$*" (randomly generated) +Generated pepper (length 1): "A" (randomly generated) +Pepper position: "end" +``` + +1. Pepper is added to the end of the plain password: "passwordA" +2. Salt is added to the end of the peppered password: "passwordA^%$*" +3. Password is hashed: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0..." +4. Salt and hashed password are then stored in the database. + diff --git a/docs/_md/v1/quart_imp_auth-generate_alphanumeric_validator.md b/docs/_md/v1/quart_imp_auth-generate_alphanumeric_validator.md new file mode 100644 index 0000000..6516b65 --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-generate_alphanumeric_validator.md @@ -0,0 +1,24 @@ +``` +Menu = quart_imp.auth/generate_alphanumeric_validator +Title = generate_alphanumeric_validator - quart_imp.auth +``` + +```python +from quart_imp.auth import generate_alphanumeric_validator +``` + +```python +generate_alphanumeric_validator(length: int = 8) -> str +``` + +--- + +Generates a random alphanumeric string of the given length. + +(letters are capitalized) + +##### Example: + +```python +generate_alphanumeric_validator(8) # >>> 'A1B2C3D4' +``` \ No newline at end of file diff --git a/docs/_md/v1/quart_imp_auth-generate_csrf_token.md b/docs/_md/v1/quart_imp_auth-generate_csrf_token.md new file mode 100644 index 0000000..724fb06 --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-generate_csrf_token.md @@ -0,0 +1,26 @@ +``` +Menu = quart_imp.auth/generate_csrf_token +Title = generate_csrf_token - quart_imp.auth +``` + +```python +from quart_imp.auth import generate_csrf_token +``` + +```python +generate_csrf_token() -> str +``` + +--- + +Generates a SHA1 using the current date and time. + +For use in Cross-Site Request Forgery. + +Also used by the [quart_imp.security / csrf_protect](quart_imp_security-include_csrf.html) decorator. + +##### Example: + +```python +generate_csrf_token() # >>> 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0' +``` \ No newline at end of file diff --git a/docs/_md/v1/quart_imp_auth-generate_email_validator.md b/docs/_md/v1/quart_imp_auth-generate_email_validator.md new file mode 100644 index 0000000..95e7ca5 --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-generate_email_validator.md @@ -0,0 +1,27 @@ +``` +Menu = quart_imp.auth/generate_email_validator +Title = generate_email_validator - quart_imp.auth +``` + +```python +from quart_imp.auth import generate_email_validator +``` + +```python +generate_email_validator() -> str +``` + +--- + +Uses `generate_alphanumeric_validator` with a length of 8 to +generate a random alphanumeric value for the specific use of +validating accounts via email. + +See [quart_imp.auth / generate_alphanumeric_validator](quart_imp_auth-generate_alphanumeric_validator.html) +for more information. + +##### Example: + +```python +generate_email_validator() # >>> 'A1B2C3D4' +``` diff --git a/docs/_md/v1/quart_imp_auth-generate_numeric_validator.md b/docs/_md/v1/quart_imp_auth-generate_numeric_validator.md new file mode 100644 index 0000000..f9aeb1b --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-generate_numeric_validator.md @@ -0,0 +1,27 @@ +``` +Menu = quart_imp.auth/generate_numeric_validator +Title = generate_numeric_validator - quart_imp.auth +``` + +```python +from quart_imp.auth import generate_numeric_validator +``` + +```python +generate_numeric_validator(length: int) -> int +``` + +--- + + +Generates random choice between 1 * (length) and 9 * (length). + +If the length is 4, it will generate a number between 1111 and 9999. + +For use in MFA email, or unique filename generation. + +##### Example: + +```python +generate_numeric_validator(4) # >>> 1234 +``` diff --git a/docs/_md/v1/quart_imp_auth-generate_password.md b/docs/_md/v1/quart_imp_auth-generate_password.md new file mode 100644 index 0000000..08e293a --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-generate_password.md @@ -0,0 +1,26 @@ +``` +Menu = quart_imp.auth/generate_password +Title = generate_password - quart_imp.auth +``` + +```python +from quart_imp.auth import generate_password +``` + +```python +generate_password(style: str = "mixed", length: int = 3) -> str +``` + +--- + +Generates a password of (length) characters. + +The Default length is 3. + +Style options: "animals", "colors", "mixed" - defaults to "mixed" + +##### Example: + +```python +generate_password(style="animals", length=3) # >>> 'Cat-Goat-Pig12' +``` diff --git a/docs/_md/v1/quart_imp_auth-generate_private_key.md b/docs/_md/v1/quart_imp_auth-generate_private_key.md new file mode 100644 index 0000000..037d6e1 --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-generate_private_key.md @@ -0,0 +1,38 @@ +``` +Menu = quart_imp.auth/generate_private_key +Title = generate_private_key - quart_imp.auth +``` + +```python +from quart_imp.auth import generate_private_key +``` + +```python +generate_private_key(hook: t.Optional[str]) -> str +``` + +--- + +Generates a sha256 private key from a passed in hook value. + +If no hook is passed in, it will generate a hook using datetime.now() and a +random number between 1 and 1000. + +```python +@app.route('/register', methods=['GET', 'POST']) +async def register(): + if request.method == "POST": + ... + salt = generate_salt() + password = request.form.get('password') + encrypted_password = encrypt_password(password, salt) + ... + user = User( + username=username, + email=email, + password=encrypted_password, + salt=salt, + private_key=generate_private_key(hook=username) + ) + ... +``` diff --git a/docs/_md/v1/quart_imp_auth-generate_salt.md b/docs/_md/v1/quart_imp_auth-generate_salt.md new file mode 100644 index 0000000..4eee480 --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-generate_salt.md @@ -0,0 +1,46 @@ +``` +Menu = quart_imp.auth/generate_salt +Title = generate_salt - quart_imp.auth +``` + +```python +from quart_imp.auth import generate_salt +``` + +```python +generate_salt(length: int = 4) -> str +``` + +--- + +Generates a string of (length) characters of punctuation. + +The Default length is 4. + +For use in password hashing and storage of passwords in the database. + +##### Example: + +```python +generate_salt() # >>> '*!$%' +``` + +```python +@app.route('/register', methods=['GET', 'POST']) +async def register(): + if request.method == "POST": + ... + salt = generate_salt() + password = request.form.get('password') + encrypted_password = encrypt_password(password, salt) + ... + + user = User( + username=username, + email=email, + password=encrypted_password, + salt=salt + ) + ... +``` + diff --git a/docs/_md/v1/quart_imp_auth-is_email_address_valid.md b/docs/_md/v1/quart_imp_auth-is_email_address_valid.md new file mode 100644 index 0000000..6f11cff --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-is_email_address_valid.md @@ -0,0 +1,46 @@ +``` +Menu = quart_imp.auth/is_email_address_valid +Title = is_email_address_valid - quart_imp.auth +``` + +```python +from quart_imp.auth import is_email_address_valid +``` + +```python +is_email_address_valid( + email_address: str +) -> bool +``` + +--- + +Checks if an email address is valid. + +Is not completely RFC 5322 compliant, but it is good enough for most use cases. + +Here are examples of mistakes that it will not catch: + +##### Valid but fails: + +```text +email@[123.123.123.123] +“email”@example.com +very.unusual.“@”.unusual.com@example.com +very.“(),:;<>[]”.VERY.“very@\\ "very”.unusual@strange.example.com +``` + +##### Invalid but passes: + +```text +email@example.com (Joe Smith) +email@111.222.333.44444 +``` + +##### Example: + +```python +is_email_address_valid('hello@example.com') # >>> True + +is_email_address_valid('hello@hello@example.com') # >>> False +``` \ No newline at end of file diff --git a/docs/_md/v1/quart_imp_auth-is_username_valid.md b/docs/_md/v1/quart_imp_auth-is_username_valid.md new file mode 100644 index 0000000..0a7c370 --- /dev/null +++ b/docs/_md/v1/quart_imp_auth-is_username_valid.md @@ -0,0 +1,58 @@ +``` +Menu = quart_imp.auth/is_username_valid +Title = is_username_valid - quart_imp.auth +``` + +```python +from quart_imp.auth import is_username_valid +``` + +```python +is_username_valid( + username: str, + allowed: t.Optional[t.List[t.Literal["all", "dot", "dash", "under"]]] = None +) -> bool +``` + +--- + +Checks if a username is valid. + +Valid usernames can only include letters, +numbers, ., -, and _ but cannot begin or end with +the last three mentioned. + +##### Example "all": + +```python +is_username_valid("username", allowed=["all"]) +``` + +Output: + +```text +username : WILL PASS : True +user.name : WILL PASS : True +user-name : WILL PASS : True +user_name : WILL PASS : True +_user_name : WILL PASS : False +``` + +##### Example "dot", "dash": + +```python + +is_username_valid("username", allowed=["dot", "dash"]) +``` + +Output: + +```text +username : WILL PASS : True +user.name : WILL PASS : True +user-name : WILL PASS : True +user-name.name : WILL PASS : True +user_name : WILL PASS : False +_user_name : WILL PASS : False +.user.name : WILL PASS : False +``` \ No newline at end of file diff --git a/docs/_md/v1/quart_imp_config-impblueprintconfig.md b/docs/_md/v1/quart_imp_config-impblueprintconfig.md new file mode 100644 index 0000000..c863479 --- /dev/null +++ b/docs/_md/v1/quart_imp_config-impblueprintconfig.md @@ -0,0 +1,33 @@ +``` +Menu = quart_imp.config/ImpBlueprintConfig +Title = ImpBlueprintConfig - quart_imp.config +``` + +```python +from quart_imp.config import ImpBlueprintConfig +``` + +```python +ImpBlueprintConfig( + enabled: bool = False, + url_prefix: str = None, + subdomain: str = None, + url_defaults: dict = None, + static_folder: t.Optional[str] = None, + template_folder: t.Optional[str] = None, + static_url_path: t.Optional[str] = None, + root_path: str = None, + cli_group: str = None, + init_session: dict = None +) +``` + +--- + +A class that holds a Quart-Imp blueprint configuration. + +Most of these values are passed to the `Blueprint` class when the blueprint is registered. + +The `enabled` argument is used to enable or disable the blueprint. This is useful for feature flags. + +`init_session` is used to set the session values in the main `before_request` function. diff --git a/docs/_md/v1/quart_imp_config-impconfig.md b/docs/_md/v1/quart_imp_config-impconfig.md new file mode 100644 index 0000000..c38b955 --- /dev/null +++ b/docs/_md/v1/quart_imp_config-impconfig.md @@ -0,0 +1,32 @@ +``` +Menu = quart_imp.config/ImpConfig +Title = ImpConfig - quart_imp.config +``` + +```python +from quart_imp.config import ImpConfig +``` + +```python +ImpConfig( + init_session: t.Optional[t.Dict[str, t.Any]] = None, +) +``` + +--- + +The `ImpConfig` class is used to set the initial session data +that the application will use. + +```python +imp_config = ImpConfig( + init_session={"key": "value"}, +) + + +def create_app(): + app = Quart(__name__) + QuartConfig(debug=True, app_instance=app) + imp.init_app(app, imp_config) + ... +``` \ No newline at end of file diff --git a/docs/_md/v1/quart_imp_config-quartconfig.md b/docs/_md/v1/quart_imp_config-quartconfig.md new file mode 100644 index 0000000..b968979 --- /dev/null +++ b/docs/_md/v1/quart_imp_config-quartconfig.md @@ -0,0 +1,59 @@ +``` +Menu = quart_imp.config/QuartConfig +Title = QuartConfig - quart_imp.config +``` + +```python +from quart_imp.config import QuartConfig +``` + +```python +QuartConfig( + 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 +) +``` + +--- + +A class that holds a Quart configuration values. + +You can set the configuration values to the app instance by either passing the app instance to the `app_instance` +parameter or by calling the `apply_config` method on the `QuartConfig` instance. + +```python +def create_app(): + app = Quart(__name__) + QuartConfig(debug=True, app_instance=app) + return app +``` +or +```python +def create_app(): + app = Quart(__name__) + config = QuartConfig(debug=True) + config.apply_config(app) + return app +``` \ No newline at end of file diff --git a/docs/_md/v1/quart_imp_security-api_login_check.md b/docs/_md/v1/quart_imp_security-api_login_check.md new file mode 100644 index 0000000..f1240c0 --- /dev/null +++ b/docs/_md/v1/quart_imp_security-api_login_check.md @@ -0,0 +1,47 @@ +``` +Menu = quart_imp.security/api_login_check +Title = api_login_check - quart_imp.security +``` + + +```python +from quart_imp.security import api_login_check +``` + +```python +api_login_check( + session_key: str, + values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool], + fail_json: t.Optional[t.Dict[str, t.Any]] = None +) +``` + +`@api_login_check(...)` + +--- + +A decorator that is used to secure API routes that return JSON responses. + +`session_key` The session key to check for. + +`values_allowed` A list of or singular value(s) that the session key must contain. + +`fail_json` JSON that is returned on failure. `{"error": "You are not logged in."}` by default. + +##### Example: + +```python +@bp.route("/api/resource", methods=["GET"]) +@api_login_check('logged_in', True) +async def api_page(): + ... +``` + +##### Example of defined fail_json: + +```python +@bp.route("/api/resource", methods=["GET"]) +@api_login_check('logged_in', True, fail_json={"failed": "You need to be logged in."}) +async def api_page(): + ... +``` diff --git a/docs/_md/v1/quart_imp_security-include_csrf.md b/docs/_md/v1/quart_imp_security-include_csrf.md new file mode 100644 index 0000000..c86f7af --- /dev/null +++ b/docs/_md/v1/quart_imp_security-include_csrf.md @@ -0,0 +1,48 @@ +``` +Menu = quart_imp.security/include_csrf +Title = include_csrf - quart_imp.security +``` + +```python +from quart_imp.security import include_csrf +``` + +```python +include_csrf( + session_key: str = "csrf", + form_key: str = "csrf", + abort_code: int = 401 +) +``` + +`@include_csrf(...)` + +--- + + +A decorator that handles CSRF protection. + +On a **GET** request, a CSRF token is generated and stored in the session key +specified by the session_key parameter. + +On a **POST** request, the form_key specified is checked against the session_key +specified. + +- If they match, the request is allowed to continue. +- If no match, the response will be abort(abort_code), default 401. + +```python +@bp.route("/admin", methods=["GET", "POST"]) +@include_csrf(session_key="csrf", form_key="csrf") +async def admin_page(): + ... + # You must pass in the CSRF token from the session into the template. + # Then add to the form. + return await render_template("admin.html", csrf=session.get("csrf")) +``` + +Form key: + +```html + +``` \ No newline at end of file diff --git a/docs/_md/v1/quart_imp_security-login_check.md b/docs/_md/v1/quart_imp_security-login_check.md new file mode 100644 index 0000000..f269f4b --- /dev/null +++ b/docs/_md/v1/quart_imp_security-login_check.md @@ -0,0 +1,66 @@ +``` +Menu = quart_imp.security/login_check +Title = login_check - quart_imp.security +``` + +```python +from quart_imp.security import login_check +``` + +```python +login_check( + session_key: str, + values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool], + fail_endpoint: t.Optional[str] = None, + pass_endpoint: t.Optional[str] = None, + endpoint_kwargs: t.Optional[t.Dict[str, t.Union[str, int]]] = None, + message: t.Optional[str] = None, + message_category: str = "message" +) +``` + +`@login_check(...)` + +--- + +A decorator that checks if the specified session key exists and contains the specified value. + +`session_key` The session key to check for. + +`values_allowed` A list of or singular value(s) that the session key must contain. + +`fail_endpoint` The endpoint to redirect to if the session key does not exist or does not contain the specified values. + +`endpoint_kwargs` A dictionary of keyword arguments to pass to the redirect endpoint. + +`message` If a message is specified, a flash message is shown. + +`message_category` The category of the flash message. + +##### Example of a route that requires a user to be logged in: + +```python +@bp.route("/admin", methods=["GET"]) +@login_check( + 'logged_in', + True, + fail_endpoint='blueprint.login_page', + message="Login needed" +) +async def admin_page(): + ... +``` + +##### Example of a route that if the user is already logged in, redirects to the specified endpoint: + +```python +@bp.route("/login-page", methods=["GET"]) +@login_check( + 'logged_in', + True, + pass_endpoint='blueprint.admin_page', + message="Already logged in" +) +async def login_page(): + ... +``` diff --git a/docs/_md/v1/quart_imp_security-pass_function_check.md b/docs/_md/v1/quart_imp_security-pass_function_check.md new file mode 100644 index 0000000..c533f3a --- /dev/null +++ b/docs/_md/v1/quart_imp_security-pass_function_check.md @@ -0,0 +1,114 @@ +``` +Menu = quart_imp.security/pass_function_check +Title = pass_function_check - quart_imp.security +``` + +```python +from quart_imp.security import pass_function_check +``` + +```python +def pass_function_check( + function: t.Callable, + predefined_args: t.Optional[t.Dict] = None, + fail_endpoint: t.Optional[str] = None, + pass_endpoint: t.Optional[str] = None, + endpoint_kwargs: t.Optional[t.Dict[str, t.Union[str, int]]] = None, + message: t.Optional[str] = None, + message_category: str = "message", + fail_on_missing_kwargs: bool = False, + with_app_context: bool = False, +) +``` + +**NOTE: This was added mostly as an experimental feature, but ended up being useful in some cases.** + +A decorator that takes the result of a function and checks if it is True or False. + +URL variables from `@route` will be read by this decorator. +To use URL variables in your passed in function, +make sure your functions argument(s) name(s) match the name(s) of the URL variable(s). + +**Example:** + +```python +def check_if_number(value): + if isinstance(value, int): + return True + return False + +@bp.route("/admin-page/", methods=["GET"]) +@login_check('logged_in', True, 'blueprint.login_page') # can be mixed with login_check +@pass_function_check( + check_if_number, + predefined_args=None, + fail_endpoint='www.index', + message="Failed message" +) +async def admin_page(): + ... + +@bp.route("/admin-page/", methods=["GET"]) +@login_check('logged_in', True, 'blueprint.login_page') # can be mixed with login_check +@pass_function_check( + check_if_number, + predefined_args={'value': 10}, + fail_endpoint='www.index', + message="Failed message" +) +async def admin_page_overwrite(): + ... +``` + +**Advanced use case:** + +Here's an example of accessing quart.session from within the passed in function. including the +`with_app_context` parameter, the function will be called with `app_context()`. + +```python +from quart import current_app +from quart import session + +... + +def check_if_number(number=1, session_=None): + if session_: + print(session_) + try: + int(number) + return True + except ValueError: + return False + +@bp.route("/pass-func-check-with-url-var/", methods=["GET"]) +@pass_function_check( + check_if_number, + predefined_args={'number': 10, 'session_': session}, + fail_endpoint="www.index", + with_app_context=True +) +async def admin_page_overwrite_with_session(): + ... +``` + +If you pass in a predefined arg that has the same key name as a session variable that exists, the value +of that predefined arg will be replaced with the session variable value. + +```python +session['car'] = 'Toyota' +... +def check_function(car): + if car == 'Toyota': + return True + return False +... +@bp.route("/pass-func-check-with-url-var/", methods=["GET"]) +@pass_function_check( + check_function, + predefined_args={'car': session}, + ... + +``` + +This will pass, as pass_function_check will replace the value of the predefined arg 'car' with the value +of the session variable 'car'. diff --git a/docs/_md/v1/quart_imp_security-permission_check.md b/docs/_md/v1/quart_imp_security-permission_check.md new file mode 100644 index 0000000..d6309ab --- /dev/null +++ b/docs/_md/v1/quart_imp_security-permission_check.md @@ -0,0 +1,57 @@ +``` +Menu = quart_imp.security/permission_check +Title = permission_check - quart_imp.security +``` + +```python +from quart_imp.security import permission_check +``` + +```python +permission_check( + session_key: str, + values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool], + fail_endpoint: t.Optional[str] = None, + endpoint_kwargs: t.Optional[t.Dict[str, t.Union[str, int]]] = None, + message: t.Optional[str] = None, + message_category: str = "message" +) +``` + +`@permission_check(...)` + +--- + +A decorator that checks if the specified session key exists and its value(s) match the specified value(s). + +`session_key` The session key to check for. + +`values_allowed` A list of or singular value(s) that the session key must contain. + +`fail_endpoint` The endpoint to redirect to if the session key does not exist or does not contain the specified values. + +`endpoint_kwargs` A dictionary of keyword arguments to pass to the redirect endpoint. + +`message` If a message is specified, a flash message is shown. + +`message_category` The category of the flash message. + +##### Example: + +```python +@bp.route("/admin-page", methods=["GET"]) +@login_check( + 'logged_in', + True, + 'blueprint.login_page' +) # can be mixed with login_check +@permission_check( + 'permissions', + ['admin'], + fail_endpoint='www.index', + message="Failed message" +) +async def admin_page(): + ... +``` + diff --git a/docs/_ssg/__init__.py b/docs/_ssg/__init__.py new file mode 100644 index 0000000..9648d99 --- /dev/null +++ b/docs/_ssg/__init__.py @@ -0,0 +1,3 @@ +from .compiler import compiler + +__all__ = ["compiler"] diff --git a/docs/_ssg/compiler.py b/docs/_ssg/compiler.py new file mode 100644 index 0000000..dbf0a99 --- /dev/null +++ b/docs/_ssg/compiler.py @@ -0,0 +1,132 @@ +import re +import typing as t +from pathlib import Path + +import mistune +from flask import render_template + +from .exceptions import NoPostDefinition +from .helpers import get_relative_files_in_the_docs_folder, pytz_dt_now, post_date +from .render_engines import HighlightRenderer + + +def _raw_markdown_processor(raw_markdown: str) -> tuple[t.Optional[list], str, str]: + """ + :param raw_markdown: The raw markdown to process + :return: publish: bool, date: str, title: str, description: str, post: str + """ + if not raw_markdown.startswith("```"): + raise NoPostDefinition + + split_md = raw_markdown.split("```")[1:] + raw_meta = split_md[0] + + menu_ptn = re.compile(r"Menu =(.*?)\n", re.IGNORECASE) + title_ptn = re.compile(r"Title =(.*?)\n", re.IGNORECASE) + + try: + menu = menu_ptn.findall(raw_meta)[0].strip().split("/") + except (ValueError, IndexError, TypeError) as _: + menu = None + + try: + title = title_ptn.findall(raw_meta)[0].strip() + except (ValueError, IndexError, TypeError) as _: + title = "[Unable to find Title]" + + try: + post = "```".join(split_md[1:]) + except (IndexError, TypeError, ValueError) as _: + post = "[Unable to find Post]" + + return menu, title, post + + +def compiler(docs_dir: Path, markdown_dir: Path): + docs_dir.mkdir(exist_ok=True) + markdown_dir.mkdir(exist_ok=True) + + markdown_menu = markdown_dir / "__menu__.md" + markdown_index = markdown_dir / "__index__.md" + + markdown_menu_dict = dict() + + with open(markdown_menu, mode="r") as menu_file: + for line in menu_file.readlines(): + if line.startswith("-"): + line_strip = line.strip() + markdown_menu_dict[line_strip.replace("- ", "").strip()] = { + "page": "", + "pages": [], + } + continue + + if line.startswith(" ") or line.startswith("\t"): + line_strip = line.strip() + if line_strip.startswith("-"): + markdown_menu_dict[list(markdown_menu_dict.keys())[-1]][ + "pages" + ].append({line_strip.replace("- ", "").strip(): ""}) + + main_index_html = docs_dir.parent / "index.html" + index_html = docs_dir / "index.html" + + docs_dir_files = get_relative_files_in_the_docs_folder(docs_dir) + markdown_dir_files = markdown_dir.glob("*.md") + html_engine = mistune.create_markdown(renderer=HighlightRenderer()) + + html_pages = dict() + dt_date = pytz_dt_now() + + main_index_html.unlink(missing_ok=True) + main_index_html.write_text( + render_template("main_index.html", latest_version=docs_dir.name) + ) + + for file in docs_dir_files: + (docs_dir / f"{file}.html").unlink() + + for file in markdown_dir_files: + if "__" in file.stem: + continue + + raw_markdown = file.read_text() + menu, title, post = _raw_markdown_processor(raw_markdown) + html_filename = f'{file.stem.lower().replace(" ", "_")}.html' + + html_pages[html_filename] = { + "menu": menu, + "title": title, + "content": html_engine(post), + } + + if menu is not None: + if len(menu) == 1: + markdown_menu_dict[menu[0]]["page"] = html_filename + else: + for keys in markdown_menu_dict[menu[0]]["pages"]: + if menu[1] in keys.keys(): + keys[menu[1]] = html_filename + + # write html files + for page, meta in html_pages.items(): + with open(docs_dir / page, mode="w") as html_file: + html_file.write( + render_template( + "__main__.html", + menu=markdown_menu_dict, + title=meta["title"], + date=post_date(dt_date), + content=meta["content"], + ) + ) + + # write main index.html + index_html.write_text( + render_template( + "index.html", + menu=markdown_menu_dict, + date=post_date(dt_date), + index=html_engine(markdown_index.read_text()), + ) + ) diff --git a/docs/_ssg/exceptions.py b/docs/_ssg/exceptions.py new file mode 100644 index 0000000..f34ca08 --- /dev/null +++ b/docs/_ssg/exceptions.py @@ -0,0 +1,38 @@ +class NoPostDefinition(Exception): + builtin_msg = f"""\n +No post definition found! + +{"_" * 10}TOP_OF_FILE{"_" * 10} +``` +Publish = Bool +Date = 0000-00-00 00:00:00 +0100 or set-on-compile +Title = String +Description = String +``` + +Must be at the top of the file, and must be followed by a blank line. + +""" + + def __str__(self): + return self.builtin_msg + + +class ErrorInPostDefinition(Exception): + builtin_msg = f"""\n +There is an error in the post description! + +{"_" * 10}TOP_OF_FILE{"_" * 10} +``` +Publish = Bool +Date = 0000-00-00 00:00:00 +0100 or set-on-compile +Title = String +Description = String +``` + +Must be at the top of the file, and must be followed by a blank line. + +""" + + def __str__(self): + return self.builtin_msg diff --git a/docs/_ssg/helpers.py b/docs/_ssg/helpers.py new file mode 100644 index 0000000..c6dbe98 --- /dev/null +++ b/docs/_ssg/helpers.py @@ -0,0 +1,53 @@ +import re +from datetime import datetime +from pathlib import Path + +from pytz import timezone + +local_tz = timezone("Europe/London") + + +def pytz_dt_now() -> datetime: + return datetime.now(local_tz) + + +def pytz_dt_epoch() -> float: + return pytz_dt_now().timestamp() + + +def pytz_dt_now_str(mask: str = "%Y-%m-%d %H:%M:%S %z") -> str: + return datetime.now(local_tz).strftime(mask) + + +def pytz_dt_to_str(pytz_dt: datetime, mask: str = "%Y-%m-%d %H:%M:%S %z") -> str: + return pytz_dt.strftime(mask) + + +def pytz_dt_str_to_dt(pytz_dt_str: str) -> datetime: + """ + :param pytz_dt_str: "2020-01-01 00:00:00 +0000" + """ + return datetime.strptime(pytz_dt_str, "%Y-%m-%d %H:%M:%S %z") + + +def post_date(pytz_dt: datetime) -> str: + return pytz_dt.strftime("%a, %d %b %Y") + + +def switch_date(content, new_date): + pattern = re.compile(r'date="(.*?)"', re.IGNORECASE) + return pattern.sub(f'date="{new_date}"', content) + + +def get_relative_files_in_the_docs_folder(docs_dir: Path) -> list: + _ = [] + for f in docs_dir.glob("*.html"): + if f.stem == "index": + continue + _.append(f.stem) + + return _ + + +def excessive_br_cleanup(base_xml: str) -> str: + return base_xml.replace("


", "

").replace("

    ", "
      ") diff --git a/docs/_ssg/render_engines.py b/docs/_ssg/render_engines.py new file mode 100644 index 0000000..cff2fe9 --- /dev/null +++ b/docs/_ssg/render_engines.py @@ -0,0 +1,18 @@ +import mistune +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import get_lexer_by_name +from pygments.util import ClassNotFound + + +class HighlightRenderer(mistune.HTMLRenderer): + def block_code(self, code, info=None): + if info: + if info == "jinja2": + info = "jinja" + try: + lexer = get_lexer_by_name(info, stripall=True) + except ClassNotFound: + lexer = get_lexer_by_name("text", stripall=True) + return highlight(code, lexer, HtmlFormatter()) + return "
      " + mistune.escape(code) + "
      " diff --git a/docs/_templates/__main__.html b/docs/_templates/__main__.html new file mode 100644 index 0000000..d9a26de --- /dev/null +++ b/docs/_templates/__main__.html @@ -0,0 +1,26 @@ + + + + + {{ title|title }} | Quart-Imp + + + + + + + + + + + + +{% include "__menu__.html" %} + +
      +

      {{ title }}

      + {{ content|safe }} +
      + + + \ No newline at end of file diff --git a/docs/_templates/__menu__.html b/docs/_templates/__menu__.html new file mode 100644 index 0000000..98dd3cc --- /dev/null +++ b/docs/_templates/__menu__.html @@ -0,0 +1,31 @@ + diff --git a/docs/_templates/index.html b/docs/_templates/index.html new file mode 100644 index 0000000..c4ff980 --- /dev/null +++ b/docs/_templates/index.html @@ -0,0 +1,26 @@ + + + + + Quart-Imp + + + + + + + + + + + + +{% include "__menu__.html" %} + +
      + {{ index|safe }} +
      + + + + \ No newline at end of file diff --git a/docs/_templates/main_index.html b/docs/_templates/main_index.html new file mode 100644 index 0000000..c2d3abf --- /dev/null +++ b/docs/_templates/main_index.html @@ -0,0 +1,15 @@ + + + + + Redirecting to latest... + + + + +Click here if redirection is not working. + + + \ No newline at end of file diff --git a/docs/config.py b/docs/config.py new file mode 100644 index 0000000..8003f18 --- /dev/null +++ b/docs/config.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class Config: + latest = "v1" + versions = ["v1"] diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..9411a8b --- /dev/null +++ b/docs/index.html @@ -0,0 +1,15 @@ + + + + + Redirecting to latest... + + + + +Click here if redirection is not working. + + + \ No newline at end of file diff --git a/docs/v1/cli_commands-quart-imp_blueprint.html b/docs/v1/cli_commands-quart-imp_blueprint.html new file mode 100644 index 0000000..10022bf --- /dev/null +++ b/docs/v1/cli_commands-quart-imp_blueprint.html @@ -0,0 +1,304 @@ + + + + + Generate A Quart-Imp Blueprint | Quart-Imp + + + + + + + + + + + + + + +
      +

      Generate a Quart-Imp Blueprint

      +

      Quart-Imp has its own type of blueprint. It comes with some methods to auto import routes, and nested blueprints etc... +see ImpBlueprint / Introduction for more information.

      +

      You have the option to generate a regular template rendering blueprint, or a API blueprint that returns a JSON response.

      +
      quart-imp blueprint --help
      +
      +

      or

      +
      quart-imp api-blueprint --help
      +
      +

      To generate a Quart-Imp blueprint, run the following command:

      +
      quart-imp blueprint
      +
      +

      or

      +
      quart-imp api-blueprint
      +
      +

      After running this command, you will be prompted to enter the location of where you want to create your blueprint:

      +
      ~ $ quart-imp blueprint
      +(Creation is relative to the current working directory)
      +Folder to create blueprint in [Current Working Directory]:
      +
      +

      As detailed in the prompt, the creation of the blueprint is relative to the current working directory. So to create a +blueprint in the folder app/blueprints, you would enter app/blueprints in the prompt.

      +
      ~ $ quart-imp blueprint
      +(Creation is relative to the current working directory)
      +Folder to create blueprint in [Current Working Directory]: app/blueprints
      +
      +

      You will then be prompted to enter a name for your blueprint:

      +
      ~ $ quart-imp blueprint
      +...
      +Name of the blueprint to create [my_new_blueprint]:
      +
      +

      The default name is 'my_new_blueprint', we will change this to 'admin'

      +
      ~ $ quart-imp blueprint
      +...
      +Name of the blueprint to create [my_new_blueprint]: admin
      +
      +

      After creating your blueprint, the folder structure will look like this:

      +
      app/
      +├── blueprints
      +│   └── admin
      +│       ├── routes
      +│       │   └── index.py
      +│       │
      +│       ├── static
      +│       │   ├── css
      +│       │   │   └── water.css
      +│       │   ├── img
      +│       │   │   └── quart-imp-logo.png
      +│       │   └── js
      +│       │       └── main.js
      +│       │
      +│       ├── templates
      +│       │   └── www
      +│       │       ├── extends
      +│       │       │   └── main.html
      +│       │       ├── includes
      +│       │       │   ├── footer.html
      +│       │       │   └── header.html
      +│       │       └── index.html
      +│       │
      +│       └── __init__.py
      +│
      +...
      +
      +

      This is a self-contained blueprint, so it has its own static, templates and routes folders. +You can now navigate '/admin'

      +

      You can streamline this process by specifying the name of the blueprint, the folder to +create it in and the configuration to use, like so:

      +
      quart-imp blueprint -n admin -f app/blueprints
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/cli_commands-quart-imp_init.html b/docs/v1/cli_commands-quart-imp_init.html new file mode 100644 index 0000000..70846f6 --- /dev/null +++ b/docs/v1/cli_commands-quart-imp_init.html @@ -0,0 +1,398 @@ + + + + + Initialising A Quart-Imp Project | Quart-Imp + + + + + + + + + + + + + + +
      +

      Initialising a Quart-Imp Project

      +

      Quart-Imp has a cli command that deploys a new ready-to-go project. +This project is structured in a way to give you the best idea of +how to use Quart-Imp.

      +
      quart-imp init --help
      +
      +

      Create a new project

      +

      Make sure you are in the virtual environment, and at the root of your +project folder, then run the following command:

      +
      quart-imp init
      +
      +

      After running this command, you will be prompted to choose what type of +app you want to deploy:

      +
      ~ $ quart-imp init
      +What type of app would you like to create? (minimal, slim, full) [minimal]:
      +
      +

      See below for the differences between the app types.

      +

      After this, you will be prompted to enter a name for your app:

      +
      ~ $ quart-imp init
      +...
      +What would you like to call your app? [app]:
      +
      +

      'app' is the default name, so if you just press enter, your app will be +called 'app'. You will then see this output:

      +
      ~ FILES CREATED WILL LOOP OUT HERE ~
      +
      +===================
      +Quart app deployed!
      +===================
      + 
      +Your app has the default name of 'app'
      +Quart will automatically look for this!
      +Run: quart run --debug
      +
      +

      If you called your app something other than 'app', like 'new' for example, you will see:

      +
      ~ FILES CREATED WILL LOOP OUT HERE ~
      +
      +===================
      +Quart app deployed!
      +===================
      +
      +Your app has the name of 'new'
      +Run: quart --app new run --debug
      +
      +

      As you can see from the output, it gives you instructions on how to start your app, +depending on the name you gave it.

      +

      You should see a new folder that has been given the name you specified in +the quart-imp init command.

      +

      Additional options

      +

      You can also specify a name for your app in the command itself, like so:

      +
      quart-imp init -n my_app
      +
      +

      This will create a new app called 'my_app'. +The default will be a minimal app, this has no blueprints.

      +

      You can also deploy a slim app, that will have one blueprint, like so:

      +
      quart-imp init -n my_app --slim
      +
      +

      You can also deploy a full app that is setup for multiple blueprints, like so:

      +
      quart-imp init -n my_app --full
      +
      +

      init Folder structures

      +

      Minimal app (default)

      +

      quart-imp init --minimal:

      +
      app/
      +├── resources
      +│   ├── static
      +│   │   ├── css
      +│   │   │   └── water.css
      +│   │   ├── img
      +│   │   │   └── quart-imp-logo.png
      +│   │   └── favicon.ico
      +│   ├── templates
      +│   │   └── index.html
      +│   └── routes.py
      +│
      +└── __init__.py
      +
      +

      Slim app

      +

      quart-imp init --slim:

      +
      app/
      +├── extensions
      +│   └── __init__.py
      +│
      +├── resources
      +│   ├── cli
      +│   │   └── cli.py
      +│   ├── error_handlers
      +│   │   └── error_handlers.py
      +│   ├── static
      +│   │   ├── css
      +│   │   │   └── water.css
      +│   │   ├── img
      +│   │   │   └── quart-imp-logo.png
      +│   │   └── favicon.ico
      +│   └── templates
      +│       └── error.html
      +│
      +├── www
      +│   ├── __init__.py
      +│   ├── routes
      +│   │   └── index.py
      +│   ├── static
      +│   │   ├── css
      +│   │   │   └── water.css
      +│   │   ├── img
      +│   │   │   └── quart-imp-logo.png
      +│   │   └── js
      +│   │       └── main.js
      +│   └── templates
      +│       └── www
      +│           ├── extends
      +│           │   └── main.html
      +│           ├── includes
      +│           │   ├── footer.html
      +│           │   └── header.html
      +│           └── index.html
      +│
      +└── __init__.py
      +
      +

      Full app

      +

      quart-imp init --full:

      +
      app/
      +├── blueprints
      +│   └── www
      +│       ├── __init__.py
      +│       ├── routes
      +│       │   └── index.py
      +│       ├── static
      +│       │   ├── css
      +│       │   │   └── water.css
      +│       │   ├── img
      +│       │   │   └── quart-imp-logo.png
      +│       │   └── js
      +│       │       └── main.js
      +│       └── templates
      +│           └── www
      +│               ├── extends
      +│               │   └── main.html
      +│               ├── includes
      +│               │   ├── footer.html
      +│               │   └── header.html
      +│               └── index.html
      +│
      +├── extensions
      +│   └── __init__.py
      +│
      +├── resources
      +│   ├── cli
      +│   │   └── cli.py
      +│   ├── context_processors
      +│   │   └── context_processors.py
      +│   ├── error_handlers
      +│   │   └── error_handlers.py
      +│   ├── filters
      +│   │   └── filters.py
      +│   ├── routes
      +│   │   └── routes.py
      +│   ├── static
      +│   │   └── favicon.ico
      +│   └── templates
      +│       └── error.html
      +│
      +└── __init__.py
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/imp-import_app_resources.html b/docs/v1/imp-import_app_resources.html new file mode 100644 index 0000000..02893d6 --- /dev/null +++ b/docs/v1/imp-import_app_resources.html @@ -0,0 +1,304 @@ + + + + + Imp.import_app_resources | Quart-Imp + + + + + + + + + + + + + + +
      +

      Imp.import_app_resources

      +
      import_app_resources(
      +    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.

      +

      This will import any resources that have been set to the Quart app.

      +

      Routes, context processors, cli, etc.

      +

      Can only be called once.

      +

      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.

      +

      Small example of usage:

      +
      imp.import_app_resources(folder="resources")
      +# or
      +imp.import_app_resources()
      +# as the default folder is "resources"
      +
      +

      Folder Structure: resources

      +
      app
      +├── resources
      +│   ├── routes.py
      +│   ├── app_fac.py
      +│   ├── static
      +│   │   └── css
      +│   │       └── style.css
      +│   └── templates
      +│       └── index.html
      +└── ...
      +...
      +
      +

      File: 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")
      +
      +

      How factories work

      +

      Factories are functions that are called when importing the app resources. Here's an example:

      +
      imp.import_app_resources(
      +    folder="resources",
      +    factories=["development_cli"]
      +)
      +
      +

      ["development_cli"] => development_cli(app) function will be called, and the current app will be passed in.

      +

      File: app_fac.py

      +
      def development_cli(app):
      +    @app.cli.command("dev")
      +    def dev():
      +        print("dev cli command")
      +
      +

      Scoping imports

      +

      By default, all files and folders will be imported.

      +

      To disable this, set files_to_import and or +folders_to_import to [None].

      +
      imp.import_app_resources(scope_import=[None], folders_to_import=[None])
      +
      +

      To scope the imports, set the files_to_import and or folders_to_import to a list of files and or folders.

      +

      files_to_import=["cli.py", "routes.py"] => will only import the files resources/cli.py +and resources/routes.py

      +

      folders_to_import=["template_filters", "context_processors"] => will import all files in the folders +resources/template_filters/*.py and resources/context_processors/*.py

      + +
      + + + \ No newline at end of file diff --git a/docs/v1/imp-import_blueprint.html b/docs/v1/imp-import_blueprint.html new file mode 100644 index 0000000..55c1bf6 --- /dev/null +++ b/docs/v1/imp-import_blueprint.html @@ -0,0 +1,324 @@ + + + + + Imp.import_blueprint | Quart-Imp + + + + + + + + + + + + + + +
      +

      Imp.import_blueprint

      +
      import_blueprint(self, blueprint: str) -> None
      +
      +
      +

      Import a specified Quart-Imp or standard Quart Blueprint relative to the Quart app root.

      +
      app
      +├── my_blueprint
      +│   ├── ...
      +│   └── __init__.py
      +├── ...
      +└── __init__.py
      +
      +

      File: app/__init__.py

      +
      from quart import Quart
      +
      +from quart_imp import Imp
      +
      +imp = Imp()
      +
      +
      +def create_app():
      +    app = Quart(__name__)
      +    imp.init_app(app)
      +
      +    imp.import_blueprint("my_blueprint")
      +
      +    return app
      +
      +

      Quart-Imp Blueprints have the ability to auto import resources, and initialize session variables.

      +

      For more information on how Quart-Imp Blueprints work, see the ImpBlueprint / Introduction

      +
      Example of 'my_blueprint' as a Quart-Imp Blueprint:
      +
      app
      +├── my_blueprint
      +│   ├── routes
      +│   │   └── index.py
      +│   ├── static
      +│   │   └── css
      +│   │       └── style.css
      +│   ├── templates
      +│   │   └── my_blueprint
      +│   │       └── index.html
      +│   ├── __init__.py
      +│   └── config.toml
      +└── ...
      +
      +

      File: __init__.py

      +
      from quart_imp import ImpBlueprint
      +from quart_imp.config import ImpBlueprintConfig
      +
      +bp = ImpBlueprint(
      +    __name__,
      +    ImpBlueprintConfig(
      +        enabled=True,
      +        url_prefix="/my-blueprint",
      +        static_folder="static",
      +        template_folder="templates",
      +        static_url_path="/static/my_blueprint",
      +        init_session={"my_blueprint": "session_value"},
      +    ),
      +)
      +
      +bp.import_resources("routes")
      +
      +

      File: routes / index.py

      +
      from .. import bp
      +
      +
      +@bp.route("/")
      +async def index():
      +    return "regular_blueprint"
      +
      +
      Example of 'my_blueprint' as a standard Quart Blueprint:
      +
      app
      +├── my_blueprint
      +│   ├── ...
      +│   └── __init__.py
      +└── ...
      +
      +

      File: __init__.py

      +
      from quart import Blueprint
      +
      +bp = Blueprint("my_blueprint", __name__, url_prefix="/my-blueprint")
      +
      +
      +@bp.route("/")
      +async def index():
      +    return "regular_blueprint"
      +
      +

      Both of the above examples will work with imp.import_blueprint("my_blueprint"), they will be registered +with the Quart app, and will be accessible via url_for("my_blueprint.index").

      + +
      + + + \ No newline at end of file diff --git a/docs/v1/imp-import_blueprints.html b/docs/v1/imp-import_blueprints.html new file mode 100644 index 0000000..13a147e --- /dev/null +++ b/docs/v1/imp-import_blueprints.html @@ -0,0 +1,271 @@ + + + + + Imp.import_blueprints | Quart-Imp + + + + + + + + + + + + + + +
      +

      Imp.import_blueprints

      +
      import_blueprints(self, folder: str) -> None
      +
      +
      +

      Import all Quart-Imp or standard Quart Blueprints from a specified folder relative to the Quart app root.

      +
      app/
      +├── blueprints/
      +│   ├── admin/
      +│   │   ├── ...
      +│   │   └── __init__.py
      +│   ├── www/
      +│   │   ├── ...
      +│   │   └── __init__.py
      +│   └── api/
      +│       ├── ...
      +│       └── __init__.py
      +├── ...
      +└── __init__.py
      +
      +

      File: app/__init__.py

      +
      from quart import Quart
      +
      +from quart_imp import Imp
      +
      +imp = Imp()
      +
      +
      +def create_app():
      +    app = Quart(__name__)
      +    imp.init_app(app)
      +
      +    imp.import_blueprints("blueprints")
      +
      +    return app
      +
      +

      This will import all Blueprints from the blueprints folder using the Imp.import_blueprint method. +See Imp / import_blueprint for more information.

      + +
      + + + \ No newline at end of file diff --git a/docs/v1/imp-init_app-init.html b/docs/v1/imp-init_app-init.html new file mode 100644 index 0000000..b0e183c --- /dev/null +++ b/docs/v1/imp-init_app-init.html @@ -0,0 +1,248 @@ + + + + + Imp.init_app, __init__ | Quart-Imp + + + + + + + + + + + + + + +
      +

      Imp.init_app, __init__

      +
      def init_app(
      +    app: Quart,
      +    config: ImpConfig
      +) -> None:
      +# -or- 
      +Imp(
      +    app: Quart,
      +    config: ImpConfig
      +)
      +
      +
      +

      Initializes the quart app to work with quart-imp.

      +

      See quart_imp_config-impconfig.md for more information on the ImpConfig class.

      + +
      + + + \ No newline at end of file diff --git a/docs/v1/imp-init_session.html b/docs/v1/imp-init_session.html new file mode 100644 index 0000000..3b7c8ac --- /dev/null +++ b/docs/v1/imp-init_session.html @@ -0,0 +1,263 @@ + + + + + Imp.init_session | Quart-Imp + + + + + + + + + + + + + + +
      +

      Imp.init_session

      +
      init_session() -> None
      +
      +
      +

      Initialize the session variables found in the config. Commonly used in app.before_request.

      +
      @app.before_request
      +async def before_request():
      +    imp._init_session()
      +
      +

      File: config.toml

      +
      ...
      +[SESSION]
      +logged_in = false
      +...
      +
      +

      logged_in is now available in the session.

      +
      @app.route('/get-session-value')
      +async def login():
      +    print(session['logged_in'])
      +    return "Check Terminal"
      +
      +

      Output: False

      +

      Can also be used to reset the values in the session. Here's an example:

      +
      @app.route('/logout')
      +async def logout():
      +    session.clear()
      +    imp._init_session()
      +    return redirect(url_for('index'))
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/imp-introduction.html b/docs/v1/imp-introduction.html new file mode 100644 index 0000000..3222099 --- /dev/null +++ b/docs/v1/imp-introduction.html @@ -0,0 +1,297 @@ + + + + + Quart-Imp Introduction | Quart-Imp + + + + + + + + + + + + + + +
      +

      Quart-Imp Introduction

      +

      Quart-Imp is a Quart extension that provides auto import methods for various Quart resources. It will import +blueprints, and other resources. It uses the importlib module to achieve this.

      +

      Quart-Imp favors the application factory pattern as a project structure, and is opinionated towards using +Blueprints. However, you can use Quart-Imp without using Blueprints.

      +

      Here's an example of a standard Quart-Imp project structure:

      +
      app/
      +├── blueprints/
      +│   ├── admin/...
      +│   ├── api/...
      +│   └── www/...
      +├── resources/
      +│   ├── filters/...
      +│   ├── context_processors/...
      +│   ├── static/...
      +│   └── templates/...
      +└── __init__.py
      +
      +

      Here's an example of the app/__init__.py file:

      +
      from quart import Quart
      +from quart_sqlalchemy import SQLAlchemy
      +from quart_imp import Imp
      +from quart_imp.config import QuartConfig, ImpConfig
      +
      +db = SQLAlchemy()
      +imp = Imp()
      +
      +
      +def create_app():
      +    app = Quart(__name__)
      +    QuartConfig(
      +        secret_key="super_secret_key",
      +        app_instance=app,
      +    )
      +    
      +    imp.init_app(app, config=ImpConfig(
      +        init_session={"logged_in": False},
      +    ))
      +    imp.import_app_resources("resources")
      +    imp.import_blueprints("blueprints")
      +    
      +    db.init_app(app)
      +
      +    return app
      +
      +

      The Quart configuration can be loaded from any standard Quart configuration method, or from the QuartConfig class +shown above.

      +

      This class contains the standard Quart configuration options found in the Quart documentation.

      +

      The ImpConfig class is used to configure the Imp instance.

      +

      The init_session option of the ImpConfig class is used to set the initial session variables for the Quart app. +This happens before the request is processed.

      +

      ImpConfig also has the ability to set SQLALCHEMY_DATABASE_URI and SQLALCHEMY_BINDS

      +

      For more information about the configuration setting see +quart_imp_config-impconfig.md.

      +

      import_app_resources will walk one level deep into the resources folder, and import +all .py files as modules. +It will also check for the existence of a static and templates folder, and register them with the Quart app.

      +

      There is a couple of options for import_app_resources to control what +is imported, see: Imp / import_app_resources

      +

      import_blueprints expects a folder that contains many Blueprint as Python packages. +It will check each blueprint folder's __init__.py file for an instance of a Quart Blueprint or a +Quart-Imp Blueprint. That instant will then be registered with the Quart app.

      +

      See more about how importing blueprints work here: ImpBlueprint / Introduction

      + +
      + + + \ No newline at end of file diff --git a/docs/v1/impblueprint-import_nested_blueprint.html b/docs/v1/impblueprint-import_nested_blueprint.html new file mode 100644 index 0000000..1773895 --- /dev/null +++ b/docs/v1/impblueprint-import_nested_blueprint.html @@ -0,0 +1,291 @@ + + + + + Impblueprint.import_nested_blueprint | Quart-Imp + + + + + + + + + + + + + + +
      +

      ImpBlueprint.import_nested_blueprint

      +
      import_nested_blueprint(self, blueprint: str) -> None
      +
      +
      +

      Import a specified Quart-Imp or standard Quart Blueprint relative to the Blueprint root.

      +

      Works the same as Imp / import_blueprint but relative to the Blueprint root.

      +

      Blueprints that are imported this way will be scoped to the parent Blueprint that imported them.

      +

      url_for('my_blueprint.my_nested_blueprint.index')

      +
      my_blueprint/
      +├── routes/...
      +├── static/...
      +├── templates/...
      +│
      +├── my_nested_blueprint/
      +│   ├── routes/
      +│   │   └── index.py
      +│   ├── static/...
      +│   ├── templates/...
      +│   ├── __init__.py
      +│
      +├── __init__.py
      +
      +

      File: my_blueprint/__init__.py

      +
      from quart_imp import ImpBlueprint
      +from quart_imp.config import ImpBlueprintConfig
      +
      +bp = ImpBlueprint(__name__, ImpBlueprintConfig(
      +    enabled=True,
      +    static_folder="static",
      +    template_folder="templates",
      +))
      +
      +bp.import_resources("routes")
      +bp.import_nested_blueprint("my_nested_blueprint")
      +
      +

      File: my_blueprint/my_nested_blueprint/__init__.py

      +
      from quart_imp import ImpBlueprint
      +from quart_imp.config import ImpBlueprintConfig
      +
      +bp = ImpBlueprint(__name__, ImpBlueprintConfig(
      +    enabled=True,
      +    static_folder="static",
      +    template_folder="templates",
      +))
      +
      +bp.import_resources("routes")
      +
      +

      File: my_blueprint/my_nested_blueprint/routes/index.py

      +
      from quart import render_template
      +
      +from .. import bp
      +
      +
      +@bp.route("/")
      +async def index():
      +    return await render_template(bp.tmpl("index.html"))
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/impblueprint-import_nested_blueprints.html b/docs/v1/impblueprint-import_nested_blueprints.html new file mode 100644 index 0000000..c10210a --- /dev/null +++ b/docs/v1/impblueprint-import_nested_blueprints.html @@ -0,0 +1,277 @@ + + + + + Impblueprint.import_nested_blueprints | Quart-Imp + + + + + + + + + + + + + + +
      +

      ImpBlueprint.import_nested_blueprints

      +
      import_nested_blueprints(self, folder: str) -> None
      +
      +
      +

      Will import all the Blueprints from the given folder relative to the Blueprint's root directory.

      +

      Uses Blueprint / import_nested_blueprint to import blueprints from +the specified folder.

      +

      Blueprints that are imported this way will be scoped to the parent Blueprint that imported them.

      +

      url_for('my_blueprint.nested_bp_one.index')

      +

      url_for('my_blueprint.nested_bp_two.index')

      +

      url_for('my_blueprint.nested_bp_three.index')

      +
      my_blueprint/
      +├── routes/...
      +├── static/...
      +├── templates/...
      +│
      +├── nested_blueprints/
      +│   │
      +│   ├── nested_bp_one/
      +│   │   ├── ...
      +│   │   ├── __init__.py
      +│   ├── nested_bp_two/
      +│   │   ├── ...
      +│   │   ├── __init__.py
      +│   └── nested_bp_three/
      +│       ├── ...
      +│       ├── __init__.py
      +│
      +├── __init__.py
      +
      +

      File: my_blueprint/__init__.py

      +
      from quart_imp import ImpBlueprint
      +from quart_imp.config import ImpBlueprintConfig
      +
      +bp = ImpBlueprint(__name__, ImpBlueprintConfig(
      +    enabled=True,
      +    static_folder="static",
      +    template_folder="templates",
      +))
      +
      +bp.import_resources("routes")
      +bp.import_nested_blueprints("nested_blueprints")
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/impblueprint-import_resources.html b/docs/v1/impblueprint-import_resources.html new file mode 100644 index 0000000..c7993c2 --- /dev/null +++ b/docs/v1/impblueprint-import_resources.html @@ -0,0 +1,276 @@ + + + + + Impblueprint.import_resources | Quart-Imp + + + + + + + + + + + + + + +
      +

      ImpBlueprint.import_resources

      +
      import_resources(folder: str = "routes") -> None
      +
      +
      +

      Will import all the resources (cli, routes, filters, context_processors...) from the given folder relative to the +Blueprint's root directory.

      +
      my_blueprint
      +├── user_routes
      +│   ├── user_dashboard.py
      +│   └── user_settings.py
      +├── car_routes
      +│   ├── car_dashboard.py
      +│   └── car_settings.py
      +├── static/...
      +├── templates/
      +│   └── my_blueprint/
      +│       ├── user_dashboard.html
      +│       └── ...
      +├── __init__.py
      +
      +

      File: my_blueprint/__init__.py

      +
      from quart_imp import ImpBlueprint
      +from quart_imp.config import ImpBlueprintConfig
      +
      +bp = ImpBlueprint(__name__, ImpBlueprintConfig(
      +    enabled=True,
      +    static_folder="static",
      +    template_folder="templates",
      +))
      +
      +bp.import_resources("user_routes")
      +bp.import_resources("car_routes")
      +
      +

      File: my_blueprint/user_routes/user_dashboard.py

      +
      from quart import render_template
      +
      +from .. import bp
      +
      +@bp.route("/user-dashboard")
      +async def user_dashboard():
      +    return await render_template(bp.tmpl("user_dashboard.html"))
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/impblueprint-init.html b/docs/v1/impblueprint-init.html new file mode 100644 index 0000000..dc4f679 --- /dev/null +++ b/docs/v1/impblueprint-init.html @@ -0,0 +1,242 @@ + + + + + Quart-Imp Blueprint __init__ | Quart-Imp + + + + + + + + + + + + + + +
      +

      Quart-Imp Blueprint __init__

      +
      ImpBlueprint(dunder_name: str, config: ImpBlueprintConfig) -> None
      +
      +
      +

      Initializes the Quart-Imp Blueprint.

      +

      dunder_name should always be set to __name__

      +

      config is an instance of ImpBlueprintConfig that will be used to load the Blueprint's configuration. +See quart_imp.config / ImpBlueprintConfig for more information.

      + +
      + + + \ No newline at end of file diff --git a/docs/v1/impblueprint-introduction.html b/docs/v1/impblueprint-introduction.html new file mode 100644 index 0000000..321789b --- /dev/null +++ b/docs/v1/impblueprint-introduction.html @@ -0,0 +1,285 @@ + + + + + Quart-Imp Blueprint Introduction | Quart-Imp + + + + + + + + + + + + + + +
      +

      Quart-Imp Blueprint Introduction

      +

      The Quart-Imp Blueprint inherits from the Quart Blueprint class, then adds some additional methods to allow for auto +importing of resources and other nested blueprints.

      +

      The Quart-Imp Blueprint requires you to provide the ImpBlueprintConfig class as the second argument to the Blueprint.

      +

      Here's an example of a Quart-Imp Blueprint structure:

      +
      www/
      +├── nested_blueprints/
      +│   ├── blueprint_one/
      +│   │   ├── ...
      +│   │   └── __init__.py
      +│   └── blueprint_two/
      +│       ├── ...
      +│       └── __init__.py
      +├── standalone_nested_blueprint/
      +│   ├── ...
      +│   └── __init__.py
      +├── routes/
      +│   └── index.py
      +├── static/
      +│   └── ...
      +├── templates/
      +│   └── www/
      +│       └── index.html
      +└── __init__.py
      +
      +

      File: __init__.py

      +
      from quart_imp import ImpBlueprint
      +from quart_imp.config import ImpBlueprintConfig
      +
      +bp = ImpBlueprint(__name__, ImpBlueprintConfig(
      +    enabled=True,
      +    url_prefix="/www",
      +    static_folder="static",
      +    template_folder="templates",
      +    init_session={"logged_in": False},
      +))
      +
      +bp.import_resources("routes")
      +bp.import_nested_blueprints("nested_blueprints")
      +bp.import_nested_blueprint("standalone_nested_blueprint")
      +
      +

      The ImpBlueprintConfig class is used to configure the Blueprint. It provides a little more flexibility than the +standard Quart Blueprint configuration, like the ability to enable or disable the Blueprint.

      +

      ImpBlueprintConfig's init_session works the same as ImpConfig's init_session, this will add the session data to +the Quart app's session object on initialization of the Quart app.

      +

      To see more about configuration see: quart_imp.config / ImpBlueprintConfig

      +

      import_resources method will walk one level deep into the routes folder, and import all .py files as modules. +For more information see: ImpBlueprint / import_resources

      +

      import_nested_blueprints will do the same as imp.import_blueprints, but will register the blueprints found as +nested to the current blueprint. For example www.blueprint_one.index

      +

      import_nested_blueprint behaves the same as import_nested_blueprints, but will only import a single blueprint.

      + +
      + + + \ No newline at end of file diff --git a/docs/v1/impblueprint-tmpl.html b/docs/v1/impblueprint-tmpl.html new file mode 100644 index 0000000..ce64853 --- /dev/null +++ b/docs/v1/impblueprint-tmpl.html @@ -0,0 +1,264 @@ + + + + + Impblueprint.tmpl | Quart-Imp + + + + + + + + + + + + + + +
      +

      ImpBlueprint.tmpl

      +
      tmpl(template: str) -> str
      +
      +
      +

      Scopes the template lookup to the name of the blueprint (this takes from the __name__ attribute of the Blueprint).

      +

      Due to the way Quart templating works, and to avoid template name collisions. +It is standard practice to place the name of the Blueprint in the template path, +then to place any templates under that folder.

      +
      my_blueprint/
      +├── routes/
      +│   └── index.py
      +├── static/...
      +│
      +├── templates/
      +│   └── my_blueprint/
      +│       └── index.html
      +│
      +├── __init__.py
      +
      +

      File: my_blueprint/routes/index.py

      +
      from quart import render_template
      +
      +from .. import bp
      +
      +
      +@bp.route("/")
      +async def index():
      +    return await render_template(bp.tmpl("index.html"))
      +
      +

      bp.tmpl("index.html") will output "my_blueprint/index.html".

      + +
      + + + \ No newline at end of file diff --git a/docs/v1/index.html b/docs/v1/index.html new file mode 100644 index 0000000..da963a1 --- /dev/null +++ b/docs/v1/index.html @@ -0,0 +1,317 @@ + + + + + Quart-Imp + + + + + + + + + + + + + + +
      +

      Welcome to the Quart-Imp Documentation

      +

      What is Quart-Imp?

      +

      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.

      +

      Install Quart-Imp

      +
      pip install quart-imp
      +
      +

      Getting Started

      +

      To get started right away, you can use the CLI commands to create a new Quart-Imp project.

      +
      quart-imp init
      +
      +

      Minimal Quart-Imp Setup

      +

      Run the following command to create a minimal Quart-Imp project.

      +
      quart-imp init -n app --minimal
      +
      +

      See CLI Commands / quart-imp init for more information.

      +

      The minimal structure

      +

      Folder Structure

      +
      app/
      +├── resources/
      +│   ├── static/...
      +│   ├── templates/
      +│   │   └── index.html
      +│   └── index.py
      +└── __init__.py
      +
      +

      File: app/__init__.py

      +
      from quart import Quart
      +
      +from quart_imp import Imp
      +from quart_imp.config import QuartConfig, ImpConfig
      +
      +imp = Imp()
      +
      +
      +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()
      +    # Takes argument 'folder' default folder is 'resources'
      +
      +    return app
      +
      +

      File: app/resources/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")
      +
      +

      File: app/resources/templates/index.html

      +
      <!DOCTYPE html>
      +<html lang="en">
      +<head>
      +    <meta charset="UTF-8">
      +    <title>Quart-Imp</title>
      +</head>
      +<body>
      +<h1>Quart-Imp</h1>
      +</body>
      +</html>
      +
      +
      +

      Setting up a virtual environment is recommended.

      +

      Linux / Darwin

      +
      python3 -m venv venv
      +
      +
      source venv/bin/activate
      +
      +

      Windows

      +
      python -m venv venv
      +
      +
      .\venv\Scripts\activate
      +
      + +
      + + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-authenticate_password.html b/docs/v1/quart_imp_auth-authenticate_password.html new file mode 100644 index 0000000..d602f96 --- /dev/null +++ b/docs/v1/quart_imp_auth-authenticate_password.html @@ -0,0 +1,272 @@ + + + + + Authenticate_password - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      authenticate_password - quart_imp.auth

      +
      from quart_imp.auth import authenticate_password
      +
      +
      authenticate_password(
      +    input_password: str,
      +    database_password: str,
      +    database_salt: str,
      +    encryption_level: int = 512,
      +    pepper_length: int = 1,
      +    pepper_position: t.Literal["start", "end"] = "end"
      +) -> bool
      +
      +
      +

      For use in password hashing.

      +

      To be used alongside the quart_imp.auth / encrypt_password function.

      +

      Takes the plain input password, the stored hashed password along with the stored salt +and will try every possible combination of pepper values to find a match.

      +

      Note:

      +
        +
      • You must know the pepper length used to hash the password.
      • +
      • You must know the position of the pepper used to hash the password.
      • +
      • You must know the encryption level used to hash the password.
      • +
      +

      Authentication Scenario:

      +
      Plain password: "password"
      +Generated salt: "^%$*" (randomly generated)
      +Generated pepper (length 1): "A" (randomly generated)
      +Pepper position: "end"
      +
      input_password = "password"
      +database_password = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0..." # pulled from database
      +database_salt = "^%$*" # pulled from database
      +
      +authenticate_password(
      +    input_password,
      +    database_password,
      +    database_salt
      +)  # >>> True
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-encrypt_password.html b/docs/v1/quart_imp_auth-encrypt_password.html new file mode 100644 index 0000000..f8e16fd --- /dev/null +++ b/docs/v1/quart_imp_auth-encrypt_password.html @@ -0,0 +1,269 @@ + + + + + Encrypt_password - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      encrypt_password - quart_imp.auth

      +
      from quart_imp.auth import encrypt_password
      +
      +
      encrypt_password(
      +    password: str,
      +    salt: str,
      +    encryption_level: int = 512,
      +    pepper_length: int = 1,
      +    pepper_position: t.Literal["start", "end"] = "end"
      +) -> str
      +
      +
      +

      For use in password hashing.

      +

      To be used alongside the quart_imp.auth / authenticate_password function.

      +

      Takes the plain password, applies a pepper, salts it, then produces a digested sha512 or sha256 if specified.

      +

      Can set the encryption level to 256 or 512, defaults to 512.

      +

      Can set the pepper length, defaults to 1. Max is 3.

      +

      Can set the pepper position, "start" or "end", defaults to "end".

      +

      Note:

      +
        +
      • You must inform the authenticate_password function of the pepper length used to hash the password.
      • +
      • You must inform the authenticate_password function of the position of the pepper used to hash the password.
      • +
      • You must inform the authenticate_password function of the encryption level used to hash the password.
      • +
      +

      Encryption Scenario:

      +
      Plain password: "password"
      +Generated salt: "^%$*" (randomly generated)
      +Generated pepper (length 1): "A" (randomly generated)
      +Pepper position: "end"
      +
        +
      1. Pepper is added to the end of the plain password: "passwordA"
      2. +
      3. Salt is added to the end of the peppered password: "passwordA^%$*"
      4. +
      5. Password is hashed: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0..."
      6. +
      7. Salt and hashed password are then stored in the database.
      8. +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-generate_alphanumeric_validator.html b/docs/v1/quart_imp_auth-generate_alphanumeric_validator.html new file mode 100644 index 0000000..f8ec0a9 --- /dev/null +++ b/docs/v1/quart_imp_auth-generate_alphanumeric_validator.html @@ -0,0 +1,245 @@ + + + + + Generate_alphanumeric_validator - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      generate_alphanumeric_validator - quart_imp.auth

      +
      from quart_imp.auth import generate_alphanumeric_validator
      +
      +
      generate_alphanumeric_validator(length: int = 8) -> str
      +
      +
      +

      Generates a random alphanumeric string of the given length.

      +

      (letters are capitalized)

      +
      Example:
      +
      generate_alphanumeric_validator(8)  # >>> 'A1B2C3D4'
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-generate_csrf_token.html b/docs/v1/quart_imp_auth-generate_csrf_token.html new file mode 100644 index 0000000..37e7537 --- /dev/null +++ b/docs/v1/quart_imp_auth-generate_csrf_token.html @@ -0,0 +1,246 @@ + + + + + Generate_csrf_token - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      generate_csrf_token - quart_imp.auth

      +
      from quart_imp.auth import generate_csrf_token
      +
      +
      generate_csrf_token() -> str
      +
      +
      +

      Generates a SHA1 using the current date and time.

      +

      For use in Cross-Site Request Forgery.

      +

      Also used by the quart_imp.security / csrf_protect decorator.

      +
      Example:
      +
      generate_csrf_token()  # >>> 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0'
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-generate_email_validator.html b/docs/v1/quart_imp_auth-generate_email_validator.html new file mode 100644 index 0000000..a81d077 --- /dev/null +++ b/docs/v1/quart_imp_auth-generate_email_validator.html @@ -0,0 +1,248 @@ + + + + + Generate_email_validator - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      generate_email_validator - quart_imp.auth

      +
      from quart_imp.auth import generate_email_validator
      +
      +
      generate_email_validator() -> str
      +
      +
      +

      Uses generate_alphanumeric_validator with a length of 8 to +generate a random alphanumeric value for the specific use of +validating accounts via email.

      +

      See quart_imp.auth / generate_alphanumeric_validator +for more information.

      +
      Example:
      +
      generate_email_validator()  # >>> 'A1B2C3D4'
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-generate_numeric_validator.html b/docs/v1/quart_imp_auth-generate_numeric_validator.html new file mode 100644 index 0000000..b98221a --- /dev/null +++ b/docs/v1/quart_imp_auth-generate_numeric_validator.html @@ -0,0 +1,246 @@ + + + + + Generate_numeric_validator - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      generate_numeric_validator - quart_imp.auth

      +
      from quart_imp.auth import generate_numeric_validator
      +
      +
      generate_numeric_validator(length: int) -> int
      +
      +
      +

      Generates random choice between 1 * (length) and 9 * (length).

      +

      If the length is 4, it will generate a number between 1111 and 9999.

      +

      For use in MFA email, or unique filename generation.

      +
      Example:
      +
      generate_numeric_validator(4)  # >>> 1234
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-generate_password.html b/docs/v1/quart_imp_auth-generate_password.html new file mode 100644 index 0000000..f8a06a7 --- /dev/null +++ b/docs/v1/quart_imp_auth-generate_password.html @@ -0,0 +1,246 @@ + + + + + Generate_password - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      generate_password - quart_imp.auth

      +
      from quart_imp.auth import generate_password
      +
      +
      generate_password(style: str = "mixed", length: int = 3) -> str
      +
      +
      +

      Generates a password of (length) characters.

      +

      The Default length is 3.

      +

      Style options: "animals", "colors", "mixed" - defaults to "mixed"

      +
      Example:
      +
      generate_password(style="animals", length=3)  # >>> 'Cat-Goat-Pig12'
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-generate_private_key.html b/docs/v1/quart_imp_auth-generate_private_key.html new file mode 100644 index 0000000..143e5fe --- /dev/null +++ b/docs/v1/quart_imp_auth-generate_private_key.html @@ -0,0 +1,260 @@ + + + + + Generate_private_key - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      generate_private_key - quart_imp.auth

      +
      from quart_imp.auth import generate_private_key
      +
      +
      generate_private_key(hook: t.Optional[str]) -> str
      +
      +
      +

      Generates a sha256 private key from a passed in hook value.

      +

      If no hook is passed in, it will generate a hook using datetime.now() and a +random number between 1 and 1000.

      +
      @app.route('/register', methods=['GET', 'POST'])
      +async def register():
      +    if request.method == "POST":
      +        ...
      +        salt = generate_salt()
      +        password = request.form.get('password')
      +        encrypted_password = encrypt_password(password, salt)
      +        ...
      +        user = User(
      +            username=username,
      +            email=email,
      +            password=encrypted_password,
      +            salt=salt,
      +            private_key=generate_private_key(hook=username)
      +        )
      +        ...
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-generate_salt.html b/docs/v1/quart_imp_auth-generate_salt.html new file mode 100644 index 0000000..85f7acc --- /dev/null +++ b/docs/v1/quart_imp_auth-generate_salt.html @@ -0,0 +1,263 @@ + + + + + Generate_salt - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      generate_salt - quart_imp.auth

      +
      from quart_imp.auth import generate_salt
      +
      +
      generate_salt(length: int = 4) -> str
      +
      +
      +

      Generates a string of (length) characters of punctuation.

      +

      The Default length is 4.

      +

      For use in password hashing and storage of passwords in the database.

      +
      Example:
      +
      generate_salt()  # >>> '*!$%'
      +
      +
      @app.route('/register', methods=['GET', 'POST'])
      +async def register():
      +    if request.method == "POST":
      +        ...
      +        salt = generate_salt()
      +        password = request.form.get('password')
      +        encrypted_password = encrypt_password(password, salt)
      +        ...
      +
      +        user = User(
      +            username=username,
      +            email=email,
      +            password=encrypted_password,
      +            salt=salt
      +        )
      +        ...
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-is_email_address_valid.html b/docs/v1/quart_imp_auth-is_email_address_valid.html new file mode 100644 index 0000000..0ba562c --- /dev/null +++ b/docs/v1/quart_imp_auth-is_email_address_valid.html @@ -0,0 +1,260 @@ + + + + + Is_email_address_valid - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      is_email_address_valid - quart_imp.auth

      +
      from quart_imp.auth import is_email_address_valid
      +
      +
      is_email_address_valid(
      +    email_address: str
      +) -> bool
      +
      +
      +

      Checks if an email address is valid.

      +

      Is not completely RFC 5322 compliant, but it is good enough for most use cases.

      +

      Here are examples of mistakes that it will not catch:

      +
      Valid but fails:
      +
      email@[123.123.123.123]
      +“email”@example.com
      +very.unusual.“@”.unusual.com@example.com
      +very.“(),:;<>[]”.VERY.“very@\\ "very”.unusual@strange.example.com
      +
      +
      Invalid but passes:
      +
      email@example.com (Joe Smith)
      +email@111.222.333.44444
      +
      +
      Example:
      +
      is_email_address_valid('hello@example.com')  # >>> True
      +
      +is_email_address_valid('hello@hello@example.com')  # >>> False
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_auth-is_username_valid.html b/docs/v1/quart_imp_auth-is_username_valid.html new file mode 100644 index 0000000..8b0e8cb --- /dev/null +++ b/docs/v1/quart_imp_auth-is_username_valid.html @@ -0,0 +1,269 @@ + + + + + Is_username_valid - Quart_imp.auth | Quart-Imp + + + + + + + + + + + + + + +
      +

      is_username_valid - quart_imp.auth

      +
      from quart_imp.auth import is_username_valid
      +
      +
      is_username_valid(
      +    username: str,
      +    allowed: t.Optional[t.List[t.Literal["all", "dot", "dash", "under"]]] = None
      +) -> bool
      +
      +
      +

      Checks if a username is valid.

      +

      Valid usernames can only include letters, +numbers, ., -, and _ but cannot begin or end with +the last three mentioned.

      +
      Example "all":
      +
      is_username_valid("username", allowed=["all"])
      +
      +

      Output:

      +
      username : WILL PASS : True
      +user.name : WILL PASS : True
      +user-name : WILL PASS : True
      +user_name : WILL PASS : True
      +_user_name : WILL PASS : False
      +
      +
      Example "dot", "dash":
      +
      is_username_valid("username", allowed=["dot", "dash"])
      +
      +

      Output:

      +
      username : WILL PASS : True
      +user.name : WILL PASS : True
      +user-name : WILL PASS : True
      +user-name.name : WILL PASS : True
      +user_name : WILL PASS : False
      +_user_name : WILL PASS : False
      +.user.name : WILL PASS : False
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_config-impblueprintconfig.html b/docs/v1/quart_imp_config-impblueprintconfig.html new file mode 100644 index 0000000..a491c3d --- /dev/null +++ b/docs/v1/quart_imp_config-impblueprintconfig.html @@ -0,0 +1,255 @@ + + + + + Impblueprintconfig - Quart_imp.config | Quart-Imp + + + + + + + + + + + + + + +
      +

      ImpBlueprintConfig - quart_imp.config

      +
      from quart_imp.config import ImpBlueprintConfig
      +
      +
      ImpBlueprintConfig(
      +    enabled: bool = False,
      +    url_prefix: str = None,
      +    subdomain: str = None,
      +    url_defaults: dict = None,
      +    static_folder: t.Optional[str] = None,
      +    template_folder: t.Optional[str] = None,
      +    static_url_path: t.Optional[str] = None,
      +    root_path: str = None,
      +    cli_group: str = None,
      +    init_session: dict = None
      +)
      +
      +
      +

      A class that holds a Quart-Imp blueprint configuration.

      +

      Most of these values are passed to the Blueprint class when the blueprint is registered.

      +

      The enabled argument is used to enable or disable the blueprint. This is useful for feature flags.

      +

      init_session is used to set the session values in the main before_request function.

      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_config-impconfig.html b/docs/v1/quart_imp_config-impconfig.html new file mode 100644 index 0000000..4d2aae0 --- /dev/null +++ b/docs/v1/quart_imp_config-impconfig.html @@ -0,0 +1,255 @@ + + + + + Impconfig - Quart_imp.config | Quart-Imp + + + + + + + + + + + + + + +
      +

      ImpConfig - quart_imp.config

      +
      from quart_imp.config import ImpConfig
      +
      +
      ImpConfig(
      +    init_session: t.Optional[t.Dict[str, t.Any]] = None,
      +)
      +
      +
      +

      The ImpConfig class is used to set the initial session data +that the application will use.

      +
      imp_config = ImpConfig(
      +    init_session={"key": "value"},
      +)
      +
      +
      +def create_app():
      +    app = Quart(__name__)
      +    QuartConfig(debug=True, app_instance=app)
      +    imp.init_app(app, imp_config)
      +    ...
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_config-quartconfig.html b/docs/v1/quart_imp_config-quartconfig.html new file mode 100644 index 0000000..ac84f8b --- /dev/null +++ b/docs/v1/quart_imp_config-quartconfig.html @@ -0,0 +1,280 @@ + + + + + Quartconfig - Quart_imp.config | Quart-Imp + + + + + + + + + + + + + + +
      +

      QuartConfig - quart_imp.config

      +
      from quart_imp.config import QuartConfig
      +
      +
      QuartConfig(
      +    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
      +)
      +
      +
      +

      A class that holds a Quart configuration values.

      +

      You can set the configuration values to the app instance by either passing the app instance to the app_instance +parameter or by calling the apply_config method on the QuartConfig instance.

      +
      def create_app():
      +    app = Quart(__name__)
      +    QuartConfig(debug=True, app_instance=app)
      +    return app
      +
      +

      or

      +
      def create_app():
      +    app = Quart(__name__)
      +    config = QuartConfig(debug=True)
      +    config.apply_config(app)
      +    return app
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_security-api_login_check.html b/docs/v1/quart_imp_security-api_login_check.html new file mode 100644 index 0000000..c2f7620 --- /dev/null +++ b/docs/v1/quart_imp_security-api_login_check.html @@ -0,0 +1,261 @@ + + + + + Api_login_check - Quart_imp.security | Quart-Imp + + + + + + + + + + + + + + +
      +

      api_login_check - quart_imp.security

      +
      from quart_imp.security import api_login_check
      +
      +
      api_login_check(
      +    session_key: str,
      +    values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool],
      +    fail_json: t.Optional[t.Dict[str, t.Any]] = None
      +)
      +
      +

      @api_login_check(...)

      +
      +

      A decorator that is used to secure API routes that return JSON responses.

      +

      session_key The session key to check for.

      +

      values_allowed A list of or singular value(s) that the session key must contain.

      +

      fail_json JSON that is returned on failure. {"error": "You are not logged in."} by default.

      +
      Example:
      +
      @bp.route("/api/resource", methods=["GET"])
      +@api_login_check('logged_in', True)
      +async def api_page():
      +    ...
      +
      +
      Example of defined fail_json:
      +
      @bp.route("/api/resource", methods=["GET"])
      +@api_login_check('logged_in', True, fail_json={"failed": "You need to be logged in."})
      +async def api_page():
      +    ...
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_security-include_csrf.html b/docs/v1/quart_imp_security-include_csrf.html new file mode 100644 index 0000000..9891d45 --- /dev/null +++ b/docs/v1/quart_imp_security-include_csrf.html @@ -0,0 +1,265 @@ + + + + + Include_csrf - Quart_imp.security | Quart-Imp + + + + + + + + + + + + + + +
      +

      include_csrf - quart_imp.security

      +
      from quart_imp.security import include_csrf
      +
      +
      include_csrf(
      +    session_key: str = "csrf",
      +    form_key: str = "csrf",
      +    abort_code: int = 401
      +)
      +
      +

      @include_csrf(...)

      +
      +

      A decorator that handles CSRF protection.

      +

      On a GET request, a CSRF token is generated and stored in the session key +specified by the session_key parameter.

      +

      On a POST request, the form_key specified is checked against the session_key +specified.

      +
        +
      • If they match, the request is allowed to continue.
      • +
      • If no match, the response will be abort(abort_code), default 401.
      • +
      +
      @bp.route("/admin", methods=["GET", "POST"])
      +@include_csrf(session_key="csrf", form_key="csrf")
      +async def admin_page():
      +    ...
      +    # You must pass in the CSRF token from the session into the template.
      +    # Then add <input type="hidden" name="csrf" value="{{ csrf }}"> to the form.
      +    return await render_template("admin.html", csrf=session.get("csrf"))
      +
      +

      Form key:

      +
      <input type="hidden" name="csrf" value="{{ csrf }}">
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_security-login_check.html b/docs/v1/quart_imp_security-login_check.html new file mode 100644 index 0000000..0f15eb9 --- /dev/null +++ b/docs/v1/quart_imp_security-login_check.html @@ -0,0 +1,278 @@ + + + + + Login_check - Quart_imp.security | Quart-Imp + + + + + + + + + + + + + + +
      +

      login_check - quart_imp.security

      +
      from quart_imp.security import login_check
      +
      +
      login_check(
      +    session_key: str,
      +    values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool],
      +    fail_endpoint: t.Optional[str] = None,
      +    pass_endpoint: t.Optional[str] = None,
      +    endpoint_kwargs: t.Optional[t.Dict[str, t.Union[str, int]]] = None,
      +    message: t.Optional[str] = None,
      +    message_category: str = "message"
      +)
      +
      +

      @login_check(...)

      +
      +

      A decorator that checks if the specified session key exists and contains the specified value.

      +

      session_key The session key to check for.

      +

      values_allowed A list of or singular value(s) that the session key must contain.

      +

      fail_endpoint The endpoint to redirect to if the session key does not exist or does not contain the specified values.

      +

      endpoint_kwargs A dictionary of keyword arguments to pass to the redirect endpoint.

      +

      message If a message is specified, a flash message is shown.

      +

      message_category The category of the flash message.

      +
      Example of a route that requires a user to be logged in:
      +
      @bp.route("/admin", methods=["GET"])
      +@login_check(
      +    'logged_in',
      +    True,
      +    fail_endpoint='blueprint.login_page',
      +    message="Login needed"
      +)
      +async def admin_page():
      +    ...
      +
      +
      Example of a route that if the user is already logged in, redirects to the specified endpoint:
      +
      @bp.route("/login-page", methods=["GET"])
      +@login_check(
      +    'logged_in',
      +    True,
      +    pass_endpoint='blueprint.admin_page',
      +    message="Already logged in"
      +)
      +async def login_page():
      +    ...
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_security-pass_function_check.html b/docs/v1/quart_imp_security-pass_function_check.html new file mode 100644 index 0000000..501b5c7 --- /dev/null +++ b/docs/v1/quart_imp_security-pass_function_check.html @@ -0,0 +1,326 @@ + + + + + Pass_function_check - Quart_imp.security | Quart-Imp + + + + + + + + + + + + + + +
      +

      pass_function_check - quart_imp.security

      +
      from quart_imp.security import pass_function_check
      +
      +
      def pass_function_check(
      +    function: t.Callable,
      +    predefined_args: t.Optional[t.Dict] = None,
      +    fail_endpoint: t.Optional[str] = None,
      +    pass_endpoint: t.Optional[str] = None,
      +    endpoint_kwargs: t.Optional[t.Dict[str, t.Union[str, int]]] = None,
      +    message: t.Optional[str] = None,
      +    message_category: str = "message",
      +    fail_on_missing_kwargs: bool = False,
      +    with_app_context: bool = False,
      +)
      +
      +

      NOTE: This was added mostly as an experimental feature, but ended up being useful in some cases.

      +

      A decorator that takes the result of a function and checks if it is True or False.

      +

      URL variables from @route will be read by this decorator. +To use URL variables in your passed in function, +make sure your functions argument(s) name(s) match the name(s) of the URL variable(s).

      +

      Example:

      +
      def check_if_number(value):
      +    if isinstance(value, int):
      +        return True
      +    return False
      +
      +@bp.route("/admin-page/<int:value>", methods=["GET"])
      +@login_check('logged_in', True, 'blueprint.login_page')  # can be mixed with login_check
      +@pass_function_check(
      +    check_if_number,
      +    predefined_args=None,
      +    fail_endpoint='www.index',
      +    message="Failed message"
      +)
      +async def admin_page():
      +    ...
      +
      +@bp.route("/admin-page/<int:value>", methods=["GET"])
      +@login_check('logged_in', True, 'blueprint.login_page')  # can be mixed with login_check
      +@pass_function_check(
      +    check_if_number,
      +    predefined_args={'value': 10},
      +    fail_endpoint='www.index',
      +    message="Failed message"
      +)
      +async def admin_page_overwrite():
      +    ...
      +
      +

      Advanced use case:

      +

      Here's an example of accessing quart.session from within the passed in function. including the +with_app_context parameter, the function will be called with app_context().

      +
      from quart import current_app
      +from quart import session
      +
      +...
      +
      +def check_if_number(number=1, session_=None):
      +    if session_:
      +        print(session_)
      +    try:
      +        int(number)
      +        return True
      +    except ValueError:
      +        return False
      +
      +@bp.route("/pass-func-check-with-url-var/<number>", methods=["GET"])
      +@pass_function_check(
      +    check_if_number,
      +    predefined_args={'number': 10, 'session_': session},
      +    fail_endpoint="www.index",
      +    with_app_context=True
      +)
      +async def admin_page_overwrite_with_session():
      +    ...
      +
      +

      If you pass in a predefined arg that has the same key name as a session variable that exists, the value +of that predefined arg will be replaced with the session variable value.

      +
      session['car'] = 'Toyota'
      +...
      +def check_function(car):
      +    if car == 'Toyota':
      +        return True
      +    return False
      +...
      +@bp.route("/pass-func-check-with-url-var/<number>", methods=["GET"])
      +@pass_function_check(
      +    check_function,
      +    predefined_args={'car': session},
      +    ...
      +
      +

      This will pass, as pass_function_check will replace the value of the predefined arg 'car' with the value +of the session variable 'car'.

      + +
      + + + \ No newline at end of file diff --git a/docs/v1/quart_imp_security-permission_check.html b/docs/v1/quart_imp_security-permission_check.html new file mode 100644 index 0000000..2e78f9a --- /dev/null +++ b/docs/v1/quart_imp_security-permission_check.html @@ -0,0 +1,271 @@ + + + + + Permission_check - Quart_imp.security | Quart-Imp + + + + + + + + + + + + + + +
      +

      permission_check - quart_imp.security

      +
      from quart_imp.security import permission_check
      +
      +
      permission_check(
      +    session_key: str,
      +    values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool],
      +    fail_endpoint: t.Optional[str] = None,
      +    endpoint_kwargs: t.Optional[t.Dict[str, t.Union[str, int]]] = None,
      +    message: t.Optional[str] = None,
      +    message_category: str = "message"
      +)
      +
      +

      @permission_check(...)

      +
      +

      A decorator that checks if the specified session key exists and its value(s) match the specified value(s).

      +

      session_key The session key to check for.

      +

      values_allowed A list of or singular value(s) that the session key must contain.

      +

      fail_endpoint The endpoint to redirect to if the session key does not exist or does not contain the specified values.

      +

      endpoint_kwargs A dictionary of keyword arguments to pass to the redirect endpoint.

      +

      message If a message is specified, a flash message is shown.

      +

      message_category The category of the flash message.

      +
      Example:
      +
      @bp.route("/admin-page", methods=["GET"])
      +@login_check(
      +    'logged_in', 
      +    True, 
      +    'blueprint.login_page'
      +)  # can be mixed with login_check
      +@permission_check(
      +    'permissions', 
      +    ['admin'], 
      +    fail_endpoint='www.index', 
      +    message="Failed message"
      +)
      +async def admin_page():
      +    ...
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/v1/static/Flask-Imp-Medium.png b/docs/v1/static/Flask-Imp-Medium.png new file mode 100644 index 0000000..1140eb4 Binary files /dev/null and b/docs/v1/static/Flask-Imp-Medium.png differ diff --git a/docs/v1/static/android-chrome-192x192.png b/docs/v1/static/android-chrome-192x192.png new file mode 100644 index 0000000..1207f94 Binary files /dev/null and b/docs/v1/static/android-chrome-192x192.png differ diff --git a/docs/v1/static/android-chrome-256x256.png b/docs/v1/static/android-chrome-256x256.png new file mode 100644 index 0000000..34e3d27 Binary files /dev/null and b/docs/v1/static/android-chrome-256x256.png differ diff --git a/docs/v1/static/android-chrome-96x96.png b/docs/v1/static/android-chrome-96x96.png new file mode 100644 index 0000000..75a7a01 Binary files /dev/null and b/docs/v1/static/android-chrome-96x96.png differ diff --git a/docs/v1/static/apple-touch-icon.png b/docs/v1/static/apple-touch-icon.png new file mode 100644 index 0000000..0ee95e6 Binary files /dev/null and b/docs/v1/static/apple-touch-icon.png differ diff --git a/docs/v1/static/browserconfig.xml b/docs/v1/static/browserconfig.xml new file mode 100644 index 0000000..b3930d0 --- /dev/null +++ b/docs/v1/static/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/docs/v1/static/favicon-16x16.png b/docs/v1/static/favicon-16x16.png new file mode 100644 index 0000000..6e2a5f7 Binary files /dev/null and b/docs/v1/static/favicon-16x16.png differ diff --git a/docs/v1/static/favicon-32x32.png b/docs/v1/static/favicon-32x32.png new file mode 100644 index 0000000..6b4ece3 Binary files /dev/null and b/docs/v1/static/favicon-32x32.png differ diff --git a/docs/v1/static/favicon.ico b/docs/v1/static/favicon.ico new file mode 100644 index 0000000..e734eba Binary files /dev/null and b/docs/v1/static/favicon.ico differ diff --git a/docs/v1/static/mstile-150x150.png b/docs/v1/static/mstile-150x150.png new file mode 100644 index 0000000..533fee5 Binary files /dev/null and b/docs/v1/static/mstile-150x150.png differ diff --git a/docs/v1/static/pygments.emacs-dull.css b/docs/v1/static/pygments.emacs-dull.css new file mode 100644 index 0000000..79ed473 --- /dev/null +++ b/docs/v1/static/pygments.emacs-dull.css @@ -0,0 +1,69 @@ +.highlight .hll { background-color: #ffffcc } +.highlight { background: #161f27; padding: 0.5rem 1rem; border-radius: 5px; } +.highlight .c { color: #008800; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: rgba(170, 34, 255, 0.8); font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .ch { color: #008800; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #008800; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #008800 } /* Comment.Preproc */ +.highlight .cpf { color: #008800; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #008800; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #008800; font-weight: bold } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: rgba(170, 34, 255, 0.8); font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: rgba(170, 34, 255, 0.8); font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: rgba(170, 34, 255, 0.8); font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #AA22FF } /* Keyword.Pseudo */ +.highlight .kr { color: rgba(170, 34, 255, 0.8); font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #00BB00; font-weight: bold } /* Keyword.Type */ +.highlight .m { color: #666666 } /* Literal.Number */ +.highlight .s { color: #BB4444 } /* Literal.String */ +.highlight .na { color: #BB4444 } /* Name.Attribute */ +.highlight .nb { color: #AA22FF } /* Name.Builtin */ +.highlight .nc { color: rgba(142, 142, 255, 0.8)} /* Name.Class */ +.highlight .no { color: #880000 } /* Name.Constant */ +.highlight .nd { color: #AA22FF } /* Name.Decorator */ +.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #00A000 } /* Name.Function */ +.highlight .nl { color: #A0A000 } /* Name.Label */ +.highlight .nn { color: rgba(142, 142, 255, 0.8); font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #B8860B } /* Name.Variable */ +.highlight .ow { color: rgba(170, 34, 255, 0.8); font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mb { color: #666666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BB4444 } /* Literal.String.Affix */ +.highlight .sb { color: #BB4444 } /* Literal.String.Backtick */ +.highlight .sc { color: #BB4444 } /* Literal.String.Char */ +.highlight .dl { color: #BB4444 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BB4444; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BB4444 } /* Literal.String.Double */ +.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BB4444 } /* Literal.String.Heredoc */ +.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #BB6688 } /* Literal.String.Regex */ +.highlight .s1 { color: #BB4444 } /* Literal.String.Single */ +.highlight .ss { color: #B8860B } /* Literal.String.Symbol */ +.highlight .bp { color: #AA22FF } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #00A000 } /* Name.Function.Magic */ +.highlight .vc { color: #B8860B } /* Name.Variable.Class */ +.highlight .vg { color: #B8860B } /* Name.Variable.Global */ +.highlight .vi { color: #B8860B } /* Name.Variable.Instance */ +.highlight .vm { color: #B8860B } /* Name.Variable.Magic */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/v1/static/safari-pinned-tab.svg b/docs/v1/static/safari-pinned-tab.svg new file mode 100644 index 0000000..c39f397 --- /dev/null +++ b/docs/v1/static/safari-pinned-tab.svg @@ -0,0 +1,17 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/docs/v1/static/site.webmanifest b/docs/v1/static/site.webmanifest new file mode 100644 index 0000000..de65106 --- /dev/null +++ b/docs/v1/static/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/docs/v1/static/water.css b/docs/v1/static/water.css new file mode 100644 index 0000000..945210c --- /dev/null +++ b/docs/v1/static/water.css @@ -0,0 +1,929 @@ +/** + * 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='%23efefef'%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: 1000px; + margin: 0 auto; + padding: 0 10px; + word-wrap: break-word; + color: #dbdbdb; + color: var(--text-main); + background: #202b38; + background: var(--background-body); + text-rendering: optimizeLegibility; + letter-spacing: 1px; + display: flex; + flex-direction: row; +} + +aside { + width: 250px; + border-right: 1px solid rgba(220, 220, 220, 0.5); + padding-right: 10px; + padding-bottom: 40px; + height: 100%; + overflow-y: auto; + position: fixed; +} + +aside ul { + list-style: none; + padding-left: 0; + padding-bottom: 10px; + margin: 0; +} + +aside ul > ul { + padding-top: 5px; + padding-left: 5px; +} + +section { + width: 750px; + padding-left: 290px; + padding-top: 30px; + padding-bottom: 30px; +} + +section > div:nth-child(2) { + margin-top: 20px; +} + +hr { + margin: 40px 0; +} + +.highlight { + margin-bottom: 1rem; + overflow-y: auto; +} + +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; + margin-bottom: 12px; +} + +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.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%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") 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 { + overflow-x: auto; +} + +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; + } +} \ No newline at end of file