{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Building and publishing\n",
    "\n",
    "Your package works locally; now you want anyone with `pip` to be\n",
    "able to install it. That means producing distribution artefacts and\n",
    "uploading them to a package index — TestPyPI for practice, then PyPI\n",
    "for real.\n",
    "\n",
    "**Time commitment:** 15 minutes\n",
    "\n",
    "**Prerequisites:**\n",
    "\n",
    "- A working package from [Authoring a package](https://agilearn.co.uk/guides/packages-and-packaging/learn/05-authoring-a-package),\n",
    "  installable with `pip install -e .`\n",
    "\n",
    "## Learning objectives\n",
    "\n",
    "By the end of this tutorial, you will be able to:\n",
    "\n",
    "- Build sdist and wheel artefacts with `python -m build`\n",
    "- Upload to TestPyPI and PyPI with `twine`\n",
    "- Bump versions and re-release safely"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "!!! note \"Browser note\"\n",
    "    The commands below run in your terminal, not in this page's browser kernel.\n",
    "    Pyodide doesn't expose a shell, so the bash blocks here are for reference —\n",
    "    copy them into a real terminal to follow along.\n",
    "\n",
    "            Every command in this notebook is a shell command — none of\n",
    "            them run in Pyodide. Read through, then try the workflow on\n",
    "            a project of your own."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Two artefacts: sdist and wheel\n",
    "\n",
    "A Python package is published as two files:\n",
    "\n",
    "- **sdist** (`my-package-0.1.0.tar.gz`) — a source distribution. A\n",
    "  tarball of your project as it appears on disk, including\n",
    "  `pyproject.toml`. `pip` can install from this by re-running the\n",
    "  build, but it's slow.\n",
    "- **wheel** (`my_package-0.1.0-py3-none-any.whl`) — a built\n",
    "  distribution. Files are already in the layout `pip` will install\n",
    "  them in. Fast to install; the format `pip` prefers when both are\n",
    "  available.\n",
    "\n",
    "Most pure-Python packages ship one wheel that works on every\n",
    "platform. Packages with C extensions ship multiple wheels — one\n",
    "per platform/Python-version combination. The\n",
    "[Why wheels exist](https://agilearn.co.uk/guides/packages-and-packaging/concepts/why-wheels-exist) concept piece\n",
    "digs into the format."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Building\n",
    "\n",
    "The standard tool is `build` (a small package, run as a module):\n",
    "\n",
    "```bash\n",
    "pip install build\n",
    "python -m build\n",
    "```\n",
    "\n",
    "Run it from your project root. It reads `pyproject.toml`, calls the\n",
    "configured build backend (hatchling, in our case), and writes\n",
    "artefacts to a `dist/` directory:\n",
    "\n",
    "```text\n",
    "dist/\n",
    "    my_package-0.1.0-py3-none-any.whl\n",
    "    my-package-0.1.0.tar.gz\n",
    "```\n",
    "\n",
    "That's everything you need to publish."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Inspecting a wheel before you upload\n",
    "\n",
    "A wheel is a zip file. You can peek inside without installing:\n",
    "\n",
    "```bash\n",
    "python -m zipfile -l dist/my_package-0.1.0-py3-none-any.whl\n",
    "```\n",
    "\n",
    "Look for the files you expect, in the import-name directory. Common\n",
    "bugs — missing modules, vendored data files left behind, accidental\n",
    "inclusion of tests — show up here before they show up for users."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## TestPyPI first\n",
    "\n",
    "TestPyPI ([test.pypi.org](https://test.pypi.org)) is a separate\n",
    "instance of PyPI for trying things out. Publishing there first is a\n",
    "habit worth forming — it catches metadata mistakes (a bad\n",
    "`description`, a missing `README`) before they're permanently in\n",
    "front of real users.\n",
    "\n",
    "You'll need an account on TestPyPI and an API token from your\n",
    "account settings. Then:\n",
    "\n",
    "```bash\n",
    "pip install twine\n",
    "python -m twine upload --repository testpypi dist/*\n",
    "```\n",
    "\n",
    "Verify the install works from a clean venv:\n",
    "\n",
    "```bash\n",
    "pip install --index-url https://test.pypi.org/simple/ my-package\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Publishing to PyPI\n",
    "\n",
    "With TestPyPI happy, the real upload is the same shape:\n",
    "\n",
    "```bash\n",
    "python -m twine upload dist/*\n",
    "```\n",
    "\n",
    "Within a minute or two, `pip install my-package` works for anyone in\n",
    "the world. The package page is live at `https://pypi.org/project/my-package/`.\n",
    "\n",
    "Two things to know:\n",
    "\n",
    "- **Names are first-come-first-served.** Once you've uploaded\n",
    "  `my-package`, nobody else can use that name. Conversely, if a\n",
    "  name is already taken, `twine upload` will tell you on the first\n",
    "  attempt — pick a different one.\n",
    "- **You can't overwrite a release.** Once `0.1.0` is on PyPI, that\n",
    "  file's bytes are fixed. To fix a mistake, bump the version and\n",
    "  upload again."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bumping the version\n",
    "\n",
    "The release workflow once `0.1.0` is out:\n",
    "\n",
    "1. Edit `pyproject.toml` — bump `version` to `0.1.1` (patch),\n",
    "   `0.2.0` (minor), or `1.0.0` (major).\n",
    "2. Update `__version__` in `__init__.py` to match.\n",
    "3. `python -m build` — produces fresh artefacts in `dist/`.\n",
    "4. `twine upload dist/*` — only the new files; PyPI ignores the\n",
    "   ones it's seen.\n",
    "5. Tag the commit in git: `git tag v0.1.1 && git push --tags`.\n",
    "\n",
    "Some teams automate this with [hatch version](https://hatch.pypa.io/latest/version/),\n",
    "[bump-my-version](https://callowayproject.github.io/bump-my-version/),\n",
    "or release-bot tooling that generates the artefacts in CI."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## API tokens and trusted publishing\n",
    "\n",
    "For uploads, never use your account password. Two safer options:\n",
    "\n",
    "- **API tokens** — generate one in your PyPI account settings; scope\n",
    "  it to a single project. `twine` will prompt for the token; store it\n",
    "  in `~/.pypirc` or a credential manager.\n",
    "- **Trusted publishing** — for projects that release from CI,\n",
    "  [trusted publishing](https://docs.pypi.org/trusted-publishers/)\n",
    "  lets PyPI accept uploads from a specific GitHub Actions workflow\n",
    "  without any secret at all. The recommended option for any project\n",
    "  that's released from a public repository."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Recap and next steps\n",
    "\n",
    "- `python -m build` produces an sdist and a wheel in `dist/`.\n",
    "- Publish to TestPyPI first; verify the install; then publish to PyPI.\n",
    "- You can never overwrite a release — bump the version and re-upload.\n",
    "- Use API tokens, or set up trusted publishing for CI-driven releases.\n",
    "\n",
    "That's the consumer-to-author arc complete. From here, the\n",
    "[Recipes](https://agilearn.co.uk/guides/packages-and-packaging/recipes) walk through specific workflows — pinning\n",
    "dependencies, fixing import errors, and the most common packaging\n",
    "traps. The [Concepts](https://agilearn.co.uk/guides/packages-and-packaging/concepts) section is for when you want to\n",
    "understand *why* the import system, the wheel format, and PyPI work\n",
    "the way they do."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}