{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6c764fb4",
   "metadata": {},
   "source": [
    "# 07 · Libraries: Use, Speed Up, Package, Publish, Contribute\n",
    "\n",
    "Python's real power is its ecosystem of libraries. This module looks at libraries from every side: using\n",
    "them well, making slow code fast with compiled extensions, turning your own code into an installable\n",
    "package, publishing it, managing versions without breaking your users, and contributing to projects you\n",
    "depend on.\n",
    "\n",
    "**You will learn**\n",
    "1. Finding and inspecting libraries: versions, dependencies, documentation\n",
    "2. Why pure Python loops are slow, and four ways to speed them up: NumPy, Numba, Cython and C (measured)\n",
    "3. Packaging: a `pyproject.toml`, building a wheel, installing it in a clean environment\n",
    "4. Publishing to PyPI safely\n",
    "5. Versions: semantic versioning, specifiers, deprecations and backward compatibility\n",
    "6. Becoming a contributor"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e3fefbf",
   "metadata": {},
   "source": [
    "## 1. Inspecting installed libraries"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "182141ae",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:24:42.971023Z",
     "iopub.status.busy": "2026-10-17T23:24:42.970057Z",
     "iopub.status.idle": "2026-10-17T23:24:43.131221Z",
     "shell.execute_reply": "2026-10-17T23:24:43.129911Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "numpy 2.5.4 from /home/patito/Developments/academy/python/.venv/lib/python3.12/site-packages/numpy/__init__.py\n",
      "pandas requires: ['numpy>=1.22.4; python_version < \"3.11\"', 'numpy>=1.23.2; python_version == \"3.11\"', 'numpy>=1.26.0; python_version >= \"3.12\"', 'python-dateutil>=2.8.2', 'pytz>=2020.1', 'tzdata>=2022.7']\n",
      "summary: Blazingly fast DataFrame library\n",
      "Clip (limit) the values in an array.\n"
     ]
    }
   ],
   "source": [
    "from importlib.metadata import version, requires, metadata\n",
    "import numpy\n",
    "\n",
    "print(\"numpy\", version(\"numpy\"), \"from\", numpy.__file__)\n",
    "print(\"pandas requires:\", [r for r in (requires(\"pandas\") or []) if \"extra\" not in r])\n",
    "print(\"summary:\", metadata(\"polars\")[\"Summary\"])\n",
    "print(numpy.clip.__doc__.strip().splitlines()[0])      # help(numpy.clip) prints the full documentation"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32abe1c4",
   "metadata": {},
   "source": [
    "## 2. Speeding up a hot loop\n",
    "Python executes each operation through the interpreter, checking types every time. For numeric loops\n",
    "that is tens to hundreds of times slower than compiled code. The same function, a numerically heavy loop,\n",
    "implemented five ways:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "0c23fdc2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:24:43.134665Z",
     "iopub.status.busy": "2026-10-17T23:24:43.134258Z",
     "iopub.status.idle": "2026-10-17T23:24:45.426550Z",
     "shell.execute_reply": "2026-10-17T23:24:45.413348Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "pure Python              1.596 s   result 1.044289\n",
      "NumPy (vectorised)       0.194 s   result 1.044289\n"
     ]
    }
   ],
   "source": [
    "import math, time, numpy as np\n",
    "\n",
    "def py_version(n: int) -> float:\n",
    "    total = 0.0\n",
    "    for i in range(1, n):\n",
    "        total += math.sin(i) * math.sqrt(i) / i\n",
    "    return total\n",
    "\n",
    "def numpy_version(n: int) -> float:\n",
    "    i = np.arange(1, n, dtype=np.float64)\n",
    "    return float(np.sum(np.sin(i) * np.sqrt(i) / i))\n",
    "\n",
    "N = 5_000_000\n",
    "def bench(fn, repeat=3):\n",
    "    best = float(\"inf\")\n",
    "    for _ in range(repeat):\n",
    "        t0 = time.perf_counter(); value = fn(N); best = min(best, time.perf_counter() - t0)\n",
    "    return best, value\n",
    "\n",
    "timings = {}\n",
    "timings[\"pure Python\"] = bench(py_version, repeat=1)\n",
    "timings[\"NumPy (vectorised)\"] = bench(numpy_version)\n",
    "for k, (t, v) in timings.items():\n",
    "    print(f\"{k:<22}{t:8.3f} s   result {v:.6f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b582372",
   "metadata": {},
   "source": [
    "**Numba** compiles a Python function to machine code the first time it is called (just-in-time). You\n",
    "keep writing loops; you add one decorator."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "5f2c36d4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:24:45.431904Z",
     "iopub.status.busy": "2026-10-17T23:24:45.431129Z",
     "iopub.status.idle": "2026-10-17T23:24:47.458050Z",
     "shell.execute_reply": "2026-10-17T23:24:47.456174Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Numba 0.152 s, result 1.044289\n"
     ]
    }
   ],
   "source": [
    "from numba import njit\n",
    "\n",
    "@njit(cache=False)\n",
    "def numba_version(n):\n",
    "    total = 0.0\n",
    "    for i in range(1, n):\n",
    "        total += math.sin(i) * math.sqrt(i) / i\n",
    "    return total\n",
    "\n",
    "numba_version(10)                                   # first call compiles; not timed\n",
    "timings[\"Numba (JIT)\"] = bench(numba_version)\n",
    "print(f\"Numba {timings['Numba (JIT)'][0]:.3f} s, result {timings['Numba (JIT)'][1]:.6f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a4ae59a1",
   "metadata": {},
   "source": [
    "**Cython** is a superset of Python that compiles to C. Declaring C types (`cdef double`) is what makes it\n",
    "fast. `pyximport` compiles a `.pyx` file on import; real packages compile it at build time instead."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "9fcf5029",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:24:47.462398Z",
     "iopub.status.busy": "2026-10-17T23:24:47.461918Z",
     "iopub.status.idle": "2026-10-17T23:24:51.958984Z",
     "shell.execute_reply": "2026-10-17T23:24:51.957194Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cython 0.159 s, result 1.044289\n"
     ]
    }
   ],
   "source": [
    "import os, sys, tempfile, importlib, textwrap\n",
    "work = tempfile.mkdtemp()\n",
    "with open(os.path.join(work, \"fastloop.pyx\"), \"w\") as f:\n",
    "    f.write(textwrap.dedent(\"\"\"\n",
    "        from libc.math cimport sin, sqrt\n",
    "        def cython_version(long n):\n",
    "            cdef double total = 0.0\n",
    "            cdef long i\n",
    "            for i in range(1, n):\n",
    "                total += sin(i) * sqrt(i) / i\n",
    "            return total\n",
    "    \"\"\"))\n",
    "import pyximport\n",
    "pyximport.install(build_dir=os.path.join(work, \"build\"), language_level=3, inplace=False)\n",
    "sys.path.insert(0, work)\n",
    "fastloop = importlib.import_module(\"fastloop\")\n",
    "timings[\"Cython (typed)\"] = bench(fastloop.cython_version)\n",
    "print(f\"Cython {timings['Cython (typed)'][0]:.3f} s, result {timings['Cython (typed)'][1]:.6f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b38efe8a",
   "metadata": {},
   "source": [
    "**C through `ctypes`**: write the function in C, compile it into a shared library, call it from Python.\n",
    "This is how many libraries wrap existing C code (others use `cffi` or, for C++, `pybind11`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "1f446267",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:24:51.963580Z",
     "iopub.status.busy": "2026-10-17T23:24:51.963198Z",
     "iopub.status.idle": "2026-10-17T23:24:52.524957Z",
     "shell.execute_reply": "2026-10-17T23:24:52.522901Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "C 0.149 s, result 1.044289\n"
     ]
    }
   ],
   "source": [
    "import ctypes, shutil, subprocess\n",
    "c_src = os.path.join(work, \"loop.c\")\n",
    "lib_path = os.path.join(work, \"libloop.so\")\n",
    "with open(c_src, \"w\") as f:\n",
    "    f.write(\"\"\"\n",
    "#include <math.h>\n",
    "double c_version(long n) {\n",
    "    double total = 0.0;\n",
    "    for (long i = 1; i < n; i++) total += sin((double)i) * sqrt((double)i) / i;\n",
    "    return total;\n",
    "}\n",
    "\"\"\")\n",
    "if shutil.which(\"gcc\"):\n",
    "    subprocess.run([\"gcc\", \"-O2\", \"-shared\", \"-fPIC\", c_src, \"-o\", lib_path, \"-lm\"], check=True)\n",
    "    lib = ctypes.CDLL(lib_path)\n",
    "    lib.c_version.argtypes, lib.c_version.restype = [ctypes.c_long], ctypes.c_double\n",
    "    timings[\"C via ctypes\"] = bench(lib.c_version)\n",
    "    print(f\"C {timings['C via ctypes'][0]:.3f} s, result {timings['C via ctypes'][1]:.6f}\")\n",
    "else:\n",
    "    print(\"no C compiler found: install gcc (Linux), Xcode command line tools (macOS) or Build Tools (Windows)\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "0854e2c5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:24:52.529481Z",
     "iopub.status.busy": "2026-10-17T23:24:52.529095Z",
     "iopub.status.idle": "2026-10-17T23:24:53.970018Z",
     "shell.execute_reply": "2026-10-17T23:24:53.967796Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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jz/g6+uLsLAw3Lt3j3s5PzAMs0RE9LFjmC2/qgiz73QD2IcoJycHAwYMQEBAAIMsERER0UfunW4A+xAtWrRI3iUQERER0Xvy0a3MEhEREVHNwTBLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESS9dF9nS1JQ8znDaGlpSXvMoiIiEjiuDJLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESSxW8AI7noFPgFFOvUkncZRERE9IaHPfbIu4QK4cosEREREUkWwywRERERSRbDLBERERFJFsMsEREREUkWwywRERERSRbDLBERERFJFsMsEREREUkWwywRERERSRbDLBERERFJFsMsEREREUkWwywRERERSRbDLBERERFJFsMsEREREZVLamoq3N3d4evr+059qpLye7kKlYu7uzt0dXXRt2/fahl/y5YtsLW1hZ6eXpWMt3//fpiZmaFdu3ZVMh4RERF9mHJzczFv3jwcP34cKioqMDQ0hLW1dYX7VAeG2f/j4+ODO3fuYNiwYTA1NRXbY2Ji4Orqivnz56NOnTrVWsOOHTvQpUuXagmzhw8fxvbt2zFr1iwAhfNNTU2Fg4ODzOuxY8diw4YNpY5lbm4OS0tL5OXlYdq0abh06RIUFBSqvGYiIiL6MOTn56NLly747bff8O233+Lu3buV6nPs2DFoaGigf//+Mu0eHh6YOnVqpfIEtxn8Hw8PDzg5OWH27Nky7ZGRkXByckJ6erqcKqsay5Ytw6JFi6CoWPgj9/DwwF9//SUeL3otCAJevHgh/vnvv//www8/yLRlZWUBABwdHREVFQUfHx+5zImIiIjeDzU1NcycObPUhb3y9MnPz4eVlRVu3rwJAEhMTAQAREdHV3phjCuzr2nTpg0CAwNx/PhxDB06tMQ+u3fvhqmpKbp16ya2HT16FCoqKuJSetF2gSZNmuDSpUvIzc2Fra0tGjdujHv37uHMmTPQ1taGra0tNDU1i10jNDQUFy9eRH5+PmxtbdGwYUPxmL+/v/gLoKOjg08//RQdO3YsdV7nzp1DZGQkRo8eXeZ7UKtWLfzyyy/i682bNyMgIECm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keTKbHk4OzvD19cXqqqqZd4s9iH57rvvSj3+Ic3rzW8iIyIiInrfJHUDWEWkpaWhZ8+eCAgI+KhuTiprXvb29jKPySAiIiL6mH3U2wzow8NtBkRERPLzMW4z+GhXZomIiIjo48cwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtEREREkqUs7wKoZlJ3jId6Jb+2joiIiKgIV2aJiIiISLIYZomIiIhIshhmiYiIiEiyGGaJiIiISLIYZomIiIhIshhmiYiIiEiyGGaJiIiISLIYZomIiIhIshhmiYiIiEiy+A1gJBf2I+OhovxS3mUQERFRGbz89eRdQqm4MktEREREksUwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtERERElfLkyRMcOHAADx48eKc+74Jhthp4e3vj9OnT8i6DiIiIqFqEhYVhxIgR6NOnDyZPngxvb+9K9akKDLMAUlNT4eXlhc2bN8Pd3R0hISHvNN7u3btx5MiRKqquZEeOHMGZM2eq9RpEREREJUlNTcXUqVPx8OFD6OjoVKpPVlYWDh06VKzd398fFy9eLHctyuUv++O0ZcsWLF26FB07doSZmRlSUlKwevVqGBsb49ixY1BQUKjwmLa2tqhbt27VF/uanTt3okWLFujfv3+1XoeIiIjoTV27dn3nPmpqavj3338BADk5OQCAEydOwNbWFhcuXCh3LTU6zLq5uWHu3Lnw9PTE6NGjZY75+vpCEIRiYfby5csIDQ3F5MmTZdrPnTuHmJgYjB8/HvXq1YOGhoZ47NSpUwgODgYA6OjooFu3bjA1NS2zvpycHPj7+yM6OhodOnRAnz59AAB+fn549OgR0tLSsH79egBAhw4dEB8fX6yus2fPIjY2FuPHj4eHhweaNGkCXV1dXLp0Cfn5+Rg+fHixfy1lZ2fDz88PMTExMDExwaBBg6CioiLT58yZMwgJCYGenh4sLCxQp06dMudDRERE9Lr169fD3d0dK1aswIoVKzBlyhQsX74c3bp1K/cYNXqbwbJlyzBmzJhiQRYArK2toahY/O159eoV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xsbGcHJywrx582SOFYXZX375RU7VMcwSERFJzYceZmvsDWAfo+3bt+PEiRNQUVEp9hQDIiIioo9Rjb0B7GP07NkzdOnSBWfPnoW6unqx459//rn4lbhEREREHwNuM6D3itsMiIiIpOVD32bAlVkiIiIikiyGWSIiIiKSLIZZIiIiIpIshlkiIiIikiyGWSIiIiKSLIZZIiIiIpIshlkiIiIikiyGWSIiIiKSLIZZIiIiIpIshlkiIiIikixleRdANdM/R3Qr/bV1REREREW4MktEREREksUwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtEREREksUwS0RERESSxTBLRERERJLFMEtEREREksWvs6X3ShAEAEBaWpqcKyEiIiJ5K8oDRfmgMhhm6b1KT08HABgaGsq5EiIiIvpQpKenQ1tbu1LnKgjvEoWJKqigoABxcXHQ1NREt27dEBQUVGVjp6WlwdDQENHR0dDS0qqycenj07Vr1yr93atpasL7J8U5fmg1y6ue93nd6rxWVY/9of4dKQgC0tPToaenB0XFyu1+5cosvVeKioowMDAAACgpKVXL/1BaWlof1P+o9OGprt+9mqImvH9SnOOHVrO86nmf163Oa9WkvyMruyJbhDeAkdzMnTtX3iVQDcXfvXdTE94/Kc7xQ6tZXvW8z+tW57U+tJ/nh4zbDOijkZaWBm1tbaSmpn5w/+okIiKSp4/570iuzNJHQ1VVFStWrICqqqq8SyEiIvqgfMx/R3JlloiIiIgkiyuzRERERCRZDLNEREREJFl8NBcRERFRDfb8+XOkpKQAKPxSI6ntq+XKLNUIeXl5mDFjBrS0tNCqVSucP39e3iURERF9EHbv3g0rKyt06NABd+7ckXc5FcYwSzXCnj178PDhQzx58gS///47JkyYgFevXsm7LCIiIrlbuHAhIiIi8Mknn8i7lEphmCXJuHfvHvz8/JCVlVXi8VevXiEwMBAXL15Edna2zLGTJ09i1qxZqFu3LqytrVG3bl2EhIS8j7KJiIiqVUZGBv755x/s2bPnrX0ePXqEvXv34tChQ0hNTX2P1VU/hln64B0/fhw9e/bEoEGDYG1tjcTExGJ97t69ixYtWmDcuHGYMWMGDA0NcebMGfF4UlISGjZsKL5u1KgRkpKS3kv9RERE1WXJ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",
      "text/plain": [
       "<Figure size 700x340 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "base = timings[\"pure Python\"][0]\n",
    "names = list(timings)\n",
    "speedups = [base / timings[k][0] for k in names]\n",
    "plt.figure(figsize=(7, 3.4))\n",
    "plt.barh(names, speedups, color=[\"#94a3b8\", \"#0ea5e9\", \"#22c55e\", \"#f59e0b\", \"#4f46e5\"][:len(names)])\n",
    "plt.gca().invert_yaxis(); plt.xscale(\"log\"); plt.xlabel(\"speed-up over pure Python (log scale)\")\n",
    "for y, s in enumerate(speedups):\n",
    "    plt.text(s, y, f\" {s:,.0f}x\", va=\"center\")\n",
    "plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7939bc81",
   "metadata": {},
   "source": [
    "All five agree on the result to many decimals, and every alternative is roughly ten times faster than the\n",
    "pure Python loop. They end up close to each other because the remaining time is spent inside `sin` and\n",
    "`sqrt` themselves; for loops with cheaper bodies (counting, comparisons, simple arithmetic) the gap to\n",
    "pure Python is often 50 to 100 times. Which to choose:\n",
    "\n",
    "| Option | Effort | Best when |\n",
    "|---|---|---|\n",
    "| NumPy vectorisation | rewrite as array operations | the computation maps onto whole arrays |\n",
    "| Numba | one decorator | numeric loops over NumPy arrays and scalars |\n",
    "| Cython | a typed `.pyx` file plus a build step | you also need to call C libraries, or ship a compiled package |\n",
    "| C / C++ (ctypes, cffi, pybind11) | write and build native code | wrapping an existing native library |\n",
    "| Rust (PyO3, maturin) | Rust | new performance-critical libraries (Polars, Pydantic and ruff are built this way) |\n",
    "\n",
    "Always **profile first** (`cProfile`, `line_profiler`, `py-spy`): speed up the function that actually\n",
    "takes the time.\n",
    "\n",
    "## 3. Packaging your own library\n",
    "A package is a folder with an `__init__.py`; a *distribution* (a **wheel**) is what `pip` installs. All\n",
    "metadata lives in `pyproject.toml`. Below, a tiny library `ministats` is created, built into a wheel,\n",
    "checked, and installed in a clean environment."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "b1bd3a0a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:24:53.974551Z",
     "iopub.status.busy": "2026-10-17T23:24:53.974020Z",
     "iopub.status.idle": "2026-10-17T23:24:53.991426Z",
     "shell.execute_reply": "2026-10-17T23:24:53.989799Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/tmp/tmpmx_vde3p/ministats/src/ministats/__init__.py\n",
      "/tmp/tmpmx_vde3p/ministats/README.md\n",
      "/tmp/tmpmx_vde3p/ministats/LICENSE\n",
      "/tmp/tmpmx_vde3p/ministats/tests/test_basic.py\n",
      "/tmp/tmpmx_vde3p/ministats/pyproject.toml\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import pathlib\n",
    "pkg = pathlib.Path(tempfile.mkdtemp()) / \"ministats\"\n",
    "(pkg / \"src\" / \"ministats\").mkdir(parents=True)\n",
    "(pkg / \"tests\").mkdir()\n",
    "(pkg / \"src\" / \"ministats\" / \"__init__.py\").write_text(textwrap.dedent(\"\"\"\n",
    "    \"Tiny statistics helpers (course example).\"\n",
    "    from importlib.metadata import version as _v\n",
    "    __version__ = _v(\"ministats\")\n",
    "\n",
    "    def mean(values):\n",
    "        values = list(values)\n",
    "        if not values:\n",
    "            raise ValueError(\"mean of an empty sequence\")\n",
    "        return sum(values) / len(values)\n",
    "\n",
    "    def zscores(values):\n",
    "        values = list(values)\n",
    "        m = mean(values)\n",
    "        sd = (sum((v - m) ** 2 for v in values) / len(values)) ** 0.5\n",
    "        return [(v - m) / sd for v in values]\n",
    "\"\"\"))\n",
    "(pkg / \"tests\" / \"test_basic.py\").write_text(\"from ministats import mean\\n\\ndef test_mean():\\n    assert mean([1, 2, 3]) == 2\\n\")\n",
    "(pkg / \"README.md\").write_text(\"# ministats\\n\\nTiny statistics helpers.\\n\")\n",
    "(pkg / \"LICENSE\").write_text(\"MIT License\\n\")\n",
    "(pkg / \"pyproject.toml\").write_text(textwrap.dedent(\"\"\"\n",
    "    [build-system]\n",
    "    requires = [\"setuptools>=70\"]\n",
    "    build-backend = \"setuptools.build_meta\"\n",
    "\n",
    "    [project]\n",
    "    name = \"ministats\"\n",
    "    version = \"0.1.0\"\n",
    "    description = \"Tiny statistics helpers (course example)\"\n",
    "    readme = \"README.md\"\n",
    "    license = \"MIT\"\n",
    "    requires-python = \">=3.10\"\n",
    "    dependencies = []                      # e.g. [\"numpy>=1.26\"]\n",
    "    authors = [{ name = \"Your Name\" }]\n",
    "    classifiers = [\"Programming Language :: Python :: 3\"]\n",
    "\n",
    "    [project.optional-dependencies]\n",
    "    dev = [\"pytest>=8\"]\n",
    "\n",
    "    [project.urls]\n",
    "    Homepage = \"https://example.org/ministats\"\n",
    "\"\"\"))\n",
    "print(subprocess.run([\"find\", str(pkg), \"-type\", \"f\"], capture_output=True, text=True).stdout)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "fedb6496",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:24:53.995303Z",
     "iopub.status.busy": "2026-10-17T23:24:53.994932Z",
     "iopub.status.idle": "2026-10-17T23:24:58.713940Z",
     "shell.execute_reply": "2026-10-17T23:24:58.712250Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[1m\u001b[92mSuccessfully built \u001b[4mministats-0.1.0.tar.gz\u001b[0m\u001b[1m\u001b[92m and \u001b[4mministats-0.1.0-py3-none-any.whl\u001b[0m\u001b[1m\u001b[92m\u001b[0m\n",
      "['ministats-0.1.0-py3-none-any.whl', 'ministats-0.1.0.tar.gz']\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Checking /tmp/tmpmx_vde3p/ministats/dist/ministats-0.1.0-py3-none-any.whl: \u001b[32mPASSED\u001b[0m\n",
      "Checking /tmp/tmpmx_vde3p/ministats/dist/ministats-0.1.0.tar.gz: \u001b[32mPASSED\u001b[0m\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# build an sdist (source archive) and a wheel; --no-isolation reuses this environment's setuptools (works offline)\n",
    "r = subprocess.run([sys.executable, \"-m\", \"build\", \"--no-isolation\", str(pkg)], capture_output=True, text=True)\n",
    "print(r.stdout.strip().splitlines()[-1])\n",
    "dist = sorted((pkg / \"dist\").iterdir())\n",
    "print([p.name for p in dist])\n",
    "print(subprocess.run([sys.executable, \"-m\", \"twine\", \"check\", *map(str, dist)], capture_output=True, text=True).stdout)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "e900e2f2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:24:58.718507Z",
     "iopub.status.busy": "2026-10-17T23:24:58.717778Z",
     "iopub.status.idle": "2026-10-17T23:25:06.908593Z",
     "shell.execute_reply": "2026-10-17T23:25:06.906537Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "inside the wheel: ['ministats/__init__.py', 'ministats-0.1.0.dist-info/licenses/LICENSE', 'ministats-0.1.0.dist-info/METADATA', 'ministats-0.1.0.dist-info/WHEEL', 'ministats-0.1.0.dist-info/top_level.txt', 'ministats-0.1.0.dist-info/RECORD']\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.1.0 [-1.224744871391589, 0.0, 1.224744871391589]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import venv, zipfile\n",
    "wheel = next(p for p in dist if p.suffix == \".whl\")\n",
    "print(\"inside the wheel:\", [n for n in zipfile.ZipFile(wheel).namelist()])\n",
    "\n",
    "clean = pathlib.Path(tempfile.mkdtemp()) / \"env\"\n",
    "venv.create(clean, with_pip=True)\n",
    "py = clean / \"bin\" / \"python\"\n",
    "subprocess.run([py, \"-m\", \"pip\", \"install\", \"-q\", str(wheel)], check=True)\n",
    "print(subprocess.run([py, \"-c\", \"import ministats; print(ministats.__version__, ministats.zscores([2, 4, 6]))\"],\n",
    "                     capture_output=True, text=True).stdout)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d03c8acd",
   "metadata": {},
   "source": [
    "## 4. Publishing\n",
    "1. Create accounts on **TestPyPI** (test.pypi.org) and **PyPI** (pypi.org), with two-factor authentication.\n",
    "2. Upload to TestPyPI first and install from there: `pip install -i https://test.pypi.org/simple/ ministats`.\n",
    "3. Publish to PyPI from CI with **trusted publishing**: GitHub Actions proves its identity to PyPI, so no\n",
    "   API token is stored anywhere."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "e747af30",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:25:06.914097Z",
     "iopub.status.busy": "2026-10-17T23:25:06.913494Z",
     "iopub.status.idle": "2026-10-17T23:25:06.921157Z",
     "shell.execute_reply": "2026-10-17T23:25:06.919209Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "# .github/workflows/publish.yml: publish when a GitHub release is created\n",
      "name: publish\n",
      "on:\n",
      "  release:\n",
      "    types: [published]\n",
      "jobs:\n",
      "  pypi:\n",
      "    runs-on: ubuntu-latest\n",
      "    environment: pypi\n",
      "    permissions:\n",
      "      id-token: write          # trusted publishing: no stored password or token\n",
      "    steps:\n",
      "      - uses: actions/checkout@v4\n",
      "      - uses: actions/setup-python@v5\n",
      "        with: { python-version: \"3.12\" }\n",
      "      - run: python -m pip install build && python -m build\n",
      "      - uses: pypa/gh-action-pypi-publish@release/v1\n",
      "\n"
     ]
    }
   ],
   "source": [
    "print(\"\"\"# .github/workflows/publish.yml: publish when a GitHub release is created\n",
    "name: publish\n",
    "on:\n",
    "  release:\n",
    "    types: [published]\n",
    "jobs:\n",
    "  pypi:\n",
    "    runs-on: ubuntu-latest\n",
    "    environment: pypi\n",
    "    permissions:\n",
    "      id-token: write          # trusted publishing: no stored password or token\n",
    "    steps:\n",
    "      - uses: actions/checkout@v4\n",
    "      - uses: actions/setup-python@v5\n",
    "        with: { python-version: \"3.12\" }\n",
    "      - run: python -m pip install build && python -m build\n",
    "      - uses: pypa/gh-action-pypi-publish@release/v1\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec50734a",
   "metadata": {},
   "source": [
    "## 5. Versions and backward compatibility\n",
    "**Semantic versioning** (`MAJOR.MINOR.PATCH`): PATCH fixes bugs, MINOR adds features without breaking\n",
    "anything, MAJOR breaks something. Users can then depend on `>=2.3,<3`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "32783f0d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:25:06.926008Z",
     "iopub.status.busy": "2026-10-17T23:25:06.925472Z",
     "iopub.status.idle": "2026-10-17T23:25:06.936447Z",
     "shell.execute_reply": "2026-10-17T23:25:06.934445Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['1.9.2', '1.10.0', '1.10.0.post1', '2.0.0rc1']\n",
      "{'2.1.4': False, '2.2.0': True, '2.9.9': True, '3.0.0': False}\n",
      "True (1, 4, 2)\n",
      "compatible release ~=1.4.2 means >=1.4.2,<1.5: True\n"
     ]
    }
   ],
   "source": [
    "from packaging.version import Version\n",
    "from packaging.specifiers import SpecifierSet\n",
    "\n",
    "print(sorted([\"1.10.0\", \"1.9.2\", \"2.0.0rc1\", \"1.10.0.post1\"], key=Version))\n",
    "spec = SpecifierSet(\">=2.2,<3\")\n",
    "print({v: Version(v) in spec for v in [\"2.1.4\", \"2.2.0\", \"2.9.9\", \"3.0.0\"]})\n",
    "print(Version(\"2.0.0rc1\").is_prerelease, Version(\"1.4.2\").release)\n",
    "print(\"compatible release ~=1.4.2 means >=1.4.2,<1.5:\", Version(\"1.4.9\") in SpecifierSet(\"~=1.4.2\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cb81900d",
   "metadata": {},
   "source": [
    "Removing or renaming something breaks your users' code. The polite way is a **deprecation period**: keep\n",
    "the old name working, warn, and remove it only in the next major version."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "6d839883",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:25:06.942275Z",
     "iopub.status.busy": "2026-10-17T23:25:06.941479Z",
     "iopub.status.idle": "2026-10-17T23:25:06.957799Z",
     "shell.execute_reply": "2026-10-17T23:25:06.955514Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2.0 2.0\n",
      "DeprecationWarning : average() is deprecated and will be removed in 2.0; use arithmetic_mean() instead\n"
     ]
    }
   ],
   "source": [
    "import warnings, functools\n",
    "\n",
    "def deprecated(new_name: str, removed_in: str):\n",
    "    def decorate(func):\n",
    "        @functools.wraps(func)\n",
    "        def wrapper(*args, **kwargs):\n",
    "            warnings.warn(f\"{func.__name__}() is deprecated and will be removed in {removed_in}; \"\n",
    "                          f\"use {new_name}() instead\", DeprecationWarning, stacklevel=2)\n",
    "            return func(*args, **kwargs)\n",
    "        return wrapper\n",
    "    return decorate\n",
    "\n",
    "def arithmetic_mean(values, *, skipna: bool = False):        # new API (new keyword-only option)\n",
    "    values = [v for v in values if not (skipna and v is None)]\n",
    "    return sum(values) / len(values)\n",
    "\n",
    "@deprecated(\"arithmetic_mean\", removed_in=\"2.0\")\n",
    "def average(values):                                           # old name keeps working for now\n",
    "    return arithmetic_mean(values)\n",
    "\n",
    "with warnings.catch_warnings(record=True) as caught:\n",
    "    warnings.simplefilter(\"always\")\n",
    "    print(average([1, 2, 3]), arithmetic_mean([1, None, 3], skipna=True))\n",
    "print(caught[0].category.__name__, \":\", caught[0].message)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "01d780e0",
   "metadata": {},
   "source": [
    "Compatibility rules of thumb for library authors:\n",
    "- New parameters get defaults and are keyword-only (`*,`), so existing calls do not change meaning.\n",
    "- Never change what an existing call returns in a minor release.\n",
    "- Keep a `CHANGELOG.md`; mark breaking changes clearly.\n",
    "- Test against the oldest and newest supported Python and dependency versions in CI.\n",
    "- Declare `requires-python` and dependency ranges honestly; do not pin exact versions in a library.\n",
    "\n",
    "## 6. Becoming a contributor\n",
    "1. **Use the project and read its CONTRIBUTING guide and code of conduct.**\n",
    "2. Start small: documentation fixes, a failing test that reproduces a bug, issues labelled *good first issue*.\n",
    "3. Discuss before large changes: open or comment on an issue first.\n",
    "4. Fork, create a branch, install the project in editable mode (`pip install -e \".[dev]\"`), run its tests.\n",
    "5. Make one focused change, with tests, formatted with the project's tools.\n",
    "6. Open a pull request that explains *why*, link the issue, respond to review kindly and patiently.\n",
    "7. Some projects ask you to sign a contributor licence agreement; check the licence of anything you copy in.\n",
    "\n",
    "Reviewing other people's pull requests, triaging issues and answering questions are contributions too,\n",
    "and they are what maintainers most often lack.\n",
    "\n",
    "## Exercises\n",
    "1. Add a `median` function to `ministats` with a test, bump the version to `0.2.0`, rebuild, and install the new wheel over the old one.\n",
    "2. Use `cProfile` (`python -m cProfile -s cumtime script.py`) to find the slowest function in one of your own scripts.\n",
    "3. Port `py_version` to Numba with `parallel=True` and `numba.prange`. How much faster is it on your cores?\n",
    "4. Find a library you use, open its issue tracker, and read three *good first issue* tickets."
   ]
  }
 ],
 "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.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
