commit 5efd61d23641161915dfb801c637d461ff3b72fe Author: Simon Diesenreiter Date: Sun Nov 10 07:27:36 2024 -0800 initial commit diff --git a/.gitea/release_message.sh b/.gitea/release_message.sh new file mode 100755 index 0000000..f5a9062 --- /dev/null +++ b/.gitea/release_message.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +previous_tag=$(git tag --sort=-creatordate | sed -n 2p) +git shortlog "${previous_tag}.." | sed 's/^./ &/' diff --git a/.gitea/rename_project.sh b/.gitea/rename_project.sh new file mode 100755 index 0000000..b5ede16 --- /dev/null +++ b/.gitea/rename_project.sh @@ -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 diff --git a/.gitea/template.yml b/.gitea/template.yml new file mode 100644 index 0000000..3386bee --- /dev/null +++ b/.gitea/template.yml @@ -0,0 +1 @@ +author: rochacbruno diff --git a/.gitea/workflows/main.yml b/.gitea/workflows/main.yml new file mode 100644 index 0000000..a47768e --- /dev/null +++ b/.gitea/workflows/main.yml @@ -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 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..76aa2c9 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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/* diff --git a/.gitea/workflows/rename_project.yml b/.gitea/workflows/rename_project.yml new file mode 100644 index 0000000..d366948 --- /dev/null +++ b/.gitea/workflows/rename_project.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10f38e6 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/ABOUT_THIS_TEMPLATE.md b/ABOUT_THIS_TEMPLATE.md new file mode 100644 index 0000000..988751a --- /dev/null +++ b/ABOUT_THIS_TEMPLATE.md @@ -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 + +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. +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..02fc87f --- /dev/null +++ b/CONTRIBUTING.md @@ -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 + +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. diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..8950ef3 --- /dev/null +++ b/Containerfile @@ -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"] diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ef198d6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE +include HISTORY.md +include Containerfile +graft tests +graft project_name diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8644a01 --- /dev/null +++ b/Makefile @@ -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 " + @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/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3da85b9 --- /dev/null +++ b/README.md @@ -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 ` 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) + + + +--- +# 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. diff --git a/apply.sh b/apply.sh new file mode 100755 index 0000000..a40e718 --- /dev/null +++ b/apply.sh @@ -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." diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..000ea34 --- /dev/null +++ b/docs/index.md @@ -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. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..33a69ca --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,2 @@ +site_name: project_name +theme: readthedocs diff --git a/project_name/VERSION b/project_name/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/project_name/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/project_name/__init__.py b/project_name/__init__.py new file mode 100644 index 0000000..8a3d8f8 --- /dev/null +++ b/project_name/__init__.py @@ -0,0 +1,3 @@ +from .base import create_app, create_app_wsgi + +__all__ = ["create_app", "create_app_wsgi"] diff --git a/project_name/__main__.py b/project_name/__main__.py new file mode 100644 index 0000000..de55e8d --- /dev/null +++ b/project_name/__main__.py @@ -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() diff --git a/project_name/base.py b/project_name/base.py new file mode 100644 index 0000000..24ec521 --- /dev/null +++ b/project_name/base.py @@ -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 diff --git a/project_name/ext/__init__.py b/project_name/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project_name/ext/admin.py b/project_name/ext/admin.py new file mode 100644 index 0000000..ba17619 --- /dev/null +++ b/project_name/ext/admin.py @@ -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)) diff --git a/project_name/ext/auth.py b/project_name/ext/auth.py new file mode 100644 index 0000000..ce09c5d --- /dev/null +++ b/project_name/ext/auth.py @@ -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) diff --git a/project_name/ext/commands.py b/project_name/ext/commands.py new file mode 100644 index 0000000..5ddb077 --- /dev/null +++ b/project_name/ext/commands.py @@ -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) diff --git a/project_name/ext/database.py b/project_name/ext/database.py new file mode 100644 index 0000000..9121c6e --- /dev/null +++ b/project_name/ext/database.py @@ -0,0 +1,7 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +def init_app(app): + db.init_app(app) diff --git a/project_name/ext/restapi/__init__.py b/project_name/ext/restapi/__init__.py new file mode 100644 index 0000000..36aef28 --- /dev/null +++ b/project_name/ext/restapi/__init__.py @@ -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/") + app.register_blueprint(bp) diff --git a/project_name/ext/restapi/resources.py b/project_name/ext/restapi/resources.py new file mode 100644 index 0000000..bb7426e --- /dev/null +++ b/project_name/ext/restapi/resources.py @@ -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()) diff --git a/project_name/ext/webui/__init__.py b/project_name/ext/webui/__init__.py new file mode 100644 index 0000000..6537e1a --- /dev/null +++ b/project_name/ext/webui/__init__.py @@ -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/", 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) diff --git a/project_name/ext/webui/templates/index.html b/project_name/ext/webui/templates/index.html new file mode 100644 index 0000000..65d0117 --- /dev/null +++ b/project_name/ext/webui/templates/index.html @@ -0,0 +1,29 @@ +{% extends "bootstrap/base.html" %} +{% block title %}{{config.get('TITLE')}}{% endblock %} + +{% block navbar %} + +{% endblock %} + +{% block content %} +
+

{{config.get('TITLE')}}

+ +
+ +
+ +
+{% endblock %} diff --git a/project_name/ext/webui/templates/product.html b/project_name/ext/webui/templates/product.html new file mode 100644 index 0000000..e787d2f --- /dev/null +++ b/project_name/ext/webui/templates/product.html @@ -0,0 +1,13 @@ +{% extends "index.html" %} +{% block content %} +
+

{{ product.name }}

+ +
+

R$ {{ "%0.2f" | format(product.price)}}

+

+ {{product.description}} +

+
+
+{% endblock %} diff --git a/project_name/ext/webui/views.py b/project_name/ext/webui/views.py new file mode 100644 index 0000000..cead2e3 --- /dev/null +++ b/project_name/ext/webui/views.py @@ -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" diff --git a/project_name/models.py b/project_name/models.py new file mode 100644 index 0000000..6986135 --- /dev/null +++ b/project_name/models.py @@ -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)) diff --git a/requirements-base.txt b/requirements-base.txt new file mode 100644 index 0000000..aef9f2a --- /dev/null +++ b/requirements-base.txt @@ -0,0 +1,10 @@ +pytest +coverage +flake8 +black +isort +pytest-cov +codecov +mypy +gitchangelog +mkdocs diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..6616055 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,5 @@ +flask-debugtoolbar +flask-shell-ipython +ipdb +pytest-flask +python-dotenv diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..031089c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +flask +flask-admin +flask-simplelogin +flask-bootstrap +flask-sqlalchemy +sqlalchemy-serializer +dynaconf +click +flask-restful diff --git a/settings.toml b/settings.toml new file mode 100644 index 0000000..39b5a59 --- /dev/null +++ b/settings.toml @@ -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' diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..10e5262 --- /dev/null +++ b/setup.py @@ -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") + }, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..25798ba --- /dev/null +++ b/tests/conftest.py @@ -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 diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..5e1004a --- /dev/null +++ b/tests/test_api.py @@ -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 diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..3197cf9 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,3 @@ +from project_name import create_app_wsgi + +app = application = create_app_wsgi() # noqa