├── 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 |
2 | 3 | 4 |
5 | 6 | 7 |
8 | 9 | 10 |
-------------------------------------------------------------------------------- /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 | logo 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 | ![tests](https://github.com/CheeseCake87/flask-imp/actions/workflows/tests.yml/badge.svg) 4 | [![PyPI version](https://img.shields.io/pypi/v/flask-imp)](https://pypi.org/project/flask-imp/) 5 | [![License](https://img.shields.io/github/license/CheeseCake87/flask-imp)](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 | ![tests](https://github.com/CheeseCake87/flask-imp/actions/workflows/tests.yml/badge.svg) 4 | [![PyPI version](https://img.shields.io/pypi/v/flask-imp)](https://pypi.org/project/flask-imp/) 5 | [![License](https://img.shields.io/github/license/CheeseCake87/flask-imp)](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 | --------------------------------------------------------------------------------