diff --git a/.flaskenv b/.flaskenv index 2571abd..d55a3e1 100644 --- a/.flaskenv +++ b/.flaskenv @@ -1,20 +1,5 @@ -FLASK_APP=app.py -FLASK_ENV=developement - -# Application secret key (cookie encryption) -SECRET_KEY="this-is-a-secret-that-should-be-changed" - -# Application name -APP_NAME="Flask Boilerplate" - -# SQLite database name (stored in the root directory) -SQLITE_DB_NAME="app.db" - -# Enable maintenance mode (prevents requests to the application when set to True) -MAINTENANCE_MODE=False - -# Email settings configuration -MAIL_SERVER_HOST="127.0.0.1" -MAIL_SERVER_PORT=1025 -MAIL_SERVER_USERNAME="" -MAIL_SERVER_PASSWORD="" \ No newline at end of file +FLASK_APP = "app.py" +SECRET_KEY = "this-is-a-secret-that-should-be-changed" +APP_NAME = "Flask Boilerplate" +SQLITE_DB_NAME = "app.db" +MAINTENANCE_MODE = 'False' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..9fe58d8 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,17 @@ +name: flake8 Lint + +on: [push, pull_request] + +jobs: + flake8-lint: + runs-on: ubuntu-latest + name: Lint + steps: + - name: Check out source repository + uses: actions/checkout@v3 + - name: Set up Python environment + uses: actions/setup-python@v4 + with: + python-version: "3.11.3" + - name: flake8 Lint + uses: py-actions/flake8@v2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6e4761..7da26ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Byte-compiled / optimized / DLL files -__pycache__/ +**/__pycache__/ *.py[cod] *$py.class @@ -127,3 +127,4 @@ dmypy.json # Pyre type checker .pyre/ +.DS_Store diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index 30bab2a..0000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/flask-boilerplate.iml b/.idea/flask-boilerplate.iml deleted file mode 100644 index 1f52083..0000000 --- a/.idea/flask-boilerplate.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 8b5f487..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 9ff572e..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 2b3e4c2..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..d807be4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,17 @@ +{ + "recommendations": [ + // linters and formatters + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "stylelint.vscode-stylelint", + "ms-python.flake8", + "ms-python.black-formatter", + // python + "ms-python.python", + "ms-python.vscode-pylance", + // Jinja + "samuelcolvin.jinjahtml", + // misc + "jakearl.search-editor-apply-changes", + ] + } \ No newline at end of file diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000..afc701a --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,9 @@ +# Contributing to Flask Boilerplate + +If you have any suggestions or improvements, then feel free to open an issue or a pull request. I'll be happy to take a look at it! + +## Code style + +Please follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide when contributing to this project (or any other Python project, for that matter). In order to make your life easier when contributing, you can install [flake8](https://flake8.pycqa.org/en/latest/) and [black](https://black.readthedocs.io/en/stable/) and run `flake8` and `black .` before committing your changes. This will ensure that your code is formatted correctly and that it follows the PEP 8 style guide. + +For VSCode users, you will be prompted to install the recommended extensions when opening the project for the first time. \ No newline at end of file diff --git a/Include/site/python3.10/greenlet/greenlet.h b/Include/site/python3.10/greenlet/greenlet.h deleted file mode 100644 index d02a16e..0000000 --- a/Include/site/python3.10/greenlet/greenlet.h +++ /dev/null @@ -1,164 +0,0 @@ -/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ - -/* Greenlet object interface */ - -#ifndef Py_GREENLETOBJECT_H -#define Py_GREENLETOBJECT_H - - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* This is deprecated and undocumented. It does not change. */ -#define GREENLET_VERSION "1.0.0" - -#ifndef GREENLET_MODULE -#define implementation_ptr_t void* -#endif - -typedef struct _greenlet { - PyObject_HEAD - PyObject* weakreflist; - PyObject* dict; - implementation_ptr_t pimpl; -} PyGreenlet; - -#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) - - -/* C API functions */ - -/* Total number of symbols that are exported */ -#define PyGreenlet_API_pointers 12 - -#define PyGreenlet_Type_NUM 0 -#define PyExc_GreenletError_NUM 1 -#define PyExc_GreenletExit_NUM 2 - -#define PyGreenlet_New_NUM 3 -#define PyGreenlet_GetCurrent_NUM 4 -#define PyGreenlet_Throw_NUM 5 -#define PyGreenlet_Switch_NUM 6 -#define PyGreenlet_SetParent_NUM 7 - -#define PyGreenlet_MAIN_NUM 8 -#define PyGreenlet_STARTED_NUM 9 -#define PyGreenlet_ACTIVE_NUM 10 -#define PyGreenlet_GET_PARENT_NUM 11 - -#ifndef GREENLET_MODULE -/* This section is used by modules that uses the greenlet C API */ -static void** _PyGreenlet_API = NULL; - -# define PyGreenlet_Type \ - (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) - -# define PyExc_GreenletError \ - ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) - -# define PyExc_GreenletExit \ - ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) - -/* - * PyGreenlet_New(PyObject *args) - * - * greenlet.greenlet(run, parent=None) - */ -# define PyGreenlet_New \ - (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ - _PyGreenlet_API[PyGreenlet_New_NUM]) - -/* - * PyGreenlet_GetCurrent(void) - * - * greenlet.getcurrent() - */ -# define PyGreenlet_GetCurrent \ - (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) - -/* - * PyGreenlet_Throw( - * PyGreenlet *greenlet, - * PyObject *typ, - * PyObject *val, - * PyObject *tb) - * - * g.throw(...) - */ -# define PyGreenlet_Throw \ - (*(PyObject * (*)(PyGreenlet * self, \ - PyObject * typ, \ - PyObject * val, \ - PyObject * tb)) \ - _PyGreenlet_API[PyGreenlet_Throw_NUM]) - -/* - * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) - * - * g.switch(*args, **kwargs) - */ -# define PyGreenlet_Switch \ - (*(PyObject * \ - (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ - _PyGreenlet_API[PyGreenlet_Switch_NUM]) - -/* - * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) - * - * g.parent = new_parent - */ -# define PyGreenlet_SetParent \ - (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ - _PyGreenlet_API[PyGreenlet_SetParent_NUM]) - -/* - * PyGreenlet_GetParent(PyObject* greenlet) - * - * return greenlet.parent; - * - * This could return NULL even if there is no exception active. - * If it does not return NULL, you are responsible for decrementing the - * reference count. - */ -# define PyGreenlet_GetParent \ - (*(PyGreenlet* (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) - -/* - * deprecated, undocumented alias. - */ -# define PyGreenlet_GET_PARENT PyGreenlet_GetParent - -# define PyGreenlet_MAIN \ - (*(int (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_MAIN_NUM]) - -# define PyGreenlet_STARTED \ - (*(int (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_STARTED_NUM]) - -# define PyGreenlet_ACTIVE \ - (*(int (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) - - - - -/* Macro that imports greenlet and initializes C API */ -/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we - keep the older definition to be sure older code that might have a copy of - the header still works. */ -# define PyGreenlet_Import() \ - { \ - _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ - } - -#endif /* GREENLET_MODULE */ - -#ifdef __cplusplus -} -#endif -#endif /* !Py_GREENLETOBJECT_H */ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3c577b0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f54f40b --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Flask Boilerplate +Your simple, yet opinionated, boilerplate for Flask projects. + +## Why? +I've been using Flask for a while now, and I've found myself repeating the same steps over and over again when starting a new project. In order to save myself (and you) some time, I decided to create this boilerplate. + +## Features +- User authentication (login, logout, register) +- 'Sane' defaults for Flask configuration +- A understandable project file structure +- A simple SQLite database (you can, of course, replace this with any other database that SQLAlchemy supports) +- Jinja2 templates with Flowbite (Tailwind) CSS (CDN version) +- A CLI for managing aspects of your project (use `kettle --help` to see the available commands) + +## Get started + +*I'm assuming you have Python 3.11+ installed on your machine and that you are comfortable with using the cli on your machine.* + +1. Clone this repository and `cd` into it +2. Create a virtual environment with `python3 -m venv venv` (or `python -m venv venv` if you're on Windows) and activate it with `source venv/bin/activate` (or `venv\Scripts\activate.bat` if you're on Windows) +3. Install the dependencies with `pip install -r requirements.txt` +4. Run the application with `flask run` (or `python -m flask run` if you're on Windows) - you can also append `--debug` to enable debug mode +5. Open your browser and go to `http://localhost:5000` + +And voilà, you're ready to open this project in your favorite editor and start working on your next big thing! + +## Contributing +If you have any suggestions or improvements, then feel free to open an issue or a pull request. I'll be happy to take a look at it! + +## Thanks + +### Icons +The boiler, gears and download icon (as seen on the front page) are sourced from [Icons8](https://icons8.com/). + +### CSS/UI +The CSS and UI is sourced from [Flowbite](https://flowbite.com/), which is a library of components and layouts for Tailwind CSS. + +### User profile avatars +The user profile avatars are sourced from [UI Avatars](https://ui-avatars.com/). + +### Libraries +- [Flask](https://flask.palletsprojects.com/en/) +- [Flask-Login](https://flask-login.readthedocs.io/en/latest/) +- [Flask-WTF](https://flask-wtf.readthedocs.io/en/) +- [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/) +- [python-dotenv](https://pypi.org/project/python-dotenv/) + +*See the `requirements.txt` for the full list of libraries that is used.* + +## License/Legal/"Don't sue me, thanks" section +The boilerplate is not licensed, however, the libraries used are. Please check the respective licenses for each library. + +Disclaimer: Flask is a trademark of [Pallets](https://palletsprojects.com/), and is not affiliated with this project in any way. \ No newline at end of file diff --git a/Scripts/Activate.ps1 b/Scripts/Activate.ps1 deleted file mode 100644 index 2057a7e..0000000 --- a/Scripts/Activate.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -$script:THIS_PATH = $myinvocation.mycommand.path -$script:BASE_DIR = Split-Path (Resolve-Path "$THIS_PATH/..") -Parent - -function global:deactivate([switch] $NonDestructive) { - if (Test-Path variable:_OLD_VIRTUAL_PATH) { - $env:PATH = $variable:_OLD_VIRTUAL_PATH - Remove-Variable "_OLD_VIRTUAL_PATH" -Scope global - } - - if (Test-Path function:_old_virtual_prompt) { - $function:prompt = $function:_old_virtual_prompt - Remove-Item function:\_old_virtual_prompt - } - - if ($env:VIRTUAL_ENV) { - Remove-Item env:VIRTUAL_ENV -ErrorAction SilentlyContinue - } - - if (!$NonDestructive) { - # Self destruct! - Remove-Item function:deactivate - Remove-Item function:pydoc - } -} - -function global:pydoc { - python -m pydoc $args -} - -# unset irrelevant variables -deactivate -nondestructive - -$VIRTUAL_ENV = $BASE_DIR -$env:VIRTUAL_ENV = $VIRTUAL_ENV - -New-Variable -Scope global -Name _OLD_VIRTUAL_PATH -Value $env:PATH - -$env:PATH = "$env:VIRTUAL_ENV/Scripts;" + $env:PATH -if (!$env:VIRTUAL_ENV_DISABLE_PROMPT) { - function global:_old_virtual_prompt { - "" - } - $function:_old_virtual_prompt = $function:prompt - - if ("" -ne "") { - function global:prompt { - # Add the custom prefix to the existing prompt - $previous_prompt_value = & $function:_old_virtual_prompt - ("() " + $previous_prompt_value) - } - } - else { - function global:prompt { - # Add a prefix to the current prompt, but don't discard it. - $previous_prompt_value = & $function:_old_virtual_prompt - $new_prompt_value = "($( Split-Path $env:VIRTUAL_ENV -Leaf )) " - ($new_prompt_value + $previous_prompt_value) - } - } -} diff --git a/Scripts/activate b/Scripts/activate deleted file mode 100644 index 9f9b273..0000000 --- a/Scripts/activate +++ /dev/null @@ -1,83 +0,0 @@ -# This file must be used with "source bin/activate" *from bash* -# you cannot run it directly - - -if [ "${BASH_SOURCE-}" = "$0" ]; then - echo "You must source this script: \$ source $0" >&2 - exit 33 -fi - -deactivate () { - unset -f pydoc >/dev/null 2>&1 || true - - # reset old environment variables - # ! [ -z ${VAR+_} ] returns true if VAR is declared at all - if ! [ -z "${_OLD_VIRTUAL_PATH:+_}" ] ; then - PATH="$_OLD_VIRTUAL_PATH" - export PATH - unset _OLD_VIRTUAL_PATH - fi - if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then - PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME" - export PYTHONHOME - unset _OLD_VIRTUAL_PYTHONHOME - fi - - # The hash command must be called to get it to forget past - # commands. Without forgetting past commands the $PATH changes - # we made may not be respected - hash -r 2>/dev/null - - if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then - PS1="$_OLD_VIRTUAL_PS1" - export PS1 - unset _OLD_VIRTUAL_PS1 - fi - - unset VIRTUAL_ENV - if [ ! "${1-}" = "nondestructive" ] ; then - # Self destruct! - unset -f deactivate - fi -} - -# unset irrelevant variables -deactivate nondestructive - -VIRTUAL_ENV='C:\Users\dom\Desktop\flask-boilerplate' -if ([ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]) && $(command -v cygpath &> /dev/null) ; then - VIRTUAL_ENV=$(cygpath -u "$VIRTUAL_ENV") -fi -export VIRTUAL_ENV - -_OLD_VIRTUAL_PATH="$PATH" -PATH="$VIRTUAL_ENV/Scripts:$PATH" -export PATH - -# unset PYTHONHOME if set -if ! [ -z "${PYTHONHOME+_}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME" - unset PYTHONHOME -fi - -if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1-}" - if [ "x" != x ] ; then - PS1="() ${PS1-}" - else - PS1="(`basename \"$VIRTUAL_ENV\"`) ${PS1-}" - fi - export PS1 -fi - -# Make sure to unalias pydoc if it's already there -alias pydoc 2>/dev/null >/dev/null && unalias pydoc || true - -pydoc () { - python -m pydoc "$@" -} - -# The hash command must be called to get it to forget past -# commands. Without forgetting past commands the $PATH changes -# we made may not be respected -hash -r 2>/dev/null diff --git a/Scripts/activate.bat b/Scripts/activate.bat deleted file mode 100644 index 5a9e8e0..0000000 --- a/Scripts/activate.bat +++ /dev/null @@ -1,39 +0,0 @@ -@echo off - -set "VIRTUAL_ENV=C:\Users\dom\Desktop\flask-boilerplate" - -if defined _OLD_VIRTUAL_PROMPT ( - set "PROMPT=%_OLD_VIRTUAL_PROMPT%" -) else ( - if not defined PROMPT ( - set "PROMPT=$P$G" - ) - if not defined VIRTUAL_ENV_DISABLE_PROMPT ( - set "_OLD_VIRTUAL_PROMPT=%PROMPT%" - ) -) -if not defined VIRTUAL_ENV_DISABLE_PROMPT ( - if "" NEQ "" ( - set "PROMPT=() %PROMPT%" - ) else ( - for %%d in ("%VIRTUAL_ENV%") do set "PROMPT=(%%~nxd) %PROMPT%" - ) -) - -REM Don't use () to avoid problems with them in %PATH% -if defined _OLD_VIRTUAL_PYTHONHOME goto ENDIFVHOME - set "_OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME%" -:ENDIFVHOME - -set PYTHONHOME= - -REM if defined _OLD_VIRTUAL_PATH ( -if not defined _OLD_VIRTUAL_PATH goto ENDIFVPATH1 - set "PATH=%_OLD_VIRTUAL_PATH%" -:ENDIFVPATH1 -REM ) else ( -if defined _OLD_VIRTUAL_PATH goto ENDIFVPATH2 - set "_OLD_VIRTUAL_PATH=%PATH%" -:ENDIFVPATH2 - -set "PATH=%VIRTUAL_ENV%\Scripts;%PATH%" diff --git a/Scripts/activate.fish b/Scripts/activate.fish deleted file mode 100644 index 2b42dc5..0000000 --- a/Scripts/activate.fish +++ /dev/null @@ -1,100 +0,0 @@ -# This file must be used using `source bin/activate.fish` *within a running fish ( http://fishshell.com ) session*. -# Do not run it directly. - -function _bashify_path -d "Converts a fish path to something bash can recognize" - set fishy_path $argv - set bashy_path $fishy_path[1] - for path_part in $fishy_path[2..-1] - set bashy_path "$bashy_path:$path_part" - end - echo $bashy_path -end - -function _fishify_path -d "Converts a bash path to something fish can recognize" - echo $argv | tr ':' '\n' -end - -function deactivate -d 'Exit virtualenv mode and return to the normal environment.' - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - # https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling - if test (echo $FISH_VERSION | head -c 1) -lt 3 - set -gx PATH (_fishify_path "$_OLD_VIRTUAL_PATH") - else - set -gx PATH $_OLD_VIRTUAL_PATH - end - set -e _OLD_VIRTUAL_PATH - end - - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME "$_OLD_VIRTUAL_PYTHONHOME" - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$_OLD_FISH_PROMPT_OVERRIDE" - and functions -q _old_fish_prompt - # Set an empty local `$fish_function_path` to allow the removal of `fish_prompt` using `functions -e`. - set -l fish_function_path - - # Erase virtualenv's `fish_prompt` and restore the original. - functions -e fish_prompt - functions -c _old_fish_prompt fish_prompt - functions -e _old_fish_prompt - set -e _OLD_FISH_PROMPT_OVERRIDE - end - - set -e VIRTUAL_ENV - - if test "$argv[1]" != 'nondestructive' - # Self-destruct! - functions -e pydoc - functions -e deactivate - functions -e _bashify_path - functions -e _fishify_path - end -end - -# Unset irrelevant variables. -deactivate nondestructive - -set -gx VIRTUAL_ENV 'C:\Users\dom\Desktop\flask-boilerplate' - -# https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling -if test (echo $FISH_VERSION | head -c 1) -lt 3 - set -gx _OLD_VIRTUAL_PATH (_bashify_path $PATH) -else - set -gx _OLD_VIRTUAL_PATH $PATH -end -set -gx PATH "$VIRTUAL_ENV"'/Scripts' $PATH - -# Unset `$PYTHONHOME` if set. -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME - set -e PYTHONHOME -end - -function pydoc - python -m pydoc $argv -end - -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - # Copy the current `fish_prompt` function as `_old_fish_prompt`. - functions -c fish_prompt _old_fish_prompt - - function fish_prompt - # Run the user's prompt first; it might depend on (pipe)status. - set -l prompt (_old_fish_prompt) - - # Prompt override provided? - # If not, just prepend the environment name. - if test -n '' - printf '(%s) ' '' - else - printf '(%s) ' (basename "$VIRTUAL_ENV") - end - - string join -- \n $prompt # handle multi-line prompts - end - - set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" -end diff --git a/Scripts/activate.nu b/Scripts/activate.nu deleted file mode 100644 index 3d04f87..0000000 --- a/Scripts/activate.nu +++ /dev/null @@ -1,41 +0,0 @@ -# Setting all environment variables for the venv -let path-name = (if ((sys).host.name == "Windows") { "Path" } { "PATH" }) -let virtual-env = "C:\Users\dom\Desktop\flask-boilerplate" -let bin = "Scripts" -let path-sep = ";" - -let old-path = ($nu.path | str collect ($path-sep)) - -let venv-path = ([$virtual-env $bin] | path join) -let new-path = ($nu.path | prepend $venv-path | str collect ($path-sep)) - -# environment variables that will be batched loaded to the virtual env -let new-env = ([ - [name, value]; - [$path-name $new-path] - [_OLD_VIRTUAL_PATH $old-path] - [VIRTUAL_ENV $virtual-env] -]) - -load-env $new-env - -# Creating the new prompt for the session -let virtual_prompt = (if ("" != "") { - "() " -} { - (build-string '(' ($virtual-env | path basename) ') ') -} -) - -# If there is no default prompt, then only the env is printed in the prompt -let new_prompt = (if ( config | select prompt | empty? ) { - ($"build-string '($virtual_prompt)'") -} { - ($"build-string '($virtual_prompt)' (config get prompt | str find-replace "build-string" "")") -}) -let-env PROMPT_COMMAND = $new_prompt - -# We are using alias as the function definitions because only aliases can be -# removed from the scope -alias pydoc = python -m pydoc -alias deactivate = source "C:\Users\dom\Desktop\flask-boilerplate\Scripts\deactivate.nu" diff --git a/Scripts/activate_this.py b/Scripts/activate_this.py deleted file mode 100644 index 3d79a53..0000000 --- a/Scripts/activate_this.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -"""Activate virtualenv for current interpreter: - -Use exec(open(this_file).read(), {'__file__': this_file}). - -This can be used when you must use an existing Python interpreter, not the virtualenv bin/python. -""" -import os -import site -import sys - -try: - abs_file = os.path.abspath(__file__) -except NameError: - raise AssertionError("You must use exec(open(this_file).read(), {'__file__': this_file}))") - -bin_dir = os.path.dirname(abs_file) -base = bin_dir[: -len("Scripts") - 1] # strip away the bin part from the __file__, plus the path separator - -# prepend bin to PATH (this file is inside the bin directory) -os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep)) -os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory - -# add the virtual environments libraries to the host python import mechanism -prev_length = len(sys.path) -for lib in "..\Lib\site-packages".split(os.pathsep): - path = os.path.realpath(os.path.join(bin_dir, lib)) - site.addsitedir(path.decode("utf-8") if "" else path) -sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length] - -sys.real_prefix = sys.prefix -sys.prefix = base diff --git a/Scripts/deactivate.bat b/Scripts/deactivate.bat deleted file mode 100644 index 7bbc568..0000000 --- a/Scripts/deactivate.bat +++ /dev/null @@ -1,19 +0,0 @@ -@echo off - -set VIRTUAL_ENV= - -REM Don't use () to avoid problems with them in %PATH% -if not defined _OLD_VIRTUAL_PROMPT goto ENDIFVPROMPT - set "PROMPT=%_OLD_VIRTUAL_PROMPT%" - set _OLD_VIRTUAL_PROMPT= -:ENDIFVPROMPT - -if not defined _OLD_VIRTUAL_PYTHONHOME goto ENDIFVHOME - set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%" - set _OLD_VIRTUAL_PYTHONHOME= -:ENDIFVHOME - -if not defined _OLD_VIRTUAL_PATH goto ENDIFVPATH - set "PATH=%_OLD_VIRTUAL_PATH%" - set _OLD_VIRTUAL_PATH= -:ENDIFVPATH diff --git a/Scripts/deactivate.nu b/Scripts/deactivate.nu deleted file mode 100644 index 4052438..0000000 --- a/Scripts/deactivate.nu +++ /dev/null @@ -1,11 +0,0 @@ -# Setting the old path -let path-name = (if ((sys).host.name == "Windows") { "Path" } { "PATH" }) -let-env $path-name = $nu.env._OLD_VIRTUAL_PATH - -# Unleting the environment variables that were created when activating the env -unlet-env VIRTUAL_ENV -unlet-env _OLD_VIRTUAL_PATH -unlet-env PROMPT_COMMAND - -unalias pydoc -unalias deactivate diff --git a/Scripts/dotenv.exe b/Scripts/dotenv.exe deleted file mode 100644 index f0c2a9b..0000000 Binary files a/Scripts/dotenv.exe and /dev/null differ diff --git a/Scripts/email_validator.exe b/Scripts/email_validator.exe deleted file mode 100644 index bf04a7a..0000000 Binary files a/Scripts/email_validator.exe and /dev/null differ diff --git a/Scripts/flask.exe b/Scripts/flask.exe deleted file mode 100644 index 0a47099..0000000 Binary files a/Scripts/flask.exe and /dev/null differ diff --git a/Scripts/pip-3.10.exe b/Scripts/pip-3.10.exe deleted file mode 100644 index 38c2f8c..0000000 Binary files a/Scripts/pip-3.10.exe and /dev/null differ diff --git a/Scripts/pip.exe b/Scripts/pip.exe deleted file mode 100644 index 38c2f8c..0000000 Binary files a/Scripts/pip.exe and /dev/null differ diff --git a/Scripts/pip3.10.exe b/Scripts/pip3.10.exe deleted file mode 100644 index 38c2f8c..0000000 Binary files a/Scripts/pip3.10.exe and /dev/null differ diff --git a/Scripts/pip3.exe b/Scripts/pip3.exe deleted file mode 100644 index 38c2f8c..0000000 Binary files a/Scripts/pip3.exe and /dev/null differ diff --git a/Scripts/pydoc.bat b/Scripts/pydoc.bat deleted file mode 100644 index 3d46a23..0000000 --- a/Scripts/pydoc.bat +++ /dev/null @@ -1 +0,0 @@ -python.exe -m pydoc %* diff --git a/Scripts/python.exe b/Scripts/python.exe deleted file mode 100644 index f14cb56..0000000 Binary files a/Scripts/python.exe and /dev/null differ diff --git a/Scripts/pythonw.exe b/Scripts/pythonw.exe deleted file mode 100644 index 99e3fb2..0000000 Binary files a/Scripts/pythonw.exe and /dev/null differ diff --git a/Scripts/wheel-3.10.exe b/Scripts/wheel-3.10.exe deleted file mode 100644 index 07b3e43..0000000 Binary files a/Scripts/wheel-3.10.exe and /dev/null differ diff --git a/Scripts/wheel.exe b/Scripts/wheel.exe deleted file mode 100644 index 07b3e43..0000000 Binary files a/Scripts/wheel.exe and /dev/null differ diff --git a/Scripts/wheel3.10.exe b/Scripts/wheel3.10.exe deleted file mode 100644 index 07b3e43..0000000 Binary files a/Scripts/wheel3.10.exe and /dev/null differ diff --git a/Scripts/wheel3.exe b/Scripts/wheel3.exe deleted file mode 100644 index 07b3e43..0000000 Binary files a/Scripts/wheel3.exe and /dev/null differ diff --git a/app.db b/app.db index df2fa0b..b2731c1 100644 Binary files a/app.db and b/app.db differ diff --git a/app.py b/app.py index 7368d5a..0a23b5a 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,3 @@ from app import create_app -app = create_app() \ No newline at end of file +app = create_app() diff --git a/app/__init__.py b/app/__init__.py index 97196e1..2d543c4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,28 +1,38 @@ -from flask import Flask +from flask import Flask, abort from config import Config from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager -from redmail import email + +from .modules.util.config import eval_bool_env_var db = SQLAlchemy() login = LoginManager() + def create_app(config_class=Config): - app = Flask(__name__, template_folder='templates') + app = Flask(__name__, template_folder="templates") app.config.from_object(Config) db.init_app(app) login.init_app(app) - login.login_view = 'auth.login' + login.login_view = "auth.login" from app.main import main as main + app.register_blueprint(main) from app.auth import auth as auth + app.register_blueprint(auth) from app import errors + app.register_blueprint(errors.error) + @app.before_request + def maintenance_mode(): + # If maintenance mode is enabled, return a 503 + if eval_bool_env_var(app.config["MAINTENANCE_MODE"]): + abort(503) - return app \ No newline at end of file + return app diff --git a/app/auth/__init__.py b/app/auth/__init__.py index 31ae5eb..5f44921 100644 --- a/app/auth/__init__.py +++ b/app/auth/__init__.py @@ -1,5 +1,5 @@ from flask import Blueprint -auth = Blueprint('auth', __name__) +auth = Blueprint("auth", __name__) -from app.auth import routes \ No newline at end of file +from app.auth import routes # noqa: E402, F401 diff --git a/app/auth/forms.py b/app/auth/forms.py index b578429..38a4cca 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -2,27 +2,21 @@ from wtforms import EmailField, StringField, PasswordField, SubmitField from wtforms.validators import DataRequired, Email, Length + class RegistrationForm(FlaskForm): - first_name = StringField('First Name', validators=[DataRequired(), Length(min=3, max=15)]) - last_name = StringField('Last Name', validators=[DataRequired(), Length(min=3, max=15)]) - email = EmailField('Email', validators=[DataRequired(), Email()]) - password = PasswordField('Password', validators=[DataRequired()]) - repeat_password = PasswordField('Password', validators=[DataRequired()]) - submit = SubmitField('Register') + first_name = StringField( + "First Name", validators=[DataRequired(), Length(min=2, max=30)] + ) + last_name = StringField( + "Last Name", validators=[DataRequired(), Length(min=2, max=30)] + ) + email = EmailField("Email", validators=[DataRequired(), Email()]) + password = PasswordField("Password", validators=[DataRequired()]) + repeat_password = PasswordField("Password", validators=[DataRequired()]) + submit = SubmitField("Register") class LoginForm(FlaskForm): - email = EmailField('Email', validators=[DataRequired(), Email()]) - password = PasswordField('Password', validators=[DataRequired()]) - submit = SubmitField('Login') - - -class RequestPasswordResetForm(FlaskForm): - email = EmailField('Email', validators=[DataRequired(), Email()]) - submit = SubmitField('Request Password Reset') - - -class ChangePasswordForm(FlaskForm): - password = PasswordField('Password', validators=[DataRequired()]) - repeat_password = PasswordField('Repeat Password', validators=[DataRequired()]) - submit = SubmitField('Change Password') \ No newline at end of file + email = EmailField("Email", validators=[DataRequired(), Email()]) + password = PasswordField("Password", validators=[DataRequired()]) + submit = SubmitField("Login") diff --git a/app/auth/routes.py b/app/auth/routes.py index 7cb7897..66faf62 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -1,75 +1,94 @@ -from flask import render_template, redirect, url_for, flash, request -from flask_login import login_user, logout_user, current_user, login_required -from app.auth.forms import LoginForm, RegistrationForm, RequestPasswordResetForm +from flask import render_template, redirect, url_for, flash +from flask_login import login_user, logout_user, login_required +from app.auth.forms import LoginForm, RegistrationForm from app.auth import auth from app.models import Users from app import db from app.modules.util.decorators import redirect_if_already_authenticated -from app.modules.util.auth import check_if_user_already_exists, check_password_confirmation, \ - check_password_strength +from app.modules.util.auth import ( + check_if_user_already_exists, + check_password_confirmation, + check_password_strength, +) from app.modules.util.forms import collect_form_data -@auth.route('/login', methods=['GET', 'POST']) + +@auth.route("/login", methods=["GET", "POST"]) @redirect_if_already_authenticated def login(): form = LoginForm() if form.validate_on_submit(): - user = Users.query.filter_by(email=form.email.data).first() if user is None or not user.check_password(form.password.data): - flash('Your credentials are invalid', 'warning') - return redirect(url_for('auth.login')) + flash("Your credentials are invalid", "warning") + return redirect(url_for("auth.login")) login_user(user) - return redirect(url_for('main.index')) + return redirect(url_for("main.index")) + + return render_template("auth/login.html", title="Sign In", form=form) - return render_template('auth/login.html', title='Sign In', form=form) -@auth.route('/register', methods=['GET', 'POST']) +@auth.route("/register", methods=["GET", "POST"]) @redirect_if_already_authenticated def register(): form = RegistrationForm() if form.validate_on_submit(): - - old_form_data = collect_form_data(form.first_name, form.last_name, form.email) + old_form_data = collect_form_data( + form.first_name, form.last_name, form.email + ) if check_if_user_already_exists(form.email.data): - flash('Email invalid or already in use', 'warning') - return render_template('auth/register.html', title='Register', form=form, old_form_data=old_form_data) - - if not check_password_confirmation(form.password.data, form.repeat_password.data): - flash('Passwords do not match', 'warning') - return render_template('auth/register.html', title='Register', form=form, old_form_data=old_form_data) + flash("Email invalid or already in use", "warning") + return render_template( + "auth/register.html", + title="Register", + form=form, + old_form_data=old_form_data, + ) + + if not check_password_confirmation( + form.password.data, form.repeat_password.data + ): + flash("Passwords do not match", "warning") + return render_template( + "auth/register.html", + title="Register", + form=form, + old_form_data=old_form_data, + ) if not check_password_strength(form.password.data): - flash('Password is not strong enough', 'warning') - return render_template('auth/register.html', title='Register', form=form, old_form_data=old_form_data) - - - user = Users(email=form.email.data, first_name=form.first_name.data, last_name=form.last_name.data) + flash("Password is not strong enough", "warning") + return render_template( + "auth/register.html", + title="Register", + form=form, + old_form_data=old_form_data, + ) + + user = Users( + email=form.email.data, + first_name=form.first_name.data, + last_name=form.last_name.data, + ) user.set_password(form.password.data) db.session.add(user) db.session.commit() - flash('Account successfully registered', 'success') - return redirect(url_for('auth.login')) + flash("Account successfully registered", "success") + return redirect(url_for("auth.login")) - return render_template('auth/register.html', title='Register', form=form, old_form_data=None) + return render_template( + "auth/register.html", title="Register", form=form, old_form_data=None + ) -@auth.route('/forgot-password', methods=['GET', 'POST']) -@redirect_if_already_authenticated -def forgot_password(): - form = RequestPasswordResetForm() - if form.validate_on_submit(): - flash('Password reset email sent', 'success') - return redirect(url_for('auth.login')) - return render_template('auth/forgot_password.html', title='Forgot Password', form=form) -@auth.route('/logout') +@auth.route("/logout") @login_required def logout(): logout_user() - flash("You've signed out", 'success') - return redirect(url_for('auth.login')) \ No newline at end of file + flash("You've signed out", "success") + return redirect(url_for("auth.login")) diff --git a/app/errors.py b/app/errors.py index 823e776..a0c06b0 100644 --- a/app/errors.py +++ b/app/errors.py @@ -1,15 +1,38 @@ -from flask import Flask, Blueprint, render_template +from flask import Blueprint, render_template + +error = Blueprint("error_bp", __name__) + + +@error.app_errorhandler(400) +def bad_request(e): + return render_template("errors/401.html", title="Bad Request"), 400 + + +@error.app_errorhandler(401) +def unauthorized(e): + return render_template("errors/401.html", title="Unauthorized"), 401 + + +@error.app_errorhandler(403) +def forbidden(e): + return render_template("errors/401.html", title="Access Forbidden"), 403 + + +@error.app_errorhandler(405) +def method_not_allowed(e): + return render_template("errors/401.html", title="Method Not Allowed"), 405 -error = Blueprint('error_bp', __name__) @error.app_errorhandler(404) def page_not_found(e): - return render_template('errors/404.html', title="Not Found"), 404 + return render_template("errors/404.html", title="Not Found"), 404 -@error.app_errorhandler(401) -def internal_server_error(e): - return render_template('errors/401.html', title="Unauthorized"), 401 @error.app_errorhandler(500) def internal_server_error(e): - return render_template('errors/500.html', title="Server Error"), 500 \ No newline at end of file + return render_template("errors/500.html", title="Server Error"), 500 + + +@error.app_errorhandler(503) +def service_unavailable(e): + return render_template("errors/503.html", title="Service Unavailable"), 503 diff --git a/app/main/__init__.py b/app/main/__init__.py index 434fa4e..1b807ac 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -1,5 +1,5 @@ from flask import Blueprint -main = Blueprint('main', __name__) +main = Blueprint("main", __name__) -from app.main import routes \ No newline at end of file +from app.main import routes # noqa: E402, F401 diff --git a/app/main/routes.py b/app/main/routes.py index 6a5bed5..dfaa3e6 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1,5 +1,4 @@ -from flask import request, render_template, flash, redirect, url_for, __version__ -from app import db +from flask import render_template, __version__ from app.main import main from os import environ @@ -15,21 +14,25 @@ from flask_wtf import __version__ as flask_wtf_version from wtforms import __version__ as wtforms_version -@main.route('/', methods=['GET', 'POST']) + +@main.route("/", methods=["GET", "POST"]) def index(): - return render_template('index.html', - flask_version=__version__, - python_version=python_version(), - pip_version=pip_version, - jinja2_version=jinja2_version, - flask_login_version=flask_login_version, - flask_sqlalchemy_version=flask_sqlalchemy_version, - sqlalchemy_version=sqlalchemy_version, - flask_wtf_version=flask_wtf_version, - wtforms_version=wtforms_version, - dotenv_version=dotenv_version.__version__, - debug_enabled=environ.get('FLASK_DEBUG')) + return render_template( + "index.html", + flask_version=__version__, + python_version=python_version(), + pip_version=pip_version, + jinja2_version=jinja2_version, + flask_login_version=flask_login_version, + flask_sqlalchemy_version=flask_sqlalchemy_version, + sqlalchemy_version=sqlalchemy_version, + flask_wtf_version=flask_wtf_version, + wtforms_version=wtforms_version, + dotenv_version=dotenv_version.__version__, + debug_enabled=environ.get("FLASK_DEBUG"), + ) + -@main.route('/main', methods=['GET', 'POST']) +@main.route("/main", methods=["GET", "POST"]) def main(): - return render_template('main/index.html') \ No newline at end of file + return render_template("main/index.html") diff --git a/app/models.py b/app/models.py index 6c80826..130536f 100644 --- a/app/models.py +++ b/app/models.py @@ -1,24 +1,22 @@ from werkzeug.security import generate_password_hash, check_password_hash -from sqlalchemy.orm import relationship, backref -from flask_sqlalchemy import SQLAlchemy from flask_login import UserMixin -from re import match, fullmatch from datetime import datetime from app import db, login + class Users(UserMixin, db.Model): - __tablename__ = 'users' - id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(120), unique=True) - first_name = db.Column(db.String(15)) - last_name = db.Column(db.String(15)) - password_hash = db.Column(db.String(128)) - created_at = db.Column(db.DateTime(), default=datetime.utcnow()) + __tablename__ = "users" + id: int = db.Column(db.Integer, primary_key=True) + email: str = db.Column(db.String(120), unique=True) + first_name: str = db.Column(db.String(15)) + last_name: str = db.Column(db.String(15)) + password_hash: str = db.Column(db.String(128)) + created_at: datetime = db.Column(db.DateTime(), default=datetime.utcnow()) def __repr__(self): - return f'" - def set_password(self, password: str): + def set_password(self, password: str) -> bool: """ Sets the password hash for the user. @@ -35,6 +33,7 @@ def check_password(self, password: str) -> bool: """ return check_password_hash(self.password_hash, password) + @login.user_loader def load_user(id): """ @@ -44,41 +43,3 @@ def load_user(id): :return: user object """ return Users.query.get(int(id)) - - -class PasswordResetTokens(db.Model): - __tablename__ = 'resets' - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('users.id')) - reset_token = db.Column(db.String(32), unique=True) - created_at = db.Column(db.DateTime(), default=datetime.utcnow()) - expires_at = db.Column(db.DateTime()) - user = relationship('Users', backref=backref('resets', uselist=False)) - - def __repr__(self): - return f'' - - def set_token(self, token: str): - """ - Sets the reset token for the password reset. - - :param token: token to set - """ - self.reset_token = token - - def check_token(self, token: str) -> bool: - """ - Checks if the given token matches the password reset token. - - :param token: token to check - :return: True if the tokens match, False otherwise - """ - return self.reset_token == token - - def check_token_expiration(self) -> bool: - """ - Checks if the password reset token has expired. - - :return: True if the token has expired, False otherwise - """ - return datetime.utcnow() > self.expires_at \ No newline at end of file diff --git a/app/modules/util/auth.py b/app/modules/util/auth.py index 666bce7..2672a28 100644 --- a/app/modules/util/auth.py +++ b/app/modules/util/auth.py @@ -6,8 +6,7 @@ """ from app.models import Users from re import fullmatch -from os import urandom -from app import db + def check_if_user_already_exists(email: str) -> bool: """ @@ -18,6 +17,7 @@ def check_if_user_already_exists(email: str) -> bool: """ return Users.query.filter_by(email=email).first() is not None + def check_password_confirmation(password: str, password_confirmation: str) -> bool: """ Checks if the password and password confirmation are the same. @@ -28,6 +28,7 @@ def check_password_confirmation(password: str, password_confirmation: str) -> bo """ return password == password_confirmation + def check_password_strength(password: str) -> bool: """ Checks if a password is at least 8 characters long, has no repeating characters, and has at least one uppercase letter. @@ -35,12 +36,8 @@ def check_password_strength(password: str) -> bool: :param password: password to check :return: True if the password is strong, False otherwise """ - return len(password) >= 8 and not fullmatch(r'(.)\1*', password) and not fullmatch(r'[a-z]*', password) - -def generate_secure_signature() -> str: - """ - Generates a random 32-byte signature. - - :return: signature - """ - return urandom(32).hex() \ No newline at end of file + return ( + len(password) >= 8 + and not fullmatch(r"(.)\1*", password) + and not fullmatch(r"[a-z]*", password) + ) diff --git a/app/modules/util/config.py b/app/modules/util/config.py new file mode 100644 index 0000000..18dddbd --- /dev/null +++ b/app/modules/util/config.py @@ -0,0 +1,17 @@ +""" + +Utility methods for the config +---------------- + +""" + + +def eval_bool_env_var(env_var: str) -> bool: + """Evaluates a boolean environment variable + + :param env_var: The environment variable to evaluate + :return: The evaluated boolean value + """ + # XXX: python-dotenv doesn't support boolean values nicely, so we have to + # do this ourselves + return True if env_var.lower() in ("true", "t", "1") else False diff --git a/app/modules/util/decorators.py b/app/modules/util/decorators.py index 77538b0..f42defc 100644 --- a/app/modules/util/decorators.py +++ b/app/modules/util/decorators.py @@ -1,16 +1,19 @@ from functools import wraps -from flask import request, redirect, url_for +from flask import redirect, url_for from flask_login import current_user + def redirect_if_already_authenticated(f): """ Redirects to the index page if the user is already authenticated. :return: redirect to `main.index` """ + @wraps(f) - def decorated_function(*args, **kwargs): + def is_authenticated(*args, **kwargs): if current_user.is_authenticated: - return redirect(url_for('main.index')) + return redirect(url_for("main.index")) return f(*args, **kwargs) - return decorated_function \ No newline at end of file + + return is_authenticated diff --git a/app/modules/util/forms.py b/app/modules/util/forms.py index 4c63bf0..25143a0 100644 --- a/app/modules/util/forms.py +++ b/app/modules/util/forms.py @@ -1,9 +1,11 @@ """ -Utilty methods for forms +Utility methods for forms ---------------- """ + + def collect_form_data(*args) -> dict: """ Collects form data from a list of fields (used for form repopulation). @@ -14,4 +16,4 @@ def collect_form_data(*args) -> dict: data = {} for field in args: data[field.name] = field.data - return data \ No newline at end of file + return data diff --git a/app/templates/auth/forgot_password.html b/app/templates/auth/forgot_password.html deleted file mode 100644 index 8be61a4..0000000 --- a/app/templates/auth/forgot_password.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends 'boilerplate/layout.html' %} -{% block content %} -
-
-

