From bfcc49dd8d224914839ea7dc8427f1bcea5741ef Mon Sep 17 00:00:00 2001 From: David Carmichael Date: Fri, 16 Aug 2024 15:09:07 +0100 Subject: [PATCH] feat: docs --- docs/__init__.py | 60 ++ .../v1/CLI Commands-quart-imp blueprint.md | 101 ++ docs/_md/v1/CLI Commands-quart-imp init.md | 215 ++++ docs/_md/v1/Imp-Introduction.md | 84 ++ docs/_md/v1/Imp-import_app_resources.md | 106 ++ docs/_md/v1/Imp-import_blueprint.md | 121 +++ docs/_md/v1/Imp-import_blueprints.md | 50 + docs/_md/v1/Imp-init_app-init.md | 22 + docs/_md/v1/Imp-init_session.md | 48 + docs/_md/v1/ImpBlueprint-Introduction.md | 68 ++ .../ImpBlueprint-import_nested_blueprint.md | 78 ++ .../ImpBlueprint-import_nested_blueprints.md | 60 ++ docs/_md/v1/ImpBlueprint-import_resources.md | 57 ++ docs/_md/v1/ImpBlueprint-init.md | 17 + docs/_md/v1/ImpBlueprint-tmpl.md | 44 + docs/_md/v1/__index__.md | 121 +++ docs/_md/v1/__menu__.md | 43 + .../quart_imp_auth-authenticate_password.md | 55 ++ .../_md/v1/quart_imp_auth-encrypt_password.md | 53 + ...mp_auth-generate_alphanumeric_validator.md | 24 + .../v1/quart_imp_auth-generate_csrf_token.md | 26 + ...quart_imp_auth-generate_email_validator.md | 27 + ...art_imp_auth-generate_numeric_validator.md | 27 + .../v1/quart_imp_auth-generate_password.md | 26 + .../v1/quart_imp_auth-generate_private_key.md | 38 + docs/_md/v1/quart_imp_auth-generate_salt.md | 46 + .../quart_imp_auth-is_email_address_valid.md | 46 + .../v1/quart_imp_auth-is_username_valid.md | 58 ++ .../v1/quart_imp_config-impblueprintconfig.md | 33 + docs/_md/v1/quart_imp_config-impconfig.md | 32 + docs/_md/v1/quart_imp_config-quartconfig.md | 59 ++ .../v1/quart_imp_security-api_login_check.md | 47 + .../_md/v1/quart_imp_security-include_csrf.md | 48 + docs/_md/v1/quart_imp_security-login_check.md | 66 ++ .../quart_imp_security-pass_function_check.md | 114 +++ .../v1/quart_imp_security-permission_check.md | 57 ++ docs/_ssg/__init__.py | 3 + docs/_ssg/compiler.py | 132 +++ docs/_ssg/exceptions.py | 38 + docs/_ssg/helpers.py | 53 + docs/_ssg/render_engines.py | 18 + docs/_templates/__main__.html | 26 + docs/_templates/__menu__.html | 31 + docs/_templates/index.html | 26 + docs/_templates/main_index.html | 15 + docs/config.py | 7 + docs/index.html | 15 + docs/v1/cli_commands-quart-imp_blueprint.html | 304 ++++++ docs/v1/cli_commands-quart-imp_init.html | 398 ++++++++ docs/v1/imp-import_app_resources.html | 304 ++++++ docs/v1/imp-import_blueprint.html | 324 ++++++ docs/v1/imp-import_blueprints.html | 271 +++++ docs/v1/imp-init_app-init.html | 248 +++++ docs/v1/imp-init_session.html | 263 +++++ docs/v1/imp-introduction.html | 297 ++++++ .../impblueprint-import_nested_blueprint.html | 291 ++++++ ...impblueprint-import_nested_blueprints.html | 277 ++++++ docs/v1/impblueprint-import_resources.html | 276 ++++++ docs/v1/impblueprint-init.html | 242 +++++ docs/v1/impblueprint-introduction.html | 285 ++++++ docs/v1/impblueprint-tmpl.html | 264 +++++ docs/v1/index.html | 317 ++++++ .../quart_imp_auth-authenticate_password.html | 272 +++++ docs/v1/quart_imp_auth-encrypt_password.html | 269 +++++ ..._auth-generate_alphanumeric_validator.html | 245 +++++ .../quart_imp_auth-generate_csrf_token.html | 246 +++++ ...art_imp_auth-generate_email_validator.html | 248 +++++ ...t_imp_auth-generate_numeric_validator.html | 246 +++++ docs/v1/quart_imp_auth-generate_password.html | 246 +++++ .../quart_imp_auth-generate_private_key.html | 260 +++++ docs/v1/quart_imp_auth-generate_salt.html | 263 +++++ ...quart_imp_auth-is_email_address_valid.html | 260 +++++ docs/v1/quart_imp_auth-is_username_valid.html | 269 +++++ .../quart_imp_config-impblueprintconfig.html | 255 +++++ docs/v1/quart_imp_config-impconfig.html | 255 +++++ docs/v1/quart_imp_config-quartconfig.html | 280 ++++++ .../quart_imp_security-api_login_check.html | 261 +++++ docs/v1/quart_imp_security-include_csrf.html | 265 +++++ docs/v1/quart_imp_security-login_check.html | 278 ++++++ ...uart_imp_security-pass_function_check.html | 326 ++++++ .../quart_imp_security-permission_check.html | 271 +++++ docs/v1/static/Flask-Imp-Medium.png | Bin 0 -> 7987 bytes docs/v1/static/android-chrome-192x192.png | Bin 0 -> 15163 bytes docs/v1/static/android-chrome-256x256.png | Bin 0 -> 20999 bytes docs/v1/static/android-chrome-96x96.png | Bin 0 -> 6119 bytes docs/v1/static/apple-touch-icon.png | Bin 0 -> 13882 bytes docs/v1/static/browserconfig.xml | 9 + docs/v1/static/favicon-16x16.png | Bin 0 -> 1210 bytes docs/v1/static/favicon-32x32.png | Bin 0 -> 1873 bytes docs/v1/static/favicon.ico | Bin 0 -> 15086 bytes docs/v1/static/mstile-150x150.png | Bin 0 -> 9755 bytes docs/v1/static/pygments.emacs-dull.css | 69 ++ docs/v1/static/safari-pinned-tab.svg | 17 + docs/v1/static/site.webmanifest | 19 + docs/v1/static/water.css | 929 ++++++++++++++++++ 95 files changed, 12960 insertions(+) create mode 100644 docs/__init__.py create mode 100644 docs/_md/v1/CLI Commands-quart-imp blueprint.md create mode 100644 docs/_md/v1/CLI Commands-quart-imp init.md create mode 100644 docs/_md/v1/Imp-Introduction.md create mode 100644 docs/_md/v1/Imp-import_app_resources.md create mode 100644 docs/_md/v1/Imp-import_blueprint.md create mode 100644 docs/_md/v1/Imp-import_blueprints.md create mode 100644 docs/_md/v1/Imp-init_app-init.md create mode 100644 docs/_md/v1/Imp-init_session.md create mode 100644 docs/_md/v1/ImpBlueprint-Introduction.md create mode 100644 docs/_md/v1/ImpBlueprint-import_nested_blueprint.md create mode 100644 docs/_md/v1/ImpBlueprint-import_nested_blueprints.md create mode 100644 docs/_md/v1/ImpBlueprint-import_resources.md create mode 100644 docs/_md/v1/ImpBlueprint-init.md create mode 100644 docs/_md/v1/ImpBlueprint-tmpl.md create mode 100644 docs/_md/v1/__index__.md create mode 100644 docs/_md/v1/__menu__.md create mode 100644 docs/_md/v1/quart_imp_auth-authenticate_password.md create mode 100644 docs/_md/v1/quart_imp_auth-encrypt_password.md create mode 100644 docs/_md/v1/quart_imp_auth-generate_alphanumeric_validator.md create mode 100644 docs/_md/v1/quart_imp_auth-generate_csrf_token.md create mode 100644 docs/_md/v1/quart_imp_auth-generate_email_validator.md create mode 100644 docs/_md/v1/quart_imp_auth-generate_numeric_validator.md create mode 100644 docs/_md/v1/quart_imp_auth-generate_password.md create mode 100644 docs/_md/v1/quart_imp_auth-generate_private_key.md create mode 100644 docs/_md/v1/quart_imp_auth-generate_salt.md create mode 100644 docs/_md/v1/quart_imp_auth-is_email_address_valid.md create mode 100644 docs/_md/v1/quart_imp_auth-is_username_valid.md create mode 100644 docs/_md/v1/quart_imp_config-impblueprintconfig.md create mode 100644 docs/_md/v1/quart_imp_config-impconfig.md create mode 100644 docs/_md/v1/quart_imp_config-quartconfig.md create mode 100644 docs/_md/v1/quart_imp_security-api_login_check.md create mode 100644 docs/_md/v1/quart_imp_security-include_csrf.md create mode 100644 docs/_md/v1/quart_imp_security-login_check.md create mode 100644 docs/_md/v1/quart_imp_security-pass_function_check.md create mode 100644 docs/_md/v1/quart_imp_security-permission_check.md create mode 100644 docs/_ssg/__init__.py create mode 100644 docs/_ssg/compiler.py create mode 100644 docs/_ssg/exceptions.py create mode 100644 docs/_ssg/helpers.py create mode 100644 docs/_ssg/render_engines.py create mode 100644 docs/_templates/__main__.html create mode 100644 docs/_templates/__menu__.html create mode 100644 docs/_templates/index.html create mode 100644 docs/_templates/main_index.html create mode 100644 docs/config.py create mode 100644 docs/index.html create mode 100644 docs/v1/cli_commands-quart-imp_blueprint.html create mode 100644 docs/v1/cli_commands-quart-imp_init.html create mode 100644 docs/v1/imp-import_app_resources.html create mode 100644 docs/v1/imp-import_blueprint.html create mode 100644 docs/v1/imp-import_blueprints.html create mode 100644 docs/v1/imp-init_app-init.html create mode 100644 docs/v1/imp-init_session.html create mode 100644 docs/v1/imp-introduction.html create mode 100644 docs/v1/impblueprint-import_nested_blueprint.html create mode 100644 docs/v1/impblueprint-import_nested_blueprints.html create mode 100644 docs/v1/impblueprint-import_resources.html create mode 100644 docs/v1/impblueprint-init.html create mode 100644 docs/v1/impblueprint-introduction.html create mode 100644 docs/v1/impblueprint-tmpl.html create mode 100644 docs/v1/index.html create mode 100644 docs/v1/quart_imp_auth-authenticate_password.html create mode 100644 docs/v1/quart_imp_auth-encrypt_password.html create mode 100644 docs/v1/quart_imp_auth-generate_alphanumeric_validator.html create mode 100644 docs/v1/quart_imp_auth-generate_csrf_token.html create mode 100644 docs/v1/quart_imp_auth-generate_email_validator.html create mode 100644 docs/v1/quart_imp_auth-generate_numeric_validator.html create mode 100644 docs/v1/quart_imp_auth-generate_password.html create mode 100644 docs/v1/quart_imp_auth-generate_private_key.html create mode 100644 docs/v1/quart_imp_auth-generate_salt.html create mode 100644 docs/v1/quart_imp_auth-is_email_address_valid.html create mode 100644 docs/v1/quart_imp_auth-is_username_valid.html create mode 100644 docs/v1/quart_imp_config-impblueprintconfig.html create mode 100644 docs/v1/quart_imp_config-impconfig.html create mode 100644 docs/v1/quart_imp_config-quartconfig.html create mode 100644 docs/v1/quart_imp_security-api_login_check.html create mode 100644 docs/v1/quart_imp_security-include_csrf.html create mode 100644 docs/v1/quart_imp_security-login_check.html create mode 100644 docs/v1/quart_imp_security-pass_function_check.html create mode 100644 docs/v1/quart_imp_security-permission_check.html create mode 100644 docs/v1/static/Flask-Imp-Medium.png create mode 100644 docs/v1/static/android-chrome-192x192.png create mode 100644 docs/v1/static/android-chrome-256x256.png create mode 100644 docs/v1/static/android-chrome-96x96.png create mode 100644 docs/v1/static/apple-touch-icon.png create mode 100644 docs/v1/static/browserconfig.xml create mode 100644 docs/v1/static/favicon-16x16.png create mode 100644 docs/v1/static/favicon-32x32.png create mode 100644 docs/v1/static/favicon.ico create mode 100644 docs/v1/static/mstile-150x150.png create mode 100644 docs/v1/static/pygments.emacs-dull.css create mode 100644 docs/v1/static/safari-pinned-tab.svg create mode 100644 docs/v1/static/site.webmanifest create mode 100644 docs/v1/static/water.css 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 0000000000000000000000000000000000000000..1140eb4cc5c4f9538ea0a3c788db40a8fe08a663 GIT binary patch literal 7987 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 literal 0 HcmV?d00001 diff --git a/docs/v1/static/android-chrome-192x192.png b/docs/v1/static/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..1207f940bfb8ce81bb79fded911cd9167265ce42 GIT binary patch literal 15163 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 literal 0 HcmV?d00001 diff --git a/docs/v1/static/android-chrome-256x256.png b/docs/v1/static/android-chrome-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..34e3d27479cf54144da963f21a635d7487f171ef GIT binary patch literal 20999 zcmV*9Kybf_P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 literal 0 HcmV?d00001 diff --git a/docs/v1/static/apple-touch-icon.png b/docs/v1/static/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0ee95e6549e0f670e080cc03409951c690c85ea7 GIT binary patch literal 13882 zcmZ`=WmFu!*I(R=Eflv!7Wbkp&LYL7IK|!F9Ts<5ym)aa?(P(KcPZ`;f1Z!;hxg2x z$xL!GIk`!${%*LUJOl%k2o(STU`R`eE5Dbu|J}%l@6XSF5joxqcry_>5dfe%7VX&p z;r*M!NJ?1_0Pv&*0Q`difXDYL{{sNPg$)4s_Z{|Nwmw9jZ(5`1q!G?ayi1K$3- zezg@PzSkf*OUp?h?W5qKfGK)^U6cXIjW3ji?u69X+-J&&(TJeJXFny7=bWAd; z&0@T@Y1YB{@otNyae=6s?t8v+^@nxx3Q z1YPl$w0DR^r>tI;?#2~h5}~c`IMwxN(1I9whZ<(XXmZ#iD4ssd8p7$*`nn|3lh~W@ zzC-PTS=vNaD{4Gk4RW|y04Crg7UMI@+oMJQU5pE+J6=fn)n}}Ki?5G z`8@6d8kgu6E2a1=)j4*r4C8GI_I7CGtI}{%81(h^)49op)nNA@mnCA)6@vZ5M!HtD zEQ$P{X{h`Q#W_xHxDdLIWZA8z1lmPHE0T9C>B$R`Vbgp-d5wAbK-PrrA4AmY<|}BF zJI*~xZnV(oq2-2Br&lLN@P-h)EhYam%tXKTodey2W<1G<3q^0>y;G+GVEVPqR`Y__ z2wU^`m4OOE2K)B$T)XqAsLi2A)P9M0g0(`wV{B_{i)Kgs1$d4IZ8;G;cBdU1YK7~| z8vq&7-CuE0b)`i{MjlR0OUB5K+zU0Lag!{qW^92vS;htk+xov0|>_~5vJ06e= zg~r_zZkR2GZ)Fd(~UbgeYU_lZX6} zRb)3k8#oF!ELZVz>2V&EB)T65-d>jwJ!A!SrNzqx5^=K*guQIDg7En_2w0m}J<{aq zlboo>9J+w2WhTP}hWkIks3l`Kd;|IPzcpnKNUFK1kss>(I?`vBeE0I=Yv&*H!5op&EiV{J=_?3X0|S;U6x-tbz%?Vz zg+cdsN0e0WkARL)YQao?`94vv5uLf!7s2y=Y}S60+ls9N6ai(?9EY!jJ>E)+btz3$ zMulK#MuDl^kC1xmF)Us>coG<>%le2YoH?fWm^HL=BBE=dm3y&q7kc)qm-+ zo@%Kf2=O1^_;7d$e~!`+g7zCz%yfpGzARX<+n|=vuG5M?xVaiX3$&eN3y$>D&BbZ- zkeq*IIJ1Mt=1F8|*bcti3UP$Y);^C+u3l@^@~77M3TTu zByQ%ba~i|TmNQ3=JUIl8B^hukN<;ca}28_YI^QXYS$KZUUK zB%+?tqG09*CxZ(KgSmp@0MIz-+E3~IoGyTXmBnx{{m*eu)5wfLK8CRb z-mrdbJ~Bohj9A(jp1`EtkjLB}L}ftbZ9m=L0WN)~`Ox^tfRFH}|}MhyQPWMj~z z)-Lro2@)fAsE6pmNrv3Wl>WcL=KKSu1e^t2;ZCE(JqX%Z0+54IBeUw3U8z-|ckL_y zc&2K(qNvhJY=!=Pb|QZe{@f0PB99j z#((k{M+V{$Bxh&gKCRlL53|>wtvv3CF(~hq)X*mnOGe_Mg z4iRcO2zPbj(+lH=zkN`Kxx%s#<(m-`0HU3kNIq<{Qi&w~X&O1KlTj3%N~`;dk1&bt z#8nnJli0fLM3)q3f6r18FG2V#l`B;A@FXJvrmjxZI#f(hUkET|pmmrN_c!r@2#5C} zQi^u2x9W=@7B4lq2aBxF%=vi-4_G;Pke*7~G21@d15>!k$Siroqgo{zM9NW)lSxr;BEh>HvdLvKLiW8diF9KvcW*lV)pR+ zA40J^<~oM(+Gt-1;AE6IO@D=CiAk+pM9LJ+q&Cd$uF;Iy$~L-@Py!JKWR8z5k8_T6pRe*=k!sfwSEqp4J%^HM0;c8Y$6~ zRv(tqdFHRvl`_RO`D-}T)6D7OKyho0(UG=1aVwC5z&YL>fxhDj5y#yEjt*kZ$~o+F zPn{5ac?FdiE8GY zeR%#T1@+p5kKO#P7$Itf&p}z6Ygg(>L9+Fa5lxGe#)2uOW_Ez7)8>x8>APc+av1_; z>ghv&<-(!b=g9f#cWo?FuK*&H01^oVXNzuK8Nnerx2`c>Uohsww!UdiU(P);4acQ% zj{^C~r37YHgx%UvdZqM?i{u;?4u(X%nl3Chgv1}sqFP=iKu;8W!NCh3<5G+&KBD*_piwi5uAk$AI}~@;;&}!3=@v982pgSyZ^s9S z{lgcUT`^{tO+6Kw-9XKSr#%osA@OXuYJR}s2)~)fs_fTjJiHZ4h4r8uFJ9d0HHPrW zC`jc15d@f>^Hf@TL&G@ML5^%zy_rAmSUww4Yutf)`JBl=#8RBf9_aFoDNrpvnDN48 z(S=;QWYIeoUbWjri!H`*Y#1q-(=^+zpy-}z_leAE>Q$;9UhfaXrRwTYsMxO>*tYhx z7d%g3QYJ5(ZECBWFM2p?p-#! zQC!y3mwUh+Mr&A9ystpP9tT%Vm(!x2$h`rQ3h7#qjke`2kgD4|CrZIxq0PUxB9)kS zdnR`?C%Ka7_f}#vLUp!lAPYG^g*8;fym2Qn&DoD1J|P!6f#bC1rNDr7ET@+o98t9V zhZuo$pp;%Sb@#=mc6CSPFXHU0qxoKdUkqq;iSOX+7$94iD#!?L>kC7a9m(2)?sF*WQMxJoC^1Ams~>Libid?B;hNW6u(FjzkV!ggPcL=g+7s*7iiim z|71&`+d+Q9;Xi!1@T`lNOk-Z1?rsRoXB-E-Mfo(Rei1#Wi0>VH+g6iInsC_oSwgU9 z$@hbf-<)(}%%EV=pb0yXNWzrsHqI+uDZ((=?ofHJ;D8qrP|Ju0akat>9c`ii$q_1N zWX>Rg*)Ri8LU+TCw0XD3UfaX9W8u}kDQHA;1B5W25hU4obYaHF3ydW%ZnZ)xSJRBz zRQhnv53TMRIg;76L&D0T>J^yJ8PXsObJHtta}&)TNj{>AAl1|#jCt|3ToB>>?@I)0 zk%@XMg)lhPV*%@Z#ZFv{^h3e@1FaLsG&IA^OZrKjk@06Qg*+P!|#I(c)mfavcXd z#;sdZi~^F6M>Z8-of`$?v46iPw2<`f35dorJhr+L70~8Ke%yMQjfcy z`kdTTk8tX!I!@(5{7v3#Kz61l(9-yR$V%4RxxLHT@V6wY5ou~bfn@JngL6p3oTd9-@yX4P1=(}_H))gd-u`YjCH>?0z?3{;AD3buRXerd=EajtWgnqPFFGLn*GV2O zz=^5QhGh@eSf+-M#3Rd)^qx1Dii?BX!E4_Qw>eM;hFj8@{lGt**QkBYaCZ}U#!ahn z)~gC9%e+K2LCP|ViqBw^acECB2Cd^gmCIpkqOJA)_1svId+RUqJd>&dUNO%oBl{J{ z7m4dz;VE{M=P8)KT@8dO0|l@3+luOob+3S&{^7+W&jIbq>Dyi00+7m4$A;R44g{rX zT8H%?@6$(}>yf|VWtW!Qm!}z$maSG)HLGvRa3)W7x0DGl>QEna6!jM8h8SvE;8z(5 zu*n3iTWPWXrZuV=jdQy6wBBaA{_!*6i}ZG%DWn{bHw>{3(p%~6U=gq1lg zlZsJ2IjTFu5t|KayLNh~ZG$&2kR9h5EnxlB7qpUGrwaM{AeECERvQ`D{dws^O5ok> z(LTbyDC}_tW|~Smapn)^H04P1g7Vbn{FfoBODWB1x47@wp;s%Ak=_JvK3YB`G#C%(&NgyRt1DOdWO#OzGLq6-BFF+w4~cpuaksC&VyW2BDjC6lxPtm@}m;TV>8P2@olmHnHlDz zvU2}qme?Hv5`T!r*MdG5_GA{6;r|uzpOQ_REOZgxt0L8 zSE#WyEL0?y#1Qq25Y{H zygNnnS(AXFA8WCOQ>lKpRQJz`N91+@z=Fn=d)%{n3b%7$0pd9NAVfgixa#7DT7nx2 zu75Jacwbwl_&Vw@KQ_wZ2#38fB@m5hJ{t?ik9Hp#l$wMl?qA#-%{R1>s2R^{2%L}} zA)HWr=Zs2aa-=eIz;C}uMkC0ZXenjS6ypmUR$<{L`YMU-QUL8|`?y5`{skvU@tsU; z^gj3&*Q`kg8sZAElF^qSAza4izZOz`HY#IiaVoGtf^kXAYt?2wInL?KSoSqHOZ_U) zD_!Dg*!AedDbL%fD$Sd0(>=&{{$bY~Hh!Sw93~g@k7&9YaP8CeEJ+sL900M&*RmOCjZJ z&oXdZX$*4aT1H2+8RDYkiVuQ$OweY?q=`!gJ2hFQ$M8|90~^B&_eh4I(@T`qflaxc z;J%l0DcR;fB99EvruJcETIw4|EcLD6A>J-oCC#g(Xsrgb1V<60-TZ<{01HE$;eY2Ok zX8g;5dR~_$Fn+Bi?69_r=aZYv6ZHGMg$sTLf223!4oeE?YN(tzRyQh>=1Vg?lUe9D zVWw-md>IpxTNed*gTVswH|p`11<1pUnb1hzazm>374zEK%C9^UTOT|#Qh6&ds)Am4 zJfmyWt*e^Hx4>8(g|>~D-$yNbXS!b z(|Eolrvu!s?GM2QQ5RmstBy3H=HZ=C05UQTKr|g*&>j8;uybGIym?S z6o;eKD$kUtB{?+~TU-{?e+A}EsZxc^1>zS3aQ~#}1^+b#jtJTdx&Hl~S>?Gw5?;O2 z?xI$~UF@eIu#a+rT({^;`rxc~LgBo7@V$Z55_34RTZo(|N_oe&=%&kJo$oeiC*;=o zD-cIDT!iTw-RO5tvNDrS>tINeNBi}N{#MYq5JWMh2ZCLqwLP~YVasf-SgBfEt1U{a zB5d2B>-|?I(=_9Jcm^FFCSG||oYJ5soCUQ+Ld2L^O=aRjMR`6KdIP4NPILWLE%VTd zv-U$QSFnjuz07Tvh${P5ixt(->L=JyR3G;@$@0t53|5{up)zt@E^;REB66A|M|OeZ zbrQV9k0U;D#2R!iwr4KI;`xX0ai5rv`j7D0&-VjAvlgUxeLW68RjxLT^skz1-MBEE z;FoB9+_EVPZ&%CBoZk+PmPwW*z>$;3QINkpWc3aZdMSEKY8e^%nF}xrFEc6uL(5J0 z#~bI?gf|c1VeS%9|5EITBq$zh>GSK;{jj>I0#jAig|u?4k1KPh^b(m~Mh&(8vP4(; z0x9VITCh^@%nZ=U$w@WASU_)T z$VvU%H;<}TbhjVjb9|*s~DBEZL&+drImdbg$sqJ zKcF|Mt9rk>?l60ITn*v1-@puy+Y`$12Q{909&-}hBT)+ElaYfZl#6lbPz(qrhB&qa zA&h;oDQB1c4IMp~Bdj&&Cw6hxWw@iPRj-TLROzV(udf^z#Fd+K^XzW6>~Gi8Geu?& zA6d?yBUw3|HeoMjxI{9r#MzArdBc<%PD^0!7s1Fh;E@6y&r}UE)y16l=7RUl^gFZCm=DZCHP-^>iX~1 z6udus2TEd)d%^rT_K^U@PQE2=?dP?#7a0Av3vS=dNc|lU`2%vDXJ%#VBS>rgnSZ|h zPWO0;NP^(cYw`GH{P-K!^p#VqVlf+`xpms*El-E^OE*GpILb;N0&St$VLrXzU4vhw zfNX=g9XEBC#oheVzK)f89yA&(&i7kmM06>~6rI(!Vj5kvzB}&D(j~2Z>X{AJ#ZS9} z`$(o5J9H%$7d9W!@y}rDUniDA(!$HHg4_^Z>jHP_cv1fd;=hR8OHFz#Gmc6cU0!^W zCsdztA_UExe4!GLp;(e5cn=@GGDqgyPKT=iV;2YRzVzWo$3*N&X%1-s?!8Z>BChk~G5 z%BDGvwp%E?^PpLvh64f?qJJa0nAK1+m1|0`YyyZ>z&QRV$?UXOH}AiTMFc*B;W{84!LgbH1MJvI`mPdG7%d~^0{fBC^?9J2%j|?Ys zg@mOUp!Q$d!#RAnjz=}$s}cRZ)fP7kAVR+vY|>ebM^eR?Cgl~`e434N%jhO1lU(q< zuv4TeQf+sfKr%zBy0i0H&?@P~9Lx#tcW(JcwQ_9D5uKC^@uEKKlnA7r7PvdH0mcgk zQ)(0nmg(0Nm%K)y&s}^FuTqiyOne!OV|BQzm8;R06;}HJ3)O^kS#s}~5KqzqpmsU_ zjU`^)F-Dt<9dEU-aS)H*NW@Yk|x^U4L#Pb0nCCk~)yK8kVCF6!%>O z?#d*G)QYVV=q+jF;RUZ0t2bq;KGsPtXTC&IY{(aMzq?-QQ)Df|`dlLy1y{dO7RkiJ zU4a&W+Wq$C>&R_x7g&EdOAUd$Gm{?zof-v^e$}=25C3C8s0yY1ij$U3+}cnpP7}lK z8*idzqps%>a+7{m_3%C9*W~wRFtYzpF2&?9lICN!iH6qqBJOfwYX~PYxHi`i{o`en z>d}e0vHsI(M^Rn~e7Zre7InEP!+Wga>5G+aJvK}0k>d*r6W>qrzoGjf%)H;66LD{Gf|%{(@TB(Af4ki6Vdk7vpKKS&40CG++__z&Qo z%-xdooGJXZW<7WeQHpqy4iq)+4iVf=RZqm^YS0MBT`NBeSp9>PVGW7!!(eXUJfmrzwMk?tJ%PA*tiOhUdP` z6crLu$)fP8A0V1mjT9?AkkIP4M(Vc(R8bS9b5DYuqC&`&$HQ&m#g8345S2* z-3jP)29+zEmqvH^-bw9PAj*kc^(8x>9H^(wilFwX8`{LQZMbAWd8EjmGWE71${v!- zAzHK!gF#wV;ML4nICvwC%oV2-Lnx^rkAWZ)uHigp)3QloFC_1db!HrYmQ3c!v>Y@T z7tNNB4R0NaO}=7j&u8OADeGb57~zUlg$EC32LYj51XGqed|g6yFKnVwn;o0HpwY_X zVjs#iKk(n1WUfW1_s2)h5s117v{%fywQ!c1MB#E{#QP`F6-?haXEBKklESHfHgr@p zEc${BRI?i0BgB{hmOlrs=)ZJ$x09u@~x1Q=9W{?>rzaa-(MU_hPN>zGxTx zD9Q`|$6C8-)OZr&oWXKXF7-A323Pm-9R1k{pWv`~!7SL6TmCmuk9Af}Tw%M^cjR># zpw<?P`^vX~*Io#xKXtl_#0>vI;bflF;AxIgfKaQu87ty=* zYHFFO($gvMA4$KbPTzgcH2OF!7#-#?x?G3yE{+y7^%jL0vfPLqfG~P4S4S$u}m@#)p#=hEFef<3FzNS2so>_EswHr0z*IKaivaK zKUDc)4hG1HJMuSu$i9M@G`2{(>&1 zrIkJGZc++N7a7zdxL}L&tS&(~$J#vOKUVc}!#b>X-12gY$)fNWjnJMYdM9py{K!1} zZDkQ@6?PJg{hKDBcw^xsY5dZyELQ1?h|a=z$6zL}IC zH(6lWsMr@zVI%V@nQ9z~vb!w)bV^$@H2dxTXb97_c82yrwMY)|NNLoojz7F^w20ml zK^~4%>Nw}^*GjZdMD5j7&l8{T+={e?*B>jz-B;7`ha;u8!B{0Vic%jK_oKxBA(VJ zv}(zrx#Q!eZV3;A*{#9Po7s6Qu%VY^&shA2EVyk)_=h=wwI(do7S4{!bBDwT9--{f zR_>#~)J~y(zRWc5CUkQZY&xW%f3U5s4dvYSN$7dx`4;e$fu!004*>k)-G&z$Jf?J# zI8z);l$EptuHCaGsR8!Az^kOZ(jEKPyIRqD{{BXZli?mmP~;<2$S3|3r;VB~U{ zP;hSs=ow*FoVq@LhF{xYr*%Fc`L@JtXf;%8`ZI#P+H**hkmt)N9r!!@+bV^1!QA!yoMkhl zmwy)*H|eO<*oVtfN%nT4>x~XUNAs0t$Iuf5GLR_ahG-Uh=E%zVgr=yGM@}_8?o@gF zF>}G7v#>ziEx>M$Zh=4th8maff{jp8OL3(amJ|>o3YW0#!Zy)onVNCGx z2dvrh*{4m2Q5Zmwc4TmyyK3&6H~m$S-sAEO+xhzCYk>AhJMAiBugn0ojS;B)!zk9B znQ>0$wqY7acHvk2So}BfvG{Fsc*?W6e~yt%NHByw1+_h*DKZO+>5Z%U9>zs?1ea&plb`NISi^X{7peorLdIf*>nU@(Ptg=%YBbfY zzoFl&jq&D`zbl?N870S0JWqeoncm#^$31*YMd}PEM1)a-@KQ|ig86eM%K=%*HOUBr+dED z8a1KAfCpfZwckdO3x=hx0ROLv9>r~ekaj!Ol2)y949B}+EVDjFB%1}1U|-~CY%SF+Shh-s%lUi&cpj^MiiRM&ynM2+&-@c&eO9=5Wb6gh>8~I@~wH zB3lzr*uU*F@M?#B{)CgvHvm&5qvS412O+8a#Kdddt33`tUl`o9SHnKDJT?Ant}Y6>RT7J{Tv`{ch|At)k07}Xbi7Dnl(S}jK*1%VMk>$) zzH9Q%ouTaI6z4CgPvpiklazky=^M65RYburp0y*jdkq18{!ECAR_q#EY-p>JIKnBs zoobFTnG-c%j3qZ5a;_cGdu+b_LPRa*758QCL*en^mRl#MnN?*(F2-cG5sI4FO?|*V zf6Bo1)Yr(aRpl<)-bKzd$;BFC|Ghl6Iaph`>NWjQ7!{%Y6MLTwn57v;Y8vtN%efbn z{Ba_7$!*+c%kN>^s#8c@J#C*X_Vf3oTwHj9N_|E;0&Z1dbJB3`gf;$OAuM?z+MiR5 z_%_~s8)x&h4+aJObKkemQK_r$?yy9Oc{OZ1H}RW$X}4H3cUxPg+QHs-Xr;jFIy!X{ z3O~5o$QqrDF_W0){r%jizPn#03V_vF(Xu*c&;f6}-c*eulEfxe~1-zM6T1 zuHJclRc)>Ffe`s&dRvx!eCeL9_tBJ-AS#k|2=+TfjX>;RrJ82QEzc4?$HhA2WgLAy zT9kgOLb)dB)`Jt>gFyP`7SNT5gGiWoS>g97>~PQaIx+s$r?#C9C+_J7P8@stQ^_+Q|maWavxzvM@4R9XgXRMTNhT_X2@7;Jz&&iJMyz#f ziir*OjlB}<-FykVU&QZO%Bz7b1&$=6Vr~WZYGs)>G$zR~bpJvcf%#aI6lsClsYOtY zFDa&-!$>h3!Tj<{Mp`^)jezaP&;EB=I>fN5Q#u^h;qMD!Le1n|S+s8{zkn3)Gh-)b zXC>SuAlq$^;sITG7}ywmNM-g1{FABP9MSiuH|Cl@hrZVzaATaw=q~*ScO%1uWZu&v z(#`;LMhuwP>x1{O^z1+QhAmXP_YL>~pO;?Abn1mwYQEx*;5(PhIKJn+%?3WzXMPgO zvDd~N6193V31b;t-ho~DFf>itfRDV67{4yb5>eK9jQPihl1I;M5jH1Yikg(2o&8rU z%7VWlyvzM}_BTmT#&RBCDVjU(hwhE*2X~}qnM5#?>lzo>w+`@yy`Qqby|C_LpfQ5l z^hyHRm4K}i=2FvX!Zp|u%iNJT7T25BXMMjlKNQ5IaYx7>`k__NA{DRR^{m`I+b(hAh#StgW+J zMNOGIG*$2l!zQ>kpa%R+-Bvb?Hu6pj9HKOJ*uLGu$K7pyfY!CD*!l6(JWO8?+s=ux zIR~685zFhm6wNT{uY-Ax9|VJtV{__%hXmp|yejC`Lb- z99i7>zwT0rmCRxB?lXRe)e?C(DmA2WP9NqpNh{kFsJ^yfiwmNQ*$!O%N_A?92W5C* zxZ5dG{UF$*I>pki3mfi=3b;w`s76_dd6wRZO;mbPwutA?c^Ws z<9~3AQP+sg={5Lwglp{O82k)g6Xu#omlC}tf#_cex`NAHHm`oi22S(FrR&Y1U$kI| zZZ*WqH}nvPjo72;Hf#B!P0qg2vmty%cM2M(l6;6DzpN*u#ys#7f2U(;Tbr1s8WS_K zi`i~nyjJfS$nz2In!J;5U{NGO)%uM`>Smg>wbV`HkuWqvV@Bd1Y&)zVr1LQuM$}55UZ2V1mO8)1*TdWXQXRG=yZNE@K!Y<9pI632XwdEpk1c-9t1n4I0`O$VQiy^$txzHy7Y zk_gE6$~zbs~Xm=xifo$kCk zuEiJlL2d?zuodHldA}@m6nX0~5@nm={OWl4a*vTOwXqJNir}PcCEX|Z;+S7`(;;fy zuT7H2Vvfj)a0H(of^8OUfSb`wNv*!5t$`5W z)o8;kfQ~PlgbmeRs^IM{0$AG~?Oq~@3YzX8+4uk1*Vm`4J^s*ONL|E#`qVSd4pxt! z89$>i3fGsQ3U~l(nh|_P)7_#}&bsmgyum7aKjCbl6g!-v@Sy+)VeQ~7#&1aTQAUM* zIAe%G#d0n9k*FxKLKr&muQ)gy*+M5a-stBawzOZ5I>{;3n|rVY4zuwk6DC&Acd{fN zmENp6SwZbbvafFYbDr+t1wzLM8|uruyYp=*B)NB$Ky9d+*a~X;bQXV^Y3APn(18eo z@bcZi@32l_2lvI&iEqJRV!N%JJ!Tt3d47z+gK4c}@j8%$6~gc)xOaMe5TvPdOM2k$ z2-4V|e>u~-UapPpWh%?9V-Z2SPjESa1kDFz!{v zO;>Ku79BvGQL#GZcuiA<~u#ih8(cgZeuGS zeOlqYVXEhZP+9>q*rlYPX0Hq(n0SG0eTHcFi}dzaTu;{j5(y|ze2cKl zUr?y+b~HC&4Xcl&UZsKu6P3*Q7v;v3pHfci1fbmQ;e+Aiu%%|1tzMgDGBH6LamyFF zy}2)+a*K+v^lwSdjXELm$h*@}waRLcEbJSY(V9yjpSAA6_hZ13CyNsmlK{+z>6;n@ z2&RxYEa~0zRST4?Jv^>u`liWDOPwTmx}Lu%hD9?MJ5uM1TSo=O=B*jlh!0-%CAVvt z&vACq0;Tt8-oOIYPA#;+hNArnJMB-2jbPIW)?4vXVJ zN5coR5s(9kI|&U$XfST)mc|u%Np9u^y+|o1vhneAvGH?qFtV}nv$1tl@}~Zu4mNhi7N#Em ze}}P4f139Wg#V48VrS~?X5eT75HYqhG@+2TF)%YxHZd^va2PWAFWc&yr>cgtvVj|g zy`!C}g|!KVvxmJ2g@wJd5dh$xv3-e-?4S+?4=ap}O7AKI;6P|BMrgPoT-ih*94>{p yKUa@29H>T>Vq{@vuy?eVq!($#U+R|(3*g^JZ0Ac+?&S9&0MZij;uWIb1OEs9AzLs2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6e2a5f7bb30024589b0cebe6e29ebbd01b985f99 GIT binary patch literal 1210 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 BIxheK literal 0 HcmV?d00001 diff --git a/docs/v1/static/favicon.ico b/docs/v1/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e734eba0d25de4264768b9e0d33d0caea8c763b6 GIT binary patch literal 15086 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 DZY)W0 literal 0 HcmV?d00001 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