From 428005dbedf7d5e1e42a37d7578a72ef52c6e67f Mon Sep 17 00:00:00 2001 From: Joe Kaufeld Date: Fri, 7 Apr 2023 00:07:16 -0400 Subject: [PATCH] :tada: working copier setup --- .gitea/workflows/build_and_release.yml.jinja | 54 ++++++++ .gitignore.jinja | 131 +++++++++++++++++++ Makefile.jinja | 10 ++ README.md.jinja | 16 +++ [[_copier_conf.answers_file]].jinja | 0 copier.yml | 34 +++++ poetry.toml | 3 + pyproject.toml.jinja | 24 ++++ src/__init__.py | 1 + src/cli.py.jinja | 117 +++++++++++++++++ src/poetry2setup.py | 16 +++ src/preamble.py | 26 ++++ 12 files changed, 432 insertions(+) create mode 100644 .gitea/workflows/build_and_release.yml.jinja create mode 100644 .gitignore.jinja create mode 100644 Makefile.jinja create mode 100644 README.md.jinja create mode 100644 [[_copier_conf.answers_file]].jinja create mode 100644 copier.yml create mode 100644 poetry.toml create mode 100644 pyproject.toml.jinja create mode 100644 src/__init__.py create mode 100644 src/cli.py.jinja create mode 100644 src/poetry2setup.py create mode 100644 src/preamble.py diff --git a/.gitea/workflows/build_and_release.yml.jinja b/.gitea/workflows/build_and_release.yml.jinja new file mode 100644 index 0000000..d85139f --- /dev/null +++ b/.gitea/workflows/build_and_release.yml.jinja @@ -0,0 +1,54 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: https://github.com/actions/checkout@v2 + - uses: https://github.com/actions/setup-python@v4 + with: + python-version: '3.11.x' + - name: Install Env + # this should be all we need because shiv will download the deps itself + run: | + pip install --upgrade pip + pip install shiv + pip install poetry + - name: Add VERSION env property + run: | + echo "VERSION=v$(poetry version | python -c 'import sys;print(sys.stdin.read().split()[1])')" >> $GITHUB_ENV + echo ${{ env.VERSION }} + - name: Build the sucker + run: | + sed -i -e "s/?????/${{ env.VERSION }}/g" src/__init__.py + make build + - name: Create release! + run: | + JSON_DATA=$( + printf '%s' \ + '{'\ + '"tag_name":"${{ env.VERSION }}",'\ + '"name":"${{ env.VERSION }}",'\ + '"body":"RELEASE THE KRAKEN"'\ + '}' \ + ) + echo """release_id=$(\ + curl -X POST \ + -s https://git.joekaufeld.com/api/v1/repos/${GITHUB_REPOSITORY%/*}/${{ github.event.repository.name }}/releases \ + -H "Authorization: token ${{ secrets.PAT }}" \ + -H 'Content-Type: application/json' \ + -d "$JSON_DATA" \ + | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])"\ + )""" >> $GITHUB_ENV + - name: Upload assets! + run: | + curl https://git.joekaufeld.com/api/v1/repos/${GITHUB_REPOSITORY%/*}/${{ github.event.repository.name }}/releases/${{ env.release_id }}/assets \ + -H "Authorization: token ${{ secrets.PAT }}" \ + -F attachment=@[[ module_name ]] diff --git a/.gitignore.jinja b/.gitignore.jinja new file mode 100644 index 0000000..be57127 --- /dev/null +++ b/.gitignore.jinja @@ -0,0 +1,131 @@ +# 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/ +setup.py +{{ module_name }} diff --git a/Makefile.jinja b/Makefile.jinja new file mode 100644 index 0000000..d80be07 --- /dev/null +++ b/Makefile.jinja @@ -0,0 +1,10 @@ +setup: + python src/poetry2setup.py > setup.py + +build: setup shiv + +clean: + rm setup.py + +shiv: + shiv --preamble src/preamble.py -c [[ module_name ]] -o [[ module_name ]] . diff --git a/README.md.jinja b/README.md.jinja new file mode 100644 index 0000000..8b6c145 --- /dev/null +++ b/README.md.jinja @@ -0,0 +1,16 @@ +# [[ project_name ]] +[[ description ]] + +Use this template by installing `copier` and running: + +```shell +copier https://git.joekaufeld.com/jkaufeld/copier-shiv.git [dir] +``` + +After that, you can set up the system with: + +```shell +poetry shell +poetry install +make build +``` \ No newline at end of file diff --git a/[[_copier_conf.answers_file]].jinja b/[[_copier_conf.answers_file]].jinja new file mode 100644 index 0000000..e69de29 diff --git a/copier.yml b/copier.yml new file mode 100644 index 0000000..eb0e89c --- /dev/null +++ b/copier.yml @@ -0,0 +1,34 @@ +# questions +project_name: + type: str + help: What is your project name? + +module_name: + type: str + help: What is your Python module name? + +description: + type: str + help: Project description? + +author_name: + type: str + help: What's your name? + +email: + type: str + help: What's your contact email? + +repo_name: + type: str + help: What's the slug of the repo name on Gitea? + +_envops: + autoescape: false + block_end_string: "%]" + block_start_string: "[%" + comment_end_string: "#]" + comment_start_string: "[#" + keep_trailing_newline: true + variable_end_string: "]]" + variable_start_string: "[[" \ No newline at end of file diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..53b35d3 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/pyproject.toml.jinja b/pyproject.toml.jinja new file mode 100644 index 0000000..467e478 --- /dev/null +++ b/pyproject.toml.jinja @@ -0,0 +1,24 @@ +[tool.poetry] +name = "src" +version = "0.1.0" +description = "[[ description ]]" +authors = ["[[ author_name ]] <[[ email ]]>"] + +[tool.poetry.dependencies] +python = "^3.10" +shiv = "^1.0.1" +httpx = "^0.23.0" +click = "^8.1.3" +rich = "^12.5.1" +art = "^5.9" + +[tool.poetry.dev-dependencies] +poetry = "^1.1.14" +black = "^22.6.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.plugins."console_scripts"] +"[[ module_name ]]" = "src.cli:main" diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..47e608a --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +__version__ = "?????" # will be replaced during build by CI diff --git a/src/cli.py.jinja b/src/cli.py.jinja new file mode 100644 index 0000000..f83346f --- /dev/null +++ b/src/cli.py.jinja @@ -0,0 +1,117 @@ +import code +import io +import random +import string +import sys +import uuid + +import art +import click +import httpx +from rich import console as rich_console, pretty +from rich.status import Status +from rich.traceback import install +from shiv.bootstrap import current_zipfile + +import src + + +def print_help(): + ctx = click.get_current_context() + click.echo(ctx.get_help()) + ctx.exit() + + +class RichGroup(click.Group): + def format_usage(self, ctx, formatter): + sio = io.StringIO() + console = rich_console.Console(file=sio, force_terminal=True) + console.print("Usage: ./[[ module_name ]] [COMMAND]") + console.print(" All dependencies are contained within. Use your") + console.print(" chosen Python version to launch this file.") + formatter.write(sio.getvalue()) + + +@click.group( + cls=RichGroup, + context_settings=dict(help_option_names=["-h", "--help", "--halp"]), +) +@click.pass_context +@click.version_option(version=src.__version__, prog_name="[[ module_name ]]") +@click.option( + "--update", + is_flag=True, + help="Check Gitea for a new version and auto-update.", +) +@click.option( + "--shell", + "shell", + is_flag=True, + default=False, + help="Launch a REPL for testing.", +) +def main(ctx, update, shell): + """ + Launch [[ project_name ]] or drop into a command line REPL. + """ + if update: + update_from_gitea() + sys.exit() + if shell: + banner = art.text2art("[[ module_name ]]") + + pretty.install() # type: ignore + install() # traceback handler + + code.interact(local=globals(), banner=banner) + sys.exit() + elif ctx.invoked_subcommand is None: + print_help() + + +@main.command() +def uuid4(): + """Generate a random UUID4.""" + click.echo(uuid.uuid4()) + + +def update_from_gitea(): + """Get the newest release from Gitea and install it.""" + status = Status("Checking for new release...") + status.start() + + response = httpx.get( + "https://git.joekaufeld.com/api/v1/repos/jkaufeld/[[ repo_name ]]/releases/latest" + ) + if response.status_code != 200: + status.stop() + click.echo( + f"Something went wrong when talking to Gitea; got a" + f" {response.status_code} with the following content:\n" + f"{response.content}" + ) + return + status.update("Checking for new release...") + release_data = response.json() + if release_data["tag_name"] == src.__version__: + status.stop() + click.echo( + "Server version is the same as current version; nothing to update." + ) + return + status.update("Updating...") + + url = release_data["assets"][0]["browser_download_url"] + with current_zipfile() as archive: + with open(archive.filename, "wb") as f, httpx.stream( + "GET", url, follow_redirects=True + ) as r: + for line in r.iter_bytes(): + f.write(line) + + status.stop() + click.echo(f"Updated to {release_data['tag_name']}! 🎉") + + +if __name__ == "__main__": + main() diff --git a/src/poetry2setup.py b/src/poetry2setup.py new file mode 100644 index 0000000..e97754f --- /dev/null +++ b/src/poetry2setup.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from poetry.core.factory import Factory +from poetry.core.masonry.builders.sdist import SdistBuilder + + +def build_setup_py(): + return SdistBuilder(Factory().create_poetry(Path(".").resolve())).build_setup() + + +def main(): + print(build_setup_py().decode("utf8")) + + +if __name__ == "__main__": + main() diff --git a/src/preamble.py b/src/preamble.py new file mode 100644 index 0000000..c0263a5 --- /dev/null +++ b/src/preamble.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# https://shiv.readthedocs.io/en/latest/#preamble +import os +import shutil + +from pathlib import Path + +# These variables are injected by shiv.bootstrap +site_packages: Path +env: "shiv.bootstrap.environment.Environment" + +# Get a handle of the current PYZ's site_packages directory +current = site_packages.parent + +# The parent directory of the site_packages directory is our shiv cache +cache_path = current.parent + + +name, build_id = current.name.split('_') + +if __name__ == "__main__": + for path in cache_path.iterdir(): + if path.name.startswith(f"{name}_") and not path.name.endswith(build_id): + shutil.rmtree(path) + if path.name.startswith(f".{name}") and not path.name.endswith(f"{build_id}_lock"): + os.remove(path)