{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a1a139a2",
   "metadata": {},
   "source": [
    "# 07 · Bibliotecas: usar, acelerar, empaquetar, publicar, contribuir\n",
    "\n",
    "La verdadera fuerza de Python es su ecosistema de bibliotecas. Este módulo las mira desde todos los ángulos:\n",
    "usarlas bien, hacer rápido el código lento con extensiones compiladas, convertir tu propio código en un\n",
    "paquete instalable, publicarlo, gestionar versiones sin romper a tus usuarios y contribuir a los proyectos de\n",
    "los que dependes.\n",
    "\n",
    "**Aprenderás**\n",
    "1. Encontrar e inspeccionar bibliotecas: versiones, dependencias, documentación\n",
    "2. Por qué los bucles en Python puro son lentos y cuatro formas de acelerarlos: NumPy, Numba, Cython y C (medido)\n",
    "3. Empaquetado: un `pyproject.toml`, construir un wheel e instalarlo en un entorno limpio\n",
    "4. Publicar en PyPI de forma segura\n",
    "5. Versiones: versionado semántico, especificadores, obsolescencias y compatibilidad hacia atrás\n",
    "6. Convertirte en colaborador"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9f0ee2ea",
   "metadata": {},
   "source": [
    "## 1. Inspeccionar las bibliotecas instaladas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "3d33ceed",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:53.688209Z",
     "iopub.status.busy": "2026-10-18T10:10:53.687547Z",
     "iopub.status.idle": "2026-10-18T10:10:53.875196Z",
     "shell.execute_reply": "2026-10-18T10:10:53.873165Z"
    }
   },
   "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 requiere: ['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",
      "resumen: 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 requiere:\", [r for r in (requires(\"pandas\") or []) if \"extra\" not in r])\n",
    "print(\"resumen:\", metadata(\"polars\")[\"Summary\"])\n",
    "print(numpy.clip.__doc__.strip().splitlines()[0])      # help(numpy.clip) imprime la documentación completa"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14157867",
   "metadata": {},
   "source": [
    "## 2. Acelerar un bucle crítico\n",
    "Python ejecuta cada operación a través del intérprete, comprobando los tipos cada vez. En bucles numéricos eso\n",
    "es de decenas a cientos de veces más lento que el código compilado. La misma función, un bucle con mucho\n",
    "cálculo numérico, implementada de cinco maneras:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "4e13b455",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:53.879484Z",
     "iopub.status.busy": "2026-10-18T10:10:53.878977Z",
     "iopub.status.idle": "2026-10-18T10:10:56.114549Z",
     "shell.execute_reply": "2026-10-18T10:10:56.112807Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Python puro              1.662 s   resultado 1.044289\n",
      "NumPy (vectorizado)      0.187 s   resultado 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[\"Python puro\"] = bench(py_version, repeat=1)\n",
    "timings[\"NumPy (vectorizado)\"] = bench(numpy_version)\n",
    "for k, (t, v) in timings.items():\n",
    "    print(f\"{k:<22}{t:8.3f} s   resultado {v:.6f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7805a3c2",
   "metadata": {},
   "source": [
    "**Numba** compila una función de Python a código máquina la primera vez que se llama (compilación justo a\n",
    "tiempo, JIT). Sigues escribiendo bucles; solo añades un decorador."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "9747ac7b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:56.118677Z",
     "iopub.status.busy": "2026-10-18T10:10:56.118321Z",
     "iopub.status.idle": "2026-10-18T10:10:58.369930Z",
     "shell.execute_reply": "2026-10-18T10:10:58.367804Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Numba 0.148 s, resultado 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)                                   # la primera llamada compila; no se mide\n",
    "timings[\"Numba (JIT)\"] = bench(numba_version)\n",
    "print(f\"Numba {timings['Numba (JIT)'][0]:.3f} s, resultado {timings['Numba (JIT)'][1]:.6f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c3f1d254",
   "metadata": {},
   "source": [
    "**Cython** es un superconjunto de Python que se compila a C. Declarar tipos de C (`cdef double`) es lo que lo\n",
    "hace rápido. `pyximport` compila un archivo `.pyx` al importarlo; los paquetes reales lo compilan al\n",
    "construirse."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "7b36f01e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:58.374027Z",
     "iopub.status.busy": "2026-10-18T10:10:58.373513Z",
     "iopub.status.idle": "2026-10-18T10:11:02.685527Z",
     "shell.execute_reply": "2026-10-18T10:11:02.680983Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cython 0.147 s, resultado 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 (tipado)\"] = bench(fastloop.cython_version)\n",
    "print(f\"Cython {timings['Cython (tipado)'][0]:.3f} s, resultado {timings['Cython (tipado)'][1]:.6f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "512df9ee",
   "metadata": {},
   "source": [
    "**C a través de `ctypes`**: escribe la función en C, compílala como biblioteca compartida y llámala desde\n",
    "Python. Así envuelven muchas bibliotecas código C existente (otras usan `cffi` o, para C++, `pybind11`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "2e17abe8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:11:02.690131Z",
     "iopub.status.busy": "2026-10-18T10:11:02.689628Z",
     "iopub.status.idle": "2026-10-18T10:11:03.276350Z",
     "shell.execute_reply": "2026-10-18T10:11:03.274427Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "C 0.145 s, resultado 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 con ctypes\"] = bench(lib.c_version)\n",
    "    print(f\"C {timings['C con ctypes'][0]:.3f} s, resultado {timings['C con ctypes'][1]:.6f}\")\n",
    "else:\n",
    "    print(\"no se encontró un compilador de C: instala gcc (Linux), las herramientas de línea de órdenes de Xcode (macOS) o Build Tools (Windows)\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "397c0c64",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:11:03.280238Z",
     "iopub.status.busy": "2026-10-18T10:11:03.279854Z",
     "iopub.status.idle": "2026-10-18T10:11:04.679441Z",
     "shell.execute_reply": "2026-10-18T10:11:04.677448Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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EUkZZ2aJiIiISLIYZomIiIhIshhmiYiIiEiyGGaJiIiISLIYZomIiIhIshhmiYiIiEiyGGaJiIiISLIYZomIiIhIshhmiYiIiEiyGGaJiIiISLL4cbZUaU0d0xdaWloVXQYRERFVYpyZJSIiIiLJYpglIiIiIslimCUiIiIiyWKYJSIiIiLJYpglIiIiIslimCUiIiIiyWKYJSIiIiLJYpglIiIiIslimCUiIiIiyeIngFGlVe+fV1BQT6/oMoiIiD5Ikq1ORZfwWePMLBERERFJFsMsEREREUkWwywRERERSRbDLBERERFJFsMsEREREUkWwywRERERSRbDLBERERFJFsMsEREREUkWwywRERERSRbDLBERERFJFsMsEREREUkWwywRERERSZZyRRdARERE9CXKzMzEjRs3EB0djebNm8PY2LiiS5Ikzsz+ny1btiA6OrqiyygzV1dXnDt37qOe48SJE/Dw8CjVMQcOHEBQUNBHqoiIiEiaHj58CCMjI8yYMQPHjh3DoEGDMHbsWGRnZ1d0aZJTqjB76tQp/PbbbwgMDJRpj4yMxG+//YbU1NRyLa64Gn777TesXbsWhw4dwuvXrz9ozKNHj2LPnj3Q09Mrpyrf78iRIzh//ny5jXfw4EGcOnWq3MaTx9XVFcePHy/1cd988w0EQfgIFREREUnTtGnT0LhxY9y7dw+urq548OABrl27hj///BMAkJaWhn379iE0NFTmuJMnT+LKlSsVUXKlVaowe+zYMSxevBgzZsyQaY+IiMDixYuRkpJSrsUVVcNff/2F9PR0xMfHY/v27WjUqBG8vb3LPOaSJUswe/ZsKCgolGOlxTtw4ECpZzmLM3r0aFhaWpbbeOXF1tYWkZGROH36dEWXQkREVGn4+/tj6NChUFJSAgBUr14dPXv2xP79+wEAVatWhbe3N4YOHYq0tDQAgLu7O0aNGgVNTc0Kq7syKvWa2ZYtW+LmzZs4efIkhg4dKrePo6MjWrdujS5duohtx48fh6qqKgYNGgQgbyZRX18fOjo68PHxQWZmJoYPHw59fX3cu3cPFy9ehLa2NqytraGtrS0zfsOGDfHbb7+J2z179sTcuXOxfv16BAUFYfr06TL9z58/j+fPn2PixImFar106RIiIiJgbW0NAPD29saDBw8wbdq0QmNERUVh0qRJAIDU1FScOnUK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7dqwsrIq9vtAREREVBYKwmf4OaOZmZlo0KABli5dWugDFIqybt062NnZwcDA4CNX92XZs2cPjI2Ni32SQ0HJycnQ1taG5l+hUFDnp5wQEZG0JdnqVHQJlVb+7/ykpKQyv4v/2YXZLVu2wNPTE48ePcL9+/e5TlOCGGaJiOhzwjBbtPIIs5/dk3czMjLQu3dveHl5McgSERERfeY+6AawymjOnDkVXQIRERERfSKf3cwsEREREX05GGaJiIiISLIYZomIiIhIshhmiYiIiEiyGGaJiIiISLIYZomIiIhIshhmiYiIiEiyGGaJiIiISLIYZomIiIhIshhmiYiIiEiyPruPs6XPR9ToOtDS0qroMoiIiKgS48wsEREREUkWwywRERERSRbDLBERERFJFsMsEREREUkWwywRERERSRbDLBERERFJFsMsEREREUkWwywRERERSRbDLBERERFJFj8BjCqtdr7ToFhNtaLLICIi+qKEme2r6BJKhTOzRERERCRZDLNEREREJFkMs0REREQkWQyzRERERCRZDLNEREREJFkMs0REREQkWQyzRERERCRZDLNEREREJFkMs0REREQkWQyzRERERCRZDLNEREREJFkMs0REREQkWQyzRERERFQiN2/exN69exEVFfVBfcoTw6zEuLi44MqVKx9t/B07diA6Orrcxjt06BAePHhQbuMRERHRp3fp0iWYmJhg0qRJmDRpEgICAsrU52NQ/iRn+Ux4eHggMDAQQ4YMgZGRkdgeFRUFZ2dnzJo1C9WqVfuoNTg6OsLExAQ9evQo97Hd3Nywe/duTJ8+HUDe9SYlJcHW1lZme9SoUdiwYUOxYxkbG8PCwgJZWVn45ptv4OPjAwUFhXKvmYiIiD6+rKws7Ny5E02bNkWNGjXK1Cc6Ohr//vsvBg8ejNq1awMAcnNzAQCBgYHo1q1bmWrjzGwpuLq6Yv78+ZgxY4ZMe3h4OObPn4+UlJQKqqx8LF68GHPmzIGiYt6PhaurK/78809xf/62IAhITEwUvy5evIhFixbJtKWlpQEA7O3tERERAQ8Pjwq5JiIiIvpw/fv3R6dOnT6oj66uLhwdHTF58mSxLX9yTFdXt8y1cWa2lFq0aAFfX1+cPn0agwcPlttnz549MDIygqmpqdh28uRJqKioYMCAAQDylgvo6emhbt268PHxQWZmJqys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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[\"Python puro\"][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(\"aceleración frente a Python puro (escala logarítmica)\")\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": "db3d364e",
   "metadata": {},
   "source": [
    "Las cinco coinciden en el resultado con muchos decimales, y cada alternativa es aproximadamente diez veces más\n",
    "rápida que el bucle en Python puro. Quedan cerca unas de otras porque el tiempo que queda se pasa dentro de\n",
    "`sin` y `sqrt`; en bucles con cuerpos más baratos (contar, comparar, aritmética sencilla) la diferencia con\n",
    "Python puro suele ser de 50 a 100 veces. Cuál elegir:\n",
    "\n",
    "| Opción | Esfuerzo | Mejor cuando |\n",
    "|---|---|---|\n",
    "| vectorización con NumPy | reescribir como operaciones con arrays | el cálculo se expresa sobre arrays completos |\n",
    "| Numba | un decorador | bucles numéricos sobre arrays de NumPy y escalares |\n",
    "| Cython | un archivo `.pyx` tipado más un paso de compilación | además necesitas llamar a bibliotecas de C o distribuir un paquete compilado |\n",
    "| C / C++ (ctypes, cffi, pybind11) | escribir y compilar código nativo | envolver una biblioteca nativa existente |\n",
    "| Rust (PyO3, maturin) | Rust | bibliotecas nuevas donde el rendimiento es crítico (Polars, Pydantic y ruff se construyen así) |\n",
    "\n",
    "**Perfila siempre primero** (`cProfile`, `line_profiler`, `py-spy`): acelera la función que de verdad consume\n",
    "el tiempo.\n",
    "\n",
    "## 3. Empaquetar tu propia biblioteca\n",
    "Un paquete es una carpeta con un `__init__.py`; una *distribución* (un **wheel**) es lo que instala `pip`.\n",
    "Todos los metadatos viven en `pyproject.toml`. A continuación se crea una biblioteca diminuta, `ministats`, se\n",
    "construye como wheel, se comprueba y se instala en un entorno limpio."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "48b5794f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:11:04.683443Z",
     "iopub.status.busy": "2026-10-18T10:11:04.682871Z",
     "iopub.status.idle": "2026-10-18T10:11:04.702330Z",
     "shell.execute_reply": "2026-10-18T10:11:04.700122Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/tmp/tmp2i54is8t/ministats/src/ministats/__init__.py\n",
      "/tmp/tmp2i54is8t/ministats/README.md\n",
      "/tmp/tmp2i54is8t/ministats/LICENSE\n",
      "/tmp/tmp2i54is8t/ministats/tests/test_basic.py\n",
      "/tmp/tmp2i54is8t/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": "996a3d90",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:11:04.706049Z",
     "iopub.status.busy": "2026-10-18T10:11:04.705372Z",
     "iopub.status.idle": "2026-10-18T10:11:09.336611Z",
     "shell.execute_reply": "2026-10-18T10:11:09.334931Z"
    }
   },
   "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/tmp2i54is8t/ministats/dist/ministats-0.1.0-py3-none-any.whl: \u001b[32mPASSED\u001b[0m\n",
      "Checking /tmp/tmp2i54is8t/ministats/dist/ministats-0.1.0.tar.gz: \u001b[32mPASSED\u001b[0m\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# construir un sdist (archivo fuente) y un wheel; --no-isolation reutiliza el setuptools de este entorno (funciona sin conexión)\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": "eec06ab3",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:11:09.340933Z",
     "iopub.status.busy": "2026-10-18T10:11:09.340467Z",
     "iopub.status.idle": "2026-10-18T10:11:16.851358Z",
     "shell.execute_reply": "2026-10-18T10:11:16.849279Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "dentro del 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(\"dentro del 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": "0d9643ae",
   "metadata": {},
   "source": [
    "## 4. Publicar\n",
    "1. Crea cuentas en **TestPyPI** (test.pypi.org) y **PyPI** (pypi.org), con autenticación en dos pasos.\n",
    "2. Sube primero a TestPyPI e instala desde allí: `pip install -i https://test.pypi.org/simple/ ministats`.\n",
    "3. Publica en PyPI desde la integración continua con **trusted publishing** (publicación de confianza): GitHub\n",
    "   Actions demuestra su identidad ante PyPI, así que no se guarda ningún token de API en ninguna parte."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "a48d3a3d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:11:16.855839Z",
     "iopub.status.busy": "2026-10-18T10:11:16.855277Z",
     "iopub.status.idle": "2026-10-18T10:11:16.863787Z",
     "shell.execute_reply": "2026-10-18T10:11:16.861568Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "# .github/workflows/publish.yml: publicar al crear una release en GitHub\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: sin contraseña ni token guardados\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: publicar al crear una release en GitHub\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: sin contraseña ni token guardados\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": "02b765df",
   "metadata": {},
   "source": [
    "## 5. Versiones y compatibilidad hacia atrás\n",
    "**Versionado semántico** (`MAYOR.MENOR.PARCHE`): PARCHE corrige errores, MENOR añade funciones sin romper\n",
    "nada y MAYOR rompe algo. Así los usuarios pueden depender de `>=2.3,<3`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "0a1943a2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:11:16.868676Z",
     "iopub.status.busy": "2026-10-18T10:11:16.867916Z",
     "iopub.status.idle": "2026-10-18T10:11:16.881952Z",
     "shell.execute_reply": "2026-10-18T10:11:16.879693Z"
    }
   },
   "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",
      "versión compatible ~=1.4.2 significa >=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(\"versión compatible ~=1.4.2 significa >=1.4.2,<1.5:\", Version(\"1.4.9\") in SpecifierSet(\"~=1.4.2\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed3c1574",
   "metadata": {},
   "source": [
    "Eliminar o renombrar algo rompe el código de tus usuarios. La forma educada es un **periodo de obsolescencia**:\n",
    "mantener funcionando el nombre antiguo, avisar y eliminarlo solo en la siguiente versión mayor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "3911a8bf",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:11:16.888490Z",
     "iopub.status.busy": "2026-10-18T10:11:16.888123Z",
     "iopub.status.idle": "2026-10-18T10:11:16.899526Z",
     "shell.execute_reply": "2026-10-18T10:11:16.898066Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2.0 2.0\n",
      "DeprecationWarning : average() está obsoleta y se eliminará en la versión 2.0; usa arithmetic_mean() en su lugar\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__}() está obsoleta y se eliminará en la versión {removed_in}; \"\n",
    "                          f\"usa {new_name}() en su lugar\", DeprecationWarning, stacklevel=2)\n",
    "            return func(*args, **kwargs)\n",
    "        return wrapper\n",
    "    return decorate\n",
    "\n",
    "def arithmetic_mean(values, *, skipna: bool = False):        # API nueva (opción nueva solo por nombre)\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):                                           # el nombre antiguo sigue funcionando por ahora\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": "14289f50",
   "metadata": {},
   "source": [
    "Reglas prácticas de compatibilidad para autores de bibliotecas:\n",
    "- Los parámetros nuevos llevan valor por defecto y son solo por nombre (`*,`), para que las llamadas existentes no cambien de significado.\n",
    "- Nunca cambies lo que devuelve una llamada existente en una versión menor.\n",
    "- Mantén un `CHANGELOG.md`; marca con claridad los cambios incompatibles.\n",
    "- Prueba en la integración continua con las versiones más antiguas y más nuevas de Python y de las dependencias que soportas.\n",
    "- Declara `requires-python` y los rangos de dependencias con honestidad; no fijes versiones exactas en una biblioteca.\n",
    "\n",
    "## 6. Convertirte en colaborador\n",
    "1. **Usa el proyecto y lee su guía CONTRIBUTING y su código de conducta.**\n",
    "2. Empieza por algo pequeño: arreglos de documentación, una prueba que falle y reproduzca un error, incidencias etiquetadas *good first issue*.\n",
    "3. Habla antes de hacer cambios grandes: abre o comenta primero una incidencia.\n",
    "4. Haz un fork, crea una rama, instala el proyecto en modo editable (`pip install -e \".[dev]\"`) y ejecuta sus pruebas.\n",
    "5. Haz un único cambio concreto, con pruebas, formateado con las herramientas del proyecto.\n",
    "6. Abre una pull request que explique el *porqué*, enlaza la incidencia y responde a la revisión con amabilidad y paciencia.\n",
    "7. Algunos proyectos piden firmar un acuerdo de licencia de contribución; comprueba la licencia de cualquier cosa que copies.\n",
    "\n",
    "Revisar pull requests de otras personas, clasificar incidencias y responder preguntas también son\n",
    "contribuciones, y es lo que más suele faltar a quienes mantienen los proyectos.\n",
    "\n",
    "## Ejercicios\n",
    "1. Añade una función `median` a `ministats` con su prueba, sube la versión a `0.2.0`, reconstruye e instala el wheel nuevo encima del antiguo.\n",
    "2. Usa `cProfile` (`python -m cProfile -s cumtime script.py`) para encontrar la función más lenta de uno de tus scripts.\n",
    "3. Lleva `py_version` a Numba con `parallel=True` y `numba.prange`. ¿Cuánto más rápida es en tus núcleos?\n",
    "4. Elige una biblioteca que uses, abre su gestor de incidencias y lee tres tickets *good first issue*."
   ]
  }
 ],
 "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
}
