initial commit
This commit is contained in:
commit
5efd61d236
3
.gitea/release_message.sh
Executable file
3
.gitea/release_message.sh
Executable 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
36
.gitea/rename_project.sh
Executable 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
1
.gitea/template.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
author: rochacbruno
|
92
.gitea/workflows/main.yml
Normal file
92
.gitea/workflows/main.yml
Normal 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
|
48
.gitea/workflows/release.yml
Normal file
48
.gitea/workflows/release.yml
Normal 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/*
|
42
.gitea/workflows/rename_project.yml
Normal file
42
.gitea/workflows/rename_project.yml
Normal 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
133
.gitignore
vendored
Normal 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
195
ABOUT_THIS_TEMPLATE.md
Normal 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
113
CONTRIBUTING.md
Normal 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
9
Containerfile
Normal 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
0
HISTORY.md
Normal file
24
LICENSE
Normal file
24
LICENSE
Normal 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
5
MANIFEST.in
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
include LICENSE
|
||||||
|
include HISTORY.md
|
||||||
|
include Containerfile
|
||||||
|
graft tests
|
||||||
|
graft project_name
|
119
Makefile
Normal file
119
Makefile
Normal 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
101
README.md
Normal 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
77
apply.sh
Executable 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
17
docs/index.md
Normal 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
2
mkdocs.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
site_name: project_name
|
||||||
|
theme: readthedocs
|
1
project_name/VERSION
Normal file
1
project_name/VERSION
Normal file
@ -0,0 +1 @@
|
|||||||
|
0.1.0
|
3
project_name/__init__.py
Normal file
3
project_name/__init__.py
Normal 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
13
project_name/__main__.py
Normal 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
21
project_name/base.py
Normal 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
|
0
project_name/ext/__init__.py
Normal file
0
project_name/ext/__init__.py
Normal file
33
project_name/ext/admin.py
Normal file
33
project_name/ext/admin.py
Normal 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
33
project_name/ext/auth.py
Normal 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)
|
43
project_name/ext/commands.py
Normal file
43
project_name/ext/commands.py
Normal 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)
|
7
project_name/ext/database.py
Normal file
7
project_name/ext/database.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
db.init_app(app)
|
13
project_name/ext/restapi/__init__.py
Normal file
13
project_name/ext/restapi/__init__.py
Normal 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)
|
35
project_name/ext/restapi/resources.py
Normal file
35
project_name/ext/restapi/resources.py
Normal 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())
|
16
project_name/ext/webui/__init__.py
Normal file
16
project_name/ext/webui/__init__.py
Normal 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)
|
29
project_name/ext/webui/templates/index.html
Normal file
29
project_name/ext/webui/templates/index.html
Normal 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 %}
|
13
project_name/ext/webui/templates/product.html
Normal file
13
project_name/ext/webui/templates/product.html
Normal 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 %}
|
26
project_name/ext/webui/views.py
Normal file
26
project_name/ext/webui/views.py
Normal 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
16
project_name/models.py
Normal 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
10
requirements-base.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
pytest
|
||||||
|
coverage
|
||||||
|
flake8
|
||||||
|
black
|
||||||
|
isort
|
||||||
|
pytest-cov
|
||||||
|
codecov
|
||||||
|
mypy
|
||||||
|
gitchangelog
|
||||||
|
mkdocs
|
5
requirements-test.txt
Normal file
5
requirements-test.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
flask-debugtoolbar
|
||||||
|
flask-shell-ipython
|
||||||
|
ipdb
|
||||||
|
pytest-flask
|
||||||
|
python-dotenv
|
9
requirements.txt
Normal file
9
requirements.txt
Normal 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
47
settings.toml
Normal 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
49
setup.py
Normal 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
0
tests/__init__.py
Normal file
33
tests/conftest.py
Normal file
33
tests/conftest.py
Normal 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
28
tests/test_api.py
Normal 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
|
Loading…
Reference in New Issue
Block a user