initial commit
Some checks failed
CI / linter (ubuntu-latest, 3.9) (push) Failing after 1m29s
Rename the project from template / rename-project (push) Has been skipped
CI / tests_linux (ubuntu-latest, 3.9) (push) Has been skipped

This commit is contained in:
Simon Diesenreiter 2024-11-10 07:27:36 -08:00
commit 5efd61d236
43 changed files with 1503 additions and 0 deletions

3
.gitea/release_message.sh Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
previous_tag=$(git tag --sort=-creatordate | sed -n 2p)
git shortlog "${previous_tag}.." | sed 's/^./ &/'

36
.gitea/rename_project.sh Executable file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env bash
while getopts a:n:u:d: flag
do
case "${flag}" in
a) author=${OPTARG};;
n) name=${OPTARG};;
u) urlname=${OPTARG};;
d) description=${OPTARG};;
esac
done
echo "Author: $author";
echo "Project Name: $name";
echo "Project URL name: $urlname";
echo "Description: $description";
echo "Renaming project..."
original_author="author_name"
original_name="project_name"
original_urlname="project_urlname"
original_description="project_description"
# for filename in $(find . -name "*.*")
for filename in $(git ls-files)
do
sed -i "s/$original_author/$author/g" $filename
sed -i "s/$original_name/$name/g" $filename
sed -i "s/$original_urlname/$urlname/g" $filename
sed -i "s/$original_description/$description/g" $filename
echo "Renamed $filename"
done
mv project_name $name
# This command runs only once on GHA!
rm -rf .gitea/template.yml

1
.gitea/template.yml Normal file
View File

@ -0,0 +1 @@
author: rochacbruno

92
.gitea/workflows/main.yml Normal file
View File

