[
  {
    "path": ".github/workflows/build.yml",
    "content": "#\nname: Create and publish a Docker image\n\n# Configures this workflow to run every time a change is pushed to the branch called `release` or a new release is created.\non:\n  release:\n    types: [published]\n  push:\n    branches: [\"release\"]\n\n# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\n# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.\njobs:\n  build-and-push-image:\n    runs-on: ubuntu-latest\n    # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.\n    permissions:\n      contents: read\n      packages: write\n      attestations: write\n      id-token: write\n      #\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n      # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.\n      - name: Log in to the Container registry\n        uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` \"meta\" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n      # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.\n      # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see \"[Usage](https://github.com/docker/build-push-action#usage)\" in the README of the `docker/build-push-action` repository.\n      # It uses the `tags` and `labels` parameters to tag and label the image with the output from the \"meta\" step.\n      - name: Build and push Docker image\n        id: push\n        uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n\n      # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see \"[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).\"\n      - name: Generate artifact attestation\n        uses: actions/attest-build-provenance@v2\n        with:\n          subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}\n          subject-digest: ${{ steps.push.outputs.digest }}\n          push-to-registry: true\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish\non:\n  push:\n    branches:\n      - main\njobs:\n  build-format-and-publish-to-pypi:\n    runs-on: ubuntu-latest\n    # FROM https://github.com/pypa/gh-action-pypi-publish\n    environment:\n      name: pypi\n      url: https://pypi.org/p/python-template-zuppif\n    permissions:\n      id-token: write # IMPORTANT: this permission is mandatory for trusted publishing\n    steps:\n      - name: 🛎️ Checkout\n        uses: actions/checkout@v4\n\n      - name: 📦 Install uv\n        uses: astral-sh/setup-uv@v4\n\n      - name: 🐍 Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version-file: \".python-version\"\n\n      - name: 🦾 Install the project\n        run: uv sync\n\n      - name: 💅 Format\n        run: uv run ruff format\n\n      - name: 🦾 Build\n        run: uv build\n\n      - name: 🚀 Publish\n        uses: pypa/gh-action-pypi-publish@release/v1\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\non:\n  pull_request:\n    branches: [main, dev]\n\njobs:\n  build-lint-format-and-test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: 🛎️ Checkout\n        uses: actions/checkout@v4\n\n      - name: 📦 Install uv\n        uses: astral-sh/setup-uv@v4\n\n      - name: 🐍 Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version-file: \".python-version\"\n\n      - name: 🦾 Install the project\n        run: |\n          uv sync --all-extras --dev\n          uv pip install -e .\n\n      - name: 💅 Link & Format Check\n        run: make check_code_quality\n\n      - name: 🧪 Test\n        run: uv run pytest tests/\n"
  },
  {
    "path": ".gitignore",
    "content": "# ruff\n.ruff_cache\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# UV\n#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#uv.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control\n.pdm.toml\n.pdm-python\n.pdm-build/\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\n# PyPI configuration file\n.pypirc\n"
  },
  {
    "path": ".python-version",
    "content": "3.12\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Build stage\nFROM python:3.12-slim AS builder\n\n# Update image\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    build-essential \\\n    && rm -rf /var/lib/apt/lists/*\n# Install uv\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/\n\n# Set working directory\nWORKDIR /app\n\n# Install the project without the the source code (only dependencies)  \nRUN --mount=type=cache,target=/root/.cache/uv \\\n    --mount=type=bind,source=uv.lock,target=uv.lock \\\n    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \\\n    uv sync --frozen --no-install-project --no-dev --compile-bytecode\n\n# Install the project's source packages\nADD .python-version pyproject.toml uv.lock src/ /app/\n\nRUN --mount=type=cache,target=/root/.cache/uv \\\n    uv pip install --compile-bytecode .\n\n# Use alpine for the final image to reduce the total size\nFROM python:3.12-alpine\n\n# Copy the installed environment from builder\nCOPY --from=builder --chown=app:app /app /app\n\n# Set working directory\nWORKDIR /app\n\n# Configure PATH for executables, packages are in the /app/.venv folder\nENV PATH=\"/app/.venv/bin:$PATH\"\n\n# Run command\nCMD [\"make_me_laugh\"]"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Francesco Saverio Zuppichini\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: style check_code_quality\n\nexport PYTHONPATH = .\ncheck_dirs := src\n\nstyle:\n\tuv run ruff format  $(check_dirs)\n\tuv run ruff check --select I --fix \n\nlink:\n\tuv run ruff check --fix\n\ncheck_code_quality:\n\tuv run ruff check $(check_dirs)\n\t# check sort\n\tuv run ruff check -e\n\tuv run ruff check --select I -e\n\t\npublish:\n\tuv build\n\tuv publish --token $UV_PUBLISH_TOKEN"
  },
  {
    "path": "README.md",
    "content": "# Python Template 🐍\n[![PyPI version](https://img.shields.io/pypi/v/python-template-zuppif)](https://pypi.org/project/python-template-zuppif/)\n\nA template for a python project for 2025\n\nFeatures:\n- [x] 🛠️ configuration in a single file [`pyproject.toml`](pyproject.toml)\n- [x] 📦 [`uv`](https://docs.astral.sh/uv/) as package manager\n- [x] 💅 [`ruff`](https://docs.astral.sh/ruff/) for linting and formatting\n- [x] 🧪 [`pytest`](https://docs.pytest.org/en/stable/) \n- [x] 🧹 [`Makefile`](Makefile) with code quality checks\n- [ ] 📚 auto doc generation\n- [x] 🐳 CI/CD Optimized Docker Image runs when a new *release* is created pushing to gh registry\n- [x] 🦾 GitHub actions:\n    - [x] auto publish to [`pypi`](https://pypi.org/) on push on `main`\n    - [ ] auto creating a new tag on push on `main`, sync versions\n    - [x] run `tests` and `lint` on `dev` and `main` when a PR is open\n\n## Getting started\n\n### Installation\n\nTo set it up and run\n\n```bash\nuv sync\n```\n\nThen\n\n```bash\npython main.py\n```\n\nWill output a random joke\n\n```\nWhy did the cow in the pasture get promoted at work? ...  Because he is OUT-STANDING in his field!\n```\n\nYou can now run, for example, a function defined as `scripts` in the [`pyproject.toml`](pyproject.toml)\n\n```bash\nmake_me_laugh\n```\n\n### Linting\n\n```\nruff check\n```\n\n\n### Formatting\n\n```\nruff format\n```\n\n## CI/CD\n\n### Tests\nTests inside `/tests` are run using [`pytest`](https://docs.pytest.org/en/stable/) on PR both on `dev` and `main`\n\n### Publish Package\n In order to publish to [pypi](https://pypi.org/) you need to create a publisher on pypi.\n\nThis is explained [here](https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/) and [here](https://docs.pypi.org/trusted-publishers/) \n\nIn practice go your pypi project -> Manage Project -> Publishing, scroll and \"add a new publisher\"\n\n\n### Docker\n[`Dockerfile`](Dockerfile) contains a multi stage build that uses `--compile-bytecode` to compite your package. For this example, the resulting image is just\n\n```bash\ndocker build -t python-template .\n```\n\n```\nREPOSITORY        TAG       IMAGE ID       CREATED          SIZE\npython-template   latest    1ded7d260b1c   58 seconds ago   55.4MB\n```\n\nThe image is build using the [`build`](.github/workflows/build.yml) workflow when a new *release* is created\n"
  },
  {
    "path": "examples/main.py",
    "content": "from jokes import make_me_laugh\n\nmake_me_laugh()\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\n# https://docs.astral.sh/uv/concepts/projects/config/\nname = \"python-template-zuppif\"\nversion = \"0.1.3\"\ndescription = \"Add your description here\"\nreadme = \"README.md\"\nrequires-python = \">=3.12\"\ndependencies = [\"requests>=2.32.3\"]\n\n[dependency-groups]\ndev = [\"pytest>=8.3.4\", \"ruff>=0.8.4\"]\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"src/jokes\"]\n\n[tool.ruff]\n# https://docs.astral.sh/ruff/configuration/\ntarget-version = \"py312\"\n\n\n[project.scripts]\n# will run the function by just typing `make_me_laugh` in the cli\nmake_me_laugh = \"jokes:make_me_laugh\"\n"
  },
  {
    "path": "src/jokes/__init__.py",
    "content": "from .jokes import get_joke\n\n\ndef make_me_laugh():\n    joke = get_joke()\n    print(f\"{joke[\"preambula\"]} ... {joke['punchline']}\")\n"
  },
  {
    "path": "src/jokes/jokes.py",
    "content": "from random import choice\nfrom typing import TypedDict\n\nimport requests\n\nJOKES_FILE_URL = \"https://gist.githubusercontent.com/FrancescoSaverioZuppichini/8e7614da3b156ccaa29d50211b790146/raw/79b00cdaa023903629052426b24cdee7212229c3/jokes.txt\"\n\n\nclass Joke(TypedDict):\n    preambula: str\n    punchline: str\n\n\ndef get_joke() -> Joke:\n    res = requests.get(JOKES_FILE_URL)\n    content = res.text\n    jokes = content.strip().split(\"\\n\")\n    joke = choice(jokes)\n    joke_splitted = joke.split(\"<>\")\n    return Joke(preambula=joke_splitted[0], punchline=joke_splitted[1])\n"
  },
  {
    "path": "tests/test_jokes.py",
    "content": "from jokes import get_joke\n\n\ndef test_jokes():\n    joke = get_joke()\n    assert \"preambula\" in joke\n    assert \"punchline\" in joke\n"
  }
]