From 5efd61d23641161915dfb801c637d461ff3b72fe Mon Sep 17 00:00:00 2001 From: Simon Diesenreiter Date: Sun, 10 Nov 2024 07:27:36 -0800 Subject: [PATCH] initial commit --- .gitea/release_message.sh | 3 + .gitea/rename_project.sh | 36 ++++ .gitea/template.yml | 1 + .gitea/workflows/main.yml | 92 +++++++++ .gitea/workflows/release.yml | 48 +++++ .gitea/workflows/rename_project.yml | 42 ++++ .gitignore | 133 ++++++++++++ ABOUT_THIS_TEMPLATE.md | 195 ++++++++++++++++++ CONTRIBUTING.md | 113 ++++++++++ Containerfile | 9 + HISTORY.md | 0 LICENSE | 24 +++ MANIFEST.in | 5 + Makefile | 119 +++++++++++ README.md | 101 +++++++++ apply.sh | 77 +++++++ docs/index.md | 17 ++ mkdocs.yml | 2 + project_name/VERSION | 1 + project_name/__init__.py | 3 + project_name/__main__.py | 13 ++ project_name/base.py | 21 ++ project_name/ext/__init__.py | 0 project_name/ext/admin.py | 33 +++ project_name/ext/auth.py | 33 +++ project_name/ext/commands.py | 43 ++++ project_name/ext/database.py | 7 + project_name/ext/restapi/__init__.py | 13 ++ project_name/ext/restapi/resources.py | 35 ++++ project_name/ext/webui/__init__.py | 16 ++ project_name/ext/webui/templates/index.html | 29 +++ project_name/ext/webui/templates/product.html | 13 ++ project_name/ext/webui/views.py | 26 +++ project_name/models.py | 16 ++ requirements-base.txt | 10 + requirements-test.txt | 5 + requirements.txt | 9 + settings.toml | 47 +++++ setup.py | 49 +++++ tests/__init__.py | 0 tests/conftest.py | 33 +++ tests/test_api.py | 28 +++ wsgi.py | 3 + 43 files changed, 1503 insertions(+) create mode 100755 .gitea/release_message.sh create mode 100755 .gitea/rename_project.sh create mode 100644 .gitea/template.yml create mode 100644 .gitea/workflows/main.yml create mode 100644 .gitea/workflows/release.yml create mode 100644 .gitea/workflows/rename_project.yml create mode 100644 .gitignore create mode 100644 ABOUT_THIS_TEMPLATE.md create mode 100644 CONTRIBUTING.md create mode 100644 Containerfile create mode 100644 HISTORY.md create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 README.md create mode 100755 apply.sh create mode 100644 docs/index.md create mode 100644 mkdocs.yml create mode 100644 project_name/VERSION create mode 100644 project_name/__init__.py create mode 100644 project_name/__main__.py create mode 100644 project_name/base.py create mode 100644 project_name/ext/__init__.py create mode 100644 project_name/ext/admin.py create mode 100644 project_name/ext/auth.py create mode 100644 project_name/ext/commands.py create mode 100644 project_name/ext/database.py create mode 100644 project_name/ext/restapi/__init__.py create mode 100644 project_name/ext/restapi/resources.py create mode 100644 project_name/ext/webui/__init__.py create mode 100644 project_name/ext/webui/templates/index.html create mode 100644 project_name/ext/webui/templates/product.html create mode 100644 project_name/ext/webui/views.py create mode 100644 project_name/models.py create mode 100644 requirements-base.txt create mode 100644 requirements-test.txt create mode 100644 requirements.txt create mode 100644 settings.toml create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_api.py create mode 100644 wsgi.py 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