@ -0,0 +1,92 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [ main ]
pull_request:
branches: [ main ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
linter:
strategy:
fail-fast: false
matrix:
python-version: [3.9]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install project
run: make install
- name: Run linter
run: make lint
tests_linux:
needs: linter
strategy:
fail-fast: false
matrix:
python-version: [3.9]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install project
run: make install
- name: Run tests
run: make test
- name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v1
# with:
# fail_ci_if_error: true
# tests_mac:
# needs: linter
# strategy:
# fail-fast: false
# matrix:
# python-version: [3.9]
# os: [macos-latest]
# runs-on: ${{ matrix.os }}
# steps:
# - uses: actions/checkout@v2
# - uses: actions/setup-python@v2
# with:
# python-version: ${{ matrix.python-version }}
# - name: Install project
# run: make install
# - name: Run tests
# run: make test
# tests_win:
# needs: linter
# strategy:
# fail-fast: false
# matrix:
# python-version: [3.9]
# os: [windows-latest]
# runs-on: ${{ matrix.os }}
# steps:
# - uses: actions/checkout@v2
# - uses: actions/setup-python@v2
# with:
# python-version: ${{ matrix.python-version }}
# - name: Install Pip
# run: pip install --user --upgrade pip
# - name: Install project
# run: pip install -e .[test]
# - name: run tests
# run: pytest -s -vvvv -l --tb=long tests

View File

@ -0,0 +1,48 @@
name: Upload Python Package
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- '*' # Push events to matching v*, i.e. v1.0, v20.15.10
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
release:
name: Create Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
# by default, it uses a depth of 1
# this fetches all history so that we can read each commit
fetch-depth: 0
- name: Generate Changelog
run: .gitea/release_message.sh > release_message.md
- name: Release
uses: softprops/action-gh-release@v1
with:
body_path: release_message.md
deploy:
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*

View File

@ -0,0 +1,42 @@
name: Rename the project from template
on: [push]
permissions: write-all
jobs:
rename-project:
if: ${{ !endsWith (github.repository, 'Templates/Flask') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
# by default, it uses a depth of 1
# this fetches all history so that we can read each commit
fetch-depth: 0
ref: ${{ github.head_ref }}
- run: echo "REPOSITORY_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}' | tr '-' '_' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
shell: bash
- run: echo "REPOSITORY_URLNAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV
shell: bash
- run: echo "REPOSITORY_OWNER=$(echo '${{ github.repository }}' | awk -F '/' '{print $1}')" >> $GITHUB_ENV
shell: bash
- name: Is this still a template
id: is_template
run: echo "::set-output name=is_template::$(ls .gitea/template.yml &> /dev/null && echo true || echo false)"
- name: Rename the project
if: steps.is_template.outputs.is_template == 'true'
run: |
echo "Renaming the project with -a(author) ${{ env.REPOSITORY_OWNER }} -n(name) ${{ env.REPOSITORY_NAME }} -u(urlname) ${{ env.REPOSITORY_URLNAME }}"
.gitea/rename_project.sh -a ${{ env.REPOSITORY_OWNER }} -n ${{ env.REPOSITORY_NAME }} -u ${{ env.REPOSITORY_URLNAME }} -d "Awesome ${{ env.REPOSITORY_NAME }} created by ${{ env.REPOSITORY_OWNER }}"
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "✅ Ready to clone and code."
# commit_options: '--amend --no-edit'
push_options: --force

133
.gitignore vendored Normal file
View File

@ -0,0 +1,133 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# templates
.gitea/templates/*
development.db

195
ABOUT_THIS_TEMPLATE.md Normal file
View File

@ -0,0 +1,195 @@
# About this template
Hi, I created this template to help you get started with a new project.
I have created and maintained a number of python libraries, applications and
frameworks and during those years I have learned a lot about how to create a
project structure and how to structure a project to be as modular and simple
as possible.
Some decisions I have made while creating this template are:
- Create a project structure that is as modular as possible.
- Keep it simple and easy to maintain.
- Allow for a lot of flexibility and customizability.
- Low dependency (this template doesn't add dependencies)
## Structure
Lets take a look at the structure of this template:
```text
├── Containerfile # The file to build a container using buildah or docker
├── CONTRIBUTING.md # Onboarding instructions for new contributors
├── docs # Documentation site (add more .md files here)
│   └── index.md # The index page for the docs site
├── .gitea # Gitea metadata for repository
│   ├── release_message.sh # A script to generate a release message
│   └── workflows # The CI pipeline for Gitea Actions
├── .gitignore # A list of files to ignore when pushing to Gitea
├── HISTORY.md # Auto generated list of changes to the project
├── LICENSE # The license for the project
├── Makefile # A collection of utilities to manage the project
├── MANIFEST.in # A list of files to include in a package
├── mkdocs.yml # Configuration for documentation site
├── project_name # The main python package for the project
│   ├── base.py # The base module for the project
│   ├── __init__.py # This tells Python that this is a package
│   ├── __main__.py # The entry point for the project
│   └── VERSION # The version for the project is kept in a static file
├── README.md # The main readme for the project
├── setup.py # The setup.py file for installing and packaging the project
├── requirements.txt # An empty file to hold the requirements for the project
├── requirements-test.txt # List of requirements for testing and devlopment
├── setup.py # The setup.py file for installing and packaging the project
└── tests # Unit tests for the project (add mote tests files here)
├── conftest.py # Configuration, hooks and fixtures for pytest
├── __init__.py # This tells Python that this is a test package
└── test_base.py # The base test case for the project
```
## FAQ
Frequent asked questions.
### Why this template is not using [Poetry](https://python-poetry.org/) ?
I really like Poetry and I think it is a great tool to manage your python projects,
if you want to switch to poetry, you can run `make switch-to-poetry`.
But for this template I wanted to keep it simple.
Setuptools is the most simple and well supported way of packaging a Python project,
it doesn't require extra dependencies and is the easiest way to install the project.
Also, poetry doesn't have a good support for installing projects in development mode yet.
### Why the `requirements.txt` is empty ?
This template is a low dependency project, so it doesn't have any extra dependencies.
You can add new dependencies as you will or you can use the `make init` command to
generate a `requirements.txt` file based on the template you choose `flask, fastapi, click etc`.
### Why there is a `requirements-test.txt` file ?
This file lists all the requirements for testing and development,
I think the development environment and testing environment should be as similar as possible.
Except those tools that are up to the developer choice (like ipython, ipdb etc).
### Why the template doesn't have a `pyproject.toml` file ?
It is possible to run `pip install https://git.disi.dev/name/repo/tarball/main` and
have pip to download the package direcly from Git repo.
For that to work you need to have a `setup.py` file, and `pyproject.toml` is not
supported for that kind of installation.
I think it is easier for example you want to install specific branch or tag you can
do `pip install https://git.disi.dev/name/repo/tarball/{TAG|REVISON|COMMIT}`
People automating CI for your project will be grateful for having a setup.py file
### Why isn't this template made as a cookiecutter template?
I really like [cookiecutter](https://github.com/cookiecutter/cookiecutter) and it is a great way to create new projects,
to use this template doesn't require to install extra tooling such as cookiecutter.
The substituions are done using gitea actions and a simple sed script.
### Why `VERSION` is kept in a static plain text file?
I used to have my version inside my main module in a `__version__` variable, then
I had to do some tricks to read that version variable inside the setuptools
`setup.py` file because that would be available only after the installation.
I decided to keep the version in a static file because it is easier to read from
wherever I want without the need to install the package.
e.g: `cat project_name/VERSION` will get the project version without harming
with module imports or anything else, it is useful for CI, logs and debugging.
### Why to include `tests`, `history` and `Containerfile` as part of the release?
The `MANIFEST.in` file is used to include the files in the release, once the
project is released to PyPI all the files listed on MANIFEST.in will be included
even if the files are static or not related to Python.
Some build systems such as RPM, DEB, AUR for some Linux distributions, and also
internal repackaging systems tends to run the tests before the packaging is performed.
The Containerfile can be useful to provide a safer execution environment for
the project when running on a testing environment.
I added those files to make it easier for packaging in different formats.
### Why conftest includes a go_to_tmpdir fixture?
When your project deals with file system operations, it is a good idea to use
a fixture to create a temporary directory and then remove it after the test.
Before executing each test pytest will create a temporary directory and will
change the working directory to that path and run the test.
So the test can create temporary artifacts isolated from other tests.
After the execution Pytest will remove the temporary directory.
### Why this template is not using [pre-commit](https://pre-commit.com/) ?
pre-commit is an excellent tool to automate checks and formatting on your code.
However I figured out that pre-commit adds extra dependency and it an entry barrier
for new contributors.
Having the linting, checks and formatting as simple commands on the [Makefile](Makefile)
makes it easier to undestand and change.
Once the project is bigger and complex, having pre-commit as a dependency can be a good idea.
### Why the CLI is not using click?
I wanted to provide a simple template for a CLI application on the project main entry point
click and typer are great alternatives but are external dependencies and this template
doesn't add dependencies besides those used for development.
### Why this doesn't provide a full example of application using Flask or Django?
as I said before, I want it to be simple and multipurpose, so I decided to not include
external dependencies and programming design decisions.
It is up to you to decide if you want to use Flask or Django and to create your application
the way you think is best.
This template provides utilities in the Makefile to make it easier to you can run:
```bash
$ make init
Which template do you want to apply? [flask, fastapi, click, typer]? > flask
Generating a new project with Flask ...
```
Then the above will download the Flask template and apply it to the project.
## The Makefile
All the utilities for the template and project are on the Makefile
```bash
make
Usage: make <target>
Targets:
help: ## Show the help.
install: ## Install the project in dev mode.
fmt: ## Format code using black & isort.
lint: ## Run pep8, black, mypy linters.
test: lint ## Run tests and generate coverage report.
watch: ## Run tests on every change.
clean: ## Clean unused files.
virtualenv: ## Create a virtual environment.
release: ## Create a new tag for release.
docs: ## Build the documentation.
switch-to-poetry: ## Switch to poetry package manager.
init: ## Initialize the project based on an application template.
```

113
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,113 @@
# How to develop on this project
project_name welcomes contributions from the community.
**You need PYTHON3!**
This instructions are for linux base systems. (Linux, MacOS, BSD, etc.)
## Setting up your own fork of this repo.
- On gitea interface click on `Fork` button.
- Clone your fork of this repo. `git clone git@git.disi.dev:YOUR_GIT_USERNAME/project_urlname.git`
- Enter the directory `cd project_urlname`
- Add upstream repo `git remote add upstream https://git.disi.dev/author_name/project_urlname`
## Setting up your own virtual environment
Run `make virtualenv` to create a virtual environment.
then activate it with `source .venv/bin/activate`.
## Install the project in develop mode
Run `make install` to install the project in develop mode.
## Run the tests to ensure everything is working
Run `make test` to run the tests.
## Create a new branch to work on your contribution
Run `git checkout -b my_contribution`
## Make your changes
Edit the files using your preferred editor. (we recommend VIM or VSCode)
## Format the code
Run `make fmt` to format the code.
## Run the linter
Run `make lint` to run the linter.
## Test your changes
Run `make test` to run the tests.
Ensure code coverage report shows `100%` coverage, add tests to your PR.
## Build the docs locally
Run `make docs` to build the docs.
Ensure your new changes are documented.
## Commit your changes
This project uses [conventional git commit messages](https://www.conventionalcommits.org/en/v1.0.0/).
Example: `fix(package): update setup.py arguments 🎉` (emojis are fine too)
## Push your changes to your fork
Run `git push origin my_contribution`
## Submit a pull request
On gitea interface, click on `Pull Request` button.
Wait CI to run and one of the developers will review your PR.
## Makefile utilities
This project comes with a `Makefile` that contains a number of useful utility.
```bash
make
Usage: make <target>
Targets:
help: ## Show the help.
install: ## Install the project in dev mode.
fmt: ## Format code using black & isort.
lint: ## Run pep8, black, mypy linters.
test: lint ## Run tests and generate coverage report.
watch: ## Run tests on every change.
clean: ## Clean unused files.
virtualenv: ## Create a virtual environment.
release: ## Create a new tag for release.
docs: ## Build the documentation.
switch-to-poetry: ## Switch to poetry package manager.
init: ## Initialize the project based on an application template.
```
## Making a new release
This project uses [semantic versioning](https://semver.org/) and tags releases with `X.Y.Z`
Every time a new tag is created and pushed to the remote repo, gitea actions will
automatically create a new release on gitea and trigger a release on PyPI.
For this to work you need to setup a secret called `PIPY_API_TOKEN` on the project settings>secrets,
this token can be generated on [pypi.org](https://pypi.org/account/).
To trigger a new release all you need to do is.
1. If you have changes to add to the repo
* Make your changes following the steps described above.
* Commit your changes following the [conventional git commit messages](https://www.conventionalcommits.org/en/v1.0.0/).
2. Run the tests to ensure everything is working.
4. Run `make release` to create a new tag and push it to the remote repo.
the `make release` will ask you the version number to create the tag, ex: type `0.1.1` when you are asked.
> **CAUTION**: The make release will change local changelog files and commit all the unstaged changes you have.

9
Containerfile Normal file
View File

@ -0,0 +1,9 @@
FROM python:3.7-alpine
COPY . /app
WORKDIR /app
RUN pip install .
RUN project_name create-db
RUN project_name populate-db
RUN project_name add-user -u admin -p admin
EXPOSE 5000
CMD ["project_name", "run"]

0
HISTORY.md Normal file
View File

24
LICENSE Normal file
View File

@ -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 <https://unlicense.org>

5
MANIFEST.in Normal file
View File

@ -0,0 +1,5 @@
include LICENSE
include HISTORY.md
include Containerfile
graft tests
graft project_name

119
Makefile Normal file
View File

@ -0,0 +1,119 @@
.ONESHELL:
ENV_PREFIX=$(shell python -c "if __import__('pathlib').Path('.venv/bin/pip').exists(): print('.venv/bin/')")
USING_POETRY=$(shell grep "tool.poetry" pyproject.toml && echo "yes")
.PHONY: help
help: ## Show the help.
@echo "Usage: make <target>"
@echo ""
@echo "Targets:"
@fgrep "##" Makefile | fgrep -v fgrep
.PHONY: show
show: ## Show the current environment.
@echo "Current environment:"
@if [ "$(USING_POETRY)" ]; then poetry env info && exit; fi
@echo "Running using $(ENV_PREFIX)"
@$(ENV_PREFIX)python -V
@$(ENV_PREFIX)python -m site
.PHONY: install
install: ## Install the project in dev mode.
@if [ "$(USING_POETRY)" ]; then poetry install && exit; fi
@echo "Don't forget to run 'make virtualenv' if you got errors."
$(ENV_PREFIX)pip install -e .[test]
.PHONY: fmt
fmt: ## Format code using black & isort.
$(ENV_PREFIX)isort project_name/
$(ENV_PREFIX)black -l 79 project_name/
$(ENV_PREFIX)black -l 79 tests/
.PHONY: lint
lint: ## Run pep8, black, mypy linters.
$(ENV_PREFIX)flake8 project_name/
$(ENV_PREFIX)black -l 79 --check project_name/
$(ENV_PREFIX)black -l 79 --check tests/
$(ENV_PREFIX)mypy --ignore-missing-imports project_name/
.PHONY: test
test: lint ## Run tests and generate coverage report.
$(ENV_PREFIX)pytest -v --cov-config .coveragerc --cov=project_name -l --tb=short --maxfail=1 tests/
$(ENV_PREFIX)coverage xml
$(ENV_PREFIX)coverage html
.PHONY: watch
watch: ## Run tests on every change.
ls **/**.py | entr $(ENV_PREFIX)pytest -s -vvv -l --tb=long --maxfail=1 tests/
.PHONY: clean
clean: ## Clean unused files.
@find ./ -name '*.pyc' -exec rm -f {} \;
@find ./ -name '__pycache__' -exec rm -rf {} \;
@find ./ -name 'Thumbs.db' -exec rm -f {} \;
@find ./ -name '*~' -exec rm -f {} \;
@rm -rf .cache
@rm -rf .pytest_cache
@rm -rf .mypy_cache
@rm -rf build
@rm -rf dist
@rm -rf *.egg-info
@rm -rf htmlcov
@rm -rf .tox/
@rm -rf docs/_build
.PHONY: virtualenv
virtualenv: ## Create a virtual environment.
@if [ "$(USING_POETRY)" ]; then poetry install && exit; fi
@echo "creating virtualenv ..."
@rm -rf .venv
@python3 -m venv .venv
@./.venv/bin/pip install -U pip
@./.venv/bin/pip install -e .[test]
@echo
@echo "!!! Please run 'source .venv/bin/activate' to enable the environment !!!"
.PHONY: release
release: ## Create a new tag for release.
@echo "WARNING: This operation will create s version tag and push to gitea"
@read -p "Version? (provide the next x.y.z semver) : " TAG
@echo "creating git tag : $${TAG}"
@git tag $${TAG}
@echo "$${TAG}" > project_name/VERSION
@$(ENV_PREFIX)gitchangelog > HISTORY.md
@git add project_name/VERSION HISTORY.md
@git commit -m "release: version $${TAG} 🚀"
@git push -u origin HEAD --tags
@echo "Gitea Actions will detect the new tag and release the new version."
.PHONY: docs
docs: ## Build the documentation.
@echo "building documentation ..."
@$(ENV_PREFIX)mkdocs build
URL="site/index.html"; xdg-open $$URL || sensible-browser $$URL || x-www-browser $$URL || gnome-open $$URL
.PHONY: switch-to-poetry
switch-to-poetry: ## Switch to poetry package manager.
@echo "Switching to poetry ..."
@if ! poetry --version > /dev/null; then echo 'poetry is required, install from https://python-poetry.org/'; exit 1; fi
@rm -rf .venv
@poetry init --no-interaction --name=a_flask_test --author=rochacbruno
@echo "" >> pyproject.toml
@echo "[tool.poetry.scripts]" >> pyproject.toml
@echo "project_name = 'project_name.__main__:main'" >> pyproject.toml
@cat requirements.txt | while read in; do poetry add --no-interaction "$${in}"; done
@cat requirements-base.txt | while read in; do poetry add --no-interaction "$${in}" --dev; done
@cat requirements-test.txt | while read in; do poetry add --no-interaction "$${in}" --dev; done
@poetry install --no-interaction
@mkdir -p .gitea/backup
@mv requirements* .gitea/backup
@mv setup.py .gitea/backup
@echo "You have switched to https://python-poetry.org/ package manager."
@echo "Please run 'poetry shell' or 'poetry run project_name'"
# This project has been generated from rochacbruno/flask-project-template
# __author__ = 'rochacbruno'
# __repo__ = https://github.com/rochacbruno/flask-project-template
# __sponsor__ = https://github.com/sponsors/rochacbruno/

101
README.md Normal file
View File

@ -0,0 +1,101 @@
# Flask Project Template
A full feature Flask project template.
See also
- [Python-Project-Template](https://git.disi.dev/Templates/Python/) for a lean, low dependency Python app.
### HOW TO USE THIS TEMPLATE
1. Create a new repository from this template and choose a name for your project
(e.g. `my_awesome_project` - recommendation is to use all lowercase and underscores separation for repo names.)
2. Wait until the first run of CI finishes (Gitea Actions will process the template and commit to your new repo)
3. If you want Automatic Release to [PyPI](https://pypi.org)
On the new repository `settings->secrets` add your `PYPI_API_TOKEN` (get the tokens on PyPI website)
4. Read the file [CONTRIBUTING.md](CONTRIBUTING.md)
5. Then clone your new project and happy coding!
> **NOTE**: **WAIT** until first CI run on gitea actions before cloning your new project.
### What is included on this template?
- 🍾 A full feature Flask application with CLI, API, Admin interface, web UI and modular configuration.
- 📦 A basic [setup.py](setup.py) file to provide installation, packaging and distribution for your project.
Template uses setuptools because it's the de-facto standard for Python packages, you can run `make switch-to-poetry` later if you want.
- 🤖 A [Makefile](Makefile) with the most useful commands to install, test, lint, format and release your project.
- 📃 Documentation structure using [mkdocs](http://www.mkdocs.org)
- 💬 Auto generation of change log using **gitchangelog** to keep a HISTORY.md file automatically based on your commit history on every release.
- 🐋 A simple [Containerfile](Containerfile) to build a container image for your project.
`Containerfile` is a more open standard for building container images than Dockerfile, you can use buildah or docker with this file.
- 🧪 Testing structure using [pytest](https://docs.pytest.org/en/latest/)
- ✅ Code linting using [flake8](https://flake8.pycqa.org/en/latest/)
- 📊 Code coverage reports using [codecov](https://about.codecov.io/sign-up/)
- 🛳️ Automatic release to [PyPI](https://pypi.org) using [twine](https://twine.readthedocs.io/en/latest/) and gitea actions.
- 🎯 Entry points to execute your program using `python -m <project_name>` or `$ project_name` with basic CLI argument parsing.
- 🔄 Continuous integration using [Gitea Actions](.gitea/workflows/) with jobs to lint, test and release your project on Linux, Mac and Windows environments.
> Curious about architectural decisions on this template? read [ABOUT_THIS_TEMPLATE.md](ABOUT_THIS_TEMPLATE.md)
<!-- DELETE THE LINES ABOVE THIS AND WRITE YOUR PROJECT README BELOW -->
---
# project_name Flask Application
project_description
## Installation
From source:
```bash
git clone https://git.disi.dev/author_name/project_urlname project_name
cd project_name
make install
```
From pypi:
```bash
pip install project_name
```
## Executing
This application has a CLI interface that extends the Flask CLI.
Just run:
```bash
$ project_name
```
or
```bash
$ python -m project_name
```
To see the help message and usage instructions.
## First run
```bash
project_name create-db # run once
project_name populate-db # run once (optional)
project_name add-user -u admin -p 1234 # ads a user
project_name run
```
Go to:
- Website: http://localhost:5000
- Admin: http://localhost:5000/admin/
- user: admin, senha: 1234
- API GET:
- http://localhost:5000/api/v1/product/
- http://localhost:5000/api/v1/product/1
- http://localhost:5000/api/v1/product/2
- http://localhost:5000/api/v1/product/3
> **Note**: You can also use `flask run` to run the application.

77
apply.sh Executable file
View File

@ -0,0 +1,77 @@
#!/usr/bin/env bash
while getopts a:n:u:d: flag
do
case "${flag}" in
a) author=${OPTARG};;
n) name=${OPTARG};;
u) urlname=${OPTARG};;
d) description=${OPTARG};;
esac
done
echo "Author: $author";
echo "Project Name: $name";
echo "Project URL name: $urlname";
echo "Description: $description";
echo "Rendering the Flask template..."
original_author="author_name"
original_name="project_name"
original_urlname="project_urlname"
original_description="project_description"
TEMPLATE_DIR="./.gitea/templates/flask"
for filename in $(find ${TEMPLATE_DIR} -name "*.*" -not \( -name "*.git*" -prune \) -not \( -name "apply.sh" -prune \))
do
sed -i "s/$original_author/$author/g" $filename
sed -i "s/$original_name/$name/g" $filename
sed -i "s/$original_urlname/$urlname/g" $filename
sed -i "s/$original_description/$description/g" $filename
echo "Renamed $filename"
done
# Add requirements
if [ ! -f pyproject.toml ]
then
cat ${TEMPLATE_DIR}/requirements.txt >> requirements.txt
cat ${TEMPLATE_DIR}/requirements-test.txt >> requirements-test.txt
else
for item in $(cat ${TEMPLATE_DIR}/requirements.txt)
do
poetry add "${item}"
done
for item in $(cat ${TEMPLATE_DIR}/requirements-test.txt)
do
poetry add --dev "${item}"
done
fi
# Move module files
rm -rf "${name}"
rm -rf tests
cp -R ${TEMPLATE_DIR}/project_name "${name}"
cp -R ${TEMPLATE_DIR}/tests tests
cp ${TEMPLATE_DIR}/README.md README.md
cp ${TEMPLATE_DIR}/Containerfile Containerfile
cp ${TEMPLATE_DIR}/wsgi.py wsgi.py
cp ${TEMPLATE_DIR}/.env .env
cp ${TEMPLATE_DIR}/settings.toml settings.toml
# install
make clean
if [ ! -f pyproject.toml ]
then
make virtualenv
make install
echo "Applied Flask template"
echo "Ensure you activate your env with 'source .venv/bin/activate'"
echo "then run 'project_name' or 'python -m project_name'"
else
poetry install
echo "Applied Flask template"
echo "Ensure you activate your env with 'poetry shell'"
echo "then run 'project_name' or 'python -m project_name' or 'poetry run project_name'"
fi
echo "README.md has instructions on how to use this Flask application."

17
docs/index.md Normal file
View File

@ -0,0 +1,17 @@
# Welcome to MkDocs
For full documentation visit [mkdocs.org](https://www.mkdocs.org).
## Commands
* `mkdocs new [dir-name]` - Create a new project.
* `mkdocs serve` - Start the live-reloading docs server.
* `mkdocs build` - Build the documentation site.
* `mkdocs -h` - Print help message and exit.
## Project layout
mkdocs.yml # The configuration file.
docs/
index.md # The documentation homepage.
... # Other markdown pages, images and other files.

2
mkdocs.yml Normal file
View File

@ -0,0 +1,2 @@
site_name: project_name
theme: readthedocs

1
project_name/VERSION Normal file
View File

@ -0,0 +1 @@
0.1.0

3
project_name/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .base import create_app, create_app_wsgi
__all__ = ["create_app", "create_app_wsgi"]

13
project_name/__main__.py Normal file
View File

@ -0,0 +1,13 @@
import click
from flask.cli import FlaskGroup
from . import create_app_wsgi
@click.group(cls=FlaskGroup, create_app=create_app_wsgi)
def main():
"""Management script for the project_name application."""
if __name__ == "__main__": # pragma: no cover
main()

21
project_name/base.py Normal file
View File

@ -0,0 +1,21 @@
from dynaconf import FlaskDynaconf
from flask import Flask
def create_app(**config):
app = Flask(__name__)
FlaskDynaconf(app) # config managed by Dynaconf
app.config.load_extensions(
"EXTENSIONS"
) # Load extensions from settings.toml
app.config.update(config) # Override with passed config
return app
def create_app_wsgi():
# workaround for Flask issue
# that doesn't allow **config
# to be passed to create_app
# https://github.com/pallets/flask/issues/4170
app = create_app()
return app

View File

33
project_name/ext/admin.py Normal file
View File

@ -0,0 +1,33 @@
from flask_admin import Admin
from flask_admin.base import AdminIndexView
from flask_admin.contrib import sqla
from flask_simplelogin import login_required
from werkzeug.security import generate_password_hash
from project_name.ext.database import db
from project_name.models import Product, User
# Proteck admin with login / Monkey Patch
AdminIndexView._handle_view = login_required(AdminIndexView._handle_view)
sqla.ModelView._handle_view = login_required(sqla.ModelView._handle_view)
admin = Admin()
class UserAdmin(sqla.ModelView):
column_list = ["username"]
can_edit = False
def on_model_change(self, form, model, is_created):
model.password = generate_password_hash(model.password)
def init_app(app):
admin.name = app.config.TITLE
admin.template_mode = app.config.FLASK_ADMIN_TEMPLATE_MODE
admin.init_app(app)
# Add admin page for Product
admin.add_view(sqla.ModelView(Product, db.session))
# Add admin page for User
admin.add_view(UserAdmin(User, db.session))

33
project_name/ext/auth.py Normal file
View File

@ -0,0 +1,33 @@
from flask_simplelogin import SimpleLogin
from werkzeug.security import check_password_hash, generate_password_hash
from project_name.ext.database import db
from project_name.models import User
def verify_login(user):
"""Validates user login"""
username = user.get("username")
password = user.get("password")
if not username or not password:
return False
existing_user = User.query.filter_by(username=username).first()
if not existing_user:
return False
if check_password_hash(existing_user.password, password):
return True
return False
def create_user(username, password):
"""Creates a new user"""
if User.query.filter_by(username=username).first():
raise RuntimeError(f"{username} already exists")
user = User(username=username, password=generate_password_hash(password))
db.session.add(user)
db.session.commit()
return user
def init_app(app):
SimpleLogin(app, login_checker=verify_login)

View File

@ -0,0 +1,43 @@
import click
from project_name.ext.auth import create_user
from project_name.ext.database import db
from project_name.models import Product
def create_db():
"""Creates database"""
db.create_all()
def drop_db():
"""Cleans database"""
db.drop_all()
def populate_db():
"""Populate db with sample data"""
data = [
Product(
id=1, name="Ciabatta", price="10", description="Italian Bread"
),
Product(id=2, name="Baguete", price="15", description="French Bread"),
Product(id=3, name="Pretzel", price="20", description="German Bread"),
]
db.session.bulk_save_objects(data)
db.session.commit()
return Product.query.all()
def init_app(app):
# add multiple commands in a bulk
for command in [create_db, drop_db, populate_db]:
app.cli.add_command(app.cli.command()(command))
# add a single command
@app.cli.command()
@click.option("--username", "-u")
@click.option("--password", "-p")
def add_user(username, password):
"""Adds a new user to the database"""
return create_user(username, password)

View File

@ -0,0 +1,7 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def init_app(app):
db.init_app(app)

View File

@ -0,0 +1,13 @@
from flask import Blueprint
from flask_restful import Api
from .resources import ProductItemResource, ProductResource
bp = Blueprint("restapi", __name__, url_prefix="/api/v1")
api = Api(bp)
def init_app(app):
api.add_resource(ProductResource, "/product/")
api.add_resource(ProductItemResource, "/product/<product_id>")
app.register_blueprint(bp)

View File

@ -0,0 +1,35 @@
from flask import abort, jsonify
from flask_restful import Resource
from flask_simplelogin import login_required
from project_name.models import Product
class ProductResource(Resource):
def get(self):
products = Product.query.all() or abort(204)
return jsonify(
{"products": [product.to_dict() for product in products]}
)
@login_required(basic=True, username="admin")
def post(self):
"""
Creates a new product.
Only admin user authenticated using basic auth can post
Basic takes base64 encripted username:password.
# curl -XPOST localhost:5000/api/v1/product/ \
# -H "Authorization: Basic Y2h1Y2s6bm9ycmlz" \
# -H "Content-Type: application/json"
"""
return NotImplementedError(
"Someone please complete this example and send a PR :)"
)
class ProductItemResource(Resource):
def get(self, product_id):
product = Product.query.filter_by(id=product_id).first() or abort(404)
return jsonify(product.to_dict())

View File

@ -0,0 +1,16 @@
from flask import Blueprint
from .views import index, only_admin, product, secret
bp = Blueprint("webui", __name__, template_folder="templates")
bp.add_url_rule("/", view_func=index)
bp.add_url_rule(
"/product/<product_id>", view_func=product, endpoint="productview"
)
bp.add_url_rule("/secret", view_func=secret, endpoint="secret")
bp.add_url_rule("/only_admin", view_func=only_admin, endpoint="onlyadmin")
def init_app(app):
app.register_blueprint(bp)

View File

@ -0,0 +1,29 @@
{% extends "bootstrap/base.html" %}
{% block title %}{{config.get('TITLE')}}{% endblock %}
{% block navbar %}
<div class="navbar">
<div class="navbar-header">
<a class="navbar-brand" href="#">
<img alt="Brand" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAB+0lEQVR4AcyYg5LkUBhG+1X2PdZGaW3btm3btm3bHttWrPomd1r/2Jn/VJ02TpxcH4CQ/dsuazWgzbIdrm9dZVd4pBz4zx2igTaFHrhvjneVXNHCSqIlFEjiwMyyyOBilRgGSqLNF1jnwNQdIvAt48C3IlBmHCiLQHC2zoHDu6zG1iXn6+y62ScxY9AODO6w0pvAqf23oSE4joOfH6OxfMoRnoGUm+de8wykbFt6wZtA07QwtNOqKh3ZbS3Wzz2F+1c/QJY0UCJ/J3kXWJfv7VhxCRRV1jGw7XI+gcO7rEFFRvdYxydwcPsVsC0bQdKScngt4iUTD4Fy/8p7PoHzRu1DclwmgmiqgUXjD3oTKHbAt869qdJ7l98jNTEblPTkXMwetpvnftA0LLHb4X8kiY9Kx6Q+W7wJtG0HR7fdrtYz+x7iya0vkEtUULIzCjC21wY+W/GYXusRH5kGytWTLxgEEhePPwhKYb7EK3BQuxWwTBuUkd3X8goUn6fMHLyTT+DCsQdAEXNzSMeVPAJHdF2DmH8poCREp3uwm7HsGq9J9q69iuunX6EgrwQVObjpBt8z6rdPfvE8kiiyhsvHnomrQx6BxYUyYiNS8f75H1w4/ISepDZLoDhNJ9cdNUquhRsv+6EP9oNH7Iff2A9g8h8CLt1gH0Qf9NMQAFnO60BJFQe0AAAAAElFTkSuQmCC">
</a>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
<h1>{{config.get('TITLE')}}</h1>
<div class="jumbotron">
<ul class="list-group">
{% for product in products %}
<li class="list-group-item">
<a href="{{url_for('webui.productview', product_id=product.id)}}">{{product.name}}- {{ "%0.2f" | format(product.price)}}</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "index.html" %}
{% block content %}
<div class="container">
<h1>{{ product.name }}</h1>
<div class="jumbotron">
<h2>R$ {{ "%0.2f" | format(product.price)}}</h2>
<p>
{{product.description}}
</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,26 @@
from flask import abort, render_template
from flask_simplelogin import login_required
from project_name.models import Product
def index():
products = Product.query.all()
return render_template("index.html", products=products)
def product(product_id):
product = Product.query.filter_by(id=product_id).first() or abort(
404, "produto nao encontrado"
)
return render_template("product.html", product=product)
@login_required
def secret():
return "This can be seen only if user is logged in"
@login_required(username="admin")
def only_admin():
return "only admin user can see this text"

16
project_name/models.py Normal file
View File

@ -0,0 +1,16 @@
from sqlalchemy_serializer import SerializerMixin
from project_name.ext.database import db
class Product(db.Model, SerializerMixin):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(140))
price = db.Column(db.Numeric())
description = db.Column(db.Text)
class User(db.Model, SerializerMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(140))
password = db.Column(db.String(512))

10
requirements-base.txt Normal file
View File

@ -0,0 +1,10 @@
pytest
coverage
flake8
black
isort
pytest-cov
codecov
mypy
gitchangelog
mkdocs

5
requirements-test.txt Normal file
View File

@ -0,0 +1,5 @@
flask-debugtoolbar
flask-shell-ipython
ipdb
pytest-flask
python-dotenv

9
requirements.txt Normal file
View File

@ -0,0 +1,9 @@
flask
flask-admin
flask-simplelogin
flask-bootstrap
flask-sqlalchemy
sqlalchemy-serializer
dynaconf
click
flask-restful

47
settings.toml Normal file
View File

@ -0,0 +1,47 @@
[default]
DEBUG = false
FLASK_ADMIN_TEMPLATE_MODE = "bootstrap3"
FLASK_ADMIN_SWATCH = 'cerulean'
SQLALCHEMY_DATABASE_URI = 'sqlite:///development.db'
TITLE = "project_name"
SECRET_KEY = "Pl3453Ch4ng3"
PASSWORD_SCHEMES = ['pbkdf2_sha512', 'md5_crypt']
EXTENSIONS = [
"flask_bootstrap:Bootstrap",
"project_name.ext.database:init_app",
"project_name.ext.auth:init_app",
"project_name.ext.admin:init_app",
"project_name.ext.commands:init_app",
"project_name.ext.webui:init_app",
"project_name.ext.restapi:init_app",
]
[development]
EXTENSIONS = [
"flask_debugtoolbar:DebugToolbarExtension",
"dynaconf_merge_unique" # to reuse extensions list from [default]
]
TEMPLATES_AUTO_RELOAD = true
DEBUG = true
DEBUG_TOOLBAR_ENABLED = true
DEBUG_TB_INTERCEPT_REDIRECTS = false
DEBUG_TB_PROFILER_ENABLED = true
DEBUG_TB_TEMPLATE_EDITOR_ENABLED = true
DEBUG_TB_PANELS = [
"flask_debugtoolbar.panels.versions.VersionDebugPanel",
"flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel",
"flask_debugtoolbar.panels.timer.TimerDebugPanel",
"flask_debugtoolbar.panels.headers.HeaderDebugPanel",
"flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel",
"flask_debugtoolbar.panels.template.TemplateDebugPanel",
"flask_debugtoolbar.panels.route_list.RouteListDebugPanel",
"flask_debugtoolbar.panels.logger.LoggingPanel",
"flask_debugtoolbar.panels.profiler.ProfilerDebugPanel",
"flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel"
]
[testing]
SQLALCHEMY_DATABASE_URI = 'sqlite:///testing.db'
[production]
SQLALCHEMY_DATABASE_URI = 'sqlite:///production.db'

49
setup.py Normal file
View File

@ -0,0 +1,49 @@
"""Python setup.py for project_name package"""
import io
import os
from setuptools import find_packages, setup
def read(*paths, **kwargs):
"""Read the contents of a text file safely.
>>> read("project_name", "VERSION")
'0.1.0'
>>> read("README.md")
...
"""
content = ""
with io.open(
os.path.join(os.path.dirname(__file__), *paths),
encoding=kwargs.get("encoding", "utf8"),
) as open_file:
content = open_file.read().strip()
return content
def read_requirements(path):
return [
line.strip()
for line in read(path).split("\n")
if not line.startswith(('"', "#", "-", "git+"))
]
setup(
name="project_name",
version=read("project_name", "VERSION"),
description="project_description",
url="https://git.disi.dev/author_name/project_urlname/",
long_description=read("README.md"),
long_description_content_type="text/markdown",
author="author_name",
packages=find_packages(exclude=["tests", ".gitea"]),
install_requires=read_requirements("requirements.txt"),
entry_points={
"console_scripts": ["project_name = project_name.__main__:main"]
},
extras_require={
"test": read_requirements("requirements-test.txt")
+ read_requirements("requirements-base.txt")
},
)

0
tests/__init__.py Normal file
View File

33
tests/conftest.py Normal file
View File

@ -0,0 +1,33 @@
import sys
import pytest
from project_name import create_app
from project_name.ext.commands import populate_db
from project_name.ext.database import db
@pytest.fixture(scope="session")
def app():
app = create_app(FORCE_ENV_FOR_DYNACONF="testing")
with app.app_context():
db.create_all(app=app)
yield app
db.drop_all(app=app)
@pytest.fixture(scope="session")
def products(app):
with app.app_context():
return populate_db()
# each test runs on cwd to its temp dir
@pytest.fixture(autouse=True)
def go_to_tmpdir(request):
# Get the fixture dynamically by its name.
tmpdir = request.getfixturevalue("tmpdir")
# ensure local test created packages can be imported
sys.path.insert(0, str(tmpdir))
# Chdir only for the duration of the test.
with tmpdir.as_cwd():
yield

28
tests/test_api.py Normal file
View File

@ -0,0 +1,28 @@
from decimal import Decimal
def test_products_get_all(client, products): # Arrange
"""Test get all products"""
# Act
response = client.get("/api/v1/product/")
# Assert
assert response.status_code == 200
data = response.json["products"]
assert len(data) == 3
for product in products:
assert product.id in [item["id"] for item in data]
assert product.name in [item["name"] for item in data]
assert product.price in [Decimal(item["price"]) for item in data]
def test_products_get_one(client, products): # Arrange
"""Test get one product"""
for product in products:
# Act
response = client.get(f"/api/v1/product/{product.id}")
data = response.json
# Assert
assert response.status_code == 200
assert data["name"] == product.name
assert Decimal(data["price"]) == product.price
assert data["description"] == product.description

3
wsgi.py Normal file
View File

@ -0,0 +1,3 @@
from project_name import create_app_wsgi
app = application = create_app_wsgi() # noqa