From 58ba6dd2f8784100baa75f59f754051ae7821700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Anh=20B=C3=ACnh?= Date: Thu, 8 Jun 2023 15:28:12 +0700 Subject: [PATCH] Initial commit --- .github/FUNDING.yml | 12 ++ .github/ISSUE_TEMPLATE/bug_report.md | 31 ++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++ .github/PULL_REQUEST_TEMPLATE.md | 15 ++ .github/init.sh | 68 ++++++++ .github/release_message.sh | 3 + .github/rename_project.sh | 36 ++++ .github/template.yml | 1 + .github/workflows/main.yml | 92 ++++++++++ .github/workflows/release.yml | 48 ++++++ .github/workflows/rename_project.yml | 42 +++++ .gitignore | 132 +++++++++++++++ ABOUT_THIS_TEMPLATE.md | 198 ++++++++++++++++++++++ CONTRIBUTING.md | 113 ++++++++++++ Containerfile | 5 + HISTORY.md | 13 ++ LICENSE | 24 +++ MANIFEST.in | 5 + Makefile | 122 +++++++++++++ README.md | 85 ++++++++++ docs/index.md | 17 ++ mkdocs.yml | 2 + project_name/VERSION | 1 + project_name/__init__.py | 0 project_name/__main__.py | 6 + project_name/base.py | 17 ++ project_name/cli.py | 28 +++ requirements-test.txt | 11 ++ requirements.txt | 5 + setup.py | 46 +++++ tests/__init__.py | 0 tests/conftest.py | 14 ++ tests/test_base.py | 5 + 33 files changed, 1217 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100755 .github/init.sh create mode 100755 .github/release_message.sh create mode 100755 .github/rename_project.sh create mode 100644 .github/template.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/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 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/cli.py create mode 100644 requirements-test.txt create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_base.py diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..85b7646 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [maycuatroi] +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/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..73b5116 --- /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@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/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d3033aa --- /dev/null +++ b/.github/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: .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@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/.github/workflows/rename_project.yml b/.github/workflows/rename_project.yml new file mode 100644 index 0000000..c076c00 --- /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@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 .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..c78f074 --- /dev/null +++ b/Containerfile @@ -0,0 +1,5 @@ +FROM python:3.7-alpine +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..2d71a6f --- /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 + +.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..16c2a86 --- /dev/null +++ b/project_name/__main__.py @@ -0,0 +1,6 @@ +"""Entry point for project_name.""" + +from .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..fc6c4c1 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,11 @@ +# This requirements are for development and testing only, not for production. +pytest +coverage +flake8 +black +isort +pytest-cov +codecov +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"