├── src
└── flask_imp
│ ├── py.typed
│ ├── _cli
│ ├── filelib
│ │ ├── __init__.py
│ │ ├── main_js.py
│ │ ├── extensions.py
│ │ ├── api_blueprint.py
│ │ ├── head_tag_generator.py
│ │ ├── templates.py
│ │ ├── init.py
│ │ ├── models.py
│ │ ├── blueprint.py
│ │ └── resources.py
│ ├── helpers.py
│ └── __init__.py
│ ├── __version__.py
│ ├── _exceptions.py
│ ├── __init__.py
│ ├── auth
│ ├── _generate_salt.py
│ ├── _generate_csrf_token.py
│ ├── _generate_email_validator.py
│ ├── _generate_numeric_validator.py
│ ├── _generate_alphanumeric_validator.py
│ ├── _generate_private_key.py
│ ├── _is_email_address_valid.py
│ ├── _generate_password.py
│ ├── _is_username_valid.py
│ ├── _encrypt_password.py
│ ├── __init__.py
│ ├── _private_funcs.py
│ └── _authenticate_password.py
│ ├── config
│ ├── __init__.py
│ ├── _imp_config.py
│ ├── _sqlite_database_config.py
│ ├── _sql_database_config.py
│ ├── _imp_blueprint_config.py
│ └── _database_config.py
│ ├── security
│ ├── __init__.py
│ └── _include_csrf.py
│ ├── _registries.py
│ └── utilities.py
├── tests
├── test_app
│ ├── tests_blueprint.sqlite
│ ├── nested_test_database.sqlite
│ ├── blueprints
│ │ ├── tests
│ │ │ ├── static
│ │ │ │ └── .keep
│ │ │ ├── templates
│ │ │ │ └── tests
│ │ │ │ │ ├── database.html
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── context_processors.html
│ │ │ │ │ ├── filters.html
│ │ │ │ │ ├── static.html
│ │ │ │ │ ├── security.html
│ │ │ │ │ ├── login_failed.html
│ │ │ │ │ ├── already_logged_in.html
│ │ │ │ │ └── get-to-post.html
│ │ │ ├── nested_test
│ │ │ │ ├── templates
│ │ │ │ │ └── nested_test
│ │ │ │ │ │ └── index.html
│ │ │ │ ├── routes
│ │ │ │ │ └── index.py
│ │ │ │ ├── models
│ │ │ │ │ └── nested_test_model.py
│ │ │ │ └── __init__.py
│ │ │ ├── group_of_nested
│ │ │ │ ├── nested_test_one
│ │ │ │ │ ├── templates
│ │ │ │ │ │ └── nested_test_one
│ │ │ │ │ │ │ └── index.html
│ │ │ │ │ ├── routes
│ │ │ │ │ │ └── index.py
│ │ │ │ │ └── __init__.py
│ │ │ │ └── nested_test_two
│ │ │ │ │ ├── templates
│ │ │ │ │ └── nested_test_two
│ │ │ │ │ │ └── index.html
│ │ │ │ │ ├── routes
│ │ │ │ │ └── index.py
│ │ │ │ │ └── __init__.py
│ │ │ ├── routes
│ │ │ │ ├── index.py
│ │ │ │ ├── filters.py
│ │ │ │ ├── static.py
│ │ │ │ ├── context_processors.py
│ │ │ │ ├── error_page.py
│ │ │ │ ├── csrf.py
│ │ │ │ ├── auth.py
│ │ │ │ └── database.py
│ │ │ ├── models
│ │ │ │ └── tests_model.py
│ │ │ └── __init__.py
│ │ ├── from_object
│ │ │ ├── templates
│ │ │ │ └── from_object
│ │ │ │ │ └── index.html
│ │ │ ├── routes
│ │ │ │ └── index.py
│ │ │ └── __init__.py
│ │ └── regular_blueprint
│ │ │ └── __init__.py
│ ├── templates
│ │ ├── includes
│ │ │ ├── footer.html
│ │ │ └── menu.html
│ │ ├── index.html
│ │ ├── errors
│ │ │ ├── 403.html
│ │ │ ├── 400.html
│ │ │ ├── 404.html
│ │ │ ├── 500.html
│ │ │ ├── 405.html
│ │ │ └── 401.html
│ │ └── extends
│ │ │ └── main.html
│ ├── static
│ │ ├── css
│ │ │ └── main.css
│ │ ├── js
│ │ │ └── main.js
│ │ └── img
│ │ │ └── Flask-Imp-Small.png
│ ├── extensions
│ │ └── __init__.py
│ ├── root_blueprint
│ │ ├── routes
│ │ │ └── index.py
│ │ └── __init__.py
│ ├── resources
│ │ ├── routes
│ │ │ └── routes.py
│ │ ├── root_routes.py
│ │ ├── context_processors
│ │ │ └── context_processors.py
│ │ ├── filters
│ │ │ └── filters.py
│ │ ├── error_handlers
│ │ │ └── error_handlers.py
│ │ └── cli
│ │ │ └── cli.py
│ ├── models
│ │ ├── example_user_bind.py
│ │ ├── example_user.py
│ │ └── example_table.py
│ └── __init__.py
└── conftest.py
├── example
└── app
│ ├── extensions
│ └── __init__.py
│ ├── blueprints
│ ├── www
│ │ ├── static
│ │ │ └── js
│ │ │ │ └── main.js
│ │ ├── nested
│ │ │ ├── static
│ │ │ │ └── js
│ │ │ │ │ └── main.js
│ │ │ ├── routes
│ │ │ │ └── index.py
│ │ │ ├── __init__.py
│ │ │ └── templates
│ │ │ │ └── nested
│ │ │ │ ├── includes
│ │ │ │ ├── footer.html
│ │ │ │ └── header.html
│ │ │ │ ├── index.html
│ │ │ │ └── extends
│ │ │ │ └── main.html
│ │ ├── routes
│ │ │ └── index.py
│ │ ├── __init__.py
│ │ └── templates
│ │ │ └── www
│ │ │ ├── includes
│ │ │ ├── footer.html
│ │ │ └── header.html
│ │ │ ├── extends
│ │ │ └── main.html
│ │ │ └── index.html
│ └── new_api_blueprint
│ │ ├── routes
│ │ └── index.py
│ │ └── __init__.py
│ ├── resources
│ ├── cli
│ │ └── cli.py
│ ├── error_handlers
│ │ └── error_handlers.py
│ ├── routes
│ │ └── routes.py
│ ├── templates
│ │ └── error.html
│ ├── context_processors
│ │ └── context_processors.py
│ └── filters
│ │ └── filters.py
│ ├── self_reg
│ ├── static
│ │ └── js
│ │ │ └── main.js
│ ├── routes
│ │ └── index.py
│ ├── __init__.py
│ └── templates
│ │ └── self_reg
│ │ ├── index.html
│ │ ├── includes
│ │ ├── footer.html
│ │ └── header.html
│ │ └── extends
│ │ └── main.html
│ ├── __init__.py
│ └── models
│ └── example_user_table.py
├── docs
├── API
│ ├── flask_imp.md
│ ├── flask_imp_auth.md
│ ├── flask_imp_config.md
│ ├── flask_imp_security.md
│ └── index.md
├── Utilities
│ ├── flask_imp_utilities-lazy_session_get.md
│ └── flask_imp_utilities-lazy_url_for.md
├── Imp
│ ├── Imp-init_app-init.md
│ ├── Imp-register_imp_blueprint.md
│ ├── Imp-import_blueprints.md
│ ├── Imp-model.md
│ ├── Imp-import_models.md
│ ├── Imp-import_blueprint.md
│ ├── Imp-Introduction.md
│ └── Imp-import_resources.md
├── Auth
│ ├── flask_imp_auth-generate_alphanumeric_validator.md
│ ├── flask_imp_auth-generate_password.md
│ ├── flask_imp_auth-generate_csrf_token.md
│ ├── flask_imp_auth-generate_numeric_validator.md
│ ├── flask_imp_auth-generate_email_validator.md
│ ├── flask_imp_auth-is_email_address_valid.md
│ ├── flask_imp_auth-generate_salt.md
│ ├── flask_imp_auth-generate_private_key.md
│ ├── flask_imp_auth-is_username_valid.md
│ ├── flask_imp_auth-encrypt_password.md
│ └── flask_imp_auth-authenticate_password.md
├── ImpBlueprint
│ ├── ImpBlueprint-init.md
│ ├── ImpBlueprint-tmpl.md
│ ├── ImpBlueprint-import_nested_blueprints.md
│ ├── ImpBlueprint-import_nested_blueprint.md
│ ├── ImpBlueprint-import_models.md
│ ├── ImpBlueprint-Introduction.md
│ └── ImpBlueprint-import_resources.md
├── Config
│ ├── flask_imp_config-sqlitedatabaseconfig.md
│ ├── flask_imp_config-sqldatabaseconfig.md
│ ├── flask_imp_config-databaseconfig.md
│ ├── flask_imp_config-impblueprintconfig.md
│ ├── flask_imp_config-impconfig.md
│ └── flask_imp_config-flaskconfig.md
├── Makefile
├── make.bat
├── SecurityCheckpoints
│ ├── flask_imp_security-createacheckpoint.md
│ ├── flask_imp_security-bearercheckpoint.md
│ ├── flask_imp_security-apikeycheckpoint.md
│ └── flask_imp_security-sessioncheckpoint.md
├── Security
│ ├── flask_imp_security-include_csrf.md
│ ├── flask_imp_security-checkpoint.md
│ └── flask_imp_security-checkpoint_callable.md
├── conf.py
├── CLI_Commands
│ ├── CLI_Commands-flask-imp_blueprint.md
│ └── CLI_Commands-flask-imp_init.md
├── index.md
└── getting-started.md
├── .gitignore
├── .editorconfig
├── .readthedocs.yaml
├── .pre-commit-config.yaml
├── .github
└── workflows
│ ├── publish.yml
│ └── tests.yml
├── README.md
├── LICENSE.txt
├── tox.ini
├── pyproject.toml
└── CHANGES.md
/src/flask_imp/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/filelib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app/tests_blueprint.sqlite:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app/nested_test_database.sqlite:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/static/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app/templates/includes/footer.html:
--------------------------------------------------------------------------------
1 |
footer
--------------------------------------------------------------------------------
/tests/test_app/templates/includes/menu.html:
--------------------------------------------------------------------------------
1 | menu
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/templates/tests/database.html:
--------------------------------------------------------------------------------
1 | {{ database }}
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/templates/tests/index.html:
--------------------------------------------------------------------------------
1 | tests index
2 |
--------------------------------------------------------------------------------
/tests/test_app/static/css/main.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | background-color: #c9c9c9;
3 | }
--------------------------------------------------------------------------------
/tests/test_app/blueprints/from_object/templates/from_object/index.html:
--------------------------------------------------------------------------------
1 | tests index
2 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/nested_test/templates/nested_test/index.html:
--------------------------------------------------------------------------------
1 | nested_test
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/templates/tests/context_processors.html:
--------------------------------------------------------------------------------
1 | {{ format_price(100) }}
--------------------------------------------------------------------------------
/tests/test_app/static/js/main.js:
--------------------------------------------------------------------------------
1 | console.log('This log is from the file global/static/js/main.js')
2 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/group_of_nested/nested_test_one/templates/nested_test_one/index.html:
--------------------------------------------------------------------------------
1 | nested_test_one
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/group_of_nested/nested_test_two/templates/nested_test_two/index.html:
--------------------------------------------------------------------------------
1 | nested_test_two
--------------------------------------------------------------------------------
/src/flask_imp/__version__.py:
--------------------------------------------------------------------------------
1 | import importlib.metadata
2 |
3 | __version__ = importlib.metadata.version(__package__ or __name__)
4 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/templates/tests/filters.html:
--------------------------------------------------------------------------------
1 | {{ "World" | example__hello_world }}, {{ 1 | example__num_to_month }}
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/templates/tests/static.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app/static/img/Flask-Imp-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CheeseCake87/flask-imp/HEAD/tests/test_app/static/img/Flask-Imp-Small.png
--------------------------------------------------------------------------------
/example/app/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import Imp
2 | from flask_sqlalchemy import SQLAlchemy
3 |
4 | imp = Imp()
5 | db = SQLAlchemy()
6 |
--------------------------------------------------------------------------------
/src/flask_imp/_exceptions.py:
--------------------------------------------------------------------------------
1 | class NoConfigProvided(Exception):
2 | """
3 | Raised when no config is provided.
4 | """
5 |
6 | pass
7 |
--------------------------------------------------------------------------------
/tests/test_app/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import Imp
2 | from flask_sqlalchemy import SQLAlchemy
3 |
4 | imp = Imp()
5 | db = SQLAlchemy()
6 |
--------------------------------------------------------------------------------
/tests/test_app/root_blueprint/routes/index.py:
--------------------------------------------------------------------------------
1 | from .. import bp
2 |
3 |
4 | @bp.route("/", methods=["GET"])
5 | def index():
6 | return "success"
7 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/static/js/main.js:
--------------------------------------------------------------------------------
1 | console.log('This log is from the file /Users/david/PycharmProjects/flask-imp/app/blueprints/www/static/main.js')
2 |
--------------------------------------------------------------------------------
/example/app/resources/cli/cli.py:
--------------------------------------------------------------------------------
1 | from flask import current_app as app
2 |
3 |
4 | @app.cli.command("show-config")
5 | def show_config():
6 | print(app.config)
7 |
--------------------------------------------------------------------------------
/docs/API/flask_imp.md:
--------------------------------------------------------------------------------
1 | # flask_imp
2 |
3 | ```{eval-rst}
4 | .. automodule:: flask_imp.__init__
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 | ```
9 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/nested/static/js/main.js:
--------------------------------------------------------------------------------
1 | console.log('This log is from the file /Users/david/PycharmProjects/flask-imp/app/blueprints/www/nested/static/main.js')
2 |
--------------------------------------------------------------------------------
/example/app/self_reg/static/js/main.js:
--------------------------------------------------------------------------------
1 | console.log('This log is from the file /Users/david/PycharmProjects/CheeseCake87-Repos/flask-imp/example/app/self_reg/static/main.js')
2 |
--------------------------------------------------------------------------------
/docs/API/flask_imp_auth.md:
--------------------------------------------------------------------------------
1 | # flask_imp.auth
2 |
3 | ```{eval-rst}
4 | .. automodule:: flask_imp.auth
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 | ```
9 |
--------------------------------------------------------------------------------
/docs/API/flask_imp_config.md:
--------------------------------------------------------------------------------
1 | # flask_imp.config
2 |
3 | ```{eval-rst}
4 | .. automodule:: flask_imp.config
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 | ```
9 |
--------------------------------------------------------------------------------
/example/app/blueprints/new_api_blueprint/routes/index.py:
--------------------------------------------------------------------------------
1 | from .. import bp
2 |
3 |
4 | @bp.route("/", methods=["GET"])
5 | def index():
6 | return {"message": "Hello, World!"}
7 |
--------------------------------------------------------------------------------
/docs/API/flask_imp_security.md:
--------------------------------------------------------------------------------
1 | # flask_imp.security
2 |
3 | ```{eval-rst}
4 | .. automodule:: flask_imp.security
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 | ```
9 |
--------------------------------------------------------------------------------
/example/app/resources/error_handlers/error_handlers.py:
--------------------------------------------------------------------------------
1 | from flask import current_app as app
2 |
3 |
4 | @app.cli.command("show-config")
5 | def show_config():
6 | print(app.config)
7 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/templates/tests/security.html:
--------------------------------------------------------------------------------
1 | Security
2 |
3 | Logged_in {{ session.get('logged_in') }}
4 | permissions {{ session.get('permissions') }}
5 |
--------------------------------------------------------------------------------
/tests/test_app/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'extends/main.html' %}
2 |
3 |
4 | {% block global %}
5 |
6 | This is the template file located in app/global/templates
7 |
8 | {% endblock %}
--------------------------------------------------------------------------------
/example/app/self_reg/routes/index.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 | from .. import bp
4 |
5 |
6 | @bp.route("/", methods=["GET"])
7 | def index():
8 | return render_template(bp.tmpl("index.html"))
9 |
--------------------------------------------------------------------------------
/example/app/resources/routes/routes.py:
--------------------------------------------------------------------------------
1 | from flask import current_app as app
2 |
3 |
4 | @app.route("/example--resources")
5 | def example_route():
6 | return "From the [app_root]/resources/routes/routes.py file"
7 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/routes/index.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 | from .. import bp
4 |
5 |
6 | @bp.route("/", methods=["GET"])
7 | def index():
8 | return render_template(bp.tmpl("index.html"))
9 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/filelib/main_js.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 |
4 | def main_js(
5 | main_js_: Path,
6 | ) -> str:
7 | return f"""\
8 | console.log('This log is from the file {main_js_}')
9 | """
10 |
--------------------------------------------------------------------------------
/tests/test_app/templates/errors/403.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 403 Forbidden
6 |
7 |
8 |
9 | Access forbidden!
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/nested/routes/index.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 | from .. import bp
4 |
5 |
6 | @bp.route("/", methods=["GET"])
7 | def index():
8 | return render_template(bp.tmpl("index.html"))
9 |
--------------------------------------------------------------------------------
/tests/test_app/templates/errors/400.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 400 Bad Request
6 |
7 |
8 |
9 | It's not us, it's you.
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/routes/index.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 |
4 | def include(bp):
5 | @bp.route("/", methods=["GET"])
6 | def index_test():
7 | return render_template(bp.tmpl("index.html"))
8 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/models/tests_model.py:
--------------------------------------------------------------------------------
1 | from ....extensions import db
2 |
3 |
4 | class TestModel(db.Model):
5 | test_id = db.Column(db.Integer, primary_key=True)
6 | test_name = db.Column(db.String(256), nullable=False)
7 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/nested_test/routes/index.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 |
4 | def include(bp):
5 | @bp.route("/", methods=["GET"])
6 | def index():
7 | return render_template(bp.tmpl("index.html"))
8 |
--------------------------------------------------------------------------------
/tests/test_app/templates/errors/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 Page Not Found
6 |
7 |
8 |
9 | No route associated with the URL
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/test_app/templates/errors/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 500 Server Error!
6 |
7 |
8 |
9 | There has been a server error!
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | app/
2 | .idea/
3 | .vscode/
4 | .venv*/
5 | venv*/
6 | __pycache__/
7 | dist/
8 | .coverage*
9 | htmlcov/
10 | .tox/
11 | docs/_build/
12 | tests/instance/
13 | example/instance/
14 | _tool.py
15 | .DS_Store
16 | */.DS_Store
17 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/from_object/routes/index.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 |
4 | def include(bp):
5 | @bp.route("/", methods=["GET"])
6 | def index_from_object():
7 | return render_template(bp.tmpl("index.html"))
8 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/regular_blueprint/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | bp = Blueprint("regular_blueprint", __name__)
4 |
5 |
6 | @bp.route("/regular-blueprint")
7 | def regular_blueprint():
8 | return "regular_blueprint"
9 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/routes/filters.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 |
4 | def include(bp):
5 | @bp.route("/filters", methods=["GET"])
6 | def filters_test():
7 | return render_template(bp.tmpl("filters.html"))
8 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/routes/static.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 |
4 | def include(bp):
5 | @bp.route("/static", methods=["GET"])
6 | def static_test():
7 | return render_template(bp.tmpl("static.html"))
8 |
--------------------------------------------------------------------------------
/example/app/resources/templates/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ error_code }}
6 |
7 |
8 |
9 | {{ error_code }}
10 | {{ error_message }}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/group_of_nested/nested_test_one/routes/index.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 |
4 | def include(bp):
5 | @bp.route("/", methods=["GET"])
6 | def index():
7 | return render_template(bp.tmpl("index.html"))
8 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/group_of_nested/nested_test_two/routes/index.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 |
4 | def include(bp):
5 | @bp.route("/", methods=["GET"])
6 | def index():
7 | return render_template(bp.tmpl("index.html"))
8 |
--------------------------------------------------------------------------------
/tests/test_app/templates/errors/405.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 405 Method Not Allowed
6 |
7 |
8 |
9 | Should of GET when you POST, or POST when you GET
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/flask_imp/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Flask-IMP
3 | """
4 |
5 | from .__version__ import __version__
6 | from ._imp import Imp
7 | from ._imp_blueprint import ImpBlueprint
8 |
9 | __all__ = [
10 | "__version__",
11 | "Imp",
12 | "ImpBlueprint",
13 | ]
14 |
--------------------------------------------------------------------------------
/tests/test_app/templates/errors/401.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 401 Unauthorized
6 |
7 |
8 |
9 | You lack valid authentication credentials for the requested resource
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/nested_test/models/nested_test_model.py:
--------------------------------------------------------------------------------
1 | from .....extensions import db
2 |
3 |
4 | class NestedTestModel(db.Model):
5 | nested_test_id = db.Column(db.Integer, primary_key=True)
6 | nested_test_name = db.Column(db.String(256), nullable=False)
7 |
--------------------------------------------------------------------------------
/tests/test_app/resources/routes/routes.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | from flask import render_template
3 |
4 |
5 | def collection(app: Flask):
6 | @app.route("/")
7 | def index():
8 | return render_template(
9 | "index.html",
10 | )
11 |
--------------------------------------------------------------------------------
/docs/API/index.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | - {doc}`flask_imp`
4 | - {doc}`flask_imp_config`
5 | - {doc}`flask_imp_auth`
6 | - {doc}`flask_imp_security`
7 |
8 | ```{toctree}
9 | :hidden:
10 |
11 | flask_imp.md
12 | flask_imp_config.md
13 | flask_imp_auth.md
14 | flask_imp_security.md
15 | ```
16 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/routes/context_processors.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 |
4 | def include(bp):
5 | @bp.route("/context-processors", methods=["GET"])
6 | def context_processors_test():
7 | return render_template(bp.tmpl("context_processors.html"))
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | end_of_line = lf
9 | charset = utf-8
10 | max_line_length = 88
11 |
12 | [*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}]
13 | indent_size = 2
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/templates/tests/login_failed.html:
--------------------------------------------------------------------------------
1 | Login failed
2 |
3 | {% with messages = get_flashed_messages(with_categories=true) %}
4 | {% if messages %}
5 | {% for category, message in messages %}
6 | {{ category }}:{{ message }},
7 | {% endfor %}
8 | {% endif %}
9 | {% endwith %}
--------------------------------------------------------------------------------
/tests/test_app/blueprints/from_object/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import ImpBlueprint
2 | from flask_imp.config import ImpBlueprintConfig
3 |
4 | bp = ImpBlueprint(
5 | __name__,
6 | ImpBlueprintConfig(
7 | enabled=True,
8 | template_folder="templates",
9 | ),
10 | )
11 |
12 | bp.import_resources("routes")
13 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/routes/error_page.py:
--------------------------------------------------------------------------------
1 | from flask import abort
2 |
3 |
4 | def include(bp):
5 | @bp.route("/error-page-404", methods=["GET"])
6 | def error_page_404():
7 | return abort(404)
8 |
9 | @bp.route("/error-page-500", methods=["GET"])
10 | def error_page_500():
11 | return abort(500)
12 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/templates/tests/already_logged_in.html:
--------------------------------------------------------------------------------
1 | Already logged in
2 |
3 | {% with messages = get_flashed_messages(with_categories=true) %}
4 | {% if messages %}
5 | {% for category, message in messages %}
6 | {{ category }}:{{ message }},
7 | {% endfor %}
8 | {% endif %}
9 | {% endwith %}
--------------------------------------------------------------------------------
/tests/test_app/root_blueprint/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import ImpBlueprint
2 | from flask_imp.config import ImpBlueprintConfig
3 |
4 | bp = ImpBlueprint(
5 | __name__,
6 | ImpBlueprintConfig(
7 | url_prefix="/root-blueprint", init_session={"root_blueprint_session": True}
8 | ),
9 | )
10 |
11 | bp.import_resources("routes")
12 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/filelib/extensions.py:
--------------------------------------------------------------------------------
1 | def extensions_init_full_py() -> str:
2 | return """\
3 | from flask_imp import Imp
4 | from flask_sqlalchemy import SQLAlchemy
5 |
6 | imp = Imp()
7 | db = SQLAlchemy()
8 | """
9 |
10 |
11 | def extensions_init_slim_py() -> str:
12 | return """\
13 | from flask_imp import Imp
14 |
15 | imp = Imp()
16 | """
17 |
--------------------------------------------------------------------------------
/tests/test_app/resources/root_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 |
3 |
4 | def collection(app: Flask):
5 | @app.route("/collection-factory-import")
6 | def collection_factory_import():
7 | return "collection_factory_import"
8 |
9 | @app.route("/current-app-import")
10 | def current_app_import():
11 | return "current_app_import"
12 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | build:
3 | os: ubuntu-24.04
4 | tools:
5 | python: "3.12"
6 | jobs:
7 | post_install:
8 | - pip install uv
9 | - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --all-extras --group docs --link-mode=copy
10 | sphinx:
11 | builder: dirhtml
12 | fail_on_warning: true
13 | configuration: docs/conf.py
14 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/nested/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import ImpBlueprint
2 | from flask_imp.config import ImpBlueprintConfig
3 |
4 | bp = ImpBlueprint(
5 | __name__,
6 | ImpBlueprintConfig(
7 | enabled=True,
8 | url_prefix="/nested",
9 | init_session={"nested_session_loaded": True},
10 | ),
11 | )
12 |
13 | bp.import_resources("routes")
14 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/group_of_nested/nested_test_one/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import ImpBlueprint
2 | from flask_imp.config import ImpBlueprintConfig
3 |
4 | bp = ImpBlueprint(
5 | __name__,
6 | ImpBlueprintConfig(
7 | enabled=True,
8 | url_prefix="/nested-test-one",
9 | template_folder="templates",
10 | ),
11 | )
12 |
13 | bp.import_resources("routes")
14 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/group_of_nested/nested_test_two/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import ImpBlueprint
2 | from flask_imp.config import ImpBlueprintConfig
3 |
4 | bp = ImpBlueprint(
5 | __name__,
6 | ImpBlueprintConfig(
7 | enabled=True,
8 | url_prefix="/nested-test-two",
9 | template_folder="templates",
10 | ),
11 | )
12 |
13 | bp.import_resources("routes")
14 |
--------------------------------------------------------------------------------
/example/app/blueprints/new_api_blueprint/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import ImpBlueprint
2 | from flask_imp.config import ImpBlueprintConfig
3 |
4 | bp = ImpBlueprint(
5 | __name__,
6 | ImpBlueprintConfig(
7 | enabled=True,
8 | url_prefix="/new_api_blueprint",
9 | init_session={"new_api_blueprint_session_loaded": True},
10 | ),
11 | )
12 |
13 | bp.import_resources("routes")
14 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import ImpBlueprint
2 | from flask_imp.config import ImpBlueprintConfig
3 |
4 | bp = ImpBlueprint(
5 | __name__,
6 | ImpBlueprintConfig(
7 | enabled=True,
8 | url_prefix="//",
9 | init_session={"www_session_loaded": True},
10 | ),
11 | )
12 |
13 | bp.import_resources("routes")
14 | bp.import_nested_blueprint("nested")
15 |
--------------------------------------------------------------------------------
/docs/Utilities/flask_imp_utilities-lazy_session_get.md:
--------------------------------------------------------------------------------
1 | # lazy_session_get
2 |
3 | ```python
4 | from flask_imp.utilities import lazy_session_get
5 | ```
6 |
7 | ```python
8 | lazy_session_get(key, default=None) -> LazySession:
9 | ```
10 |
11 | ---
12 |
13 | Indented for use in checkpoint decorators.
14 |
15 | Returns a LazySession object that can be used to evaluate a session
16 | value in checkpoint decorators.
17 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/templates/tests/get-to-post.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_generate_salt.py:
--------------------------------------------------------------------------------
1 | from random import choice
2 | from string import punctuation
3 |
4 |
5 | def generate_salt(length: int = 4) -> str:
6 | """
7 | Generates a string of (length) characters of punctuation.
8 |
9 | The Default length is 4.
10 |
11 | For use in password salting
12 |
13 | :return: a salt of (length)
14 | """
15 | return "".join(choice(punctuation) for _ in range(length))
16 |
--------------------------------------------------------------------------------
/docs/Imp/Imp-init_app-init.md:
--------------------------------------------------------------------------------
1 | # Imp.init_app, \_\_init\_\_
2 |
3 | ```python
4 | def init_app(
5 | app: Flask,
6 | config: ImpConfig
7 | ) -> None:
8 | # -or-
9 | Imp(
10 | app: Flask,
11 | config: ImpConfig
12 | )
13 | ```
14 |
15 | ---
16 |
17 | Initializes the flask app to work with flask-imp.
18 |
19 | See [flask_imp_config-impconfig](../Config/flask_imp_config-impconfig.md) for more information on the `ImpConfig` class.
20 |
21 |
--------------------------------------------------------------------------------
/example/app/self_reg/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import ImpBlueprint
2 | from flask_imp.config import ImpBlueprintConfig
3 |
4 | bp = ImpBlueprint(
5 | __name__,
6 | ImpBlueprintConfig(
7 | enabled=True,
8 | url_prefix="/self_reg",
9 | static_folder="static",
10 | template_folder="templates",
11 | init_session={"self_reg_session_loaded": True},
12 | ),
13 | )
14 |
15 | bp.import_resources("routes")
16 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_generate_csrf_token.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from hashlib import sha1
3 |
4 |
5 | def generate_csrf_token() -> str:
6 | """
7 | Generates a SHA1 using the current date and time.
8 |
9 | For use in Cross-Site Request Forgery.
10 |
11 | :return: sha1 hash of the current date and time
12 | """
13 | sha = sha1()
14 | sha.update(str(datetime.now()).encode("utf-8"))
15 | return sha.hexdigest()
16 |
--------------------------------------------------------------------------------
/example/app/resources/context_processors/context_processors.py:
--------------------------------------------------------------------------------
1 | from flask import current_app as app
2 |
3 |
4 | @app.context_processor
5 | def example__utility_processor():
6 | """
7 | Usage:
8 | {{ example__format_price(100.33) }} -> $100.33
9 | """
10 |
11 | def example__format_price(amount, currency="$"):
12 | return "{1}{0:.2f}".format(amount, currency)
13 |
14 | return dict(example__format_price=example__format_price)
15 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | files: ^src/
2 | repos:
3 | - repo: https://github.com/astral-sh/ruff-pre-commit
4 | rev: v0.7.4
5 | hooks:
6 | - id: ruff
7 | - id: ruff-format
8 | - repo: https://github.com/pre-commit/pre-commit-hooks
9 | rev: v5.0.0
10 | hooks:
11 | - id: check-merge-conflict
12 | - id: debug-statements
13 | - id: fix-byte-order-marker
14 | - id: trailing-whitespace
15 | - id: end-of-file-fixer
16 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_generate_email_validator.py:
--------------------------------------------------------------------------------
1 | from ._generate_alphanumeric_validator import generate_alphanumeric_validator
2 |
3 |
4 | def generate_email_validator() -> str:
5 | """
6 | Uses generate_alphanumeric_validator with a length of 8 to
7 | generate a random alphanumeric value for the specific use of
8 | validating accounts via email.
9 |
10 | :return: alphanumeric of length 8
11 | """
12 | return str(generate_alphanumeric_validator(length=8))
13 |
--------------------------------------------------------------------------------
/tests/test_app/resources/context_processors/context_processors.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 |
3 |
4 | def collection(app: Flask):
5 | @app.context_processor
6 | def example__utility_processor():
7 | """
8 | Usage:
9 | {{ format_price(100.33) }} -> $100.33
10 | """
11 |
12 | def example__format_price(amount, currency="$"):
13 | return "{1}{0:.2f}".format(amount, currency)
14 |
15 | return dict(format_price=example__format_price)
16 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-generate_alphanumeric_validator.md:
--------------------------------------------------------------------------------
1 | # generate_alphanumeric_validator
2 |
3 | ```python
4 | from flask_imp.auth import generate_alphanumeric_validator
5 | ```
6 |
7 | ```python
8 | generate_alphanumeric_validator(length: int = 8) -> str
9 | ```
10 |
11 | ---
12 |
13 | Generates a random alphanumeric string of the given length.
14 |
15 | (letters are capitalized)
16 |
17 | *Example:*
18 |
19 | ```python
20 | generate_alphanumeric_validator(8) # >>> 'A1B2C3D4'
21 | ```
22 |
23 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/templates/www/includes/footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This is the footer, located here: /Users/david/PycharmProjects/flask-imp/app/blueprints/www/templates/www/includes/footer.html
4 |
It's being imported in the /Users/david/PycharmProjects/flask-imp/app/blueprints/www/templates/www/extends/main.html template.
5 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/ImpBlueprint/ImpBlueprint-init.md:
--------------------------------------------------------------------------------
1 | # Flask-Imp Blueprint \_\_init\_\_
2 |
3 | ```python
4 | ImpBlueprint(dunder_name: str, config: ImpBlueprintConfig) -> None
5 | ```
6 |
7 | ---
8 |
9 | Initializes the Flask-Imp Blueprint.
10 |
11 | `dunder_name` should always be set to `__name__`
12 |
13 | `config` is an instance of `ImpBlueprintConfig` that will be used to load the Blueprint's configuration.
14 | See [flask_imp.config / ImpBlueprintConfig](../Config/flask_imp_config-impblueprintconfig.md) for more information.
15 |
16 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/nested/templates/nested/includes/footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This is the footer, located here: /Users/david/PycharmProjects/flask-imp/app/blueprints/www/nested/templates/nested/includes/footer.html
4 |
It's being imported in the /Users/david/PycharmProjects/flask-imp/app/blueprints/www/nested/templates/nested/extends/main.html template.
5 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-generate_password.md:
--------------------------------------------------------------------------------
1 | # generate_password
2 |
3 | ```python
4 | from flask_imp.auth import generate_password
5 | ```
6 |
7 | ```python
8 | generate_password(style: str = "mixed", length: int = 3) -> str
9 | ```
10 |
11 | ---
12 |
13 | Generates a password of (length) characters.
14 |
15 | The Default length is 3.
16 |
17 | Style options: "animals", "colors", "mixed" - defaults to "mixed"
18 |
19 | *Example:*
20 |
21 | ```python
22 | generate_password(style="animals", length=3) # >>> 'Cat-Goat-Pig12'
23 | ```
24 |
25 |
--------------------------------------------------------------------------------
/example/app/self_reg/templates/self_reg/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'self_reg/extends/main.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
Blueprint: self_reg
7 |
Here's your new blueprint.
8 |
Located here: /Users/david/PycharmProjects/CheeseCake87-Repos/flask-imp/example/app/self_reg
9 |
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/example/app/self_reg/templates/self_reg/includes/footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This is the footer, located here: /Users/david/PycharmProjects/CheeseCake87-Repos/flask-imp/example/app/self_reg/templates/self_reg/includes/footer.html
4 |
It's being imported in the /Users/david/PycharmProjects/CheeseCake87-Repos/flask-imp/example/app/self_reg/templates/self_reg/extends/main.html template.
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_generate_numeric_validator.py:
--------------------------------------------------------------------------------
1 | from random import randrange
2 |
3 |
4 | def generate_numeric_validator(length: int) -> int:
5 | """
6 | Generates random choice between 1 * (length) and 9 * (length).
7 |
8 | Example return if length = 4: 5468
9 |
10 | For use in MFA email, or unique filename generation.
11 |
12 | :param length: length of number to generate
13 | :return: random integer of (length)
14 | """
15 | start = int("1" * length)
16 | end = int("9" * length)
17 | return randrange(start, end)
18 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-generate_csrf_token.md:
--------------------------------------------------------------------------------
1 | # generate_csrf_token
2 |
3 | ```python
4 | from flask_imp.auth import generate_csrf_token
5 | ```
6 |
7 | ```python
8 | generate_csrf_token() -> str
9 | ```
10 |
11 | ---
12 |
13 | Generates a SHA1 using the current date and time.
14 |
15 | For use in Cross-Site Request Forgery.
16 |
17 | Also used by the [flask_imp.security / csrf_protect](../Security/flask_imp_security-include_csrf.md) decorator.
18 |
19 | *Example:*
20 |
21 | ```python
22 | generate_csrf_token() # >>> 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0'
23 | ```
24 |
25 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-generate_numeric_validator.md:
--------------------------------------------------------------------------------
1 | # generate_numeric_validator
2 |
3 | ```python
4 | from flask_imp.auth import generate_numeric_validator
5 | ```
6 |
7 | ```python
8 | generate_numeric_validator(length: int) -> int
9 | ```
10 |
11 | ---
12 |
13 |
14 | Generates random choice between 1 * (length) and 9 * (length).
15 |
16 | If the length is 4, it will generate a number between 1111 and 9999.
17 |
18 | For use in MFA email, or unique filename generation.
19 |
20 | *Example:*
21 |
22 | ```python
23 | generate_numeric_validator(4) # >>> 1234
24 | ```
25 |
26 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/templates/www/includes/header.html:
--------------------------------------------------------------------------------
1 |
3 |
Flask-Imp 🧚
4 |
5 |
6 |
This is the header, located here: /Users/david/PycharmProjects/flask-imp/app/blueprints/www/templates/www/includes/header.html
7 |
It's being imported in the /Users/david/PycharmProjects/flask-imp/app/blueprints/www/templates/www/extends/main.html template.
8 |
9 |
--------------------------------------------------------------------------------
/tests/test_app/models/example_user_bind.py:
--------------------------------------------------------------------------------
1 | from ..extensions import db
2 |
3 |
4 | class ExampleUserBind(db.Model):
5 | __bind_key__ = "another"
6 | user_id = db.Column(db.Integer, primary_key=True)
7 | username = db.Column(db.String(256), nullable=False)
8 | password = db.Column(db.String(512), nullable=False)
9 | salt = db.Column(db.String(4), nullable=False)
10 | private_key = db.Column(db.String(256), nullable=False)
11 | disabled = db.Column(db.Boolean)
12 |
13 | @classmethod
14 | def get_by_id(cls, user_id):
15 | return cls.query.filter_by(user_id=user_id).first()
16 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_generate_alphanumeric_validator.py:
--------------------------------------------------------------------------------
1 | from random import choice
2 | from string import ascii_uppercase, digits
3 |
4 |
5 | def generate_alphanumeric_validator(length: int) -> str:
6 | """
7 | Generates (length) of alphanumeric.
8 |
9 | For use in MFA email, or unique filename generation.
10 |
11 | Example return of "F5R6" if length is 4
12 |
13 | :param length: length of alphanumeric to generate
14 | :return: alphanumeric of length (length)
15 | """
16 |
17 | _alpha_numeric = ascii_uppercase + digits
18 | return "".join([choice(_alpha_numeric) for _ in range(length)])
19 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/nested/templates/nested/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'nested/extends/main.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
Blueprint: nested
7 |
Here's your new blueprint.
8 |
Located here: /Users/david/PycharmProjects/flask-imp/app/blueprints/www/nested
9 |
Remember to double-check the config.toml file.
10 |
11 |
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/nested/templates/nested/includes/header.html:
--------------------------------------------------------------------------------
1 |
3 |
Flask-Imp 🧚
4 |
5 |
6 |
This is the header, located here: /Users/david/PycharmProjects/flask-imp/app/blueprints/www/nested/templates/nested/includes/header.html
7 |
It's being imported in the /Users/david/PycharmProjects/flask-imp/app/blueprints/www/nested/templates/nested/extends/main.html template.
8 |
9 |
--------------------------------------------------------------------------------
/docs/Config/flask_imp_config-sqlitedatabaseconfig.md:
--------------------------------------------------------------------------------
1 | # SQLiteDatabaseConfig
2 |
3 | ```python
4 | from flask_imp.config import SQLiteDatabaseConfig
5 | ```
6 |
7 | ```python
8 | SQLiteDatabaseConfig(
9 | database_name: str = "database",
10 | sqlite_db_extension: str = ".sqlite",
11 | location: t.Optional[Path] = None,
12 | bind_key: t.Optional[str] = None,
13 | enabled: bool = True,
14 | )
15 | ```
16 |
17 | ---
18 |
19 | A class that holds a SQLite database configuration.
20 |
21 | This configuration is parsed into a database URI and
22 | used in either the `SQLALCHEMY_DATABASE_URI` or `SQLALCHEMY_BINDS` configuration variables.
23 |
24 |
--------------------------------------------------------------------------------
/example/app/self_reg/templates/self_reg/includes/header.html:
--------------------------------------------------------------------------------
1 |
3 |
Flask-Imp 🧚
4 |
5 |
6 |
This is the header, located here: /Users/david/PycharmProjects/CheeseCake87-Repos/flask-imp/example/app/self_reg/templates/self_reg/includes/header.html
7 |
It's being imported in the /Users/david/PycharmProjects/CheeseCake87-Repos/flask-imp/example/app/self_reg/templates/self_reg/extends/main.html template.
8 |
9 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 |
5 | from test_app import create_app
6 |
7 | instance_folder = Path(__file__).parent / "test_app" / "instance"
8 |
9 | if not instance_folder.exists():
10 | instance_folder.mkdir()
11 |
12 |
13 | @pytest.fixture(scope="session")
14 | def app():
15 | app = create_app()
16 | app.config.update(
17 | {
18 | "TESTING": True,
19 | }
20 | )
21 | yield app
22 |
23 |
24 | @pytest.fixture(scope="session")
25 | def client(app):
26 | return app.test_client()
27 |
28 |
29 | @pytest.fixture()
30 | def runner(app):
31 | return app.test_cli_runner()
32 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/nested_test/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import ImpBlueprint
2 | from flask_imp.config import ImpBlueprintConfig, DatabaseConfig
3 |
4 | bp = ImpBlueprint(
5 | __name__,
6 | ImpBlueprintConfig(
7 | enabled=True,
8 | url_prefix="/nested-test",
9 | template_folder="templates",
10 | database_binds=[
11 | DatabaseConfig(
12 | enabled=False,
13 | bind_key="nested_test_db",
14 | database_name="nested_test_database",
15 | )
16 | ],
17 | ),
18 | )
19 |
20 | bp.import_resources("routes")
21 | bp.import_models("models")
22 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-generate_email_validator.md:
--------------------------------------------------------------------------------
1 | # generate_email_validator
2 |
3 | ```python
4 | from flask_imp.auth import generate_email_validator
5 | ```
6 |
7 | ```python
8 | generate_email_validator() -> str
9 | ```
10 |
11 | ---
12 |
13 | Uses `generate_alphanumeric_validator` with a length of 8 to
14 | generate a random alphanumeric value for the specific use of
15 | validating accounts via email.
16 |
17 | See [flask_imp.auth / generate_alphanumeric_validator](../Auth/flask_imp_auth-generate_alphanumeric_validator.md)
18 | for more information.
19 |
20 | *Example:*
21 |
22 | ```python
23 | generate_email_validator() # >>> 'A1B2C3D4'
24 | ```
25 |
26 |
--------------------------------------------------------------------------------
/tests/test_app/templates/extends/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% block title %} {% endblock %}
8 |
9 |
10 |
11 |
12 |
13 |
14 | {% include "includes/menu.html" %}
15 | {% block global %}
16 |
17 | {% endblock %}
18 | {% include "includes/footer.html" %}
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/docs/Config/flask_imp_config-sqldatabaseconfig.md:
--------------------------------------------------------------------------------
1 | # SQLDatabaseConfig
2 |
3 | ```python
4 | from flask_imp.config import SQLDatabaseConfig
5 | ```
6 |
7 | ```python
8 | SQLDatabaseConfig(
9 | dialect: t.Literal["mysql", "postgresql", "oracle", "mssql"],
10 | database_name: str,
11 | location: str,
12 | port: int,
13 | username: str,
14 | password: str,
15 | bind_key: t.Optional[str] = None,
16 | enabled: bool = True,
17 | )
18 | ```
19 |
20 | ---
21 |
22 | A class that holds a SQL database configuration.
23 |
24 | This configuration is parsed into a database URI and
25 | used in either the `SQLALCHEMY_DATABASE_URI` or `SQLALCHEMY_BINDS` configuration variables.
26 |
27 |
--------------------------------------------------------------------------------
/docs/Utilities/flask_imp_utilities-lazy_url_for.md:
--------------------------------------------------------------------------------
1 | # lazy_url_for
2 |
3 | ```python
4 | from flask_imp.utilities import lazy_url_for
5 | ```
6 |
7 | ```python
8 | lazy_url_for(
9 | endpoint: str,
10 | *,
11 | _anchor: str | None = None,
12 | _method: str | None = None,
13 | _scheme: str | None = None,
14 | _external: bool | None = None,
15 | **values: Any
16 | ) -> partial[str]:
17 | ```
18 |
19 | ---
20 |
21 | Indented for use in checkpoint decorators.
22 |
23 | Takes the same arguments as Flask's url_for function and loads url_for and the
24 | arguments passed to it into a partial to be run later.
25 |
26 | This allows url_for to be set outside of context and later ran inside context.
27 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_generate_private_key.py:
--------------------------------------------------------------------------------
1 | import typing as t
2 | from datetime import datetime
3 | from hashlib import sha256
4 | from random import randrange
5 |
6 |
7 | def generate_private_key(hook: t.Optional[str]) -> str:
8 | """
9 | Generates a sha256 private key from a passed in hook value.
10 |
11 | If no hook is passed in, it will generate a hook using datetime.now() and a
12 | random number between 1 and 1000.
13 |
14 | :param hook: hook value to generate private key from
15 | :return: digested sha256
16 | """
17 |
18 | if hook is None:
19 | _range = randrange(1, 1000)
20 | hook = f"{datetime.now()}-{_range}"
21 |
22 | sha = sha256()
23 | sha.update(hook.encode("utf-8"))
24 | return sha.hexdigest()
25 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/routes/csrf.py:
--------------------------------------------------------------------------------
1 | from flask import render_template, session, request
2 |
3 | from flask_imp.security import include_csrf
4 |
5 |
6 | def include(bp):
7 | @bp.get("/csrf-get-to-post")
8 | @include_csrf()
9 | def csrf_get_to_post():
10 | return render_template(bp.tmpl("get-to-post.html"), crsf=session["csrf"])
11 |
12 | @bp.get("/csrf-session")
13 | @include_csrf()
14 | def csrf_session():
15 | return session["csrf"]
16 |
17 | @bp.post("/csrf-post-pass")
18 | @include_csrf()
19 | def csrf_post_to_me_pass():
20 | return request.form.get("csrf")
21 |
22 | @bp.post("/csrf-post-fail")
23 | @include_csrf()
24 | def csrf_post_to_me_fail():
25 | return request.form.get("csrf")
26 |
--------------------------------------------------------------------------------
/tests/test_app/models/example_user.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.orm import relationship
2 |
3 | from ..extensions import db
4 |
5 |
6 | class ExampleUser(db.Model):
7 | user_id = db.Column(db.Integer, primary_key=True)
8 | username = db.Column(db.String(256), nullable=False)
9 | password = db.Column(db.String(512), nullable=False)
10 | salt = db.Column(db.String(4), nullable=False)
11 | private_key = db.Column(db.String(256), nullable=False)
12 | disabled = db.Column(db.Boolean)
13 |
14 | rel_example_table = relationship(
15 | "ExampleTable",
16 | lazy="joined",
17 | order_by="ExampleTable.thing",
18 | )
19 |
20 | @classmethod
21 | def get_by_id(cls, user_id):
22 | return cls.query.filter_by(user_id=user_id).first()
23 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/filelib/api_blueprint.py:
--------------------------------------------------------------------------------
1 | from ..helpers import strip_leading_slash
2 |
3 |
4 | def api_blueprint_init_py(url_prefix: str, name: str) -> str:
5 | return f"""\
6 | from flask_imp import ImpBlueprint
7 | from flask_imp.config import ImpBlueprintConfig
8 |
9 | bp = ImpBlueprint(__name__, ImpBlueprintConfig(
10 | enabled=True,
11 | url_prefix="/{strip_leading_slash(url_prefix)}",
12 | init_session={{"{name}_session_loaded": True}},
13 | ))
14 |
15 | bp.import_resources()
16 | """
17 |
18 |
19 | def api_blueprint_resources_index_py() -> str:
20 | return """\
21 | from flask_imp import ImpBlueprint
22 |
23 | def include(bp: ImpBlueprint):
24 | @bp.route("/", methods=["GET"])
25 | def index():
26 | return {"message": "Hello, World!"}
27 | """
28 |
--------------------------------------------------------------------------------
/example/app/resources/filters/filters.py:
--------------------------------------------------------------------------------
1 | from flask import current_app as app
2 |
3 |
4 | @app.template_filter("example__num_to_month")
5 | def example__num_to_month(num: str) -> str:
6 | """
7 | Usage:
8 | {{ 1 | example__num_to_month }} -> January
9 | """
10 | if isinstance(num, int):
11 | num = str(num)
12 |
13 | months = {
14 | "1": "January",
15 | "2": "February",
16 | "3": "March",
17 | "4": "April",
18 | "5": "May",
19 | "6": "June",
20 | "7": "July",
21 | "8": "August",
22 | "9": "September",
23 | "10": "October",
24 | "11": "November",
25 | "12": "December",
26 | }
27 |
28 | if num in months:
29 | return months[num]
30 | return "Month not found"
31 |
--------------------------------------------------------------------------------
/docs/Config/flask_imp_config-databaseconfig.md:
--------------------------------------------------------------------------------
1 | # DatabaseConfig
2 |
3 | ```python
4 | from flask_imp.config import DatabaseConfig
5 | ```
6 |
7 | ```python
8 | DatabaseConfig(
9 | dialect: t.Literal[
10 | "mysql", "postgresql", "sqlite", "oracle", "mssql"
11 | ] = "sqlite",
12 | database_name: str = "database",
13 | location: str = "",
14 | port: int = 0,
15 | username: str = "",
16 | password: str = "",
17 | sqlite_db_extension: str = ".sqlite",
18 | bind_key: t.Optional[str] = None,
19 | enabled: bool = True,
20 | )
21 | ```
22 |
23 | ---
24 |
25 | A class that holds a database configuration.
26 |
27 | This configuration is parsed into a database URI and
28 | used in either the `SQLALCHEMY_DATABASE_URI` or `SQLALCHEMY_BINDS` configuration variables.
29 |
30 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | permissions:
8 | contents: read
9 |
10 | jobs:
11 | deploy:
12 | runs-on: ubuntu-latest
13 | environment:
14 | name: pypi
15 | url: https://pypi.org/p/flask-imp
16 | permissions:
17 | id-token: write
18 |
19 | steps:
20 | - uses: actions/checkout@v4
21 |
22 | - name: Set up Python
23 | uses: actions/setup-python@v5
24 | with:
25 | python-version: '3.x'
26 |
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip
30 | pip install flit
31 | - name: Build package
32 | run: flit build
33 |
34 | - name: Publish package
35 | uses: pypa/gh-action-pypi-publish@release/v1
36 |
--------------------------------------------------------------------------------
/docs/Imp/Imp-register_imp_blueprint.md:
--------------------------------------------------------------------------------
1 | # Imp.register_imp_blueprint
2 |
3 | ```python
4 | register_imp_blueprint(self, imp_blueprint: ImpBlueprint) -> None
5 | ```
6 |
7 | ---
8 |
9 | Manually register a ImpBlueprint.
10 |
11 | ```text
12 | app
13 | ├── my_blueprint
14 | │ ├── ...
15 | │ └── __init__.py
16 | ├── ...
17 | └── __init__.py
18 | ```
19 |
20 | File: `app/__init__.py`
21 |
22 | ```python
23 | from flask import Flask
24 |
25 | from flask_imp import Imp
26 |
27 | imp = Imp()
28 |
29 | DO_IMPORT = True
30 |
31 |
32 | def create_app():
33 | app = Flask(
34 | __name__,
35 | static_folder="static",
36 | template_folder="templates"
37 | )
38 | imp.init_app(app)
39 |
40 | if DO_IMPORT:
41 | from app.my_blueprint import bp
42 |
43 | imp.register_imp_blueprint(bp)
44 |
45 | return app
46 | ```
47 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/templates/www/extends/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Flask-Imp
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 | {% include 'www/includes/header.html' %}
20 | {% block content %}{% endblock %}
21 | {% include 'www/includes/footer.html' %}
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/nested/templates/nested/extends/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Flask-Imp
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 | {% include 'nested/includes/header.html' %}
20 | {% block content %}{% endblock %}
21 | {% include 'nested/includes/footer.html' %}
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/example/app/self_reg/templates/self_reg/extends/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Flask-Imp
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 | {% include 'self_reg/includes/header.html' %}
20 | {% block content %}{% endblock %}
21 | {% include 'self_reg/includes/footer.html' %}
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flask-Imp 🧚
2 |
3 | 
4 | [](https://pypi.org/project/flask-imp/)
5 | [](https://raw.githubusercontent.com/CheeseCake87/flask-imp/master/LICENSE)
6 |
7 | ## What is Flask-Imp?
8 |
9 | Flask-Imp's main purpose is to help simplify the importing of blueprints, resources, and models.
10 | It has a few extra features built in to help with securing pages and password authentication.
11 |
12 | ## Documentation
13 |
14 | [https://flask-imp.readthedocs.io/en/latest/](https://flask-imp.readthedocs.io/en/latest/)
15 |
16 | ### Install Flask-Imp
17 |
18 | ```bash
19 | pip install flask-imp
20 | ```
21 |
22 | ### Generate a Flask app
23 |
24 | ```bash
25 | flask-imp init
26 | ```
27 |
--------------------------------------------------------------------------------
/tests/test_app/resources/filters/filters.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 |
3 |
4 | def collection(app: Flask):
5 | @app.template_filter("example__num_to_month")
6 | def example__num_to_month(num: str) -> str:
7 | """
8 | Usage:
9 | {{ 1 | example__num_to_month }} -> January
10 | """
11 | if isinstance(num, int):
12 | num = str(num)
13 |
14 | months = {
15 | "1": "January",
16 | "2": "February",
17 | "3": "March",
18 | "4": "April",
19 | "5": "May",
20 | "6": "June",
21 | "7": "July",
22 | "8": "August",
23 | "9": "September",
24 | "10": "October",
25 | "11": "November",
26 | "12": "December",
27 | }
28 |
29 | if num in months:
30 | return months[num]
31 | return "Month not found"
32 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_imp import ImpBlueprint
2 | from flask_imp.config import ImpBlueprintConfig, DatabaseConfig
3 |
4 | bp = ImpBlueprint(
5 | __name__,
6 | ImpBlueprintConfig(
7 | enabled=True,
8 | url_prefix="/tests",
9 | template_folder="templates",
10 | static_folder="static",
11 | static_url_path="/tests/static",
12 | init_session={"tests_session": True},
13 | database_binds=[
14 | DatabaseConfig(
15 | enabled=False,
16 | bind_key="tests_db",
17 | database_name="tests_blueprint",
18 | )
19 | ],
20 | ),
21 | )
22 |
23 | bp.import_resources("routes")
24 | bp.import_nested_blueprint("nested_test")
25 | bp.import_nested_blueprints("group_of_nested")
26 | bp.import_models("models")
27 |
28 | print(":::-- tests nested bps", bp.nested_blueprints)
29 | print(":::-- tests config id", id(bp.config))
30 |
--------------------------------------------------------------------------------
/docs/SecurityCheckpoints/flask_imp_security-createacheckpoint.md:
--------------------------------------------------------------------------------
1 | # Create a Checkpoint
2 |
3 | You can create your own checkpoint by inheriting from the `BaseCheckpoint` class:
4 |
5 | ```python
6 | from flask_imp.security import BaseCheckpoint
7 | ```
8 |
9 | ```python
10 | class MyCheckpoint(BaseCheckpoint):
11 | my_attrs_here: str
12 |
13 | def __init__(self, passed_in_arg: str):
14 | self.my_attrs_here = passed_in_arg
15 |
16 | def pass_(self) -> :
17 | # conditional check here, must return truly.
18 | ...
19 | ```
20 |
21 | ```python
22 | MyCheckpoint(
23 | passed_in_arg: str,
24 | ).action(
25 | fail_url: t.Optional[t.Union[str, t.Callable[[], t.Any]]] = None,
26 | fail_json: t.Optional[t.Dict[str, t.Any]] = None,
27 | fail_status: int = 403,
28 | pass_url: t.Optional[t.Union[str, t.Callable[[], t.Any]]] = None,
29 | message: t.Optional[str] = None,
30 | message_category: str = "message",
31 | )
32 | ```
33 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-is_email_address_valid.md:
--------------------------------------------------------------------------------
1 | # is_email_address_valid
2 |
3 | ```python
4 | from flask_imp.auth import is_email_address_valid
5 | ```
6 |
7 | ```python
8 | is_email_address_valid(
9 | email_address: str
10 | ) -> bool
11 | ```
12 |
13 | ---
14 |
15 | Checks if an email address is valid.
16 |
17 | Is not completely RFC 5322 compliant, but it is good enough for most use cases.
18 |
19 | Here are examples of mistakes that it will not catch:
20 |
21 | **Valid but fails:**
22 |
23 | ```text
24 | email@[123.123.123.123]
25 | “email”@example.com
26 | very.unusual.“@”.unusual.com@example.com
27 | very.“(),:;<>[]”.VERY.“very@\\ "very”.unusual@strange.example.com
28 | ```
29 |
30 | **Invalid but passes:**
31 |
32 | ```text
33 | email@example.com (Joe Smith)
34 | email@111.222.333.44444
35 | ```
36 |
37 | *Example:*
38 |
39 | ```python
40 | is_email_address_valid('hello@example.com') # >>> True
41 |
42 | is_email_address_valid('hello@hello@example.com') # >>> False
43 | ```
44 |
45 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-generate_salt.md:
--------------------------------------------------------------------------------
1 | # generate_salt
2 |
3 | ```python
4 | from flask_imp.auth import generate_salt
5 | ```
6 |
7 | ```python
8 | generate_salt(length: int = 4) -> str
9 | ```
10 |
11 | ---
12 |
13 | Generates a string of (length) characters of punctuation.
14 |
15 | The Default length is 4.
16 |
17 | For use in password hashing and storage of passwords in the database.
18 |
19 | *Example:*
20 |
21 | ```python
22 | generate_salt() # >>> '*!$%'
23 | ```
24 |
25 | ```python
26 | @app.route('/register', methods=['GET', 'POST'])
27 | def register():
28 | if request.method == "POST":
29 | ...
30 | salt = generate_salt()
31 | password = request.form.get('password')
32 | encrypted_password = encrypt_password(password, salt)
33 | ...
34 |
35 | user = User(
36 | username=username,
37 | email=email,
38 | password=encrypted_password,
39 | salt=salt
40 | )
41 | ...
42 | ```
43 |
44 |
45 |
--------------------------------------------------------------------------------
/docs/ImpBlueprint/ImpBlueprint-tmpl.md:
--------------------------------------------------------------------------------
1 | # ImpBlueprint.tmpl
2 |
3 | ```python
4 | tmpl(template: str) -> str
5 | ```
6 |
7 | ---
8 |
9 | Scopes the template lookup to the name of the blueprint (this takes from the `__name__` attribute of the Blueprint).
10 |
11 | Due to the way Flask templating works, and to avoid template name collisions.
12 | It is standard practice to place the name of the Blueprint in the template path,
13 | then to place any templates under that folder.
14 |
15 | ```text
16 | my_blueprint/
17 | ├── routes/
18 | │ └── index.py
19 | ├── static/...
20 | │
21 | ├── templates/
22 | │ └── my_blueprint/
23 | │ └── index.html
24 | │
25 | ├── __init__.py
26 | ```
27 |
28 | File: `my_blueprint/routes/index.py`
29 |
30 | ```python
31 | from flask import render_template
32 |
33 | from .. import bp
34 |
35 |
36 | @bp.route("/")
37 | def index():
38 | return render_template(bp.tmpl("index.html"))
39 | ```
40 |
41 | `bp.tmpl("index.html")` will output `"my_blueprint/index.html"`.
42 |
43 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-generate_private_key.md:
--------------------------------------------------------------------------------
1 | # generate_private_key
2 |
3 | ```python
4 | from flask_imp.auth import generate_private_key
5 | ```
6 |
7 | ```python
8 | generate_private_key(hook: t.Optional[str]) -> str
9 | ```
10 |
11 | ---
12 |
13 | Generates a sha256 private key from a passed in hook value.
14 |
15 | If no hook is passed in, it will generate a hook using datetime.now() and a
16 | random number between 1 and 1000.
17 |
18 | ```python
19 | @app.route('/register', methods=['GET', 'POST'])
20 | def register():
21 | if request.method == "POST":
22 | ...
23 | salt = generate_salt()
24 | password = request.form.get('password')
25 | encrypted_password = encrypt_password(password, salt)
26 | ...
27 | user = User(
28 | username=username,
29 | email=email,
30 | password=encrypted_password,
31 | salt=salt,
32 | private_key=generate_private_key(hook=username)
33 | )
34 | ...
35 | ```
36 |
37 |
--------------------------------------------------------------------------------
/tests/test_app/models/example_table.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import ForeignKey
2 |
3 | from ..extensions import db
4 |
5 |
6 | class ExampleTable(db.Model):
7 | example_id = db.Column(db.Integer, primary_key=True)
8 | user_id = db.Column(db.Integer, ForeignKey("example_user.user_id"))
9 | thing = db.Column(db.String(256), nullable=False)
10 |
11 | @classmethod
12 | def get_first(cls):
13 | return cls.query.first()
14 |
15 | @classmethod
16 | def get_by_user_id(cls, user_id):
17 | return cls.query.filter_by(user_id=user_id).first()
18 |
19 | @classmethod
20 | def delete(cls, user_id):
21 | pass
22 |
23 | @classmethod
24 | def update(cls, user_id, **kwargs):
25 | pass
26 |
27 | @classmethod
28 | def add(cls, **kwargs):
29 | pass
30 |
31 |
32 | class ExampleTableOne(db.Model):
33 | example_id = db.Column(db.Integer, primary_key=True)
34 | user_id = db.Column(db.Integer, ForeignKey("example_user.user_id"))
35 | thing = db.Column(db.String(256), nullable=False)
36 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/filelib/head_tag_generator.py:
--------------------------------------------------------------------------------
1 | def head_tag_generator(static_url_endpoint: str = "static", no_js: bool = False) -> str:
2 | """Generate the head tag for the HTML template files."""
3 |
4 | js = (
5 | (
6 | f""
8 | )
9 | if not no_js
10 | else ""
11 | )
12 |
13 | favicon = (
14 | '🧚 ">'
17 | )
18 |
19 | return f"""\
20 |
21 |
22 | Flask-Imp
23 | {favicon}
24 |
25 | {js}
26 |
27 |
30 | """
31 |
--------------------------------------------------------------------------------
/example/app/blueprints/www/templates/www/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'www/extends/main.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
Blueprint: www
7 |
This is the index route of the included example blueprint.
8 |
9 | This template page is located in /Users/david/PycharmProjects/flask-imp/app/blueprints/www/templates/www/index.html
10 | it extends from /Users/david/PycharmProjects/flask-imp/app/blueprints/www/templates/www/extends/main.html
11 | with its route defined in /Users/david/PycharmProjects/flask-imp/app/blueprints/www/routes/index.py
12 | It's being imported by bp.import_resources("routes")
13 | in the /Users/david/PycharmProjects/flask-imp/app/blueprints/www/__init__.py file.
14 |
15 |
16 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/src/flask_imp/config/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains the configuration classes for the Flask Imp:
3 |
4 | Classes:
5 |
6 | - FlaskConfig: The configuration class for the Flask application.
7 | - ImpConfig: The configuration class for the Flask Imp.
8 | - ImpBlueprintConfig: The configuration class for the Flask Blueprint.
9 | - DatabaseConfig: The base class for database configurations.
10 | - SQLDatabaseConfig: The base class for SQL database configurations.
11 | - SQLiteDatabaseConfig: The base class for SQLite database configurations.
12 |
13 | """
14 |
15 | from ._database_config import DatabaseConfig
16 | from ._flask_config import FlaskConfig
17 | from ._imp_blueprint_config import ImpBlueprintConfig
18 | from ._imp_config import ImpConfig
19 | from ._sql_database_config import SQLDatabaseConfig
20 | from ._sqlite_database_config import SQLiteDatabaseConfig
21 |
22 | __all__ = [
23 | "FlaskConfig",
24 | "DatabaseConfig",
25 | "SQLDatabaseConfig",
26 | "SQLiteDatabaseConfig",
27 | "ImpConfig",
28 | "ImpBlueprintConfig",
29 | ]
30 |
--------------------------------------------------------------------------------
/tests/test_app/resources/error_handlers/error_handlers.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 |
3 |
4 | def collection(app: Flask):
5 | from flask import render_template
6 |
7 | @app.errorhandler(400)
8 | def error_400(error):
9 | return render_template(
10 | "errors/400.html",
11 | ), 400
12 |
13 | @app.errorhandler(401)
14 | def error_401(error):
15 | return render_template(
16 | "errors/401.html",
17 | ), 401
18 |
19 | @app.errorhandler(403)
20 | def error_403(error):
21 | return render_template(
22 | "errors/403.html",
23 | ), 403
24 |
25 | @app.errorhandler(404)
26 | def error_404(error):
27 | return render_template(
28 | "errors/404.html",
29 | ), 404
30 |
31 | @app.errorhandler(405)
32 | def error_405(error):
33 | return render_template(
34 | "errors/405.html",
35 | ), 405
36 |
37 | @app.errorhandler(500)
38 | def error_500(error):
39 | return render_template(
40 | "errors/500.html",
41 | ), 500
42 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2024 David Carmichael
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the “Software”), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19 | IN THE SOFTWARE.
--------------------------------------------------------------------------------
/src/flask_imp/auth/_is_email_address_valid.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | def is_email_address_valid(email_address: str) -> bool:
5 | """
6 | Checks if email_address is a valid email address.
7 |
8 | Is not completely RFC 5322 compliant, but it is good enough for most use cases.
9 |
10 | Here are examples of mistakes that it will not catch::
11 |
12 | VALID but fails:
13 | - email@[123.123.123.123]
14 | - “email”@example.com
15 | - very.unusual.“@”.unusual.com@example.com
16 | - very.“(),:;<>[]”.VERY.“very@\\ "very”.unusual@strange.example.com
17 |
18 | INVALID but passes:
19 | - email@example.com (Joe Smith)
20 | - email@111.222.333.44444
21 |
22 | -----
23 |
24 | :param email_address: email address to validate
25 | :return: True if email_address is valid, False otherwise
26 | """
27 | pattern = re.compile(
28 | r"[a-z\d!#$%&'*+?^_`{|}~-]+(?:\.[a-z\d!#$%&'*+?^_`"
29 | r"{|}~-]+)*@(?:[a-z\d](?:[a-z\d-]*[a-z\d])?\.)+[a-z\d](?:[a-z\d-]*[a-z\d])?",
30 | re.IGNORECASE,
31 | )
32 | return bool(pattern.match(email_address))
33 |
--------------------------------------------------------------------------------
/docs/Imp/Imp-import_blueprints.md:
--------------------------------------------------------------------------------
1 | # Imp.import_blueprints
2 |
3 | ```python
4 | import_blueprints(self, folder: str) -> None
5 | ```
6 |
7 | ---
8 |
9 | Import all Flask-Imp or standard Flask Blueprints from a specified folder relative to the Flask app root.
10 |
11 | ```text
12 | app/
13 | ├── blueprints/
14 | │ ├── admin/
15 | │ │ ├── ...
16 | │ │ └── __init__.py
17 | │ ├── www/
18 | │ │ ├── ...
19 | │ │ └── __init__.py
20 | │ └── api/
21 | │ ├── ...
22 | │ └── __init__.py
23 | ├── ...
24 | └── __init__.py
25 | ```
26 |
27 | File: `app/__init__.py`
28 |
29 | ```python
30 | from flask import Flask
31 |
32 | from flask_imp import Imp
33 |
34 | imp = Imp()
35 |
36 |
37 | def create_app():
38 | app = Flask(
39 | __name__,
40 | static_folder="static",
41 | template_folder="templates"
42 | )
43 | imp.init_app(app)
44 |
45 | imp.import_blueprints("blueprints")
46 |
47 | return app
48 | ```
49 |
50 | This will import all Blueprints from the `blueprints` folder using the `Imp.import_blueprint` method.
51 | See [Imp / import_blueprint](../Imp/Imp-import_blueprint.md) for more information.
52 |
53 |
--------------------------------------------------------------------------------
/docs/Imp/Imp-model.md:
--------------------------------------------------------------------------------
1 | # Imp.model
2 |
3 | ```python
4 | model(class_: str) -> DefaultMeta
5 | ```
6 |
7 | ---
8 |
9 | Returns the SQLAlchemy model class for the given class name that was imported using `Imp.import_models` or
10 | `Blueprint.import_models`.
11 |
12 | This method has convenience for being able to omit the need to import the model class from the file it was defined in.
13 | However, it is not compatible with IDE type hinting.
14 |
15 | For example:
16 |
17 | ```python
18 | from app.models.boats import Boats
19 | from app.models.cars import Cars
20 | ```
21 |
22 | Can be replaced with:
23 |
24 | ```python
25 | from app import imp
26 |
27 | Boats = imp.model("Boats")
28 | Cars = imp.model("Cars")
29 | ```
30 |
31 | Or used directly:
32 |
33 | ```python
34 | from app import imp
35 |
36 | all_boats = imp.model("Boats").select_all()
37 | ```
38 |
39 |
40 | file: `models/boats.py`
41 |
42 | ```python
43 | from app import db
44 |
45 |
46 | class Boats(db.Model):
47 | name = db.Column(db.String())
48 |
49 | @classmethod
50 | def select_all(cls):
51 | return db.session.execute(
52 | db.select(cls)
53 | ).scalars().all()
54 | ```
55 |
56 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist =
3 | py3{13,12,11,10}
4 | style
5 | typing
6 | docs
7 | skip_missing_interpreters = true
8 |
9 | [testenv]
10 | package = wheel
11 | wheel_build_env = .pkg
12 | constrain_package_deps = true
13 | use_frozen_constraints = true
14 | runner = uv-venv-lock-runner
15 | commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}
16 |
17 | [testenv:style]
18 | description = run code formatter and linter (auto-fix)
19 | skip_install = true
20 | deps =
21 | pre-commit-uv>=4.1.1
22 | commands =
23 | pre-commit run --all-files --show-diff-on-failure
24 |
25 | [testenv:typing]
26 | description = run type checkers
27 | runner = uv-venv-lock-runner
28 | commands =
29 | mypy
30 | pyright
31 | pyright --verifytypes flask_imp --ignoreexternal
32 |
33 | [testenv:docs]
34 | description = build the docs
35 | runner = uv-venv-lock-runner
36 | commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml
37 |
38 | [testenv:update-actions]
39 | labels = update
40 | deps = gha-update
41 | skip_install = true
42 | commands = gha-update
43 |
44 | [testenv:update-pre_commit]
45 | labels = update
46 | deps = pre-commit
47 | skip_install = true
48 | commands = pre-commit autoupdate -j4
49 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-is_username_valid.md:
--------------------------------------------------------------------------------
1 | # is_username_valid
2 |
3 | ```python
4 | from flask_imp.auth import is_username_valid
5 | ```
6 |
7 | ```python
8 | is_username_valid(
9 | username: str,
10 | allowed: t.Optional[t.List[t.Literal["all", "dot", "dash", "under"]]] = None
11 | ) -> bool
12 | ```
13 |
14 | ---
15 |
16 | Checks if a username is valid.
17 |
18 | Valid usernames can only include letters,
19 | numbers, ., -, and _ but cannot begin or end with
20 | the last three mentioned.
21 |
22 | **Example "all":**
23 |
24 | ```python
25 | is_username_valid("username", allowed=["all"])
26 | ```
27 |
28 | Output:
29 |
30 | ```text
31 | username : WILL PASS : True
32 | user.name : WILL PASS : True
33 | user-name : WILL PASS : True
34 | user_name : WILL PASS : True
35 | _user_name : WILL PASS : False
36 | ```
37 |
38 | **Example "dot", "dash":**
39 |
40 | ```python
41 |
42 | is_username_valid("username", allowed=["dot", "dash"])
43 | ```
44 |
45 | Output:
46 |
47 | ```text
48 | username : WILL PASS : True
49 | user.name : WILL PASS : True
50 | user-name : WILL PASS : True
51 | user-name.name : WILL PASS : True
52 | user_name : WILL PASS : False
53 | _user_name : WILL PASS : False
54 | .user.name : WILL PASS : False
55 | ```
56 |
57 |
--------------------------------------------------------------------------------
/docs/Config/flask_imp_config-impblueprintconfig.md:
--------------------------------------------------------------------------------
1 | # ImpBlueprintConfig
2 |
3 | ```python
4 | from flask_imp.config import ImpBlueprintConfig
5 | ```
6 |
7 | ```python
8 | ImpBlueprintConfig(
9 | enabled: bool = False,
10 | url_prefix: str = None,
11 | subdomain: str = None,
12 | url_defaults: dict = None,
13 | static_folder: t.Optional[str] = None,
14 | template_folder: t.Optional[str] = None,
15 | static_url_path: t.Optional[str] = None,
16 | root_path: str = None,
17 | cli_group: str = None,
18 | init_session: dict = None,
19 | database_binds: t.Iterable[DatabaseConfig] = None
20 | )
21 | ```
22 |
23 | ---
24 |
25 | A class that holds a Flask-Imp blueprint configuration.
26 |
27 | Most of these values are passed to the `Blueprint` class when the blueprint is registered.
28 |
29 | The `enabled` argument is used to enable or disable the blueprint. This is useful for feature flags.
30 |
31 | `init_session` is used to set the session values in the main `before_request` function.
32 |
33 | `database_binds` is a list of `DatabaseConfig` instances that are used to create `SQLALCHEMY_BINDS` configuration
34 | variables. Again this is useful for feature flags, or for creating multiple databases per blueprint.
35 |
36 |
--------------------------------------------------------------------------------
/docs/Security/flask_imp_security-include_csrf.md:
--------------------------------------------------------------------------------
1 | # include_csrf
2 |
3 | ```python
4 | from flask_imp.security import include_csrf
5 | ```
6 |
7 | ```python
8 | include_csrf(
9 | session_key: str = "csrf",
10 | form_key: str = "csrf",
11 | abort_code: int = 401
12 | )
13 | ```
14 |
15 | `@include_csrf(...)`
16 |
17 | ---
18 |
19 |
20 | A decorator that handles CSRF protection.
21 |
22 | On a **GET** request, a CSRF token is generated and stored in the session key
23 | specified by the session_key parameter.
24 |
25 | On a **POST** request, the form_key specified is checked against the session_key
26 | specified.
27 |
28 | - If they match, the request is allowed to continue.
29 | - If no match, the response will be abort(abort_code), default 401.
30 |
31 | ```python
32 | @bp.route("/admin", methods=["GET", "POST"])
33 | @include_csrf(session_key="csrf", form_key="csrf")
34 | def admin_page():
35 | ...
36 | # You must pass in the CSRF token from the session into the template.
37 | # Then add to the form.
38 | return render_template("admin.html", csrf=session.get("csrf"))
39 | ```
40 |
41 | Form key:
42 |
43 | ```html
44 |
45 | ```
46 |
47 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/filelib/templates.py:
--------------------------------------------------------------------------------
1 | def templates_minimal_index_html(
2 | head_tag: str, index_py: str, index_html: str, init_py: str
3 | ) -> str:
4 | return f"""\
5 |
6 |
7 |
8 |
9 | {head_tag}
10 |
11 |
12 |
13 |
15 |
Flask-Imp 🧚
16 |
17 |
18 |
19 |
20 | This template page is located in {index_html}
21 | with its route defined in {index_py}
22 | It's being imported by app.import_resources()
23 | in the {init_py} file.
24 |
25 |
26 |
27 |
28 |
29 |
30 | """
31 |
32 |
33 | def templates_error_html() -> str:
34 | return """\
35 |
36 |
37 |
38 |
39 | {{ error_code }}
40 |
41 |
42 |
43 | {{ error_code }}
44 | {{ error_message }}
45 |
46 |
47 | """
48 |
--------------------------------------------------------------------------------
/example/app/__init__.py:
--------------------------------------------------------------------------------
1 | from app.extensions import imp, db
2 | from flask import Flask
3 | from flask_imp.config import ImpConfig, FlaskConfig, DatabaseConfig
4 |
5 | flask_config = FlaskConfig(
6 | secret_key="30c52be45906c36d57f73081f4996a0c0dec32115510aaab",
7 | additional={
8 | "test2": "Hello, World!",
9 | },
10 | )
11 | flask_config.set_additional(
12 | test="Hello, World!",
13 | )
14 |
15 |
16 | def create_app():
17 | app = Flask(__name__, static_url_path="/")
18 | flask_config.init_app(app)
19 |
20 | imp.init_app(
21 | app,
22 | ImpConfig(
23 | init_session={"logged_in": False},
24 | database_main=DatabaseConfig(enabled=True, dialect="sqlite"),
25 | ),
26 | )
27 |
28 | imp.import_app_resources()
29 | imp.import_blueprints("blueprints")
30 | imp.import_models("models")
31 |
32 | # example of imp.register_imp_blueprint
33 | def self_register_blueprint():
34 | from app.self_reg import bp as self_reg_bp
35 |
36 | imp.register_imp_blueprint(self_reg_bp)
37 |
38 | self_register_blueprint()
39 |
40 | db.init_app(app)
41 |
42 | print(app.config["TEST"])
43 | print(app.config["TEST2"])
44 |
45 | with app.app_context():
46 | db.create_all()
47 |
48 | return app
49 |
--------------------------------------------------------------------------------
/docs/Config/flask_imp_config-impconfig.md:
--------------------------------------------------------------------------------
1 | # ImpConfig
2 |
3 | ```python
4 | from flask_imp.config import ImpConfig
5 | ```
6 |
7 | ```python
8 | ImpConfig(
9 | init_session: t.Optional[t.Dict[str, t.Any]] = None,
10 | database_main: t.Optional[
11 | t.Union[DatabaseConfig, SQLiteDatabaseConfig, SQLDatabaseConfig]
12 | ] = None,
13 | database_binds: t.Optional[
14 | t.List[t.Union[DatabaseConfig, SQLiteDatabaseConfig, SQLDatabaseConfig]]
15 | ] = None,
16 | )
17 | ```
18 |
19 | ---
20 |
21 | The `ImpConfig` class is used to set the initial session, the main database, and any additional databases
22 | that the application will use.
23 |
24 | ```python
25 | imp_config = ImpConfig(
26 | init_session={"key": "value"},
27 | database_main=SQLiteDatabaseConfig(
28 | name="test1",
29 | ),
30 | database_binds=[
31 | DatabaseConfig(
32 | enabled=True,
33 | dialect="sqlite",
34 | name="test2",
35 | bind_key="test2"
36 | )
37 | ]
38 | )
39 |
40 |
41 | def create_app():
42 | app = Flask(
43 | __name__,
44 | static_folder="static",
45 | template_folder="templates"
46 | )
47 | FlaskConfig(debug=True, app_instance=app)
48 | imp.init_app(app, imp_config)
49 | ...
50 | ```
51 |
52 |
--------------------------------------------------------------------------------
/src/flask_imp/security/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains the security utilities for a Flask application.
3 |
4 | Functions:
5 |
6 | - include_csrf: Includes a CSRF token in a GET response, and checks it during a
7 | POST request.
8 | - checkpoint: Checks if the passed in Checkpoint passes or fails.
9 | - checkpoint_callable: Checks if a function you give passes.
10 |
11 | Classes:
12 |
13 | - APIKeyCheckpoint: A checkpoint focused around checking the header or query param
14 | for a valid token.
15 | - BearerCheckpoint: A checkpoint focused around working with the Bearer tokens.
16 | - SessionCheckpoint: A checkpoint focused around working with Flask's session.
17 |
18 | """
19 |
20 | from ._include_csrf import include_csrf
21 | from ._checkpoint_callable import checkpoint_callable
22 | from ._checkpoint import checkpoint
23 |
24 | from ._checkpoints import BaseCheckpoint
25 | from ._checkpoints import APIKeyCheckpoint
26 | from ._checkpoints import BearerCheckpoint
27 | from ._checkpoints import SessionCheckpoint
28 |
29 | __all__ = [
30 | "include_csrf",
31 | "checkpoint_callable",
32 | "checkpoint",
33 | # Checkpoints
34 | "BaseCheckpoint",
35 | "APIKeyCheckpoint",
36 | "BearerCheckpoint",
37 | "SessionCheckpoint",
38 | ]
39 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/routes/auth.py:
--------------------------------------------------------------------------------
1 | from flask_imp.auth import encrypt_password, authenticate_password
2 |
3 |
4 | def include(bp):
5 | @bp.route("/auth/password/correct", methods=["GET"])
6 | def auth_test_password_correct():
7 | password = "password"
8 |
9 | encrypted_password = encrypt_password(password, "salt", 512, 1, "start")
10 |
11 | result = authenticate_password(
12 | password, encrypted_password, "salt", 512, 1, "start"
13 | )
14 |
15 | return f"{result}"
16 |
17 | @bp.route("/auth/password/incorrect", methods=["GET"])
18 | def auth_test_password_incorrect():
19 | password = "password"
20 |
21 | encrypted_password = encrypt_password(password, "salt", 512, 1, "start")
22 |
23 | result = authenticate_password("wrong", encrypted_password, "salt", 512, 1,
24 | "start")
25 |
26 | return f"{result}"
27 |
28 | @bp.route("/auth/password/correct/long", methods=["GET"])
29 | def auth_test_password_correct_long():
30 | password = "password"
31 |
32 | encrypted_password = encrypt_password(password, "salt", 512, 3, "start")
33 |
34 | result = authenticate_password(
35 | password, encrypted_password, "salt", 512, 3, "start"
36 | )
37 |
38 | return f"{result}"
39 |
--------------------------------------------------------------------------------
/tests/test_app/resources/cli/cli.py:
--------------------------------------------------------------------------------
1 | from importlib import import_module
2 |
3 | from flask import Flask
4 |
5 |
6 | def collection(app: Flask):
7 | @app.cli.command("add-example-user")
8 | def add_example_user():
9 | from ...models.example_table import ExampleTable
10 |
11 | ExampleTable.add(
12 | username="admin",
13 | password="password",
14 | disabled=False,
15 | )
16 |
17 | @app.cli.command("update-example-user")
18 | def update_example_user():
19 | from ...models.example_table import ExampleTable
20 |
21 | ExampleTable.update(
22 | user_id=1,
23 | username="admin-updated",
24 | private_key="private_key",
25 | disabled=False,
26 | )
27 |
28 | @app.cli.command("delete-example-user")
29 | def delete_example_user():
30 | from ...models.example_table import ExampleTable
31 |
32 | ExampleTable.delete(
33 | user_id=1,
34 | )
35 |
36 | @app.cli.command("example-model-function")
37 | def example_model_function():
38 | from ...extensions import imp
39 |
40 | imp.import_models("models")
41 |
42 | example_table_meta = imp.model_meta("ExampleTable")
43 | users_module = import_module(example_table_meta["location"])
44 | users_module.example_function()
45 |
--------------------------------------------------------------------------------
/tests/test_app/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 |
3 | from flask_imp.config import (
4 | FlaskConfig,
5 | ImpConfig,
6 | DatabaseConfig,
7 | SQLiteDatabaseConfig,
8 | )
9 | from .extensions import db
10 | from .extensions import imp
11 |
12 |
13 | def create_app():
14 | app = Flask(
15 | __name__,
16 | static_url_path="/static",
17 | static_folder="static",
18 | template_folder="templates",
19 | )
20 | FlaskConfig(
21 | secret_key="0000",
22 | ).apply_config(app)
23 |
24 | app.config["TEST"] = "Hello, World!"
25 |
26 | imp.init_app(
27 | app,
28 | ImpConfig(
29 | init_session={"logged_in": False},
30 | database_main=SQLiteDatabaseConfig(
31 | database_name="my_database",
32 | ),
33 | database_binds=[
34 | DatabaseConfig(
35 | dialect="sqlite",
36 | database_name="database_another",
37 | bind_key="another",
38 | )
39 | ],
40 | ),
41 | )
42 |
43 | imp.import_resources(factories=["collection"])
44 | imp.import_blueprint("root_blueprint")
45 | imp.import_blueprints("blueprints")
46 | imp.import_models("models")
47 |
48 | db.init_app(app)
49 |
50 | with app.app_context():
51 | db.create_all()
52 |
53 | return app
54 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - '../../archive_docs/**'
9 | - '*.md'
10 | pull_request:
11 | branches:
12 | - main
13 | - '*.x'
14 | paths-ignore:
15 | - '../../archive_docs/**'
16 | - '*.md'
17 |
18 | jobs:
19 | tests:
20 | name: ${{ matrix.name }}
21 | runs-on: ${{ matrix.os }}
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | include:
26 | - { name: Linux, python: '3.11', os: ubuntu-latest }
27 | - { name: Windows, python: '3.11', os: windows-latest }
28 | - { name: Mac, python: '3.11', os: macos-latest }
29 | - { name: '3.12', python: '3.12', os: ubuntu-latest }
30 | - { name: '3.11', python: '3.11', os: ubuntu-latest }
31 | - { name: '3.10', python: '3.10', os: ubuntu-latest }
32 | - { name: '3.9', python: '3.9', os: ubuntu-latest }
33 | - { name: 'PyPy', python: 'pypy-3.10', os: ubuntu-latest }
34 | steps:
35 | - uses: actions/checkout@v5
36 |
37 | - name: Install uv and set the Python version
38 | uses: astral-sh/setup-uv@v6
39 | with:
40 | python-version: ${{ matrix.python }}
41 |
42 | - name: Install package
43 | run: |
44 | uv sync --all-extras --dev
45 | - name: Test with pytest
46 | run: |
47 | uv run pytest
48 |
--------------------------------------------------------------------------------
/docs/ImpBlueprint/ImpBlueprint-import_nested_blueprints.md:
--------------------------------------------------------------------------------
1 | # ImpBlueprint.import_nested_blueprints
2 |
3 | ```python
4 | import_nested_blueprints(self, folder: str) -> None
5 | ```
6 |
7 | ---
8 |
9 | Will import all the Blueprints from the given folder relative to the Blueprint's root directory.
10 |
11 | Uses [Blueprint / import_nested_blueprint](../ImpBlueprint/ImpBlueprint-import_nested_blueprint.md) to import blueprints from
12 | the specified folder.
13 |
14 | Blueprints that are imported this way will be scoped to the parent Blueprint that imported them.
15 |
16 | `url_for('my_blueprint.nested_bp_one.index')`
17 |
18 | `url_for('my_blueprint.nested_bp_two.index')`
19 |
20 | `url_for('my_blueprint.nested_bp_three.index')`
21 |
22 | ```text
23 | my_blueprint/
24 | ├── routes/...
25 | ├── static/...
26 | ├── templates/...
27 | │
28 | ├── nested_blueprints/
29 | │ │
30 | │ ├── nested_bp_one/
31 | │ │ ├── ...
32 | │ │ ├── __init__.py
33 | │ ├── nested_bp_two/
34 | │ │ ├── ...
35 | │ │ ├── __init__.py
36 | │ └── nested_bp_three/
37 | │ ├── ...
38 | │ ├── __init__.py
39 | │
40 | ├── __init__.py
41 | ```
42 |
43 | File: `my_blueprint/__init__.py`
44 |
45 | ```python
46 | from flask_imp import ImpBlueprint
47 | from flask_imp.config import ImpBlueprintConfig
48 |
49 | bp = ImpBlueprint(__name__, ImpBlueprintConfig(
50 | enabled=True,
51 | static_folder="static",
52 | template_folder="templates",
53 | ))
54 |
55 | bp.import_resources("routes")
56 | bp.import_nested_blueprints("nested_blueprints")
57 | ```
58 |
59 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_generate_password.py:
--------------------------------------------------------------------------------
1 | from random import choice
2 | from typing import Literal
3 |
4 | from ._dataclasses import PasswordGeneration
5 | from ._generate_numeric_validator import generate_numeric_validator
6 |
7 |
8 | def generate_password(
9 | style: Literal["animals", "colors", "mixed"] = "mixed", length: int = 3
10 | ) -> str:
11 | """
12 | Generates a plain text password based on choice of style and length.
13 |
14 | (length) of random numbers are appended to the end of every generated password.
15 |
16 | style options: "animals", "colors", "mixed" - defaults to "mixed"
17 |
18 | :param style: "animals", "colors", "mixed" - defaults to "mixed"
19 | :param length: the number of words joined - defaults to 3
20 | :return: a generated password
21 | """
22 | if style == "animals":
23 | return "-".join(
24 | [choice(PasswordGeneration.animals) for _ in range(length)]
25 | ) + str(generate_numeric_validator(length=length))
26 |
27 | if style == "colors":
28 | return "-".join(
29 | [choice(PasswordGeneration.colors) for _ in range(length)]
30 | ) + str(generate_numeric_validator(length=length))
31 |
32 | if style == "mixed":
33 | return "-".join(
34 | [
35 | choice([*PasswordGeneration.animals, *PasswordGeneration.colors])
36 | for _ in range(length)
37 | ]
38 | ) + str(generate_numeric_validator(length=length))
39 |
40 | raise ValueError(f"Invalid style passed in {style}")
41 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_is_username_valid.py:
--------------------------------------------------------------------------------
1 | import re
2 | import typing as t
3 |
4 |
5 | def is_username_valid(
6 | username: str,
7 | allowed: t.Optional[t.List[t.Literal["all", "dot", "dash", "under"]]] = None,
8 | ) -> bool:
9 | """
10 | Checks if a username is valid.
11 |
12 | Valid usernames can only include letters,
13 | numbers, ., -, and _ but cannot begin or end with
14 | the last three mentioned.
15 |
16 | Example use::
17 |
18 | is_username_valid("username", allowed=["all"])
19 |
20 | Passes: username, user.name, user-name, user_name
21 | Fails: _user_name
22 |
23 | is_username_valid("username", allowed=["dot", "dash"])
24 |
25 | Passes: username, user.name, user-name, user-name.name
26 | Fails: user_name, _user_name, .user.name
27 |
28 | :param username: username to validate
29 | :param allowed: ["all", "dot", "dash", "under"] - defaults to ["all"]
30 | :return: True if username is valid, False otherwise
31 | """
32 |
33 | if not username[0].isalnum() or not username[-1].isalnum():
34 | return False
35 |
36 | if allowed is None:
37 | allowed = ["all"]
38 |
39 | if "all" in allowed:
40 | return bool(re.match(r"^[a-zA-Z0-9._-]+$", username))
41 |
42 | if "under" not in allowed:
43 | if "_" in username:
44 | return False
45 |
46 | if "dot" not in allowed:
47 | if "." in username:
48 | return False
49 |
50 | if "dash" not in allowed:
51 | if "-" in username:
52 | return False
53 |
54 | return True
55 |
--------------------------------------------------------------------------------
/docs/SecurityCheckpoints/flask_imp_security-bearercheckpoint.md:
--------------------------------------------------------------------------------
1 | # BearerCheckpoint
2 |
3 | ```python
4 | from flask_imp.security import BearerCheckpoint
5 | ```
6 |
7 | ```python
8 | BearerCheckpoint(
9 | token: str,
10 | ).action(
11 | fail_url: t.Optional[t.Union[str, t.Callable[[], t.Any]]] = None,
12 | fail_json: t.Optional[t.Dict[str, t.Any]] = None,
13 | fail_status: int = 403,
14 | pass_url: t.Optional[t.Union[str, t.Callable[[], t.Any]]] = None,
15 | message: t.Optional[str] = None,
16 | message_category: str = "message",
17 | )
18 | ```
19 |
20 | ---
21 |
22 | A checkpoint that checks if the authorization header is of type Bearer,
23 | and that the token in the request is valid.
24 |
25 | `token` The token to check for.
26 |
27 | `.action(...)`:
28 |
29 | `fail_url` The url to redirect to if the key value fails.
30 |
31 | `fail_json` JSON that is returned on failure.
32 |
33 | `fail_status` The status code to return if the check fails, defaults to `403`.
34 |
35 | `pass_url` The url to redirect to if the key value passes.
36 |
37 | `message` If a message is specified, a flash message is shown.
38 |
39 | `message_category` The category of the flash message.
40 |
41 | If fail_json is provided, passing to endpoints will be disabled.
42 |
43 | `pass_url` and `fail_url` take either a string or the `utilities.lazy_url_for` function.
44 |
45 | **Examples:**
46 |
47 | ```python
48 | BEARER_REQ = BearerCheckpoint("hello,world").action(fail_json={"error": "token"})
49 |
50 |
51 | @bp.route("/admin", methods=["GET"])
52 | @checkpoint(BEARER_REQ)
53 | def admin_page():
54 | ...
55 | ```
56 |
--------------------------------------------------------------------------------
/docs/Imp/Imp-import_models.md:
--------------------------------------------------------------------------------
1 | # Imp.import_models
2 |
3 | ```python
4 | import_models(file_or_folder: str) -> None
5 | ```
6 |
7 | ---
8 |
9 | Imports all the models from the given file or folder relative to the Flask app root.
10 |
11 | Each Model that is imported will be available in the `imp.model` lookup method.
12 | See [Imp / model](../Imp/Imp-model.md) for more information.
13 |
14 | *Example of importing models from a file*
15 |
16 | ```text
17 | app
18 | ├── my_blueprint
19 | │ ├── ...
20 | │ └── __init__.py
21 | ├── users_model.py
22 | ├── ...
23 | └── __init__.py
24 | ```
25 |
26 | File: `app/__init__.py`
27 |
28 | ```python
29 |
30 | from flask import Flask
31 | from flask_sqlalchemy import SQLAlchemy
32 |
33 | from flask_imp import Imp
34 |
35 | db = SQLAlchemy()
36 | imp = Imp()
37 |
38 |
39 | def create_app():
40 | app = Flask(
41 | __name__,
42 | static_folder="static",
43 | template_folder="templates"
44 | )
45 | imp.init_app(app)
46 | imp.import_blueprint("my_blueprint")
47 | imp.import_models("users_model.py")
48 | db.init_app(app) # must be below imp.import_models
49 |
50 | return app
51 | ```
52 |
53 | File: `app/users_model.py`
54 |
55 | ```python
56 | from app import db
57 |
58 |
59 | class User(db.Model):
60 | attribute = db.Column(db.String(255))
61 | ```
62 |
63 | *Example of importing models from a folder*
64 |
65 | ```text
66 | app
67 | ├── my_blueprint
68 | │ ├── ...
69 | │ └── __init__.py
70 | ├── models/
71 | │ ├── boats.py
72 | │ ├── cars.py
73 | │ └── users.py
74 | ├── ...
75 | └── __init__.py
76 | ```
77 |
78 | ```python
79 | def create_app():
80 | ...
81 | imp.import_models("models")
82 | ...
83 | ```
84 |
85 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-encrypt_password.md:
--------------------------------------------------------------------------------
1 | # encrypt_password
2 |
3 | ```python
4 | from flask_imp.auth import encrypt_password
5 | ```
6 |
7 | ```python
8 | encrypt_password(
9 | password: str,
10 | salt: str,
11 | encryption_level: int = 512,
12 | pepper_length: int = 1,
13 | pepper_position: t.Literal["start", "end"] = "end"
14 | ) -> str
15 | ```
16 |
17 | ---
18 |
19 | For use in password hashing.
20 |
21 | To be used alongside the [flask_imp.auth / authenticate_password](../Auth/flask_imp_auth-authenticate_password.md) function.
22 |
23 | Takes the plain password, applies a pepper, salts it, then produces a digested sha512 or sha256 if specified.
24 |
25 | Can set the encryption level to 256 or 512, defaults to 512.
26 |
27 | Can set the pepper length, defaults to 1. Max is 3.
28 |
29 | Can set the pepper position, "start" or "end", defaults to "end".
30 |
31 | **Note:**
32 |
33 | - You must inform the authenticate_password function of the pepper length used to hash the password.
34 | - You must inform the authenticate_password function of the position of the pepper used to hash the password.
35 | - You must inform the authenticate_password function of the encryption level used to hash the password.
36 |
37 | **Encryption Scenario:**
38 |
39 | ```
40 | Plain password: "password"
41 | Generated salt: "^%$*" (randomly generated)
42 | Generated pepper (length 1): "A" (randomly generated)
43 | Pepper position: "end"
44 | ```
45 |
46 | 1. Pepper is added to the end of the plain password: "passwordA"
47 | 2. Salt is added to the end of the peppered password: "passwordA^%$*"
48 | 3. Password is hashed: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0..."
49 | 4. Salt and hashed password are then stored in the database.
50 |
51 |
52 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | project = "Flask-Imp"
10 | copyright = "2024, David Carmichael"
11 | author = "David Carmichael"
12 | release = "6.0.x"
13 |
14 | # General --------------------------------------------------------------
15 |
16 | default_role = "code"
17 | extensions = [
18 | "sphinx.ext.autodoc",
19 | "sphinx.ext.extlinks",
20 | "sphinx.ext.intersphinx",
21 | "myst_parser",
22 | ]
23 | autodoc_member_order = "bysource"
24 | autodoc_typehints = "description"
25 | autodoc_preserve_defaults = True
26 | extlinks = {
27 | "issue": ("https://github.com/CheeseCake87/flask-imp/issues/%s", "#%s"),
28 | "pr": ("https://github.com/CheeseCake87/flask-imp/pull/%s", "#%s"),
29 | }
30 | intersphinx_mapping = {}
31 | myst_enable_extensions = [
32 | "fieldlist",
33 | ]
34 | myst_heading_anchors = 2
35 |
36 | # HTML -----------------------------------------------------------------
37 |
38 | html_theme = "furo"
39 | html_copy_source = False
40 | html_theme_options = {
41 | "source_repository": "https://github.com/CheeseCake87/flask-imp",
42 | "source_branch": "main",
43 | "source_directory": "docs/",
44 | "light_css_variables": {
45 | "font-stack": "'Atkinson Hyperlegible', sans-serif",
46 | "font-stack--monospace": "'Source Code Pro', monospace",
47 | },
48 | }
49 | pygments_style = "default"
50 | pygments_style_dark = "github-dark"
51 | html_show_copyright = False
52 | html_use_index = False
53 | html_domain_indices = False
54 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_encrypt_password.py:
--------------------------------------------------------------------------------
1 | import typing as t
2 | from hashlib import sha256, sha512
3 | from random import choice
4 | from string import ascii_letters
5 |
6 | from ._private_funcs import _pps, _ppe
7 |
8 |
9 | def encrypt_password(
10 | password: str,
11 | salt: str,
12 | encryption_level: int = 512,
13 | pepper_length: int = 1,
14 | pepper_position: t.Literal["start", "end"] = "end",
15 | ) -> str:
16 | """
17 | Takes the plain password, applies a pepper, salts it, then produces a digested sha512 or sha256 if specified.
18 |
19 | - Can set the encryption level to 256 or 512, defaults to 512.
20 | - Can set the pepper length, defaults to 1. Max is 3.
21 | - Can set the pepper position, "start" or "end", defaults to "end".
22 |
23 | For use in password hashing.
24 |
25 | You must inform the authenticate_password function of:
26 |
27 | - the pepper length used to hash the password.
28 | - the position of the pepper used to hash the password.
29 | - the encryption level used to hash the password.
30 |
31 | :param password: str - plain password
32 | :param salt: str - salt
33 | :param encryption_level: int - 256 or 512 - defaults to 512
34 | :param pepper_length: int - length of pepper
35 | :param pepper_position: str - "start" or "end" - defaults to "end"
36 | :return str: hash:
37 | """
38 |
39 | if pepper_length > 3:
40 | pepper_length = 3
41 |
42 | _sha = sha512() if encryption_level == 512 else sha256()
43 | _pepper = "".join(choice(ascii_letters) for _ in range(pepper_length))
44 |
45 | _sha.update(
46 | (
47 | _pps(_pepper, password, salt)
48 | if pepper_position == "start"
49 | else _ppe(_pepper, password, salt)
50 | ).encode("utf-8")
51 | )
52 | return _sha.hexdigest()
53 |
--------------------------------------------------------------------------------
/docs/Auth/flask_imp_auth-authenticate_password.md:
--------------------------------------------------------------------------------
1 | # authenticate_password
2 |
3 | ```python
4 | from flask_imp.auth import authenticate_password
5 | ```
6 |
7 | ```python
8 | authenticate_password(
9 | input_password: str,
10 | database_password: str,
11 | database_salt: str,
12 | encryption_level: int = 512,
13 | pepper_length: int = 1,
14 | pepper_position: t.Literal["start", "end"] = "end",
15 | use_multiprocessing: bool = False
16 | ) -> bool
17 | ```
18 |
19 | ---
20 |
21 | For use in password hashing.
22 |
23 | To be used alongside the [flask_imp.auth / encrypt_password](../Auth/flask_imp_auth-encrypt_password.md) function.
24 |
25 | Takes the plain input password, the stored hashed password along with the stored salt
26 | and will try every possible combination of pepper values to find a match.
27 |
28 | **Note:**
29 |
30 | **use_multiprocessing is not compatible with coroutine workers, e.g. eventlet/gevent
31 | commonly used with socketio.**
32 |
33 | If you are using socketio, you must set use_multiprocessing to False (default).
34 |
35 | **Note:**
36 |
37 | - You must know the pepper length used to hash the password.
38 | - You must know the position of the pepper used to hash the password.
39 | - You must know the encryption level used to hash the password.
40 |
41 | **Authentication Scenario:**
42 |
43 | ```
44 | Plain password: "password"
45 | Generated salt: "^%$*" (randomly generated)
46 | Generated pepper (length 1): "A" (randomly generated)
47 | Pepper position: "end"
48 | ```
49 |
50 | ```python
51 | input_password = "password"
52 | database_password = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0..." # pulled from database
53 | database_salt = "^%$*" # pulled from database
54 |
55 | authenticate_password(
56 | input_password,
57 | database_password,
58 | database_salt
59 | ) # >>> True
60 | ```
61 |
62 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains the authentication utilities for a Flask application.
3 |
4 | Functions:
5 |
6 | - authenticate_password: Authenticates a password against a hashed password.
7 | - encrypt_password: Encrypts a password with a salt and pepper.
8 | - generate_alphanumeric_validator: Generates a validator for alphanumeric strings.
9 | - generate_csrf_token: Generates a CSRF token.
10 | - generate_email_validator: Generates a validator for email addresses.
11 | - generate_numeric_validator: Generates a validator for numeric strings.
12 | - generate_password: Generates a password.
13 | - generate_private_key: Generates a private key.
14 | - generate_salt: Generates a salt.
15 | - is_email_address_valid: Validates an email address.
16 | - is_username_valid: Validates a username.
17 | """
18 |
19 | from ._authenticate_password import authenticate_password
20 | from ._encrypt_password import encrypt_password
21 | from ._generate_alphanumeric_validator import generate_alphanumeric_validator
22 | from ._generate_csrf_token import generate_csrf_token
23 | from ._generate_email_validator import generate_email_validator
24 | from ._generate_numeric_validator import generate_numeric_validator
25 | from ._generate_password import generate_password
26 | from ._generate_private_key import generate_private_key
27 | from ._generate_salt import generate_salt
28 | from ._is_email_address_valid import is_email_address_valid
29 | from ._is_username_valid import is_username_valid
30 |
31 | __all__ = [
32 | "is_email_address_valid",
33 | "is_username_valid",
34 | "generate_csrf_token",
35 | "generate_private_key",
36 | "generate_numeric_validator",
37 | "generate_alphanumeric_validator",
38 | "generate_email_validator",
39 | "generate_salt",
40 | "encrypt_password",
41 | "authenticate_password",
42 | "generate_password",
43 | ]
44 |
--------------------------------------------------------------------------------
/docs/ImpBlueprint/ImpBlueprint-import_nested_blueprint.md:
--------------------------------------------------------------------------------
1 | # ImpBlueprint.import_nested_blueprint
2 |
3 | ```python
4 | import_nested_blueprint(self, blueprint: str) -> None
5 | ```
6 |
7 | ---
8 |
9 | Import a specified Flask-Imp or standard Flask Blueprint relative to the Blueprint root.
10 |
11 | Works the same as [Imp / import_blueprint](../Imp/Imp-import_blueprint.md) but relative to the Blueprint root.
12 |
13 | Blueprints that are imported this way will be scoped to the parent Blueprint that imported them.
14 |
15 | `url_for('my_blueprint.my_nested_blueprint.index')`
16 |
17 | ```text
18 | my_blueprint/
19 | ├── routes/...
20 | ├── static/...
21 | ├── templates/...
22 | │
23 | ├── my_nested_blueprint/
24 | │ ├── routes/
25 | │ │ └── index.py
26 | │ ├── static/...
27 | │ ├── templates/...
28 | │ ├── __init__.py
29 | │
30 | ├── __init__.py
31 | ```
32 |
33 | File: `my_blueprint/__init__.py`
34 |
35 | ```python
36 | from flask_imp import ImpBlueprint
37 | from flask_imp.config import ImpBlueprintConfig
38 |
39 | bp = ImpBlueprint(__name__, ImpBlueprintConfig(
40 | enabled=True,
41 | static_folder="static",
42 | template_folder="templates",
43 | ))
44 |
45 | bp.import_resources("routes")
46 | bp.import_nested_blueprint("my_nested_blueprint")
47 | ```
48 |
49 | File: `my_blueprint/my_nested_blueprint/__init__.py`
50 |
51 | ```python
52 | from flask_imp import ImpBlueprint
53 | from flask_imp.config import ImpBlueprintConfig
54 |
55 | bp = ImpBlueprint(__name__, ImpBlueprintConfig(
56 | enabled=True,
57 | static_folder="static",
58 | template_folder="templates",
59 | ))
60 |
61 | bp.import_resources("routes")
62 | ```
63 |
64 | File: `my_blueprint/my_nested_blueprint/routes/index.py`
65 |
66 | ```python
67 | from flask import render_template
68 |
69 | from .. import bp
70 |
71 |
72 | @bp.route("/")
73 | def index():
74 | return render_template(bp.tmpl("index.html"))
75 | ```
76 |
77 |
--------------------------------------------------------------------------------
/src/flask_imp/_registries.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import typing as t
4 |
5 | if t.TYPE_CHECKING:
6 | from flask_sqlalchemy.model import DefaultMeta
7 |
8 |
9 | class ModelRegistry:
10 | """
11 | A registry for SQLAlchemy models.
12 | This is used to store all imported SQLAlchemy models in a central location.
13 | Accessible via Imp.__model_registry__
14 | """
15 |
16 | registry: t.Dict[str, t.Any]
17 |
18 | def __init__(self) -> None:
19 | self.registry = {}
20 |
21 | def assert_exists(self, class_name: str) -> None:
22 | """
23 | Assert that the model exists in the registry.
24 |
25 | :param class_name: the name of the model to check for
26 | """
27 | if class_name not in self.registry:
28 | raise KeyError(
29 | f"Model {class_name} not found in model registry \n"
30 | f"Available models: {', '.join(self.registry.keys())}"
31 | )
32 |
33 | def add(self, ref: str, model: t.Any) -> None:
34 | """
35 | Add a model to the registry.
36 |
37 | :param ref: the name of the model
38 | :param model: the model to add
39 | """
40 | self.registry[ref] = model
41 |
42 | def class_(self, class_name: str) -> t.Union[DefaultMeta, t.Any]:
43 | """
44 | Get a model from the registry.
45 |
46 | :param class_name: the name of the model to get
47 | :return: the model
48 | """
49 | self.assert_exists(class_name)
50 | return self.registry[class_name]
51 |
52 | @property
53 | def instance(self) -> "ModelRegistry":
54 | """
55 | Return the instance of the ModelRegistry.
56 |
57 | :return: the instance of the ModelRegistry
58 | """
59 | return self
60 |
61 | def __repr__(self) -> str:
62 | return f"ModelRegistry({self.registry})"
63 |
--------------------------------------------------------------------------------
/src/flask_imp/config/_imp_config.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import typing as t
4 |
5 | if t.TYPE_CHECKING:
6 | from flask_imp.config import DatabaseConfig, SQLiteDatabaseConfig, SQLDatabaseConfig
7 |
8 |
9 | class ImpConfig:
10 | """
11 | Imp configuration class.
12 | """
13 |
14 | IMP_INIT_SESSION: t.Optional[t.Dict[str, t.Any]]
15 |
16 | IMP_DATABASE_MAIN: t.Optional[
17 | t.Union[DatabaseConfig, SQLiteDatabaseConfig, SQLDatabaseConfig]
18 | ]
19 | IMP_DATABASE_BINDS: t.Optional[
20 | t.Iterable[t.Union[DatabaseConfig, SQLiteDatabaseConfig, SQLDatabaseConfig]]
21 | ]
22 |
23 | def __init__(
24 | self,
25 | init_session: t.Optional[t.Dict[str, t.Any]] = None,
26 | database_main: t.Optional[
27 | t.Union[DatabaseConfig, SQLiteDatabaseConfig, SQLDatabaseConfig]
28 | ] = None,
29 | database_binds: t.Optional[
30 | t.Iterable[t.Union[DatabaseConfig, SQLiteDatabaseConfig, SQLDatabaseConfig]]
31 | ] = None,
32 | ):
33 | """
34 | The Imp configuration class.
35 |
36 | This class is used to configure the global session cookie values
37 | used by your application.
38 |
39 | It's also used to store any database configurations that are
40 | used by Flask-SQLAlchemy.
41 |
42 | :param init_session: The initial session dictionary.
43 | :param database_main: The main database configuration.
44 | :param database_binds: An iterable of database bind configurations.
45 | """
46 | if not init_session:
47 | self.IMP_INIT_SESSION = {}
48 | else:
49 | self.IMP_INIT_SESSION = init_session
50 |
51 | self.IMP_DATABASE_MAIN = database_main
52 |
53 | if database_binds:
54 | self.IMP_DATABASE_BINDS = database_binds
55 | else:
56 | self.IMP_DATABASE_BINDS = []
57 |
--------------------------------------------------------------------------------
/docs/ImpBlueprint/ImpBlueprint-import_models.md:
--------------------------------------------------------------------------------
1 | # ImpBlueprint.import_models
2 |
3 | ```python
4 | import_models(folder: str = "models") -> None
5 | ```
6 |
7 | ---
8 |
9 | Will import all the models from the given folder relative to the Blueprint's root directory.
10 |
11 | Works the same as [Imp / import_models](../Imp/Imp-import_models.md) but relative to the Blueprint root.
12 |
13 | Blueprint models will also be available in the [Imp / model](../Imp/Imp-model.md) lookup.
14 |
15 | ```text
16 | my_blueprint/
17 | ├── routes/...
18 | ├── static/...
19 | ├── templates/...
20 | │
21 | ├── animal_models.py
22 | │
23 | ├── __init__.py
24 | ```
25 |
26 | **or**
27 |
28 | ```text
29 | my_blueprint/
30 | ├── routes/...
31 | ├── static/...
32 | ├── templates/...
33 | │
34 | ├── models/
35 | │ └── animals.py
36 | │
37 | ├── __init__.py
38 | ```
39 |
40 | File: `my_blueprint/__init__.py`
41 |
42 | ```python
43 | from flask_imp import ImpBlueprint
44 | from flask_imp.config import ImpBlueprintConfig
45 |
46 | bp = ImpBlueprint(__name__, ImpBlueprintConfig(
47 | enabled=True,
48 | static_folder="static",
49 | template_folder="templates",
50 | ))
51 |
52 | bp.import_resources("routes")
53 | bp.import_models("animal_models.py")
54 | ```
55 |
56 | **or**
57 |
58 | ```python
59 | from flask_imp import ImpBlueprint
60 | from flask_imp.config import ImpBlueprintConfig
61 |
62 | bp = ImpBlueprint(__name__, ImpBlueprintConfig(
63 | enabled=True,
64 | static_folder="static",
65 | template_folder="templates",
66 | ))
67 |
68 | bp.import_resources("routes")
69 | bp.import_models("models")
70 | ```
71 |
72 | File: `my_blueprint/animal_models.py` or `my_blueprint/models/animals.py`
73 |
74 | ```python
75 | from app import db
76 |
77 |
78 | class Animals(db.Model):
79 | animal_id = db.Column(db.Integer, primary_key=True)
80 | name = db.Column(db.String(64), index=True, unique=True)
81 | species = db.Column(db.String(64), index=True, unique=True)
82 | ```
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/docs/Security/flask_imp_security-checkpoint.md:
--------------------------------------------------------------------------------
1 | # checkpoint
2 |
3 | ```python
4 | from flask_imp.security import checkpoint
5 | ```
6 |
7 | ```python
8 | checkpoint(
9 | checkpoint_: t.Union[APIKeyCheckpoint, BearerCheckpoint, SessionCheckpoint]
10 | )
11 | ```
12 |
13 | `@checkpoint(...)`
14 |
15 | ---
16 |
17 | A decorator that checks if the specified checkpoint will pass or fail.
18 |
19 | `checkpoint_` The checkpoint class to pass or fail.
20 |
21 | **Example of a route that requires a user to be logged in:**
22 |
23 | ```python
24 | from flask_imp.security import checkpoint, SessionCheckpoint
25 | from flask_imp.utilities import lazy_url_for
26 |
27 | ...
28 |
29 | LOG_IN_REQ = SessionCheckpoint(
30 | session_key="logged_in",
31 | values_allowed=True,
32 | ).action(
33 | fail_url=lazy_url_for("login") # If logged_in is False, this will trigger
34 | )
35 |
36 |
37 | @bp.route("/admin", methods=["GET"])
38 | @checkpoint(LOG_IN_REQ)
39 | def admin_page():
40 | ...
41 | ```
42 |
43 | **Example of multiple checks:**
44 |
45 | ```python
46 | LOG_IN_REQ = SessionCheckpoint(
47 | session_key="logged_in",
48 | values_allowed=True,
49 | ).action(
50 | fail_url=lazy_url_for("blueprint.login_page"),
51 | message="Login needed" # This will set Flask's flash message
52 | )
53 |
54 | ADMIN_PERM = SessionCheckpoint(
55 | session_key="user_type",
56 | values_allowed="admin",
57 | ).action(
58 | fail_url=lazy_url_for("blueprint.index"),
59 | message="You need to be an admin to access this page"
60 | )
61 |
62 |
63 | @bp.route("/admin", methods=["GET"])
64 | @checkpoint(LOG_IN_REQ)
65 | @checkpoint(ADMIN_PERM)
66 | def admin_page():
67 | ...
68 | ```
69 |
70 | **Example of a route that if the user is already logged in, redirects to the specified endpoint:**
71 |
72 | ```python
73 | IS_LOGGED_IN = SessionCheckpoint(
74 | session_key='logged_in',
75 | values_allowed=True,
76 | ).action(
77 | pass_endpoint='blueprint.admin_page',
78 | message="Already logged in"
79 | )
80 |
81 |
82 | @bp.route("/login-page", methods=["GET"])
83 | @checkpoint(IS_LOGGED_IN)
84 | def login_page():
85 | ...
86 | ```
87 |
88 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/helpers.py:
--------------------------------------------------------------------------------
1 | import re
2 | import typing as t
3 |
4 | import click
5 |
6 |
7 | def strip_leading_slash(url_prefix: str) -> str:
8 | if url_prefix.startswith("/"):
9 | return url_prefix[1:]
10 | return url_prefix
11 |
12 |
13 | def to_snake_case(string: str) -> str:
14 | """
15 | Thank you openai
16 | """
17 | # Replace any non-alphanumeric characters with underscores
18 | string = re.sub(r"[^a-zA-Z0-9]", "_", string)
19 | # Remove any consecutive underscores
20 | string = re.sub(r"_{2,}", "_", string)
21 | # Convert the string to lowercase
22 | string = string.lower()
23 | return string
24 |
25 |
26 | class Sprinkles:
27 | HEADER = "\033[95m"
28 | OKBLUE = "\033[94m"
29 | OKCYAN = "\033[96m"
30 | OKGREEN = "\033[92m"
31 | WARNING = "\033[93m"
32 | FAIL = "\033[91m"
33 | BOLD = "\033[1m"
34 | UNDERLINE = "\033[4m"
35 | END = "\033[0m"
36 |
37 |
38 | def build(
39 | folders: t.Dict[str, t.Any], files: t.Dict[str, t.Any], building: str = "App"
40 | ) -> None:
41 | # write_bytes: t.List[str] = []
42 |
43 | for folder, path in folders.items():
44 | if not path.exists():
45 | path.mkdir(parents=True)
46 | click.echo(
47 | f"{Sprinkles.OKGREEN}{building} folder: {folder}, created{Sprinkles.END}"
48 | )
49 | else:
50 | click.echo(
51 | f"{Sprinkles.WARNING}{building} folder already exists: {folder}, skipping{Sprinkles.END}"
52 | )
53 |
54 | for file, (path, content) in files.items():
55 | if not path.exists():
56 | # write files in bytes (this was old code, keeping it for reference)
57 | # if file in write_bytes:
58 | # path.write_bytes(bytes.fromhex(content))
59 | # continue
60 |
61 | path.write_text(content, encoding="utf-8")
62 |
63 | click.echo(
64 | f"{Sprinkles.OKGREEN}{building} file: {file}, created{Sprinkles.END}"
65 | )
66 | else:
67 | click.echo(
68 | f"{Sprinkles.WARNING}{building} file already exists: {file}, skipping{Sprinkles.END}"
69 | )
70 |
--------------------------------------------------------------------------------
/src/flask_imp/config/_sqlite_database_config.py:
--------------------------------------------------------------------------------
1 | import typing as t
2 | from pathlib import Path
3 |
4 |
5 | class SQLiteDatabaseConfig:
6 | database_name: str
7 | sqlite_db_extension: str
8 | location: t.Optional[Path]
9 | bind_key: t.Optional[str]
10 | enabled: bool
11 |
12 | def __init__(
13 | self,
14 | database_name: str = "database",
15 | sqlite_db_extension: str = ".sqlite",
16 | location: t.Optional[Path] = None,
17 | bind_key: t.Optional[str] = None,
18 | enabled: bool = True,
19 | ):
20 | """
21 | SQLite database configuration
22 |
23 | Database will be stored in the app instance path if no location is provided
24 |
25 | :param database_name: name of the database - defaults to "database"
26 | :param sqlite_db_extension: extension of the database - defaults to ".sqlite"
27 | :param location: location of the database - Optional - defaults to app instance path
28 | :param bind_key: bind key to be used in SQLAlchemy - Optional
29 | :param enabled: whether the database is enabled - defaults to True
30 | """
31 | self.enabled = enabled
32 | self.database_name = database_name
33 | self.bind_key = bind_key
34 | self.sqlite_db_extension = sqlite_db_extension
35 | self.location = location
36 |
37 | def as_dict(self) -> t.Dict[str, t.Any]:
38 | return {
39 | "enabled": self.enabled,
40 | "database_name": self.database_name,
41 | "bind_key": self.bind_key,
42 | "location": self.location,
43 | "sqlite_db_extension": self.sqlite_db_extension,
44 | }
45 |
46 | def uri(self, app_instance_path: Path) -> str:
47 | if isinstance(self.location, Path):
48 | if not self.location.exists():
49 | raise FileNotFoundError(f"Location {self.location} does not exist")
50 |
51 | filepath = self.location / f"{self.database_name}{self.sqlite_db_extension}"
52 | else:
53 | filepath = (
54 | app_instance_path / f"{self.database_name}{self.sqlite_db_extension}"
55 | )
56 |
57 | return f"sqlite:///{filepath}"
58 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/filelib/init.py:
--------------------------------------------------------------------------------
1 | def init_full_py(app_name: str, secret_key: str) -> str:
2 | return f"""\
3 | from flask import Flask
4 |
5 | from {app_name}.extensions import imp, db
6 | from flask_imp.config import ImpConfig, FlaskConfig, DatabaseConfig
7 |
8 |
9 | def create_app():
10 | app = Flask(
11 | __name__,
12 | static_url_path="/",
13 | static_folder="static",
14 | template_folder="templates",
15 | )
16 |
17 | FlaskConfig(
18 | secret_key="{secret_key}",
19 | app_instance=app
20 | )
21 |
22 | imp.init_app(app, ImpConfig(
23 | init_session={{"logged_in": False}},
24 | database_main=DatabaseConfig(
25 | enabled=True,
26 | dialect="sqlite"
27 | )
28 | ))
29 |
30 | imp.import_resources()
31 | imp.import_blueprints("blueprints")
32 | imp.import_models("models")
33 |
34 | db.init_app(app)
35 |
36 | with app.app_context():
37 | db.create_all()
38 |
39 | return app
40 | """
41 |
42 |
43 | def init_slim_py(app_name: str, secret_key: str) -> str:
44 | return f"""\
45 | from flask import Flask
46 |
47 | from {app_name}.extensions import imp
48 | from flask_imp.config import ImpConfig, FlaskConfig
49 |
50 |
51 | def create_app():
52 | app = Flask(
53 | __name__,
54 | static_url_path="/",
55 | static_folder="static",
56 | template_folder="templates",
57 | )
58 |
59 | FlaskConfig(
60 | secret_key="{secret_key}",
61 | app_instance=app
62 | )
63 |
64 | imp.init_app(app, ImpConfig())
65 | imp.import_resources()
66 | imp.import_blueprint("www")
67 |
68 | return app
69 | """
70 |
71 |
72 | def init_minimal_py(secret_key: str) -> str:
73 | return f"""\
74 | from flask import Flask
75 |
76 | from flask_imp import Imp
77 | from flask_imp.config import ImpConfig, FlaskConfig
78 |
79 |
80 | def create_app():
81 | app = Flask(
82 | __name__,
83 | static_url_path="/",
84 | static_folder="static",
85 | template_folder="templates",
86 | )
87 |
88 | FlaskConfig(
89 | secret_key="{secret_key}",
90 | app_instance=app
91 | )
92 |
93 | imp = Imp(app, ImpConfig())
94 | imp.import_resources()
95 |
96 | return app
97 | """
98 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_private_funcs.py:
--------------------------------------------------------------------------------
1 | import typing as t
2 | from hashlib import sha256, sha512
3 |
4 |
5 | def _pps(pepper_: str, pass_: str, salt_: str) -> str:
6 | """
7 | Part of the private functions used in the password authentication process.
8 |
9 | Adds the pepper to the start of the password.
10 |
11 | :param pepper_: the pepper to add
12 | :param pass_: the password to add the pepper to
13 | :param salt_: the salt to add to the password
14 | :return: the password with the pepper added to the start
15 | """
16 | return pepper_ + pass_ + salt_
17 |
18 |
19 | def _ppe(pepper_: str, pass_: str, salt_: str) -> str:
20 | """
21 | Part of the private functions used in the password authentication process.
22 |
23 | Adds the pepper to the end of the password.
24 |
25 | :param pepper_: the pepper to add
26 | :param pass_: the password to add the pepper to
27 | :param salt_: the salt to add to the password
28 | :return: the password with the pepper added to the end
29 | """
30 | return pass_ + pepper_ + salt_
31 |
32 |
33 | def _guess_block(
34 | guesses: t.Set[str],
35 | input_password: str,
36 | database_password: str,
37 | database_salt: str,
38 | encryption_level: int = 512,
39 | pepper_position: t.Literal["start", "end"] = "end",
40 | ) -> bool:
41 | """
42 | Part of the private functions used in the password authentication process.
43 |
44 | Compares a set of guesses to a database password.
45 |
46 | :param guesses: a set of guesses to compare
47 | :param input_password: the input password
48 | :param database_password: the database password
49 | :param database_salt: the database salt
50 | :param encryption_level: the encryption level - defaults to 512
51 | :param pepper_position: the pepper position - defaults to "end"
52 | :return: True if a match is found, False otherwise
53 | """
54 | for guess in guesses:
55 | _sha = sha512() if encryption_level == 512 else sha256()
56 | _sha.update(
57 | (
58 | _pps(guess, input_password, database_salt)
59 | if pepper_position == "start"
60 | else _ppe(guess, input_password, database_salt)
61 | ).encode("utf-8")
62 | )
63 | if _sha.hexdigest() == database_password:
64 | return True
65 |
66 | return False
67 |
--------------------------------------------------------------------------------
/tests/test_app/blueprints/tests/routes/database.py:
--------------------------------------------------------------------------------
1 | from test_app import imp, db
2 |
3 | from flask_imp.auth import (
4 | generate_salt,
5 | generate_password,
6 | encrypt_password,
7 | generate_private_key,
8 | )
9 |
10 |
11 | def include(bp):
12 | @bp.route("/database-creation", methods=["GET"])
13 | def database_creation_test():
14 | db.drop_all()
15 | db.create_all()
16 | return "Database created."
17 |
18 | @bp.route("/database-population", methods=["GET"])
19 | def database_population_test():
20 | db.drop_all()
21 | db.create_all()
22 |
23 | m_example_user = imp.model("ExampleUser")
24 | m_example_user_bind = imp.model("ExampleUserBind")
25 | m_example_table = imp.model("ExampleTable")
26 |
27 | if not m_example_user.get_by_id(1):
28 | salt = generate_salt()
29 | gen_password = generate_password("animals")
30 | password = encrypt_password(gen_password, salt)
31 |
32 | new_example_user = m_example_user(
33 | username="David",
34 | password=password,
35 | salt=salt,
36 | private_key=generate_private_key(salt),
37 | disabled=False,
38 | )
39 | db.session.add(new_example_user)
40 | new_example_user_bind = m_example_user_bind(
41 | username="David",
42 | password=password,
43 | salt=salt,
44 | private_key=generate_private_key(salt),
45 | disabled=False,
46 | )
47 | db.session.add(new_example_user_bind)
48 | db.session.flush()
49 | new_example_user_rel = m_example_table(
50 | user_id=new_example_user.user_id, thing=gen_password
51 | )
52 | db.session.add(new_example_user_rel)
53 | db.session.flush()
54 | db.session.commit()
55 |
56 | user_in_example_table = imp.model("ExampleTable").get_by_user_id(
57 | new_example_user.user_id
58 | )
59 |
60 | if user_in_example_table:
61 | return (
62 | f"{new_example_user.username} created and {user_in_example_table.thing}"
63 | f"in ExampleTable, and {new_example_user_bind.username} created in ExampleUserBind."
64 | )
65 |
66 | return "Failed Auto Test, User already exists."
67 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "flask-imp"
3 | version = "6.0.3"
4 | description = 'A Flask auto importer that allows your Flask apps to grow big.'
5 | authors = [{ name = "David Carmichael", email = "david@uilix.com" }]
6 | readme = "README.md"
7 | license = { file = "LICENSE.txt" }
8 | classifiers = [
9 | 'Development Status :: 5 - Production/Stable',
10 | "License :: OSI Approved :: MIT License",
11 | "Framework :: Flask",
12 | "Natural Language :: English",
13 | ]
14 | requires-python = ">=3.9"
15 | dependencies = [
16 | 'click',
17 | 'Flask',
18 | 'Flask-SQLAlchemy',
19 | 'more-itertools'
20 | ]
21 |
22 | [dependency-groups]
23 | dev = [
24 | "flit>=3.12.0",
25 | "furo>=2025.9.25",
26 | "mypy>=1.18.2",
27 | "myst-parser>=3.0.1",
28 | "pre-commit>=4.3.0",
29 | "pyqwe>=3.1.1",
30 | "pyright>=1.1.406",
31 | "pytest>=8.4.2",
32 | "ruff>=0.14.0",
33 | "sphinx>=7.4.7",
34 | "tox>=4.30.3",
35 | "tox-uv>=1.28.1",
36 | ]
37 | docs = [
38 | "sphinx>=7.4.7",
39 | ]
40 | lint = [
41 | "ruff>=0.14.0",
42 | ]
43 | test = [
44 | "pytest>=8.4.2",
45 | ]
46 | typing = [
47 | "mypy>=1.18.2",
48 | "pyright>=1.1.406",
49 | ]
50 |
51 | [project.urls]
52 | Documentation = "https://cheesecake87.github.io/flask-imp/"
53 | Source = "https://github.com/CheeseCake87/flask-imp"
54 |
55 | [project.scripts]
56 | flask-imp = "flask_imp._cli:cli"
57 |
58 | [build-system]
59 | requires = ["flit_core >=3.2,<4"]
60 | build-backend = "flit_core.buildapi"
61 |
62 | [tool.flit.sdist]
63 | exclude = [
64 | ".github",
65 | "_assets",
66 | "app",
67 | "instance",
68 | "dist",
69 | "docs",
70 | "tests_docker",
71 | ".gitignore",
72 | ".env",
73 | "CONTRIBUTING.md",
74 | ]
75 |
76 | [tool.mypy]
77 | python_version = "3.9"
78 | files = ["src/flask_imp"]
79 | show_error_codes = true
80 | pretty = true
81 | strict = true
82 |
83 | [tool.pyright]
84 | pythonVersion = "3.9"
85 | include = ["src/flask_imp"]
86 | typeCheckingMode = "basic"
87 |
88 | [tool.ruff]
89 | src = ["src"]
90 | fix = true
91 | show-fixes = true
92 | output-format = "full"
93 |
94 | [tool.pyqwe]
95 | build = "*:flit build"
96 | install = "*:flit install --symlink"
97 | docs = [
98 | "*:sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml",
99 | "*:echo docs: http://localhost:8080/",
100 | "*(docs/_build/dirhtml):python3 -m http.server 8080"
101 | ]
102 |
--------------------------------------------------------------------------------
/example/app/models/example_user_table.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import select, update, delete, insert
2 |
3 | from ..extensions import db
4 | from flask_imp.auth import (
5 | authenticate_password,
6 | encrypt_password,
7 | generate_private_key,
8 | generate_salt,
9 | )
10 |
11 |
12 | class ExampleUserTable(db.Model):
13 | user_id = db.Column(db.Integer, primary_key=True)
14 | username = db.Column(db.String(256), nullable=False)
15 | password = db.Column(db.String(512), nullable=False)
16 | salt = db.Column(db.String(4), nullable=False)
17 | private_key = db.Column(db.String(256), nullable=False)
18 | disabled = db.Column(db.Boolean)
19 |
20 | @classmethod
21 | def login(cls, username, password: str) -> bool:
22 | user = cls.get_by_username(username)
23 | if user is None:
24 | return False
25 | return authenticate_password(password, user.password, user.salt)
26 |
27 | @classmethod
28 | def get_by_id(cls, user_id: int):
29 | return db.session.execute(
30 | select(cls).filter_by(user_id=user_id).limit(1)
31 | ).scalar_one_or_none()
32 |
33 | @classmethod
34 | def get_by_username(cls, username: str):
35 | return db.session.execute(
36 | select(cls).filter_by(username=username).limit(1)
37 | ).scalar_one_or_none()
38 |
39 | @classmethod
40 | def create(cls, username, password, disabled):
41 | salt = generate_salt()
42 | salt_pepper_password = encrypt_password(password, salt)
43 | private_key = generate_private_key(username)
44 |
45 | db.session.execute(
46 | insert(cls).values(
47 | username=username,
48 | password=salt_pepper_password,
49 | salt=salt,
50 | private_key=private_key,
51 | disabled=disabled,
52 | )
53 | )
54 | db.session.commit()
55 |
56 | @classmethod
57 | def update(cls, user_id: int, username, private_key, disabled):
58 | db.session.execute(
59 | update(cls)
60 | .where(cls.user_id == user_id)
61 | .values(
62 | username=username,
63 | private_key=private_key,
64 | disabled=disabled,
65 | )
66 | )
67 | db.session.commit()
68 |
69 | @classmethod
70 | def delete(cls, user_id: int):
71 | db.session.execute(delete(cls).where(cls.user_id == user_id))
72 | db.session.commit()
73 |
--------------------------------------------------------------------------------
/src/flask_imp/utilities.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 | from typing import Any, Optional
3 |
4 | from flask import url_for
5 |
6 | from ._utilities import LazySession
7 |
8 |
9 | def lazy_url_for(
10 | endpoint: str,
11 | *,
12 | _anchor: Optional[str] = None,
13 | _method: Optional[str] = None,
14 | _scheme: Optional[str] = None,
15 | _external: Optional[bool] = None,
16 | **values: Any,
17 | ) -> partial[str]:
18 | """
19 | Indented for use in checkpoint decorators.
20 |
21 | Takes the same arguments as Flask's url_for function and loads url_for and the
22 | arguments passed to it into a partial to be run later.
23 |
24 | This allows url_for to be set outside of context and later ran inside context.
25 |
26 | `url_for` docstring:
27 |
28 | Generate a URL to the given endpoint with the given values.
29 |
30 | This requires an active request or application context, and calls
31 | :meth:`current_app.url_for() `. See that method
32 | for full documentation.
33 |
34 | :param endpoint: The endpoint name associated with the URL to
35 | generate. If this starts with a ``.``, the current blueprint
36 | name (if any) will be used.
37 | :param _anchor: If given, append this as ``#anchor`` to the URL.
38 | :param _method: If given, generate the URL associated with this
39 | method for the endpoint.
40 | :param _scheme: If given, the URL will have this scheme if it is
41 | external.
42 | :param _external: If given, prefer the URL to be internal (False) or
43 | require it to be external (True). External URLs include the
44 | scheme and domain. When not in an active request, URLs are
45 | external by default.
46 | :param values: Values to use for the variable parts of the URL rule.
47 | Unknown keys are appended as query string arguments, like
48 | ``?a=b&c=d``.
49 | """
50 | return partial(
51 | url_for,
52 | endpoint=endpoint,
53 | _scheme=_scheme,
54 | _anchor=_anchor,
55 | _method=_method,
56 | **values,
57 | )
58 |
59 |
60 | def lazy_session_get(key: str, default: Any = None) -> LazySession:
61 | """
62 | Indented for use in checkpoint decorators.
63 |
64 | Returns a LazySession object that can be used to get a session
65 | value in checkpoint decorators.
66 | """
67 |
68 | return LazySession(key, default)
69 |
70 |
71 | __all__ = [
72 | "lazy_url_for",
73 | "lazy_session_get",
74 | ]
75 |
--------------------------------------------------------------------------------
/src/flask_imp/security/_include_csrf.py:
--------------------------------------------------------------------------------
1 | import typing as t
2 | from functools import wraps
3 |
4 | from flask import abort
5 | from flask import request
6 | from flask import session
7 | from flask_imp.auth import generate_csrf_token
8 |
9 |
10 | def include_csrf(
11 | session_key: str = "csrf", form_key: str = "csrf", abort_status: int = 401
12 | ) -> t.Callable[..., t.Any]:
13 | """
14 | A decorator that handles CSRF protection.
15 |
16 | On a **GET** request, a CSRF token is generated and stored in the session key
17 | specified by the session_key parameter.
18 |
19 | On a **POST** request, the form_key specified is checked against the session_key
20 | specified.
21 |
22 | If they match, the request is allowed to continue.
23 |
24 | If no match, the response will be aborted, flask.abort(abort_code), default 401.
25 |
26 | Example of a route that requires CSRF protection::
27 |
28 | @bp.route("/admin", methods=["GET", "POST"])
29 | @include_csrf(session_key="csrf", form_key="csrf")
30 | def admin_page():
31 | ...
32 | # You must pass in the CSRF token from the session into the template.
33 | # Then add to the form.
34 | return render_template("admin.html", csrf=session.get("csrf"))
35 |
36 | :param session_key: session key to store the CSRF token in.
37 | :param form_key: form key to check against the session key.
38 | :param abort_status: abort status code to use if the CSRF check fails.
39 | :return: decorated function, or abort(abort_code) response.
40 | """
41 |
42 | def include_csrf_wrapper(func: t.Any) -> t.Callable[..., t.Any]:
43 | @wraps(func)
44 | def inner(*args: t.Any, **kwargs: t.Any) -> t.Any:
45 | if request.method == "GET":
46 | session[session_key] = generate_csrf_token()
47 |
48 | return func(*args, **kwargs)
49 |
50 | if request.method == "POST":
51 | _session_key = session.get(session_key)
52 | _form_key = request.form.get(form_key)
53 |
54 | if _form_key is None:
55 | return abort(abort_status)
56 |
57 | if _session_key is None:
58 | return abort(abort_status)
59 |
60 | if _session_key != _form_key:
61 | return abort(abort_status)
62 |
63 | return func(*args, **kwargs)
64 |
65 | return inner
66 |
67 | return include_csrf_wrapper
68 |
--------------------------------------------------------------------------------
/docs/SecurityCheckpoints/flask_imp_security-apikeycheckpoint.md:
--------------------------------------------------------------------------------
1 | # APIKeyCheckpoint
2 |
3 | ```python
4 | from flask_imp.security import APIKeyCheckpoint
5 | ```
6 |
7 | ```python
8 | APIKeyCheckpoint(
9 | key: str,
10 | type_: t.Literal["header", "query_param"] = "header",
11 | header_or_param: str = "x-api-key",
12 | ).action(
13 | fail_url: t.Optional[t.Union[str, t.Callable[[], t.Any]]] = None,
14 | fail_json: t.Optional[t.Dict[str, t.Any]] = None,
15 | fail_status: int = 403,
16 | pass_url: t.Optional[t.Union[str, t.Callable[[], t.Any]]] = None,
17 | message: t.Optional[str] = None,
18 | message_category: str = "message",
19 | )
20 | ```
21 |
22 | ---
23 |
24 | A checkpoint that checks if the specified header or query parameter exists, and that
25 | the key in the request is valid.
26 |
27 | `key` The key to validate against.
28 |
29 | `type_` Where to look for the key.
30 |
31 | `header_or_param` What header or query param value will the key be expected.
32 |
33 | `.action(...)`:
34 |
35 | `fail_url` The url to redirect to if the key value fails.
36 |
37 | `fail_json` JSON that is returned on failure.
38 |
39 | `fail_status` The status code to return if the check fails, defaults to `403`.
40 |
41 | `pass_url` The url to redirect to if the key value passes.
42 |
43 | `message` If a message is specified, a flash message is shown.
44 |
45 | `message_category` The category of the flash message.
46 |
47 | If fail_json is provided, passing to endpoints will be disabled.
48 |
49 | `pass_url` and `fail_url` take either a string or the `utilities.lazy_url_for` function.
50 |
51 | **Examples:**
52 |
53 | This will look for the `x-api-key` key in the request header, and match it to the value
54 | of `hello`:
55 |
56 | ```python
57 | API_KEY_HEADER = APIKeyCheckpoint("hello")
58 |
59 | @bp.route("/admin", methods=["GET"])
60 | @checkpoint(API_KEY_HEADER)
61 | def admin_page():
62 | ...
63 | ```
64 |
65 | This will do the same check as above but look in the url params instead:
66 |
67 | `https://example.com/admin?x-api-key=hello`
68 |
69 | ```python
70 | API_KEY_QUERY_PARAM = APIKeyCheckpoint("hello", type_="query_param")
71 |
72 | @bp.route("/admin", methods=["GET"])
73 | @checkpoint(API_KEY_QUERY_PARAM)
74 | def admin_page():
75 | ...
76 | ```
77 |
78 | This will send JSON if the key is invalid:
79 |
80 | ```python
81 | API_KEY_HEADER = APIKeyCheckpoint("hello").action(fail_json={"error": "invalid key"})
82 |
83 | @bp.route("/admin", methods=["GET"])
84 | @checkpoint(API_KEY_HEADER)
85 | def admin_page():
86 | ...
87 | ```
88 |
--------------------------------------------------------------------------------
/docs/Security/flask_imp_security-checkpoint_callable.md:
--------------------------------------------------------------------------------
1 | # checkpoint_callable
2 |
3 | ```python
4 | from flask_imp.security import checkpoint_callable
5 | ```
6 |
7 | ```python
8 | def checkpoint_callable(
9 | callable_: t.Callable[..., t.Any],
10 | predefined_args: t.Optional[t.Dict[str, t.Any]] = None,
11 | include_url_args: bool = False,
12 | fail_url: t.Optional[t.Union[str, t.Callable[[], t.Any]]] = None,
13 | fail_json: t.Optional[t.Dict[str, t.Any]] = None,
14 | fail_status: int = 403,
15 | pass_url: t.Optional[t.Union[str, t.Callable[[], t.Any]]] = None,
16 | message: t.Optional[str] = None,
17 | message_category: str = "message",
18 | )
19 | ```
20 |
21 | `@checkpoint_callable(...)`
22 |
23 | ---
24 |
25 | A decorator that evaluates if the passed in callable is truly.
26 |
27 | Useful for feature flags or other checks that need to be done before a route is accessed.
28 |
29 | If `include_url_args` is set, the url variables of the route will be passed
30 | into the callable as `__url_vars__` after any predefined_args.
31 |
32 | *Example of using predefined_args*
33 |
34 | ```python
35 | def check_if_number(value):
36 | if isinstance(value, int):
37 | if value > 10:
38 | return True
39 | return False
40 |
41 |
42 | @bp.route("/number", methods=["GET"])
43 | @checkpoint_callable(
44 | check_if_number,
45 | predefined_args={"value": os.getenv("NUMBER")},
46 | fail_url=lazy_url_for("www.index"),
47 | message="Failed message"
48 | )
49 | def number():
50 | ...
51 | ```
52 |
53 | *Example of checking route variable*
54 |
55 | ```python
56 | def check_url_vars(__url_vars__):
57 | if __url_vars__["value"] == 10:
58 | return True
59 | return False
60 |
61 |
62 | ...
63 |
64 |
65 | @bp.route("/number/", methods=["GET"])
66 | @checkpoint_callable(
67 | check_url_vars,
68 | include_url_args=True,
69 | fail_url=lazy_url_for("wrong_number"),
70 | message="Failed message"
71 | )
72 | def number():
73 | ...
74 | ```
75 |
76 | *Example of using predefined_args from session*
77 |
78 | ```python
79 | def check_session_vars(value):
80 | # lazy_session_get is evaluated in the decorator.
81 | if value == 10:
82 | return True
83 | return False
84 |
85 |
86 | ...
87 |
88 |
89 | @bp.route("/number", methods=["GET"])
90 | @checkpoint_callable(
91 | check_session_vars,
92 | predefined_args={"value": lazy_session_get("NUMBER")},
93 | fail_url=lazy_url_for("www.index"),
94 | message="Failed message"
95 | )
96 | def number():
97 | ...
98 | ```
99 |
--------------------------------------------------------------------------------
/docs/Config/flask_imp_config-flaskconfig.md:
--------------------------------------------------------------------------------
1 | # FlaskConfig
2 |
3 | ```python
4 | from flask_imp.config import FlaskConfig
5 | ```
6 |
7 | ```python
8 | FlaskConfig(
9 | debug: t.Optional[bool] = None,
10 | propagate_exceptions: t.Optional[bool] = None,
11 | trap_http_exceptions: t.Optional[bool] = None,
12 | trap_bad_request_errors: t.Optional[bool] = None,
13 | secret_key: t.Optional[str] = None,
14 | session_cookie_name: t.Optional[str] = None,
15 | session_cookie_domain: t.Optional[str] = None,
16 | session_cookie_path: t.Optional[str] = None,
17 | session_cookie_httponly: t.Optional[bool] = None,
18 | session_cookie_secure: t.Optional[bool] = None,
19 | session_cookie_samesite: t.Optional[t.Literal["Lax", "Strict"]] = None,
20 | permanent_session_lifetime: t.Optional[int] = None,
21 | session_refresh_each_request: t.Optional[bool] = None,
22 | use_x_sendfile: t.Optional[bool] = None,
23 | send_file_max_age_default: t.Optional[int] = None,
24 | error_404_help: t.Optional[bool] = None,
25 | server_name: t.Optional[str] = None,
26 | application_root: t.Optional[str] = None,
27 | preferred_url_scheme: t.Optional[str] = None,
28 | max_content_length: t.Optional[int] = None,
29 | templates_auto_reload: t.Optional[bool] = None,
30 | explain_template_loading: t.Optional[bool] = None,
31 | max_cookie_size: t.Optional[int] = None,
32 | app_instance: t.Optional["Flask"] = None
33 | )
34 | ```
35 |
36 | ---
37 |
38 | A class that holds a Flask configuration values.
39 |
40 | You can set the configuration values to the app instance by either passing the app instance to the `app_instance`
41 | parameter or by calling the `apply_config` method on the `FlaskConfig` instance.
42 |
43 | ```python
44 | def create_app():
45 | app = Flask(
46 | __name__,
47 | static_folder="static",
48 | template_folder="templates"
49 | )
50 | FlaskConfig(debug=True, app_instance=app)
51 | return app
52 | ```
53 | or
54 | ```python
55 | flask_config = FlaskConfig(debug=True)
56 |
57 | def create_app():
58 | app = Flask(
59 | __name__,
60 | static_folder="static",
61 | template_folder="templates"
62 | )
63 | flask_config.apply_config(app)
64 | return app
65 | ```
66 | or
67 | ```python
68 | flask_config = FlaskConfig(debug=True)
69 |
70 | def create_app():
71 | app = Flask(
72 | __name__,
73 | static_folder="static",
74 | template_folder="templates"
75 | )
76 | app.config.from_object(flask_config.as_object())
77 | return app
78 | ```
79 |
--------------------------------------------------------------------------------
/src/flask_imp/config/_sql_database_config.py:
--------------------------------------------------------------------------------
1 | import typing as t
2 |
3 |
4 | class SQLDatabaseConfig:
5 | dialect: t.Literal["mysql", "postgresql", "oracle", "mssql"]
6 | database_name: str
7 | location: str
8 | port: int
9 | username: str
10 | password: str
11 | bind_key: t.Optional[str] = None
12 | enabled: bool = False
13 |
14 | allowed_dialects: t.Tuple[str, ...] = ("mysql", "postgresql", "oracle", "mssql")
15 |
16 | def __init__(
17 | self,
18 | dialect: t.Literal["mysql", "postgresql", "oracle", "mssql"],
19 | database_name: str,
20 | location: str,
21 | port: int,
22 | username: str,
23 | password: str,
24 | bind_key: t.Optional[str] = None,
25 | enabled: bool = True,
26 | ) -> None:
27 | """
28 | SQL database configuration
29 |
30 | Allowed dialects: mysql, postgresql, oracle, mssql
31 |
32 | :param dialect: database dialect - one of: mysql, postgresql, oracle, mssql
33 | :param database_name: name of the database
34 | :param location: location of the database
35 | :param port: port of the database
36 | :param username: username to connect to the database
37 | :param password: password to connect to the database
38 | :param bind_key: bind key to be used in SQLAlchemy - Optional
39 | :param enabled: whether the database is available to the application - defaults to True
40 | """
41 | if dialect not in self.allowed_dialects:
42 | raise ValueError(
43 | f"Database dialect must be one of: {', '.join(self.allowed_dialects)}"
44 | )
45 |
46 | self.dialect = dialect
47 | self.enabled = enabled
48 | self.database_name = database_name
49 | self.bind_key = bind_key
50 | self.location = location
51 | self.port = port
52 | self.username = username
53 | self.password = password
54 |
55 | def as_dict(self) -> t.Dict[str, t.Any]:
56 | return {
57 | "enabled": self.enabled,
58 | "dialect": self.dialect,
59 | "database_name": self.database_name,
60 | "bind_key": self.bind_key,
61 | "location": self.location,
62 | "port": self.port,
63 | "username": self.username,
64 | "password": self.password,
65 | }
66 |
67 | def uri(self) -> str:
68 | return (
69 | f"{self.dialect}://{self.username}:"
70 | f"{self.password}@{self.location}:"
71 | f"{self.port}/{self.database_name}"
72 | )
73 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/filelib/models.py:
--------------------------------------------------------------------------------
1 | def models_example_user_table_py(app_name: str) -> str:
2 | return f"""\
3 | from sqlalchemy import select, update, delete, insert
4 |
5 | from {app_name}.extensions import db
6 | from flask_imp.auth import (
7 | authenticate_password,
8 | encrypt_password,
9 | generate_private_key,
10 | generate_salt,
11 | )
12 |
13 |
14 | class ExampleUserTable(db.Model):
15 | user_id = db.Column(db.Integer, primary_key=True)
16 | username = db.Column(db.String(256), nullable=False)
17 | password = db.Column(db.String(512), nullable=False)
18 | salt = db.Column(db.String(4), nullable=False)
19 | private_key = db.Column(db.String(256), nullable=False)
20 | disabled = db.Column(db.Boolean)
21 |
22 | @classmethod
23 | def login(cls, username, password: str) -> bool:
24 | user = cls.get_by_username(username)
25 | if user is None:
26 | return False
27 | return authenticate_password(password, user.password, user.salt)
28 |
29 | @classmethod
30 | def get_by_id(cls, user_id: int):
31 | return db.session.execute(
32 | select(cls).filter_by(user_id=user_id).limit(1)
33 | ).scalar_one_or_none()
34 |
35 | @classmethod
36 | def get_by_username(cls, username: str):
37 | return db.session.execute(
38 | select(cls).filter_by(username=username).limit(1)
39 | ).scalar_one_or_none()
40 |
41 | @classmethod
42 | def create(cls, username, password, disabled):
43 | salt = generate_salt()
44 | salt_pepper_password = encrypt_password(password, salt)
45 | private_key = generate_private_key(username)
46 |
47 | db.session.execute(
48 | insert(cls).values(
49 | username=username,
50 | password=salt_pepper_password,
51 | salt=salt,
52 | private_key=private_key,
53 | disabled=disabled,
54 | )
55 | )
56 | db.session.commit()
57 |
58 | @classmethod
59 | def update(cls, user_id: int, username, private_key, disabled):
60 | db.session.execute(
61 | update(cls).where(
62 | cls.user_id == user_id
63 | ).values(
64 | username=username,
65 | private_key=private_key,
66 | disabled=disabled,
67 | )
68 | )
69 | db.session.commit()
70 |
71 | @classmethod
72 | def delete(cls, user_id: int):
73 | db.session.execute(
74 | delete(cls).where(
75 | cls.user_id == user_id
76 | )
77 | )
78 | db.session.commit()
79 | """
80 |
--------------------------------------------------------------------------------
/docs/ImpBlueprint/ImpBlueprint-Introduction.md:
--------------------------------------------------------------------------------
1 | # Flask-Imp Blueprint Introduction
2 |
3 | The Flask-Imp Blueprint inherits from the Flask Blueprint class, then adds some additional methods to allow for auto
4 | importing of models, resources and other nested blueprints.
5 |
6 | The Flask-Imp Blueprint requires you to provide the `ImpBlueprintConfig` class as the second argument to the Blueprint.
7 |
8 | Here's an example of a Flask-Imp Blueprint structure:
9 |
10 | ```text
11 | www/
12 | ├── nested_blueprints/
13 | │ ├── blueprint_one/
14 | │ │ ├── ...
15 | │ │ └── __init__.py
16 | │ └── blueprint_two/
17 | │ ├── ...
18 | │ └── __init__.py
19 | ├── standalone_nested_blueprint/
20 | │ ├── ...
21 | │ └── __init__.py
22 | ├── models/
23 | │ └── ...
24 | ├── routes/
25 | │ └── index.py
26 | ├── static/
27 | │ └── ...
28 | ├── templates/
29 | │ └── www/
30 | │ └── index.html
31 | └── __init__.py
32 | ```
33 |
34 | File: `__init__.py`
35 |
36 | ```python
37 | from flask_imp import ImpBlueprint
38 | from flask_imp.config import ImpBlueprintConfig
39 |
40 | bp = ImpBlueprint(__name__, ImpBlueprintConfig(
41 | enabled=True,
42 | url_prefix="/www",
43 | static_folder="static",
44 | template_folder="templates",
45 | init_session={"logged_in": False},
46 | ))
47 |
48 | bp.import_resources("routes")
49 | bp.import_models("models")
50 | bp.import_nested_blueprints("nested_blueprints")
51 | bp.import_nested_blueprint("standalone_nested_blueprint")
52 | ```
53 |
54 | The `ImpBlueprintConfig` class is used to configure the Blueprint. It provides a little more flexibility than the
55 | standard Flask Blueprint configuration, like the ability to enable or disable the Blueprint.
56 |
57 | `ImpBlueprintConfig`'s `init_session` works the same as `ImpConfig`'s `init_session`, this will add the session data to
58 | the Flask app's session object on initialization of the Flask app.
59 |
60 | To see more about configuration see: [flask_imp.config / ImpBlueprintConfig](../Config/flask_imp_config-impblueprintconfig.md)
61 |
62 | `import_resources` method will walk one level deep into the `routes` folder, and import all `.py` files as modules.
63 | For more information see: [ImpBlueprint / import_resources](../ImpBlueprint/ImpBlueprint-import_resources.md)
64 |
65 | `import_models` works the same as `imp.import_models`, it will look for instances of `db.Model` and import them. These
66 | will also be available in the model lookup method `imp.model`.
67 | For more information see: [Imp / import_models](../Imp/Imp-import_models.md)
68 |
69 | `import_nested_blueprints` will do the same as `imp.import_blueprints`, but will register the blueprints found as
70 | nested to the current blueprint. For example `www.blueprint_one.index`
71 |
72 | `import_nested_blueprint` behaves the same as `import_nested_blueprints`, but will only import a single blueprint.
73 |
74 |
--------------------------------------------------------------------------------
/docs/Imp/Imp-import_blueprint.md:
--------------------------------------------------------------------------------
1 | # Imp.import_blueprint
2 |
3 | ```python
4 | import_blueprint(self, blueprint: str) -> None
5 | ```
6 |
7 | ---
8 |
9 | Import a specified Flask-Imp or standard Flask Blueprint relative to the Flask app root.
10 |
11 |
12 | ```text
13 | app
14 | ├── my_blueprint
15 | │ ├── ...
16 | │ └── __init__.py
17 | ├── ...
18 | └── __init__.py
19 | ```
20 |
21 | File: `app/__init__.py`
22 |
23 | ```python
24 | from flask import Flask
25 |
26 | from flask_imp import Imp
27 |
28 | imp = Imp()
29 |
30 |
31 | def create_app():
32 | app = Flask(
33 | __name__,
34 | static_folder="static",
35 | template_folder="templates"
36 | )
37 | imp.init_app(app)
38 |
39 | imp.import_blueprint("my_blueprint")
40 |
41 | return app
42 | ```
43 |
44 | Flask-Imp Blueprints have the ability to auto import resources, and initialize session variables.
45 |
46 | For more information on how Flask-Imp Blueprints work, see the [ImpBlueprint / Introduction](../ImpBlueprint/ImpBlueprint-Introduction.md)
47 |
48 | **Example of 'my_blueprint' as a Flask-Imp Blueprint:**
49 |
50 | ```text
51 | app
52 | ├── my_blueprint
53 | │ ├── routes
54 | │ │ └── index.py
55 | │ ├── static
56 | │ │ └── css
57 | │ │ └── style.css
58 | │ ├── templates
59 | │ │ └── my_blueprint
60 | │ │ └── index.html
61 | │ ├── __init__.py
62 | │ └── config.toml
63 | └── ...
64 | ```
65 |
66 | File: `__init__.py`
67 |
68 | ```python
69 | from flask_imp import ImpBlueprint
70 | from flask_imp.config import ImpBlueprintConfig
71 |
72 | bp = ImpBlueprint(
73 | __name__,
74 | ImpBlueprintConfig(
75 | enabled=True,
76 | url_prefix="/my-blueprint",
77 | static_folder="static",
78 | template_folder="templates",
79 | static_url_path="/static/my_blueprint",
80 | init_session={"my_blueprint": "session_value"},
81 | ),
82 | )
83 |
84 | bp.import_resources("routes")
85 | ```
86 |
87 | File: `routes / index.py`
88 |
89 | ```python
90 | from .. import bp
91 |
92 |
93 | @bp.route("/")
94 | def index():
95 | return "regular_blueprint"
96 | ```
97 |
98 | **Example of 'my_blueprint' as a standard Flask Blueprint:**
99 |
100 | ```text
101 | app
102 | ├── my_blueprint
103 | │ ├── ...
104 | │ └── __init__.py
105 | └── ...
106 | ```
107 |
108 | File: `__init__.py`
109 |
110 | ```python
111 | from flask import Blueprint
112 |
113 | bp = Blueprint("my_blueprint", __name__, url_prefix="/my-blueprint")
114 |
115 |
116 | @bp.route("/")
117 | def index():
118 | return "regular_blueprint"
119 | ```
120 |
121 | Both of the above examples will work with `imp.import_blueprint("my_blueprint")`, they will be registered
122 | with the Flask app, and will be accessible via `url_for("my_blueprint.index")`.
123 |
124 |
--------------------------------------------------------------------------------
/src/flask_imp/auth/_authenticate_password.py:
--------------------------------------------------------------------------------
1 | import multiprocessing
2 | import typing as t
3 | from itertools import product
4 | from string import ascii_letters
5 |
6 | from more_itertools import batched
7 |
8 | from ._private_funcs import _guess_block
9 |
10 |
11 | def authenticate_password(
12 | input_password: str,
13 | database_password: str,
14 | database_salt: str,
15 | encryption_level: int = 512,
16 | pepper_length: int = 1,
17 | pepper_position: t.Literal["start", "end"] = "end",
18 | use_multiprocessing: bool = False,
19 | ) -> bool:
20 | """
21 | Takes the plain input password, the stored hashed password along with the stored salt
22 | and will try every possible combination of pepper values to find a match.
23 |
24 | *Note:* use_multiprocessing is not compatible with coroutine workers, e.g. eventlet/gevent
25 | commonly used with socketio.
26 |
27 | **You must know:**
28 |
29 | - the length of the pepper used to hash the password.
30 | - the position of the pepper used to hash the password.
31 | - the encryption level used to hash the password.
32 |
33 | :param input_password: plain password
34 | :param database_password: hashed password from database
35 | :param database_salt: salt from database
36 | :param encryption_level: encryption used to generate database password
37 | :param pepper_length: length of pepper used to generate database password
38 | :param pepper_position: "start" or "end" - position of pepper used to generate database password
39 | :param use_multiprocessing: use multiprocessing to speed up the process (not compatible with eventlet/gevent)
40 | :return: True if match, False if not
41 | """
42 |
43 | if pepper_length > 3:
44 | pepper_length = 3
45 |
46 | _guesses = {"".join(i) for i in product(ascii_letters, repeat=pepper_length)}
47 |
48 | if not use_multiprocessing:
49 | for guess in _guesses:
50 | if _guess_block(
51 | {guess},
52 | input_password,
53 | database_password,
54 | database_salt,
55 | encryption_level,
56 | pepper_position,
57 | ):
58 | return True
59 |
60 | return False
61 |
62 | thread_pool = multiprocessing.Pool(processes=pepper_length)
63 | threads = []
64 |
65 | for batch in batched(_guesses, 1000):
66 | threads.append(
67 | thread_pool.apply_async(
68 | _guess_block,
69 | args=(
70 | batch,
71 | input_password,
72 | database_password,
73 | database_salt,
74 | encryption_level,
75 | pepper_position,
76 | ),
77 | )
78 | )
79 |
80 | for thread in threads:
81 | if thread.get():
82 | return True
83 |
84 | return False
85 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/__init__.py:
--------------------------------------------------------------------------------
1 | import click
2 |
3 | from .blueprint import add_api_blueprint as _add_api_blueprint
4 | from .blueprint import add_blueprint as _add_blueprint
5 | from .helpers import Sprinkles as Sp
6 | from .init import init_app as _init_app
7 | from .. import __version__
8 |
9 |
10 | @click.group()
11 | @click.version_option(__version__)
12 | def cli() -> None:
13 | pass # Entry Point
14 |
15 |
16 | @cli.command("blueprint", help="Create a flask-imp blueprint")
17 | @click.option(
18 | "-n",
19 | "--name",
20 | nargs=1,
21 | default="new_blueprint",
22 | prompt="Name",
23 | help="The name of the blueprint to create.",
24 | )
25 | @click.option(
26 | "-f",
27 | "--folder",
28 | nargs=1,
29 | default=".",
30 | prompt=(f"Folder {Sp.WARNING}(relative to CWD){Sp.END}"),
31 | help="The folder to create the blueprint in, creation is relative to the current working directory.",
32 | )
33 | def add_blueprint(name: str, folder: str) -> None:
34 | _add_blueprint(name=name, folder=folder)
35 |
36 |
37 | @cli.command("api-blueprint", help="Create a flask-imp api blueprint")
38 | @click.option(
39 | "-n",
40 | "--name",
41 | nargs=1,
42 | default="new_api_blueprint",
43 | prompt="Name",
44 | help="The name of the api blueprint to create.",
45 | )
46 | @click.option(
47 | "-f",
48 | "--folder",
49 | nargs=1,
50 | default=".",
51 | prompt=(f"Folder {Sp.WARNING}(relative to CWD){Sp.END}"),
52 | help="The folder to create the api blueprint in, creation is relative to the current working directory.",
53 | )
54 | def add_api_blueprint(name: str, folder: str) -> None:
55 | _add_api_blueprint(name=name, folder=folder)
56 |
57 |
58 | @cli.command("init", help="Create a new flask-imp app.")
59 | @click.option(
60 | "-n",
61 | "--name",
62 | nargs=1,
63 | default=None,
64 | help="The name of the app folder that will be created.",
65 | )
66 | @click.option("-s", "--slim", is_flag=True, default=False, help="Create a slim app.")
67 | @click.option(
68 | "-m", "--minimal", is_flag=True, default=False, help="Create a minimal app."
69 | )
70 | @click.option("-f", "--full", is_flag=True, default=False, help="Create a full app.")
71 | def init_new_app(name: str, full: bool, slim: bool, minimal: bool) -> None:
72 | if not full and not slim and not minimal:
73 | choice = click.prompt(
74 | "What type of app would you like to create?",
75 | default="minimal",
76 | type=click.Choice(["minimal", "slim", "full"]),
77 | )
78 |
79 | if choice == "full":
80 | full = True
81 | elif choice == "slim":
82 | slim = True
83 | elif choice == "minimal":
84 | minimal = True
85 |
86 | if name is None:
87 | set_name = click.prompt("What would you like to call your app?", default="app")
88 |
89 | else:
90 | set_name = name
91 |
92 | _init_app(set_name, full, slim, minimal)
93 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | ## Version x.x.x
2 |
3 | ---
4 |
5 | Unreleased
6 |
7 | - x
8 |
9 | ## Version 6.0.3
10 |
11 | ---
12 |
13 | Released 2025-11-16
14 |
15 | - fix to 'SessionCheckpoint' value checker
16 |
17 | ## Version 6.0.2
18 |
19 | ---
20 |
21 | Released 2025-10-21
22 |
23 | - further fixes to prevent the import of hidden and dunder files and folders
24 |
25 | ## Version 6.0.1
26 |
27 | ---
28 |
29 | Released 2025-10-21
30 |
31 | - fix to prevent hidden files and folders being imported, this is files that start with
32 | `.` for example `.cli.py` and folders that start with `.` for example `.DS_Store`
33 |
34 | ## Version 6.0.0
35 |
36 | ---
37 |
38 | Released 2025-10-16
39 |
40 | - beta-3 + beta-2 + beta-1
41 |
42 | ## Version 6.0.0-beta.3
43 |
44 | ---
45 |
46 | Released 2025-10-16
47 |
48 | - Replaced `import_app_resources` with
49 | `import_resources` and made it more scoped towards
50 | importing and setting up factories. Static and template folder settings moved back to
51 | being set in the Flask object creation.
52 | - factories are now mandatory when using `import_resources`
53 | - Update CLI init command to reflect method replacement.
54 | - Update Docs to reflect method `import_app_resources` replacement.
55 | - Actually return `ImpBlueprint.as_flask_blueprint` as a Flask Blueprint
56 | - replace `pass_function_check` with `checkpoint_callable`
57 | - add `APIKeyCheckpoint`
58 | - refactor all checkpoint args
59 | - add `utilities.lazy_url_for`
60 | - add `utilities.lazy_session_get`
61 | - update overall docs
62 |
63 | ## Version 6.0.0-beta.2
64 |
65 | ---
66 |
67 | Released 2025-05-27
68 |
69 | - bug fixes
70 | - move checkpoints to package
71 |
72 | ## Version 6.0.0-beta.1
73 |
74 | ---
75 |
76 | Released 2025-05-27
77 |
78 | - Simplify `flask_imp.security.checkpoint` decorator by adding checkpoint types.
79 |
80 | ## Version 5.7.0
81 |
82 | ---
83 |
84 | Released 2025-02-10
85 |
86 | - add new method: `FlaskConfig.as_object`
87 | - refactored _flask_config.py
88 |
89 | ## Version 5.6.0
90 |
91 | ---
92 |
93 | Released 2025-02-04
94 |
95 | - New method added to register ImpBlueprints
96 | - Addition of two new decorators @checkpoint and @api_checkpoint these will eventually
97 | replace login_check, api_login_check and permission_check
98 | - remove old reminder to check old settings file
99 | - update classifiers in pyproject.toml
100 |
101 | ## Version 5.5.1
102 |
103 | ---
104 |
105 | Released 2024-12-04
106 |
107 | - switched logo for emoji
108 | - fixed `initial-scale` value in generated templates
109 | - updated example app
110 |
111 | ## Version 5.5.0
112 |
113 | ---
114 |
115 | Released 2024-11-21
116 |
117 | - updated project structure.
118 | - docs now using sphinx + readthedocs.
119 | - the start of the changes.md file.
120 | - changes to the order of arguments in database configs
121 | - argument 'name' changed to 'database_name' in configs
122 | - addition of abort_status and fail_status args in security decorators
123 |
--------------------------------------------------------------------------------
/docs/CLI_Commands/CLI_Commands-flask-imp_blueprint.md:
--------------------------------------------------------------------------------
1 | # Generate a Flask-Imp Blueprint
2 |
3 | Flask-Imp has its own type of blueprint. It comes with some methods to auto import routes, and models etc... see
4 | [ImpBlueprint-Introduction](../ImpBlueprint/ImpBlueprint-Introduction.md) for more information.
5 |
6 | You have the option to generate a regular template rendering blueprint, or a API blueprint that returns a JSON response.
7 |
8 | ```bash
9 | flask-imp blueprint --help
10 | ```
11 | or
12 | ```bash
13 | flask-imp api-blueprint --help
14 | ```
15 |
16 | To generate a Flask-Imp blueprint, run the following command:
17 |
18 | ```bash
19 | flask-imp blueprint
20 | ```
21 | or
22 | ```bash
23 | flask-imp api-blueprint
24 | ```
25 |
26 | After running this command, you will be prompted to enter the location of where you want to create your blueprint:
27 |
28 | ```text
29 | ~ $ flask-imp blueprint
30 | (Creation is relative to the current working directory)
31 | Folder to create blueprint in [Current Working Directory]:
32 | ```
33 |
34 | As detailed in the prompt, the creation of the blueprint is relative to the current working directory. So to create a
35 | blueprint in the folder `app/blueprints`, you would enter `app/blueprints` in the prompt.
36 |
37 | ```text
38 | ~ $ flask-imp blueprint
39 | (Creation is relative to the current working directory)
40 | Folder to create blueprint in [Current Working Directory]: app/blueprints
41 | ```
42 |
43 | You will then be prompted to enter a name for your blueprint:
44 |
45 | ```text
46 | ~ $ flask-imp blueprint
47 | ...
48 | Name of the blueprint to create [my_new_blueprint]:
49 | ```
50 |
51 | The default name is 'my_new_blueprint', we will change this to 'admin'
52 |
53 | ```text
54 | ~ $ flask-imp blueprint
55 | ...
56 | Name of the blueprint to create [my_new_blueprint]: admin
57 | ```
58 |
59 | After creating your blueprint, the folder structure will look like this:
60 |
61 | ```text
62 | app/
63 | ├── blueprints
64 | │ └── admin
65 | │ ├── routes
66 | │ │ └── index.py
67 | │ │
68 | │ ├── static
69 | │ │ ├── css
70 | │ │ │ └── water.css
71 | │ │ ├── img
72 | │ │ │ └── flask-imp-logo.png
73 | │ │ └── js
74 | │ │ └── main.js
75 | │ │
76 | │ ├── templates
77 | │ │ └── www
78 | │ │ ├── extends
79 | │ │ │ └── main.html
80 | │ │ ├── includes
81 | │ │ │ ├── footer.html
82 | │ │ │ └── header.html
83 | │ │ └── index.html
84 | │ │
85 | │ └── __init__.py
86 | │
87 | ...
88 | ```
89 |
90 | This is a self-contained blueprint, so it has its own static, templates and routes folders.
91 | You can now navigate '/admin'
92 |
93 | You can streamline this process by specifying the name of the blueprint, the folder to
94 | create it in and the configuration to use, like so:
95 |
96 | ```bash
97 | flask-imp blueprint -n admin -f app/blueprints
98 | ```
99 |
100 |
--------------------------------------------------------------------------------
/docs/SecurityCheckpoints/flask_imp_security-sessioncheckpoint.md:
--------------------------------------------------------------------------------
1 | # SessionCheckpoint
2 |
3 | ```python
4 | from flask_imp.security import SessionCheckpoint
5 | ```
6 |
7 | ```python
8 | SessionCheckpoint(
9 | session_key: str,
10 | values_allowed: t.Union[t.List[t.Union[str, int, bool]], str, int, bool],
11 | ).action(
12 | fail_url: t.Optional[t.Union[str, t.Callable[[], t.Any]]] = None,
13 | fail_json: t.Optional[t.Dict[str, t.Any]] = None,
14 | fail_status: int = 403,
15 | pass_url: t.Optional[t.Union[str, t.Callable[[], t.Any]]] = None,
16 | message: t.Optional[str] = None,
17 | message_category: str = "message",
18 | )
19 | ```
20 |
21 | ---
22 |
23 | A checkpoint that checks if the specified session key exists and its value(s) match the specified value(s).
24 |
25 | `session_key` The session key to check for.
26 |
27 | `values_allowed` A list of or singular value(s) that the session key must contain.
28 |
29 | `.action(...)`:
30 |
31 | `pass_url` The url to redirect to if the key value passes.
32 |
33 | `fail_url` The url to redirect to if the key value fails.
34 |
35 | `fail_json` JSON that is returned on failure.
36 |
37 | `fail_status` The status code to return if the check fails, defaults to `403`.
38 |
39 | `message` If a message is specified, a flash message is shown.
40 |
41 | `message_category` The category of the flash message.
42 |
43 | If fail_json is provided, passing to endpoints will be disabled.
44 |
45 | `pass_url` and `fail_url` take either a string or the `utilities.lazy_url_for` function.
46 |
47 | **Examples:**
48 |
49 | ```python
50 | LOG_IN_REQ = SessionCheckpoint(
51 | "logged_in", True
52 | ).action(
53 | lazy_url_for("login_page"),
54 | message="Login required for this page!"
55 | )
56 |
57 |
58 | @bp.route("/admin", methods=["GET"])
59 | @checkpoint(LOG_IN_REQ)
60 | def admin_page():
61 | ...
62 | ```
63 |
64 | **Example of multiple checks:**
65 |
66 | ```python
67 | LOG_IN_REQ = SessionCheckpoint(
68 | session_key="logged_in",
69 | values_allowed=True,
70 | ).action(
71 | fail_url=lazy_url_for("blueprint.login_page"),
72 | message="Login needed" # This will set Flask's flash message
73 | )
74 |
75 | ADMIN_PERM = SessionCheckpoint(
76 | session_key="user_type",
77 | values_allowed="admin",
78 | ).action(
79 | fail_url=lazy_url_for("blueprint.index"),
80 | message="You need to be an admin to access this page"
81 | )
82 |
83 |
84 | @bp.route("/admin", methods=["GET"])
85 | @checkpoint(LOG_IN_REQ)
86 | @checkpoint(ADMIN_PERM)
87 | def admin_page():
88 | ...
89 | ```
90 |
91 | **Example of a route that if the user is already logged in, redirects to the specified endpoint:**
92 |
93 | ```python
94 | IS_LOGGED_IN = SessionCheckpoint(
95 | session_key='logged_in',
96 | values_allowed=True,
97 | ).action(
98 | pass_endpoint='blueprint.admin_page',
99 | message="Already logged in"
100 | )
101 |
102 |
103 | @bp.route("/login-page", methods=["GET"])
104 | @checkpoint(IS_LOGGED_IN)
105 | def login_page():
106 | ...
107 | ```
108 |
--------------------------------------------------------------------------------
/docs/Imp/Imp-Introduction.md:
--------------------------------------------------------------------------------
1 | # Flask-Imp Introduction
2 |
3 | Flask-Imp is a Flask extension that provides auto import methods for various Flask resources. It will import models,
4 | blueprints, and other resources. It uses the importlib module to achieve this.
5 |
6 | Flask-Imp favors the application factory pattern as a project structure, and is opinionated towards using
7 | Blueprints. However, you can use Flask-Imp without using Blueprints.
8 |
9 | Here's an example of a standard Flask-Imp project structure:
10 |
11 | ```text
12 | app/
13 | ├── blueprints/
14 | │ ├── admin/...
15 | │ ├── api/...
16 | │ └── www/...
17 | ├── resources/
18 | │ ├── filters/...
19 | │ └── context_processors/...
20 | ├── models/...
21 | ├── static/...
22 | ├── templates/...
23 | └── __init__.py
24 | ```
25 |
26 | Here's an example of the `app/__init__.py` file:
27 |
28 | ```python
29 | from flask import Flask
30 | from flask_sqlalchemy import SQLAlchemy
31 | from flask_imp import Imp
32 | from flask_imp.config import FlaskConfig, ImpConfig
33 |
34 | db = SQLAlchemy()
35 | imp = Imp()
36 |
37 |
38 | def create_app():
39 | app = Flask(
40 | __name__,
41 | static_folder="static",
42 | template_folder="templates"
43 | )
44 | FlaskConfig(
45 | secret_key="super_secret_key",
46 | app_instance=app,
47 | )
48 |
49 | imp.init_app(app, config=ImpConfig(
50 | init_session={"logged_in": False},
51 | ))
52 | imp.import_resources("resources")
53 | imp.import_models("models")
54 | imp.import_blueprints("blueprints")
55 |
56 | db.init_app(app)
57 |
58 | return app
59 | ```
60 |
61 | The Flask configuration can be loaded from any standard Flask configuration method, or from the `FlaskConfig` class
62 | shown above.
63 |
64 | This class contains the standard Flask configuration options found in the Flask documentation.
65 |
66 | The `ImpConfig` class is used to configure the `Imp` instance.
67 |
68 | The `init_session` option of the `ImpConfig` class is used to set the initial session variables for the Flask app.
69 | This happens before the request is processed.
70 |
71 | `ImpConfig` also has the ability to set `SQLALCHEMY_DATABASE_URI` and `SQLALCHEMY_BINDS`
72 |
73 | For more information about the configuration setting see
74 | [flask_imp_config-impconfig](../Config/flask_imp_config-impconfig.md).
75 |
76 | `import_resources` will walk one level deep into the `resources` folder, and import
77 | all `.py` files as modules. It will search the imports for a function called `include`
78 | and pass the app as the first argument.
79 |
80 | There is a couple of options for `import_resources` to control what
81 | is imported, see: [Imp / import_resources](../Imp/Imp-import_resources.md)
82 |
83 | `import_models` will import all Model classes from the specified file or folder. It will also place each model found
84 | into a lookup table that you can access via `imp.model`
85 |
86 | See more about how import_models and the lookup
87 | here: [Imp / import_models](../Imp/Imp-import_models.md) and [Imp / model](../Imp/Imp-model.md)
88 |
89 | `import_blueprints` expects a folder that contains many Blueprint as Python packages.
90 | It will check each blueprint folder's `__init__.py` file for an instance of a Flask Blueprint or a
91 | Flask-Imp Blueprint. That instant will then be registered with the Flask app.
92 |
93 | See more about how importing blueprints work here: [ImpBlueprint / Introduction](../ImpBlueprint/ImpBlueprint-Introduction.md)
94 |
95 |
--------------------------------------------------------------------------------
/docs/Imp/Imp-import_resources.md:
--------------------------------------------------------------------------------
1 | # Imp.import_resources
2 |
3 | ```python
4 | def import_resources(
5 | folder: str = "resources",
6 | factories: t.Optional[t.List[str], str] = "include",
7 | scope_import: t.Optional[
8 | t.Dict[str, t.Union[t.List[str], str]]
9 | ] = None
10 | ) -> None:
11 | ```
12 |
13 | ---
14 |
15 | Will import all the resources (cli, routes, filters, context_processors...)
16 | from the given folder wrapped by the defined factory/factories.
17 |
18 | The given folder must be relative to the root of the app.
19 |
20 | `folder` the folder to import from - must be relative
21 |
22 | `factories` a list of or single function name(s) to pass the app
23 | instance to and call. Defaults to "include"
24 |
25 | `scope_import` a dict of files to import e.g. `{"folder_name": "*"}`
26 |
27 | **Examples:**
28 |
29 | ```python
30 | imp.import_resources(folder="resources")
31 | # or
32 | imp.import_resources()
33 | # as the default folder is "resources"
34 | ```
35 |
36 | Folder Structure: `resources`
37 |
38 | ```text
39 | app
40 | ├── resources
41 | │ ├── routes.py
42 | │ └── app_fac.py
43 | └── ...
44 | ...
45 | ```
46 |
47 | File: `routes.py`
48 |
49 | ```python
50 | from flask import Flask
51 | from flask import render_template
52 |
53 | def include(app: Flask):
54 | @app.route("/")
55 | def index():
56 | return render_template("index.html")
57 | ```
58 |
59 | ## How factories work
60 |
61 | Factories are the names of functions that are called when importing the resource.
62 | The default factory is `include`, Here's an example of changing the default:
63 |
64 | ```python
65 | imp.import_resources(
66 | folder="resources",
67 | factories="development"
68 | )
69 | ```
70 |
71 | `"development"` => `development(app)` function will be called, and the current app will be passed in.
72 |
73 | File: `app_fac.py`
74 |
75 | ```python
76 | def development(app: Flask):
77 | @app.cli.command("dev")
78 | def dev():
79 | print("dev cli command")
80 | ```
81 |
82 | A list of factories can be passed in:
83 |
84 | ```python
85 | imp.import_resources(
86 | folder="resources",
87 | factories=["development", "production"]
88 | )
89 | ```
90 |
91 | ```python
92 | def development(app: Flask):
93 | @app.cli.command("dev")
94 | def dev():
95 | print("dev cli command")
96 |
97 | def production(app: Flask):
98 | @app.cli.command("create-db")
99 | def prod():
100 | print("create-db cli command")
101 | ```
102 |
103 | This feature can be useful to feature flag certain resources.
104 |
105 | ## Scoping imports
106 |
107 | All files and folders will be imported by default. Here's an example of how to scope to
108 | specific folders or files:
109 |
110 | ```python
111 | imp.import_resources(scope_import={"*": ["cli.py"]})
112 | ```
113 |
114 | This will import the file `cli.py` from any folder found in the `resources` folder.
115 |
116 | ```text
117 | app/
118 | ├── resources/
119 | │ ├── clients/
120 | │ │ ├── cli.py
121 | │ │ └── other.py <- Will not be included
122 | │ └── database/
123 | │ └── cli.py
124 | └── ...
125 | ...
126 | ```
127 |
128 | This will only import the file named `cli.py` from the `clients` folder:
129 |
130 | ```python
131 | scope_import={"clients": ["cli.py"]}
132 | ```
133 |
134 | This will only import from the `resouces` folder itself, and skip any other folder:
135 |
136 | ```python
137 | scope_import={".": ["cli.py"]}
138 | ```
139 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Flask-Imp 🧚
2 |
3 | 
4 | [](https://pypi.org/project/flask-imp/)
5 | [](https://raw.githubusercontent.com/CheeseCake87/flask-imp/master/LICENSE)
6 |
7 | ## What is Flask-Imp?
8 |
9 | Flask-Imp's main purpose is to help simplify the importing of blueprints, resources, and
10 | models. It has a few extra
11 | features built in to help with securing pages and password authentication.
12 |
13 | ```bash
14 | pip install flask-imp
15 | ```
16 |
17 |
18 |
19 | ```{toctree}
20 | :maxdepth: 1
21 |
22 | getting-started.md
23 | Imp/Imp-Introduction.md
24 | ImpBlueprint/ImpBlueprint-Introduction.md
25 | ```
26 |
27 | ```{toctree}
28 | :caption: CLI Commands
29 | :maxdepth: 1
30 |
31 | CLI_Commands/CLI_Commands-flask-imp_init.md
32 | CLI_Commands/CLI_Commands-flask-imp_blueprint.md
33 | ```
34 |
35 | ```{toctree}
36 | :caption: Config
37 | :maxdepth: 1
38 |
39 | Config/flask_imp_config-flaskconfig.md
40 | Config/flask_imp_config-impconfig.md
41 | Config/flask_imp_config-impblueprintconfig.md
42 | Config/flask_imp_config-databaseconfig.md
43 | Config/flask_imp_config-sqldatabaseconfig.md
44 | Config/flask_imp_config-sqlitedatabaseconfig.md
45 | ```
46 |
47 | ```{toctree}
48 | :caption: Imp
49 | :maxdepth: 1
50 |
51 | Imp/Imp-init_app-init.md
52 | Imp/Imp-import_resources.md
53 | Imp/Imp-import_blueprint.md
54 | Imp/Imp-import_blueprints.md
55 | Imp/Imp-register_imp_blueprint.md
56 | Imp/Imp-import_models.md
57 | Imp/Imp-model.md
58 | ```
59 |
60 | ```{toctree}
61 | :caption: ImpBlueprint
62 | :maxdepth: 1
63 |
64 | ImpBlueprint/ImpBlueprint-init.md
65 | ImpBlueprint/ImpBlueprint-import_resources.md
66 | ImpBlueprint/ImpBlueprint-import_nested_blueprint.md
67 | ImpBlueprint/ImpBlueprint-import_nested_blueprints.md
68 | ImpBlueprint/ImpBlueprint-import_models.md
69 | ImpBlueprint/ImpBlueprint-tmpl.md
70 | ```
71 |
72 | ```{toctree}
73 | :caption: Security
74 | :maxdepth: 1
75 |
76 | Security/flask_imp_security-checkpoint.md
77 | Security/flask_imp_security-checkpoint_callable.md
78 | Security/flask_imp_security-include_csrf.md
79 | ```
80 |
81 | ```{toctree}
82 | :caption: Security Checkpoints
83 | :maxdepth: 1
84 |
85 | SecurityCheckpoints/flask_imp_security-apikeycheckpoint.md
86 | SecurityCheckpoints/flask_imp_security-bearercheckpoint.md
87 | SecurityCheckpoints/flask_imp_security-sessioncheckpoint.md
88 | SecurityCheckpoints/flask_imp_security-createacheckpoint.md
89 | ```
90 |
91 | ```{toctree}
92 | :caption: Auth
93 | :maxdepth: 1
94 |
95 | Auth/flask_imp_auth-authenticate_password.md
96 | Auth/flask_imp_auth-encrypt_password.md
97 | Auth/flask_imp_auth-generate_alphanumeric_validator.md
98 | Auth/flask_imp_auth-generate_csrf_token.md
99 | Auth/flask_imp_auth-generate_email_validator.md
100 | Auth/flask_imp_auth-generate_numeric_validator.md
101 | Auth/flask_imp_auth-generate_password.md
102 | Auth/flask_imp_auth-generate_private_key.md
103 | Auth/flask_imp_auth-generate_salt.md
104 | Auth/flask_imp_auth-is_email_address_valid.md
105 | Auth/flask_imp_auth-is_username_valid.md
106 | ```
107 |
108 | ```{toctree}
109 | :caption: Utilities
110 | :maxdepth: 1
111 |
112 | Utilities/flask_imp_utilities-lazy_url_for.md
113 | Utilities/flask_imp_utilities-lazy_session_get.md
114 | ```
115 |
116 | ```{toctree}
117 | :caption: API
118 |
119 | API/index
120 | ```
121 |
--------------------------------------------------------------------------------
/src/flask_imp/config/_imp_blueprint_config.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import typing as t
4 | from dataclasses import dataclass
5 |
6 | if t.TYPE_CHECKING:
7 | from ._database_config import DatabaseConfig
8 | from ._sql_database_config import SQLDatabaseConfig
9 | from ._sqlite_database_config import SQLiteDatabaseConfig
10 |
11 |
12 | @dataclass
13 | class ImpBlueprintConfig:
14 | """
15 | Blueprint configuration class used by the ImpBlueprint class.
16 | """
17 |
18 | enabled: bool
19 | url_prefix: t.Optional[str] = None
20 | subdomain: t.Optional[str] = None
21 | url_default: t.Optional[t.Dict[str, t.Any]] = None
22 | static_folder: t.Optional[str] = None
23 | template_folder: t.Optional[str] = None
24 | static_url_path: t.Optional[str] = None
25 | root_path: t.Optional[str] = None
26 | cli_group: t.Optional[str] = None
27 |
28 | init_session: t.Optional[t.Dict[str, t.Any]] = None
29 |
30 | database_binds: t.Optional[
31 | t.Iterable[t.Union[DatabaseConfig, SQLDatabaseConfig, SQLiteDatabaseConfig]]
32 | ] = None
33 |
34 | _blueprint_attrs = {
35 | "url_prefix",
36 | "subdomain",
37 | "url_defaults",
38 | "static_folder",
39 | "template_folder",
40 | "static_url_path",
41 | "root_path",
42 | "cli_group",
43 | }
44 |
45 | def __init__(
46 | self,
47 | enabled: bool = True,
48 | url_prefix: t.Optional[str] = None,
49 | subdomain: t.Optional[str] = None,
50 | url_defaults: t.Optional[t.Dict[str, t.Any]] = None,
51 | static_folder: t.Optional[str] = None,
52 | template_folder: t.Optional[str] = None,
53 | static_url_path: t.Optional[str] = None,
54 | root_path: t.Optional[str] = None,
55 | cli_group: t.Optional[str] = None,
56 | init_session: t.Optional[t.Dict[str, t.Any]] = None,
57 | database_binds: t.Optional[
58 | t.Iterable[t.Union[DatabaseConfig, SQLDatabaseConfig, SQLiteDatabaseConfig]]
59 | ] = None,
60 | ):
61 | """
62 | Blueprint configuration class used by the ImpBlueprint class.
63 |
64 | This configuration is used to configure a regular Flask Blueprint with additional
65 | configuration options.
66 |
67 | :param enabled: whether the blueprint is enabled - defaults to False
68 | :param url_prefix: the blueprint URL prefix - defaults to None
69 | :param subdomain: the blueprint subdomain - defaults to None
70 | :param url_defaults: the blueprint URL defaults - defaults to None
71 | :param static_folder: the blueprint static folder - defaults to None
72 | :param template_folder: the blueprint template folder - defaults to None
73 | :param static_url_path: the blueprint static URL path - defaults to None
74 | :param root_path: the blueprint root path - defaults to None
75 | :param cli_group: the blueprint CLI group - defaults to None
76 | :param init_session: the blueprint initial session - defaults to None
77 | :param database_binds: the blueprint database binds - defaults to None
78 | """
79 | self.enabled = enabled
80 | self.url_prefix = url_prefix
81 | self.subdomain = subdomain
82 | self.url_defaults = url_defaults
83 | self.static_folder = static_folder
84 | self.template_folder = template_folder
85 | self.static_url_path = static_url_path
86 | self.root_path = root_path
87 | self.cli_group = cli_group
88 | self.init_session = init_session
89 |
90 | if database_binds is None:
91 | self.database_binds = []
92 | else:
93 | self.database_binds = database_binds
94 |
95 | def flask_blueprint_args(self) -> t.Dict[str, t.Any]:
96 | return {k: getattr(self, k) for k in self._blueprint_attrs if getattr(self, k)}
97 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ## Install Flask-Imp
4 |
5 | **Install in a [Virtual environment](#setup-a-virtual-environment)**
6 |
7 | ```bash
8 | pip install flask-imp
9 | ```
10 |
11 | To get started right away, you can use the CLI commands to create a new Flask-Imp project.
12 |
13 | ```bash
14 | flask-imp init
15 | ```
16 |
17 | ### Minimal Flask-Imp Setup
18 |
19 | Run the following command to create a minimal Flask-Imp project.
20 |
21 | ```bash
22 | flask-imp init -n app --minimal
23 | ```
24 |
25 | ### The minimal structure
26 |
27 | #### Folder Structure
28 |
29 | ```text
30 | app/
31 | ├── resources/
32 | │ ├── static/...
33 | │ ├── templates/
34 | │ │ └── index.html
35 | │ └── index.py
36 | └── __init__.py
37 | ```
38 |
39 | File: `app/__init__.py`
40 |
41 | ```python
42 | from flask import Flask
43 |
44 | from flask_imp import Imp
45 | from flask_imp.config import FlaskConfig, ImpConfig
46 |
47 | imp = Imp()
48 |
49 |
50 | def create_app():
51 | app = Flask(__name__, static_url_path="/")
52 | FlaskConfig(
53 | secret_key="secret_key",
54 | app_instance=app
55 | )
56 |
57 | imp.init_app(app, ImpConfig())
58 |
59 | imp.import_resources()
60 | # Takes argument 'folder' default folder is 'resources'
61 |
62 | return app
63 | ```
64 |
65 | File: `app/resources/routes.py`
66 |
67 | ```python
68 | from flask import Flask
69 | from flask import render_template
70 |
71 | def include(app: Flask):
72 | @app.route("/")
73 | def index():
74 | return render_template("index.html")
75 | ```
76 |
77 | File: `app/resources/templates/index.html`
78 |
79 | ```html
80 |
81 |
82 |
83 |
84 | Flask-Imp
85 |
86 |
87 | Flask-Imp
88 |
89 |
90 | ```
91 |
92 | **For more examples see: [CLI Commands / flask-imp init](CLI_Commands/CLI_Commands-flask-imp_init.md)**
93 |
94 | ---
95 |
96 | ## Securing Routes
97 |
98 | Use the `checkpoint` decorator to secure routes, here's an example:
99 |
100 | Update the `app/resources/routes.py` file with the following code:
101 |
102 | ```python
103 | from flask import Flask
104 | from flask import render_template
105 | from flask import url_for
106 | from flask import redirect
107 | from flask import session
108 | from flask_imp.security import checkpoint, SessionCheckpoint
109 | from flask_imp.utilities import lazy_url_for
110 |
111 | LOGIN_REQUIRED = SessionCheckpoint(
112 | session_key="logged_in",
113 | values_allowed=True,
114 | ).action(
115 | fail_url=lazy_url_for("login_required")
116 | )
117 |
118 |
119 | def include(app: Flask):
120 | @app.route("/")
121 | def index():
122 | return render_template("index.html")
123 |
124 | @app.route("/protected")
125 | @checkpoint(LOGIN_REQUIRED)
126 | def protected():
127 | return "You are logged in!"
128 |
129 | @app.route("/login-required")
130 | def login_required():
131 | return "You need to login first!"
132 |
133 | @app.route("/login")
134 | def login():
135 | session["logged_in"] = True
136 | return redirect(url_for("protected"))
137 |
138 | @app.route("/logout")
139 | def logout():
140 | session["logged_in"] = False
141 | return redirect(url_for("index"))
142 | ```
143 |
144 | **See more at: [Security / checkpoint](Security/flask_imp_security-checkpoint.md)**
145 |
146 | ## Setup a Virtual Environment
147 |
148 | Setting up a virtual environment is recommended.
149 |
150 | **Linux / Darwin**
151 |
152 | ```bash
153 | python3 -m venv venv
154 | ```
155 |
156 | ```bash
157 | source venv/bin/activate
158 | ```
159 |
160 | **Windows**
161 |
162 | ```bash
163 | python -m venv venv
164 | ```
165 |
166 | ```text
167 | .\venv\Scripts\activate
168 | ```
169 |
--------------------------------------------------------------------------------
/docs/ImpBlueprint/ImpBlueprint-import_resources.md:
--------------------------------------------------------------------------------
1 | # ImpBlueprint.import_resources
2 |
3 | ```python
4 | def import_resources(
5 | folder: str = "resources",
6 | factories: t.Optional[t.List[str], str] = "include",
7 | scope_import: t.Optional[
8 | t.Dict[str, t.Union[t.List[str], str]]
9 | ] = None
10 | ) -> None:
11 | ```
12 |
13 | ---
14 |
15 | Will import all the resources (cli, routes, filters, context_processors...)
16 | from the given folder wrapped by the defined factory/factories.
17 |
18 | The given folder must be relative to the root of the blueprint.
19 |
20 | `folder` the folder to import from - must be relative
21 |
22 | `factories` a list of or single function name(s) to pass the blueprint
23 | instance to and call. Defaults to "include"
24 |
25 | `scope_import` a dict of files to import e.g. `{"folder_name": "*"}`
26 |
27 | **Examples:**
28 |
29 | ```python
30 | bp = ImpBlueprint(__name__, ImpBlueprintConfig(...))
31 |
32 | bp.import_resources(folder="resources")
33 | # or
34 | bp.import_resources()
35 | # as the default folder is "resources"
36 | ```
37 |
38 | Here's an example blueprint folder structure:
39 |
40 | ```text
41 | my_blueprint
42 | ├── user_routes
43 | │ ├── user_dashboard.py
44 | │ └── user_settings.py
45 | ├── static/...
46 | ├── templates/
47 | │ └── my_blueprint/
48 | │ ├── user_dashboard.html
49 | │ └── ...
50 | ├── __init__.py
51 | ```
52 |
53 | File: `user_routes/user_dashboard.py`
54 |
55 | ```python
56 | from flask_imp import ImpBlueprint
57 | from flask import render_template
58 |
59 | def include(bp: ImpBlueprint):
60 | @bp.route("/")
61 | def user_dashboard():
62 | return render_template("user_dashboard.html")
63 | ```
64 |
65 | ## How factories work
66 |
67 | Factories are the names of functions that are called when importing the resource.
68 | The default factory is `include`, Here's an example of changing the default:
69 |
70 | ```python
71 | bp.import_resources(
72 | folder="resources",
73 | factories="development"
74 | )
75 | ```
76 |
77 | `"development"` => `development(app)` function will be called, and the current app will be passed in.
78 |
79 | File: `user_routes/user_settings.py`
80 |
81 | ```python
82 | def development(bp: ImpBlueprint):
83 | @bp.cli.command("reset-user-settings")
84 | def reset_user_settings():
85 | print("reset-user-settings cli command")
86 | ```
87 |
88 | A list of factories can be passed in:
89 |
90 | ```python
91 | bp.import_resources(
92 | folder="resources",
93 | factories=["development", "production"]
94 | )
95 | ```
96 |
97 | ```python
98 | def development(bp: ImpBlueprint):
99 | @bp.cli.command("reset-user-settings")
100 | def reset_user_settings():
101 | print("reset-user-settings cli command")
102 |
103 | def production(bp: ImpBlueprint):
104 | @bp.cli.command("show-user-settings")
105 | def show_user_settings():
106 | print("show-user-settings cli command")
107 | ```
108 |
109 | This feature can be useful to feature flag certain resources.
110 |
111 | ## Scoping imports
112 |
113 | All files and folders will be imported by default. Here's an example of how to scope to
114 | specific folders or files:
115 |
116 | ```python
117 | bp.import_resources(scope_import={"*": ["cli.py"]})
118 | ```
119 |
120 | This will import the file `cli.py` from any folder found in the `resources` folder.
121 |
122 | ```text
123 | my_blueprint/
124 | ├── resources/
125 | │ ├── clients/
126 | │ │ ├── cli.py
127 | │ │ └── other.py <- Will not be included
128 | │ └── database/
129 | │ └── cli.py
130 | └── ...
131 | ...
132 | ```
133 |
134 | This will only import the file named `cli.py` from the `clients` folder:
135 |
136 | ```python
137 | scope_import={"clients": ["cli.py"]}
138 | ```
139 |
140 | This will only import from the `resouces` folder itself, and skip any other folder:
141 |
142 | ```python
143 | scope_import={".": ["cli.py"]}
144 | ```
145 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/filelib/blueprint.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from ..helpers import strip_leading_slash
4 |
5 |
6 | def blueprint_init_py(url_prefix: str, name: str) -> str:
7 | return f"""\
8 | from flask_imp import ImpBlueprint
9 | from flask_imp.config import ImpBlueprintConfig
10 |
11 |
12 | bp = ImpBlueprint(__name__, ImpBlueprintConfig(
13 | enabled=True,
14 | url_prefix="/{strip_leading_slash(url_prefix)}",
15 | static_folder="static",
16 | template_folder="templates",
17 | init_session={{"{name}_session_loaded": True}},
18 | ))
19 |
20 | bp.import_resources()
21 | """
22 |
23 |
24 | def blueprint_resources_index_py() -> str:
25 | return """\
26 | from flask import render_template
27 | from flask_imp import ImpBlueprint
28 |
29 |
30 | def include(bp: ImpBlueprint):
31 | @bp.route("/", methods=["GET"])
32 | def index():
33 | return render_template(bp.tmpl("index.html"))
34 | """
35 |
36 |
37 | def blueprint_templates_index_html(blueprint_name: str, root: Path) -> str:
38 | return f"""\
39 | {{% extends '{blueprint_name}/extends/main.html' %}}
40 |
41 | {{% block content %}}
42 |
43 |
44 |
Blueprint: {blueprint_name}
45 |
Here's your new blueprint.
46 |
Located here: {root}
47 |
48 |
49 | {{% endblock %}}
50 | """
51 |
52 |
53 | def blueprint_init_app_templates_index_html(
54 | blueprint_name: str,
55 | index_html: Path,
56 | extends_main_html: Path,
57 | index_py: Path,
58 | init_py: Path,
59 | ) -> str:
60 | return f"""\
61 | {{% extends 'www/extends/main.html' %}}
62 |
63 | {{% block content %}}
64 |
65 |
66 |
Blueprint: {blueprint_name}
67 |
This is the index route of the included example blueprint.
68 |
69 | This template page is located in {index_html}
70 | it extends from {extends_main_html}
71 | with its route defined in {index_py}
72 | It's being imported by bp.import_resources("routes")
73 | in the {init_py} file.
74 |
75 |
76 |
77 | {{% endblock %}}
78 | """
79 |
80 |
81 | def blueprint_templates_extends_main_html(name: str, head_tag: str) -> str:
82 | return f"""\
83 |
84 |
85 |
86 |
87 | {head_tag}
88 |
89 |
90 |
91 | {{% include '{name}/includes/header.html' %}}
92 | {{% block content %}}{{% endblock %}}
93 | {{% include '{name}/includes/footer.html' %}}
94 |
95 |
96 |
97 | """
98 |
99 |
100 | def blueprint_templates_includes_header_html(header_html: Path, main_html: Path) -> str:
101 | return f"""\
102 |
104 |
Flask-Imp 🧚
105 |
106 |
107 |
This is the header, located here: {header_html}
108 |
It's being imported in the {main_html} template.
109 |
110 | """
111 |
112 |
113 | def blueprint_templates_includes_footer_html(footer_html: Path, main_html: Path) -> str:
114 | return f"""\
115 |
116 |
117 |
This is the footer, located here: {footer_html}
118 |
It's being imported in the {main_html} template.
119 |
120 |
121 | """
122 |
--------------------------------------------------------------------------------
/src/flask_imp/config/_database_config.py:
--------------------------------------------------------------------------------
1 | import typing as t
2 | from pathlib import Path
3 |
4 |
5 | class DatabaseConfig:
6 | """
7 | Database configuration class used by ImpConfig, or ImpBlueprintConfig.
8 | """
9 |
10 | enabled: bool
11 | dialect: t.Literal["mysql", "postgresql", "sqlite", "oracle", "mssql"]
12 | bind_key: t.Optional[str]
13 | database_name: str
14 | location: str
15 | port: int
16 | username: str
17 | password: str
18 |
19 | sqlite_db_extension: str
20 |
21 | allowed_dialects: t.Tuple[str, ...] = (
22 | "mysql",
23 | "postgresql",
24 | "sqlite",
25 | "oracle",
26 | "mssql",
27 | )
28 |
29 | def __init__(
30 | self,
31 | dialect: t.Literal[
32 | "mysql", "postgresql", "sqlite", "oracle", "mssql"
33 | ] = "sqlite",
34 | database_name: str = "database",
35 | location: str = "",
36 | port: int = 0,
37 | username: str = "",
38 | password: str = "",
39 | sqlite_db_extension: str = ".sqlite",
40 | bind_key: t.Optional[str] = None,
41 | enabled: bool = True,
42 | ):
43 | """
44 | Database configuration class used by ImpConfig, or ImpBlueprintConfig.
45 |
46 | Allowed dialects: mysql, postgresql, sqlite, oracle, mssql
47 |
48 | sqlite database will be stored in the app instance path.
49 |
50 | **Note:**
51 |
52 | - If the dialect is sqlite, the location, port, username, and password are not used.
53 |
54 | *Replaced by:*
55 |
56 | - :class:`flask_imp.config.SQLDatabaseConfig`
57 | - :class:`flask_imp.config.SQLiteDatabaseConfig`
58 |
59 | :param enabled: whether the database is enabled - defaults to True
60 | :param dialect: the database dialect - defaults to sqlite
61 | :param name: the database name - defaults to database
62 | :param bind_key: the database bind key - Optional
63 | :param location: the database location - Optional
64 | :param port: the database port - Optional
65 | :param username: the database username - Optional
66 | :param password: the database password - Optional
67 | :param sqlite_db_extension: the sqlite database extension - defaults to .sqlite
68 | """
69 | if dialect not in self.allowed_dialects:
70 | raise ValueError(
71 | f"Database dialect must be one of: {', '.join(self.allowed_dialects)}"
72 | )
73 |
74 | self.enabled = enabled
75 | self.dialect = dialect
76 | self.database_name = database_name
77 | self.bind_key = bind_key
78 | self.location = location
79 | self.port = port
80 | self.username = username
81 | self.password = password
82 |
83 | self.sqlite_db_extension = sqlite_db_extension
84 |
85 | def as_dict(self) -> t.Dict[str, t.Any]:
86 | """
87 | Return the database configuration as a dictionary.
88 |
89 | :return: the database configuration as a dictionary
90 | """
91 | return {
92 | "enabled": self.enabled,
93 | "dialect": self.dialect,
94 | "database_name": self.database_name,
95 | "bind_key": self.bind_key,
96 | "location": self.location,
97 | "port": self.port,
98 | "username": self.username,
99 | "password": self.password,
100 | "sqlite_db_extension": self.sqlite_db_extension,
101 | }
102 |
103 | def uri(self, app_instance_path: Path) -> str:
104 | """
105 | Return the database URI.
106 |
107 | :param app_instance_path: the app instance path
108 | :return: the database URI
109 | """
110 | if self.dialect == "sqlite":
111 | filepath = app_instance_path / (
112 | self.database_name + self.sqlite_db_extension
113 | )
114 | return f"{self.dialect}:///{filepath}"
115 |
116 | return (
117 | f"{self.dialect}://{self.username}:"
118 | f"{self.password}@{self.location}:"
119 | f"{self.port}/{self.database_name}"
120 | )
121 |
--------------------------------------------------------------------------------
/src/flask_imp/_cli/filelib/resources.py:
--------------------------------------------------------------------------------
1 | def resources_cli_py() -> str:
2 | return """\
3 | from flask import Flask
4 |
5 |
6 | def include(app: Flask):
7 | @app.cli.command("show-config")
8 | def show_config():
9 | print(app.config)
10 | """
11 |
12 |
13 | def resources_context_processors_py() -> str:
14 | return """\
15 | from flask import Flask
16 |
17 |
18 | def include(app: Flask):
19 | @app.context_processor
20 | def example__utility_processor():
21 | \"""
22 | Usage:
23 | {{ example__format_price(100.33) }} -> $100.33
24 | \"""
25 |
26 | def example__format_price(amount, currency='$'):
27 | return '{1}{0:.2f}'.format(amount, currency)
28 |
29 | return dict(example__format_price=example__format_price)
30 | """
31 |
32 |
33 | def resources_error_handlers_py() -> str:
34 | return """\
35 | from flask import Flask
36 |
37 | from flask import render_template
38 |
39 |
40 | def include(app: Flask):
41 | @app.errorhandler(400)
42 | def bad_request(e):
43 | return render_template(
44 | "error.html",
45 | error_code=400,
46 | error_message="The request is invalid.",
47 | ), 400
48 |
49 |
50 | @app.errorhandler(401)
51 | def unauthorized(e):
52 | return render_template(
53 | "error.html",
54 | error_code=401,
55 | error_message="You are not authorized to access this page.",
56 | ), 401
57 |
58 |
59 | @app.errorhandler(403)
60 | def forbidden(e):
61 | return render_template(
62 | "error.html",
63 | error_code=403,
64 | error_message="You do not have permission to access this page.",
65 | ), 403
66 |
67 |
68 | @app.errorhandler(404)
69 | def page_not_found(e):
70 | return render_template(
71 | "error.html",
72 | error_code=404,
73 | error_message="The page you are looking for does not exist.",
74 |
75 | ), 404
76 |
77 |
78 | @app.errorhandler(405)
79 | def method_not_allowed(e):
80 | return render_template(
81 | "error.html",
82 | error_code=405,
83 | error_message="The method is not allowed for the requested URL.",
84 | ), 405
85 |
86 |
87 | @app.errorhandler(410)
88 | def gone(e):
89 | return render_template(
90 | "error.html",
91 | error_code=410,
92 | error_message="This page is no longer available.",
93 | ), 410
94 |
95 |
96 | @app.errorhandler(429)
97 | def too_many_requests(e):
98 | return render_template(
99 | "error.html",
100 | error_code=429,
101 | error_message="You have made too many requests.",
102 | ), 429
103 |
104 |
105 | @app.errorhandler(500)
106 | def server_error(e):
107 | return render_template(
108 | "error.html",
109 | error_code=500,
110 | error_message="An internal server error has occurred.",
111 | ), 500
112 |
113 | """
114 |
115 |
116 | def resources_filters_py() -> str:
117 | return """\
118 | from flask import Flask
119 |
120 |
121 | def include(app: Flask):
122 | @app.template_filter('example__num_to_month')
123 | def example__num_to_month(num: str) -> str:
124 | \"""
125 | Usage:
126 | {{ 1 | example__num_to_month }} -> January
127 | \"""
128 | if isinstance(num, int):
129 | num = str(num)
130 |
131 | months = {
132 | "1": "January",
133 | "2": "February",
134 | "3": "March",
135 | "4": "April",
136 | "5": "May",
137 | "6": "June",
138 | "7": "July",
139 | "8": "August",
140 | "9": "September",
141 | "10": "October",
142 | "11": "November",
143 | "12": "December",
144 | }
145 |
146 | if num in months:
147 | return months[num]
148 | return "Month not found"
149 | """
150 |
151 |
152 | def resources_routes_py() -> str:
153 | return """\
154 | from flask import Flask
155 |
156 |
157 | def include(app: Flask):
158 | @app.route("/example--resources")
159 | def example_route():
160 | return "From the [app_root]/resources/routes/routes.py file"
161 | """
162 |
163 |
164 | def resources_minimal_routes_py() -> str:
165 | return """\
166 | from flask import Flask
167 | from flask import render_template
168 |
169 |
170 | def include(app: Flask):
171 | @app.route("/")
172 | def index():
173 | return render_template("index.html")
174 | """
175 |
--------------------------------------------------------------------------------
/docs/CLI_Commands/CLI_Commands-flask-imp_init.md:
--------------------------------------------------------------------------------
1 | # Initialising a Flask-Imp Project
2 |
3 | Flask-Imp has a cli command that deploys a new ready-to-go project.
4 | This project is structured in a way to give you the best idea of
5 | how to use Flask-Imp.
6 |
7 | ```bash
8 | flask-imp init --help
9 | ```
10 |
11 | ## Create a new project
12 |
13 | Make sure you are in the virtual environment, and at the root of your
14 | project folder, then run the following command:
15 |
16 | ```bash
17 | flask-imp init
18 | ```
19 |
20 | After running this command, you will be prompted to choose what type of
21 | app you want to deploy:
22 |
23 | ```text
24 | ~ $ flask-imp init
25 | What type of app would you like to create? (minimal, slim, full) [minimal]:
26 | ```
27 |
28 | See below for the differences between the app types.
29 |
30 | After this, you will be prompted to enter a name for your app:
31 |
32 | ```text
33 | ~ $ flask-imp init
34 | ...
35 | What would you like to call your app? [app]:
36 | ```
37 |
38 | 'app' is the default name, so if you just press enter, your app will be
39 | called 'app'. You will then see this output:
40 |
41 | ```text
42 | ~ FILES CREATED WILL LOOP OUT HERE ~
43 |
44 | ===================
45 | Flask app deployed!
46 | ===================
47 |
48 | Your app has the default name of 'app'
49 | Flask will automatically look for this!
50 | Run: flask run --debug
51 |
52 | ```
53 |
54 | If you called your app something other than 'app', like 'new' for example, you will see:
55 |
56 | ```text
57 | ~ FILES CREATED WILL LOOP OUT HERE ~
58 |
59 | ===================
60 | Flask app deployed!
61 | ===================
62 |
63 | Your app has the name of 'new'
64 | Run: flask --app new run --debug
65 |
66 | ```
67 |
68 | As you can see from the output, it gives you instructions on how to start your app,
69 | depending on the name you gave it.
70 |
71 | You should see a new folder that has been given the name you specified in
72 | the `flask-imp init` command.
73 |
74 | ### Additional options
75 |
76 | You can also specify a name for your app in the command itself, like so:
77 |
78 | ```bash
79 | flask-imp init -n my_app
80 | ```
81 |
82 | This will create a new app called 'my_app'.
83 | The default will be a minimal app, this has no blueprints or database models.
84 |
85 | You can also deploy a slim app, that will have one blueprint and no database models,
86 | like so:
87 |
88 | ```bash
89 | flask-imp init -n my_app --slim
90 | ```
91 |
92 | You can also deploy a full app that is setup for multiple blueprints and database
93 | models, like so:
94 |
95 | ```bash
96 | flask-imp init -n my_app --full
97 | ```
98 |
99 | ## init Folder structures
100 |
101 | ### Minimal app (default)
102 |
103 | `flask-imp init --minimal`:
104 |
105 | ```text
106 | app/
107 | ├── resources
108 | │ └── routes.py
109 | │
110 | ├── static
111 | │ ├── css
112 | │ │ └── water.css
113 | │ ├── img
114 | │ │ └── flask-imp-logo.png
115 | │ └── favicon.ico
116 | ├── templates
117 | │ └── index.html
118 | │
119 | └── __init__.py
120 | ```
121 |
122 | ### Slim app
123 |
124 | `flask-imp init --slim`:
125 |
126 | ```text
127 | app/
128 | ├── extensions
129 | │ └── __init__.py
130 | │
131 | ├── resources
132 | │ ├── cli
133 | │ │ └── cli.py
134 | │ └── error_handlers
135 | │ └── error_handlers.py
136 | │
137 | ├── www
138 | │ ├── __init__.py
139 | │ ├── routes
140 | │ │ └── index.py
141 | │ ├── static
142 | │ │ ├── css
143 | │ │ │ └── water.css
144 | │ │ ├── img
145 | │ │ │ └── flask-imp-logo.png
146 | │ │ └── js
147 | │ │ └── main.js
148 | │ └── templates
149 | │ └── www
150 | │ ├── extends
151 | │ │ └── main.html
152 | │ ├── includes
153 | │ │ ├── footer.html
154 | │ │ └── header.html
155 | │ └── index.html
156 | │
157 | ├── static
158 | │ ├── css
159 | │ │ └── water.css
160 | │ ├── img
161 | │ │ └── flask-imp-logo.png
162 | │ └── favicon.ico
163 | ├── templates
164 | │ └── index.html
165 | │
166 | └── __init__.py
167 | ```
168 |
169 | ### Full app
170 |
171 | `flask-imp init --full`:
172 |
173 | ```text
174 | app/
175 | ├── blueprints
176 | │ └── www
177 | │ ├── __init__.py
178 | │ ├── routes
179 | │ │ └── index.py
180 | │ ├── static
181 | │ │ ├── css
182 | │ │ │ └── water.css
183 | │ │ ├── img
184 | │ │ │ └── flask-imp-logo.png
185 | │ │ └── js
186 | │ │ └── main.js
187 | │ └── templates
188 | │ └── www
189 | │ ├── extends
190 | │ │ └── main.html
191 | │ ├── includes
192 | │ │ ├── footer.html
193 | │ │ └── header.html
194 | │ └── index.html
195 | │
196 | ├── extensions
197 | │ └── __init__.py
198 | │
199 | ├── resources
200 | │ ├── cli
201 | │ │ └── cli.py
202 | │ ├── context_processors
203 | │ │ └── context_processors.py
204 | │ ├── error_handlers
205 | │ │ └── error_handlers.py
206 | │ ├── filters
207 | │ │ └── filters.py
208 | │ └── routes
209 | │ └── routes.py
210 | │
211 | ├── models
212 | │ └── example_user_table.py
213 | │
214 | ├── static
215 | │ └── favicon.ico
216 | ├── templates
217 | │ └── error.html
218 | │
219 | └── __init__.py
220 | ```
221 |
222 |
--------------------------------------------------------------------------------