diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1e9d2a1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [rochacbruno] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..0d9360d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug, help wanted +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..cc98b69 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement, question +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..9ccc736 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +### Summary :memo: +_Write an overview about it._ + +### Details +_Describe more what you did on changes._ +1. (...) +2. (...) + +### Bugfixes :bug: (delete if dind't have any) +- + +### Checks +- [ ] Closed #798 +- [ ] Tested Changes +- [ ] Stakeholder Approval diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..120c689 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/init.sh b/.github/init.sh new file mode 100755 index 0000000..8a32ee3 --- /dev/null +++ b/.github/init.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +overwrite_template_dir=0 + +while getopts t:o flag +do + case "${flag}" in + t) template=${OPTARG};; + o) overwrite_template_dir=1;; + esac +done + +if [ -z "${template}" ]; then + echo "Available templates: flask" + read -p "Enter template name: " template +fi + +repo_urlname=$(basename -s .git `git config --get remote.origin.url`) +repo_name=$(basename -s .git `git config --get remote.origin.url` | tr '-' '_' | tr '[:upper:]' '[:lower:]') +repo_owner=$(git config --get remote.origin.url | awk -F ':' '{print $2}' | awk -F '/' '{print $1}') +echo "Repo name: ${repo_name}" +echo "Repo owner: ${repo_owner}" +echo "Repo urlname: ${repo_urlname}" + +if [ -f ".github/workflows/rename_project.yml" ]; then + .github/rename_project.sh -a "${repo_owner}" -n "${repo_name}" -u "${repo_urlname}" -d "Awesome ${repo_name} created by ${repo_owner}" +fi + +function download_template { + rm -rf "${template_dir}" + mkdir -p .github/templates + git clone "${template_url}" "${template_dir}" +} + +echo "Using template:${template}" +template_url="https://github.com/rochacbruno/${template}-project-template" +template_dir=".github/templates/${template}" +if [ -d "${template_dir}" ]; then + # Template directory already exists + if [ "${overwrite_template_dir}" -eq 1 ]; then + # user passed -o flag, delete and re-download + echo "Overwriting ${template_dir}" + download_template + else + # Ask user if they want to overwrite + echo "Directory ${template_dir} already exists." + read -p "Do you want to overwrite it? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Overwriting ${template_dir}" + download_template + else + # User decided not to overwrite + echo "Using existing ${template_dir}" + fi + fi +else + # Template directory does not exist, download it + echo "Downloading ${template_url}" + download_template +fi + +echo "Applying ${template} template to this project"} +./.github/templates/${template}/apply.sh -a "${repo_owner}" -n "${repo_name}" -u "${repo_urlname}" -d "Awesome ${repo_name} created by ${repo_owner}" + +# echo "Removing temporary template files" +# rm -rf .github/templates/${template} + +echo "Done! review, commit and push the changes" diff --git a/.github/release_message.sh b/.github/release_message.sh new file mode 100755 index 0000000..f5a9062 --- /dev/null +++ b/.github/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/.github/rename_project.sh b/.github/rename_project.sh new file mode 100755 index 0000000..8f05495 --- /dev/null +++ b/.github/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 .github/template.yml diff --git a/.github/template.yml b/.github/template.yml new file mode 100644 index 0000000..3386bee --- /dev/null +++ b/.github/template.yml @@ -0,0 +1 @@ +author: rochacbruno diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5e82e08 --- /dev/null +++ b/.github/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@v3 + - uses: actions/setup-python@v4 + 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@v3 + - uses: actions/setup-python@v4 + 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@v3 + # 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@v3 + - uses: actions/setup-python@v4 + 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@v3 + - uses: actions/setup-python@v4 + 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/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c38b48e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Upload Python Package +permissions: + contents: write + +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 + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + 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: .github/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@v3 + - name: Set up Python + uses: actions/setup-python@v4 + 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/.github/workflows/rename_project.yml b/.github/workflows/rename_project.yml new file mode 100644 index 0000000..ae6d2eb --- /dev/null +++ b/.github/workflows/rename_project.yml @@ -0,0 +1,42 @@ +name: Rename the project from template + +on: [push] + +permissions: write-all + +jobs: + rename-project: + if: ${{ !contains (github.repository, '/python-project-template') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + 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 .github/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 }}" + .github/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..2d0fadb --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# 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 +.github/templates/* diff --git a/ABOUT_THIS_TEMPLATE.md b/ABOUT_THIS_TEMPLATE.md new file mode 100644 index 0000000..11795f3 --- /dev/null +++ b/ABOUT_THIS_TEMPLATE.md @@ -0,0 +1,198 @@ +# 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 +├── .github # Github metadata for repository +│   ├── release_message.sh # A script to generate a release message +│   └── workflows # The CI pipeline for Github Actions +├── .gitignore # A list of files to ignore when pushing to Github +├── 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://github.com/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://github.com/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, +but for this template I wanted to use the Github `Use this template` button, +to use this template doesn't require to install extra tooling such as cookiecutter. + +Just click on [Use this template](https://github.com/rochacbruno/python-project-template/generate) and you are good to go. + +The substituions are done using github 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..0d0dd72 --- /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 github interface click on `Fork` button. +- Clone your fork of this repo. `git clone git@github.com:YOUR_GIT_USERNAME/project_urlname.git` +- Enter the directory `cd project_urlname` +- Add upstream repo `git remote add upstream https://github.com/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 github 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, github actions will +automatically create a new release on github 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..83bb605 --- /dev/null +++ b/Containerfile @@ -0,0 +1,5 @@ +FROM python:3.7-slim +COPY . /app +WORKDIR /app +RUN pip install . +CMD ["project_name"] diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..9bf6ef0 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,13 @@ +Changelog +========= + + +0.1.2 (2021-08-14) +------------------ +- Fix release, README and windows CI. [Bruno Rocha] +- Release: version 0.1.0. [Bruno Rocha] + + +0.1.0 (2021-08-14) +------------------ +- Add release command. [Bruno Rocha] 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..52d91ac --- /dev/null +++ b/Makefile @@ -0,0 +1,122 @@ +.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 github" + @read -p "Version? (provide the next x.y.z semver) : " TAG + @echo "$${TAG}" > project_name/VERSION + @$(ENV_PREFIX)gitchangelog > HISTORY.md + @git add project_name/VERSION HISTORY.md + @git commit -m "release: version $${TAG} 🚀" + @echo "creating git tag : $${TAG}" + @git tag $${TAG} + @git push -u origin HEAD --tags + @echo "Github 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 || 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-test.txt | while read in; do poetry add --no-interaction "$${in}" --dev; done + @poetry install --no-interaction + @mkdir -p .github/backup + @mv requirements* .github/backup + @mv setup.py .github/backup + @echo "You have switched to https://python-poetry.org/ package manager." + @echo "Please run 'poetry shell' or 'poetry run project_name'" + +.PHONY: init +init: ## Initialize the project based on an application template. + @./.github/init.sh + + +# This project has been generated from rochacbruno/python-project-template +# __author__ = 'rochacbruno' +# __repo__ = https://github.com/rochacbruno/python-project-template +# __sponsor__ = https://github.com/sponsors/rochacbruno/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..21f444c --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ + +# Python Project Template + +A low dependency and really simple to start project template for Python Projects. + +See also +- [Flask-Project-Template](https://github.com/rochacbruno/flask-project-template/) for a full feature Flask project including database, API, admin interface, etc. +- [FastAPI-Project-Template](https://github.com/rochacbruno/fastapi-project-template/) The base to start an openapi project featuring: SQLModel, Typer, FastAPI, JWT Token Auth, Interactive Shell, Management Commands. + +### HOW TO USE THIS TEMPLATE + +> **DO NOT FORK** this is meant to be used from **[Use this template](https://github.com/rochacbruno/python-project-template/generate)** feature. + +1. Click on **[Use this template](https://github.com/rochacbruno/python-project-template/generate)** +3. Give a name to your project + (e.g. `my_awesome_project` recommendation is to use all lowercase and underscores separation for repo names.) +3. Wait until the first run of CI finishes + (Github Actions will process the template and commit to your new repo) +4. If you want [codecov](https://about.codecov.io/sign-up/) Reports and Automatic Release to [PyPI](https://pypi.org) + On the new repository `settings->secrets` add your `PYPI_API_TOKEN` and `CODECOV_TOKEN` (get the tokens on respective websites) +4. Read the file [CONTRIBUTING.md](CONTRIBUTING.md) +5. Then clone your new project and happy coding! + +> **NOTE**: **WAIT** until first CI run on github actions before cloning your new project. + +### What is included on this template? + +- 🖼️ Templates for starting multiple application types: + * **Basic low dependency** Python program (default) [use this template](https://github.com/rochacbruno/python-project-template/generate) + * **Flask** with database, admin interface, restapi and authentication [use this template](https://github.com/rochacbruno/flask-project-template/generate). + **or Run `make init` after cloning to generate a new project based on a template.** +- 📦 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 github actions. +- 🎯 Entry points to execute your program using `python -m ` or `$ project_name` with basic CLI argument parsing. +- 🔄 Continuous integration using [Github Actions](.github/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) +> If you want to contribute to this template please open an [issue](https://github.com/rochacbruno/python-project-template/issues) or fork and send a PULL REQUEST. + +[❤️ Sponsor this project](https://github.com/sponsors/rochacbruno/) + + + +--- +# project_name + +[![codecov](https://codecov.io/gh/author_name/project_urlname/branch/main/graph/badge.svg?token=project_urlname_token_here)](https://codecov.io/gh/author_name/project_urlname) +[![CI](https://github.com/author_name/project_urlname/actions/workflows/main.yml/badge.svg)](https://github.com/author_name/project_urlname/actions/workflows/main.yml) + +project_description + +## Install it from PyPI + +```bash +pip install project_name +``` + +## Usage + +```py +from project_name import BaseClass +from project_name import base_function + +BaseClass().base_method() +base_function() +``` + +```bash +$ python -m project_name +#or +$ project_name +``` + +## Development + +Read the [CONTRIBUTING.md](CONTRIBUTING.md) file. 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..e69de29 diff --git a/project_name/__main__.py b/project_name/__main__.py new file mode 100644 index 0000000..2ba8b18 --- /dev/null +++ b/project_name/__main__.py @@ -0,0 +1,6 @@ +"""Entry point for project_name.""" + +from project_name.cli import main # pragma: no cover + +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..ac590b9 --- /dev/null +++ b/project_name/base.py @@ -0,0 +1,17 @@ +""" +project_name base module. + +This is the principal module of the project_name project. +here you put your main classes and objects. + +Be creative! do whatever you want! + +If you want to replace this with a Flask application run: + + $ make init + +and then choose `flask` as template. +""" + +# example constant variable +NAME = "project_name" diff --git a/project_name/cli.py b/project_name/cli.py new file mode 100644 index 0000000..66e9ca1 --- /dev/null +++ b/project_name/cli.py @@ -0,0 +1,28 @@ +"""CLI interface for project_name project. + +Be creative! do whatever you want! + +- Install click or typer and create a CLI app +- Use builtin argparse +- Start a web application +- Import things from your .base module +""" + + +def main(): # pragma: no cover + """ + The main function executes on commands: + `python -m project_name` and `$ project_name `. + + This is your program's entry point. + + You can change this function to do whatever you want. + Examples: + * Run a test suite + * Run a server + * Do some other stuff + * Run a command line application (Click, Typer, ArgParse) + * List all available tasks + * Run an application (Flask, FastAPI, Django, etc.) + """ + print("This will do something") diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..e89ee5c --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,10 @@ +# These requirements are for development and testing only, not for production. +pytest +coverage +flake8 +black +isort +pytest-cov +mypy +gitchangelog +mkdocs diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b05f2a6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# This template is a low-dependency template. +# By default there is no requirements added here. +# Add the requirements you need to this file. +# or run `make init` to create this file automatically based on the template. +# You can also run `make switch-to-poetry` to use the poetry package manager. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7547627 --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +"""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://github.com/author_name/project_urlname/", + long_description=read("README.md"), + long_description_content_type="text/markdown", + author="author_name", + packages=find_packages(exclude=["tests", ".github"]), + install_requires=read_requirements("requirements.txt"), + entry_points={ + "console_scripts": ["project_name = project_name.__main__:main"] + }, + extras_require={"test": read_requirements("requirements-test.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..1cbb7b1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import sys +import pytest + + +# 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_base.py b/tests/test_base.py new file mode 100644 index 0000000..f1b765f --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,5 @@ +from project_name.base import NAME + + +def test_base(): + assert NAME == "project_name"