Account recovery

-

Forgotten your password?

-
- {% include 'boilerplate/flash_message.html' %} - {{ form.csrf_token }} -
-
-
-
- -
-

- Remember your details? Sign in. -

-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html index 767e360..1828554 100644 --- a/app/templates/auth/login.html +++ b/app/templates/auth/login.html @@ -1,3 +1,8 @@ +{#- + Boilerplate - Login page + ------------------------------------------------------------ + Jinja2 template for the login page. +-#} {% extends 'boilerplate/layout.html' %} {% block content %}
@@ -18,7 +23,6 @@

Please sign in to your a

- Forgot password?

No account? Sign up. diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html index 713e791..184b33c 100644 --- a/app/templates/auth/register.html +++ b/app/templates/auth/register.html @@ -1,8 +1,8 @@ -{# +{#- Boilerplate - Registration page ------------------------------------------------------------ Jinja2 template for the registration page. -#} +-#} {% extends 'boilerplate/layout.html' %} {% block content %}

diff --git a/app/templates/boilerplate/email_layout.html b/app/templates/boilerplate/email_layout.html deleted file mode 100644 index 8e9bfa7..0000000 --- a/app/templates/boilerplate/email_layout.html +++ /dev/null @@ -1,409 +0,0 @@ - - - - - - Simple Transactional Email - - - - This is preheader text. Some clients will show this text as a preview. - e - - - - - - - - - \ No newline at end of file diff --git a/app/templates/boilerplate/flash_message.html b/app/templates/boilerplate/flash_message.html index 9774278..72d00d4 100644 --- a/app/templates/boilerplate/flash_message.html +++ b/app/templates/boilerplate/flash_message.html @@ -1,4 +1,4 @@ -{# +{#- Boilerplate - Flash Messages ------------------------------------------------------------ Jinja2 template for displaying flash() messages. @@ -6,7 +6,7 @@ Example usage: flash("Your password is incorrect", 'warning') -#} +-#} {% with success = get_flashed_messages(category_filter=['success']) %} {% if success %} diff --git a/cli/commands/boil.py b/cli/commands/boil.py new file mode 100644 index 0000000..eabb75d --- /dev/null +++ b/cli/commands/boil.py @@ -0,0 +1,23 @@ +import click +import time + + +@click.command("boil") +@click.option( + "temperature", + "-t", + type=click.IntRange(min=23, clamp=True), + default=100, + help="The temperature to heat the water to", +) +def boil(temperature: int) -> None: + """Heats water to a specified temperature (an example command)""" + + boil_time = (1 * 4.186 * (temperature - 23)) / 1 + click.secho(f"Boiling water to {temperature}°C...", fg="yellow") + + with click.progressbar(range(int(boil_time)), label="Progress:") as bar: + for i in bar: + time.sleep(1) + + click.secho(f"Water has now been heated {temperature}°C", fg="green") diff --git a/cli/commands/maintenance.py b/cli/commands/maintenance.py new file mode 100644 index 0000000..f103927 --- /dev/null +++ b/cli/commands/maintenance.py @@ -0,0 +1,67 @@ +import click +import pathlib + +from dotenv import set_key, get_key + +from util import ClickUtils + + +@click.command("maintenance") +@click.option( + "--status", "-s", is_flag=True, + help="Returns the current maintenance mode status" +) +def maintenance_mode(status: bool) -> None: + """Toggle maintenance mode for the site""" + + spinner = ClickUtils.Spinner( + f"{'Checking' if status else 'Toggling'} maintenance mode..." + ) + spinner.start() + + # Get the config file path + config_file_path = ( + pathlib.Path(__file__).parent.parent.parent.absolute() / ".flaskenv" + ) + + try: + # Get the current maintenance mode state + maintenance_mode_state = get_key(config_file_path, "MAINTENANCE_MODE") + assert maintenance_mode_state + except AssertionError: + spinner.stop() + click.secho( + "ERROR: Config file not found or is missing the " + "`MAINTENANCE_MODE` key value.", + fg="red", + ) + exit(1) + + cfg = ClickUtils.Config() + maintenance_mode_state = cfg.eval_bool_env_var(maintenance_mode_state) + + if status: + spinner.stop() + click.secho( + "Maintenance mode is currently " + + f"{'enabled' if maintenance_mode_state else 'disabled'}.", + fg="yellow", + ) + exit(0) + + # Toggle the maintenance mode state + if not maintenance_mode_state: + set_key(config_file_path, "MAINTENANCE_MODE", "True") + else: + set_key(config_file_path, "MAINTENANCE_MODE", "False") + + spinner.stop() + click.secho( + "Maintenance mode is now " + + f"{'enabled' if not maintenance_mode_state else 'disabled'}.", + fg="green", + ) + click.secho( + "Please restart the server for the changes to take " + "effect.", fg="yellow" + ) diff --git a/cli/commands/shell.py b/cli/commands/shell.py new file mode 100644 index 0000000..dba23f7 --- /dev/null +++ b/cli/commands/shell.py @@ -0,0 +1,74 @@ +import click +import sys +from flask.cli import with_appcontext +from flask.globals import current_app as app + +from util import ClickUtils + + +@click.command("shell") +@click.option( + "--verbose", + "-v", + is_flag=True, + help="Enable verbose context output" +) +@click.option( + "--no-banner", + "-nb", + is_flag=True, + help="Disable the banner message" +) +@with_appcontext +def shell(verbose: bool, no_banner: bool) -> None: + """Starts an interactive shell with the app context""" + + spinner = ClickUtils.Spinner("Starting interactive shell...") + spinner.start() + + try: + from IPython.terminal.ipapp import TerminalIPythonApp + from IPython import __version__ as ipython_version + except ImportError: + spinner.stop() + click.secho( + "ERROR: IPython is not installed. Please install it with", + " `pip install ipython`.", + fg="red", + ) + exit(1) + + ctx = app.make_shell_context() + + # Banner message + cf = ClickUtils.Colors() + banner = cf.cformat( + f"{cf.YELLOW}** [Boilerplate Interactive Shell Started] **\n" + f"{cf.MAGENTA}Boilerplate is ready for your commands! " + f"Use `exit`/`quit` or use Ctrl + D to quit.\n" + f"Autocompletion is enabled, press Tab to use it.\n" + ) + + if verbose: + # Verbose context and shell information output + banner += cf.cformat(f"\n{cf.CYAN}** [Context Variables] **") + for key, value in ctx.items(): + banner += cf.cformat(f"\n{cf.YELLOW}* {key} -> {cf.GREEN}{value}") + banner += ( + f"\n\n{cf.CYAN}** [Shell Information] **" + f"\n{cf.YELLOW}* Python Version -> {cf.GREEN}v{sys.version}" + f"\n{cf.YELLOW}* IPython Version -> {cf.GREEN}v{ipython_version}\n" + ) + + ipython_app = TerminalIPythonApp.instance( + user_ns=ctx, + display_banner=False + ) + ipython_app.initialize(argv=[]) + click.clear() + + if not no_banner: + ipython_app.shell.show_banner(banner) + + spinner.stop() + ipython_app.start() diff --git a/cli/kettle.py b/cli/kettle.py new file mode 100644 index 0000000..41394b7 --- /dev/null +++ b/cli/kettle.py @@ -0,0 +1,20 @@ +import click + +from commands.maintenance import maintenance_mode +from commands.shell import shell +from commands.boil import boil + + +@click.group() +def kettle(): + pass + + +# Add commands below +kettle.add_command(maintenance_mode) +kettle.add_command(shell) +kettle.add_command(boil) + + +if __name__ == "__main__": + kettle() diff --git a/cli/setup.py b/cli/setup.py new file mode 100644 index 0000000..09d4732 --- /dev/null +++ b/cli/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + +setup( + name="kettle", + version="1.0", + description="Command line interface for the boilerplate", + py_modules=["kettle"], + install_requires=[ + "Click", + ], + entry_points={ + "console_scripts": [ + "kettle=kettle:kettle", + ], + }, +) diff --git a/cli/util.py b/cli/util.py new file mode 100644 index 0000000..f0abaf3 --- /dev/null +++ b/cli/util.py @@ -0,0 +1,96 @@ +import click +import time +import threading +import itertools + + +class ClickUtils: + """Utility functions for the click-based CLI""" + + class Spinner: + def __init__(self, message: str, color: str = "yellow") -> None: + """A spinner that can be used to indicate that a process is running + + :param message: Message to display next to the spinner + :param color: Color of the spinner and message (uses colors from + :func:`click.style`) - Optional (will default to `yellow`) + """ + self.frames = itertools.cycle([ + "⢿", "⣻", "⣽", "⣾", + "⣷", "⣯", "⣟", "⡿" + ]) + self.thread = None + self.active = False + self.message = message + self.color = color + + def start(self) -> None: + """Starts the spinner animation""" + if self.thread: + raise RuntimeError("Spinner is already running") + + self.active = True + self.thread = threading.Thread(target=self._spin) + self.thread.start() + + def stop(self) -> None: + """Stops the spinner animation""" + if not self.thread: + raise RuntimeError("Spinner is not running") + + self.active = False + self.thread.join() + + def _clear(self) -> None: + """Clears the spinner animation line from the cli""" + message_length = len(self.message) + 2 + click.secho("\r" + " " * message_length + "\r", nl=False) + + def _spin(self) -> None: + """Displays the spinner animation with the message""" + while self.active: + frame = next(self.frames) + message = f"\r{frame} {self.message}" + click.secho(message, fg=self.color, nl=False) + time.sleep(0.1) + self._clear() + + class Colors: + """ + ASNI color codes and formatting + (where :func:`click.style` cannot be used) + """ + + # Color codes pallet + RED = "\033[0;31m" + GREEN = "\033[0;32m" + YELLOW = "\033[1;33m" + CYAN = "\033[0;36m" + MAGENTA = "\033[0;35m" + + # Formatting codes pallet + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + RESET = "\033[0m" # Reset all colors and formatting + + def cformat(self, text: str) -> str: + """Formats text with the given styles + + :param text: The text to format + :param styles: The pallet style(s) to apply to the text + :return: The formatted text + """ + return f"{text}{self.RESET}" + + class Config: + """Config utility methods for the CLI""" + + def eval_bool_env_var(self, env_var: str) -> bool: + """Evaluates a boolean environment variable + + :param env_var: The environment variable to evaluate + :return: The evaluated boolean value + """ + # XXX: python-dotenv doesn't support boolean values nicely, + # so we have to do it manually + return True if env_var.lower() in ("true", "t", "1") else False diff --git a/config.py b/config.py index 0226161..99ac769 100644 --- a/config.py +++ b/config.py @@ -1,17 +1,22 @@ import os, secrets +import pathlib + +basedir = pathlib.Path(__file__).parent.absolute() -basedir = os.path.abspath(os.path.dirname(__file__)) class Config(object): """ - Configuration variables for the application (used by Flask and third-party packages). + Config values for the application (used by Flask and third-party packages). - These variables are set in the `.flaskenv` file. If no `.flaskenv` file is found, it will default - use the default values below. + Values are set in the `.flaskenv` file. If no `.flaskenv` file is found, + it will default use the default values below (where applicable). """ - SECRET_KEY = os.environ.get('SECRET_KEY') or secrets.token_hex(32) - SQLALCHEMY_DATABASE_URI = f'sqlite:///{os.path.join(basedir, os.environ.get("SQLITE_DB_NAME"))}' or \ - f'sqlite:///{os.path.join(basedir, "app.db")}' + + FLASK_APP = "app.py" + SECRET_KEY = os.environ.get("SECRET_KEY") or secrets.token_hex(32) + SQLALCHEMY_DATABASE_URI = os.environ.get( + "DATABASE_URL" + ) or "sqlite:///" + os.path.join(basedir, "app.db") SQLALCHEMY_TRACK_MODIFICATIONS = False - APP_NAME = os.environ.get('APP_NAME') or 'Flask App' - MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE') or False \ No newline at end of file + APP_NAME = os.environ.get("APP_NAME") or "Flask App" + MAINTENANCE_MODE = os.environ.get("MAINTENANCE_MODE") or False diff --git a/main/HEAD b/main/HEAD deleted file mode 100644 index cb089cd..0000000 --- a/main/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/master diff --git a/main/config b/main/config deleted file mode 100644 index 64280b8..0000000 --- a/main/config +++ /dev/null @@ -1,6 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = false - bare = true - symlinks = false - ignorecase = true diff --git a/main/description b/main/description deleted file mode 100644 index 498b267..0000000 --- a/main/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/main/hooks/applypatch-msg.sample b/main/hooks/applypatch-msg.sample deleted file mode 100644 index a5d7b84..0000000 --- a/main/hooks/applypatch-msg.sample +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -# -# An example hook script to check the commit log message taken by -# applypatch from an e-mail message. -# -# The hook should exit with non-zero status after issuing an -# appropriate message if it wants to stop the commit. The hook is -# allowed to edit the commit message file. -# -# To enable this hook, rename this file to "applypatch-msg". - -. git-sh-setup -commitmsg="$(git rev-parse --git-path hooks/commit-msg)" -test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} -: diff --git a/main/hooks/commit-msg.sample b/main/hooks/commit-msg.sample deleted file mode 100644 index b58d118..0000000 --- a/main/hooks/commit-msg.sample +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -# An example hook script to check the commit log message. -# Called by "git commit" with one argument, the name of the file -# that has the commit message. The hook should exit with non-zero -# status after issuing an appropriate message if it wants to stop the -# commit. The hook is allowed to edit the commit message file. -# -# To enable this hook, rename this file to "commit-msg". - -# Uncomment the below to add a Signed-off-by line to the message. -# Doing this in a hook is a bad idea in general, but the prepare-commit-msg -# hook is more suited to it. -# -# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') -# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" - -# This example catches duplicate Signed-off-by lines. - -test "" = "$(grep '^Signed-off-by: ' "$1" | - sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { - echo >&2 Duplicate Signed-off-by lines. - exit 1 -} diff --git a/main/hooks/fsmonitor-watchman.sample b/main/hooks/fsmonitor-watchman.sample deleted file mode 100644 index e673bb3..0000000 --- a/main/hooks/fsmonitor-watchman.sample +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; -use IPC::Open2; - -# An example hook script to integrate Watchman -# (https://facebook.github.io/watchman/) with git to speed up detecting -# new and modified files. -# -# The hook is passed a version (currently 1) and a time in nanoseconds -# formatted as a string and outputs to stdout all files that have been -# modified since the given time. Paths must be relative to the root of -# the working tree and separated by a single NUL. -# -# To enable this hook, rename this file to "query-watchman" and set -# 'git config core.fsmonitor .git/hooks/query-watchman' -# -my ($version, $time) = @ARGV; - -# Check the hook interface version - -if ($version == 1) { - # convert nanoseconds to seconds - $time = int $time / 1000000000; -} else { - die "Unsupported query-fsmonitor hook version '$version'.\n" . - "Falling back to scanning...\n"; -} - -my $git_work_tree; -if ($^O =~ 'msys' || $^O =~ 'cygwin') { - $git_work_tree = Win32::GetCwd(); - $git_work_tree =~ tr/\\/\//; -} else { - require Cwd; - $git_work_tree = Cwd::cwd(); -} - -my $retry = 1; - -launch_watchman(); - -sub launch_watchman { - - my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') - or die "open2() failed: $!\n" . - "Falling back to scanning...\n"; - - # In the query expression below we're asking for names of files that - # changed since $time but were not transient (ie created after - # $time but no longer exist). - # - # To accomplish this, we're using the "since" generator to use the - # recency index to select candidate nodes and "fields" to limit the - # output to file names only. Then we're using the "expression" term to - # further constrain the results. - # - # The category of transient files that we want to ignore will have a - # creation clock (cclock) newer than $time_t value and will also not - # currently exist. - - my $query = <<" END"; - ["query", "$git_work_tree", { - "since": $time, - "fields": ["name"], - "expression": ["not", ["allof", ["since", $time, "cclock"], ["not", "exists"]]] - }] - END - - print CHLD_IN $query; - close CHLD_IN; - my $response = do {local $/; }; - - die "Watchman: command returned no output.\n" . - "Falling back to scanning...\n" if $response eq ""; - die "Watchman: command returned invalid output: $response\n" . - "Falling back to scanning...\n" unless $response =~ /^\{/; - - my $json_pkg; - eval { - require JSON::XS; - $json_pkg = "JSON::XS"; - 1; - } or do { - require JSON::PP; - $json_pkg = "JSON::PP"; - }; - - my $o = $json_pkg->new->utf8->decode($response); - - if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) { - print STDERR "Adding '$git_work_tree' to watchman's watch list.\n"; - $retry--; - qx/watchman watch "$git_work_tree"/; - die "Failed to make watchman watch '$git_work_tree'.\n" . - "Falling back to scanning...\n" if $? != 0; - - # Watchman will always return all files on the first query so - # return the fast "everything is dirty" flag to git and do the - # Watchman query just to get it over with now so we won't pay - # the cost in git to look up each individual file. - print "/\0"; - eval { launch_watchman() }; - exit 0; - } - - die "Watchman: $o->{error}.\n" . - "Falling back to scanning...\n" if $o->{error}; - - binmode STDOUT, ":utf8"; - local $, = "\0"; - print @{$o->{files}}; -} diff --git a/main/hooks/post-update.sample b/main/hooks/post-update.sample deleted file mode 100644 index ec17ec1..0000000 --- a/main/hooks/post-update.sample +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# -# An example hook script to prepare a packed repository for use over -# dumb transports. -# -# To enable this hook, rename this file to "post-update". - -exec git update-server-info diff --git a/main/hooks/pre-applypatch.sample b/main/hooks/pre-applypatch.sample deleted file mode 100644 index 4142082..0000000 --- a/main/hooks/pre-applypatch.sample +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed -# by applypatch from an e-mail message. -# -# The hook should exit with non-zero status after issuing an -# appropriate message if it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-applypatch". - -. git-sh-setup -precommit="$(git rev-parse --git-path hooks/pre-commit)" -test -x "$precommit" && exec "$precommit" ${1+"$@"} -: diff --git a/main/hooks/pre-commit.sample b/main/hooks/pre-commit.sample deleted file mode 100644 index 6a75641..0000000 --- a/main/hooks/pre-commit.sample +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-commit". - -if git rev-parse --verify HEAD >/dev/null 2>&1 -then - against=HEAD -else - # Initial commit: diff against an empty tree object - against=$(git hash-object -t tree /dev/null) -fi - -# If you want to allow non-ASCII filenames set this variable to true. -allownonascii=$(git config --bool hooks.allownonascii) - -# Redirect output to stderr. -exec 1>&2 - -# Cross platform projects tend to avoid non-ASCII filenames; prevent -# them from being added to the repository. We exploit the fact that the -# printable range starts at the space character and ends with tilde. -if [ "$allownonascii" != "true" ] && - # Note that the use of brackets around a tr range is ok here, (it's - # even required, for portability to Solaris 10's /usr/bin/tr), since - # the square bracket bytes happen to fall in the designated range. - test $(git diff --cached --name-only --diff-filter=A -z $against | - LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 -then - cat <<\EOF -Error: Attempt to add a non-ASCII file name. - -This can cause problems if you want to work with people on other platforms. - -To be portable it is advisable to rename the file. - -If you know what you are doing you can disable this check using: - - git config hooks.allownonascii true -EOF - exit 1 -fi - -# If there are whitespace errors, print the offending file names and fail. -exec git diff-index --check --cached $against -- diff --git a/main/hooks/pre-merge-commit.sample b/main/hooks/pre-merge-commit.sample deleted file mode 100644 index 399eab1..0000000 --- a/main/hooks/pre-merge-commit.sample +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git merge" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message to -# stderr if it wants to stop the merge commit. -# -# To enable this hook, rename this file to "pre-merge-commit". - -. git-sh-setup -test -x "$GIT_DIR/hooks/pre-commit" && - exec "$GIT_DIR/hooks/pre-commit" -: diff --git a/main/hooks/pre-push.sample b/main/hooks/pre-push.sample deleted file mode 100644 index 6187dbf..0000000 --- a/main/hooks/pre-push.sample +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh - -# An example hook script to verify what is about to be pushed. Called by "git -# push" after it has checked the remote status, but before anything has been -# pushed. If this script exits with a non-zero status nothing will be pushed. -# -# This hook is called with the following parameters: -# -# $1 -- Name of the remote to which the push is being done -# $2 -- URL to which the push is being done -# -# If pushing without using a named remote those arguments will be equal. -# -# Information about the commits which are being pushed is supplied as lines to -# the standard input in the form: -# -# -# -# This sample shows how to prevent push of commits where the log message starts -# with "WIP" (work in progress). - -remote="$1" -url="$2" - -z40=0000000000000000000000000000000000000000 - -while read local_ref local_sha remote_ref remote_sha -do - if [ "$local_sha" = $z40 ] - then - # Handle delete - : - else - if [ "$remote_sha" = $z40 ] - then - # New branch, examine all commits - range="$local_sha" - else - # Update to existing branch, examine new commits - range="$remote_sha..$local_sha" - fi - - # Check for WIP commit - commit=`git rev-list -n 1 --grep '^WIP' "$range"` - if [ -n "$commit" ] - then - echo >&2 "Found WIP commit in $local_ref, not pushing" - exit 1 - fi - fi -done - -exit 0 diff --git a/main/hooks/pre-rebase.sample b/main/hooks/pre-rebase.sample deleted file mode 100644 index 6cbef5c..0000000 --- a/main/hooks/pre-rebase.sample +++ /dev/null @@ -1,169 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2006, 2008 Junio C Hamano -# -# The "pre-rebase" hook is run just before "git rebase" starts doing -# its job, and can prevent the command from running by exiting with -# non-zero status. -# -# The hook is called with the following parameters: -# -# $1 -- the upstream the series was forked from. -# $2 -- the branch being rebased (or empty when rebasing the current branch). -# -# This sample shows how to prevent topic branches that are already -# merged to 'next' branch from getting rebased, because allowing it -# would result in rebasing already published history. - -publish=next -basebranch="$1" -if test "$#" = 2 -then - topic="refs/heads/$2" -else - topic=`git symbolic-ref HEAD` || - exit 0 ;# we do not interrupt rebasing detached HEAD -fi - -case "$topic" in -refs/heads/??/*) - ;; -*) - exit 0 ;# we do not interrupt others. - ;; -esac - -# Now we are dealing with a topic branch being rebased -# on top of master. Is it OK to rebase it? - -# Does the topic really exist? -git show-ref -q "$topic" || { - echo >&2 "No such branch $topic" - exit 1 -} - -# Is topic fully merged to master? -not_in_master=`git rev-list --pretty=oneline ^master "$topic"` -if test -z "$not_in_master" -then - echo >&2 "$topic is fully merged to master; better remove it." - exit 1 ;# we could allow it, but there is no point. -fi - -# Is topic ever merged to next? If so you should not be rebasing it. -only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` -only_next_2=`git rev-list ^master ${publish} | sort` -if test "$only_next_1" = "$only_next_2" -then - not_in_topic=`git rev-list "^$topic" master` - if test -z "$not_in_topic" - then - echo >&2 "$topic is already up to date with master" - exit 1 ;# we could allow it, but there is no point. - else - exit 0 - fi -else - not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` - /usr/bin/perl -e ' - my $topic = $ARGV[0]; - my $msg = "* $topic has commits already merged to public branch:\n"; - my (%not_in_next) = map { - /^([0-9a-f]+) /; - ($1 => 1); - } split(/\n/, $ARGV[1]); - for my $elem (map { - /^([0-9a-f]+) (.*)$/; - [$1 => $2]; - } split(/\n/, $ARGV[2])) { - if (!exists $not_in_next{$elem->[0]}) { - if ($msg) { - print STDERR $msg; - undef $msg; - } - print STDERR " $elem->[1]\n"; - } - } - ' "$topic" "$not_in_next" "$not_in_master" - exit 1 -fi - -<<\DOC_END - -This sample hook safeguards topic branches that have been -published from being rewound. - -The workflow assumed here is: - - * Once a topic branch forks from "master", "master" is never - merged into it again (either directly or indirectly). - - * Once a topic branch is fully cooked and merged into "master", - it is deleted. If you need to build on top of it to correct - earlier mistakes, a new topic branch is created by forking at - the tip of the "master". This is not strictly necessary, but - it makes it easier to keep your history simple. - - * Whenever you need to test or publish your changes to topic - branches, merge them into "next" branch. - -The script, being an example, hardcodes the publish branch name -to be "next", but it is trivial to make it configurable via -$GIT_DIR/config mechanism. - -With this workflow, you would want to know: - -(1) ... if a topic branch has ever been merged to "next". Young - topic branches can have stupid mistakes you would rather - clean up before publishing, and things that have not been - merged into other branches can be easily rebased without - affecting other people. But once it is published, you would - not want to rewind it. - -(2) ... if a topic branch has been fully merged to "master". - Then you can delete it. More importantly, you should not - build on top of it -- other people may already want to - change things related to the topic as patches against your - "master", so if you need further changes, it is better to - fork the topic (perhaps with the same name) afresh from the - tip of "master". - -Let's look at this example: - - o---o---o---o---o---o---o---o---o---o "next" - / / / / - / a---a---b A / / - / / / / - / / c---c---c---c B / - / / / \ / - / / / b---b C \ / - / / / / \ / - ---o---o---o---o---o---o---o---o---o---o---o "master" - - -A, B and C are topic branches. - - * A has one fix since it was merged up to "next". - - * B has finished. It has been fully merged up to "master" and "next", - and is ready to be deleted. - - * C has not merged to "next" at all. - -We would want to allow C to be rebased, refuse A, and encourage -B to be deleted. - -To compute (1): - - git rev-list ^master ^topic next - git rev-list ^master next - - if these match, topic has not merged in next at all. - -To compute (2): - - git rev-list master..topic - - if this is empty, it is fully merged to "master". - -DOC_END diff --git a/main/hooks/pre-receive.sample b/main/hooks/pre-receive.sample deleted file mode 100644 index a1fd29e..0000000 --- a/main/hooks/pre-receive.sample +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -# An example hook script to make use of push options. -# The example simply echoes all push options that start with 'echoback=' -# and rejects all pushes when the "reject" push option is used. -# -# To enable this hook, rename this file to "pre-receive". - -if test -n "$GIT_PUSH_OPTION_COUNT" -then - i=0 - while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" - do - eval "value=\$GIT_PUSH_OPTION_$i" - case "$value" in - echoback=*) - echo "echo from the pre-receive-hook: ${value#*=}" >&2 - ;; - reject) - exit 1 - esac - i=$((i + 1)) - done -fi diff --git a/main/hooks/prepare-commit-msg.sample b/main/hooks/prepare-commit-msg.sample deleted file mode 100644 index 10fa14c..0000000 --- a/main/hooks/prepare-commit-msg.sample +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh -# -# An example hook script to prepare the commit log message. -# Called by "git commit" with the name of the file that has the -# commit message, followed by the description of the commit -# message's source. The hook's purpose is to edit the commit -# message file. If the hook fails with a non-zero status, -# the commit is aborted. -# -# To enable this hook, rename this file to "prepare-commit-msg". - -# This hook includes three examples. The first one removes the -# "# Please enter the commit message..." help message. -# -# The second includes the output of "git diff --name-status -r" -# into the message, just before the "git status" output. It is -# commented because it doesn't cope with --amend or with squashed -# commits. -# -# The third example adds a Signed-off-by line to the message, that can -# still be edited. This is rarely a good idea. - -COMMIT_MSG_FILE=$1 -COMMIT_SOURCE=$2 -SHA1=$3 - -/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" - -# case "$COMMIT_SOURCE,$SHA1" in -# ,|template,) -# /usr/bin/perl -i.bak -pe ' -# print "\n" . `git diff --cached --name-status -r` -# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; -# *) ;; -# esac - -# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') -# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" -# if test -z "$COMMIT_SOURCE" -# then -# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" -# fi diff --git a/main/hooks/update.sample b/main/hooks/update.sample deleted file mode 100644 index 80ba941..0000000 --- a/main/hooks/update.sample +++ /dev/null @@ -1,128 +0,0 @@ -#!/bin/sh -# -# An example hook script to block unannotated tags from entering. -# Called by "git receive-pack" with arguments: refname sha1-old sha1-new -# -# To enable this hook, rename this file to "update". -# -# Config -# ------ -# hooks.allowunannotated -# This boolean sets whether unannotated tags will be allowed into the -# repository. By default they won't be. -# hooks.allowdeletetag -# This boolean sets whether deleting tags will be allowed in the -# repository. By default they won't be. -# hooks.allowmodifytag -# This boolean sets whether a tag may be modified after creation. By default -# it won't be. -# hooks.allowdeletebranch -# This boolean sets whether deleting branches will be allowed in the -# repository. By default they won't be. -# hooks.denycreatebranch -# This boolean sets whether remotely creating branches will be denied -# in the repository. By default this is allowed. -# - -# --- Command line -refname="$1" -oldrev="$2" -newrev="$3" - -# --- Safety check -if [ -z "$GIT_DIR" ]; then - echo "Don't run this script from the command line." >&2 - echo " (if you want, you could supply GIT_DIR then run" >&2 - echo " $0 )" >&2 - exit 1 -fi - -if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then - echo "usage: $0 " >&2 - exit 1 -fi - -# --- Config -allowunannotated=$(git config --bool hooks.allowunannotated) -allowdeletebranch=$(git config --bool hooks.allowdeletebranch) -denycreatebranch=$(git config --bool hooks.denycreatebranch) -allowdeletetag=$(git config --bool hooks.allowdeletetag) -allowmodifytag=$(git config --bool hooks.allowmodifytag) - -# check for no description -projectdesc=$(sed -e '1q' "$GIT_DIR/description") -case "$projectdesc" in -"Unnamed repository"* | "") - echo "*** Project description file hasn't been set" >&2 - exit 1 - ;; -esac - -# --- Check types -# if $newrev is 0000...0000, it's a commit to delete a ref. -zero="0000000000000000000000000000000000000000" -if [ "$newrev" = "$zero" ]; then - newrev_type=delete -else - newrev_type=$(git cat-file -t $newrev) -fi - -case "$refname","$newrev_type" in - refs/tags/*,commit) - # un-annotated tag - short_refname=${refname##refs/tags/} - if [ "$allowunannotated" != "true" ]; then - echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 - echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 - exit 1 - fi - ;; - refs/tags/*,delete) - # delete tag - if [ "$allowdeletetag" != "true" ]; then - echo "*** Deleting a tag is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/tags/*,tag) - # annotated tag - if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 - then - echo "*** Tag '$refname' already exists." >&2 - echo "*** Modifying a tag is not allowed in this repository." >&2 - exit 1 - fi - ;; - refs/heads/*,commit) - # branch - if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then - echo "*** Creating a branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/heads/*,delete) - # delete branch - if [ "$allowdeletebranch" != "true" ]; then - echo "*** Deleting a branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/remotes/*,commit) - # tracking branch - ;; - refs/remotes/*,delete) - # delete tracking branch - if [ "$allowdeletebranch" != "true" ]; then - echo "*** Deleting a tracking branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - *) - # Anything else (is there anything else?) - echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 - exit 1 - ;; -esac - -# --- Finished -exit 0 diff --git a/main/info/exclude b/main/info/exclude deleted file mode 100644 index a5196d1..0000000 --- a/main/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/pyvenv.cfg b/pyvenv.cfg deleted file mode 100644 index 1e777a6..0000000 --- a/pyvenv.cfg +++ /dev/null @@ -1,6 +0,0 @@ -home = C:\Users\dom\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0 -implementation = CPython -version_info = 3.10.9.final.0 -virtualenv = 20.13.0 -include-system-site-packages = false -version = 3.10.9 diff --git a/requirements.txt b/requirements.txt index f9801f7..392ceb4 100644 Binary files a/requirements.txt and b/requirements.txt differ