{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "541717ff",
   "metadata": {},
   "source": [
    "# 04 · Entornos, paquetes, contenedores y editores\n",
    "\n",
    "El código que solo funciona en tu máquina no está terminado. Este módulo cubre las prácticas que hacen que\n",
    "los proyectos de Python sean reproducibles y desplegables: entornos aislados, gestores de paquetes, archivos\n",
    "de bloqueo de versiones, contenedores y los editores en los que escriben código los profesionales.\n",
    "\n",
    "**Aprenderás**\n",
    "1. Por qué cada proyecto necesita su propio entorno y cómo funciona `venv` (demostrado en vivo)\n",
    "2. Gestores de paquetes comparados: `pip`, `uv`, `pipx`, `conda`/`mamba` y los del sistema (`apt`, `snap`, `brew`, `winget`)\n",
    "3. Fijar dependencias: archivos de requisitos, archivos de bloqueo y `pyproject.toml`\n",
    "4. Docker: empaquetar una aplicación con todo su entorno y desplegarla\n",
    "5. Editores e IDE: VS Code, PyCharm, Eclipse con PyDev, Vim/Neovim, JupyterLab, Spyder\n",
    "6. Una estructura de proyecto y hábitos que conviene adoptar desde el primer día"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2cef648d",
   "metadata": {},
   "source": [
    "## 1. ¿Qué Python estoy ejecutando?\n",
    "Rara vez hay un solo Python en una máquina: el del sistema operativo, uno de un gestor de paquetes, uno por\n",
    "proyecto. Sabe siempre cuál ejecuta tu código."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "57ea3026",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:09:55.504816Z",
     "iopub.status.busy": "2026-10-18T10:09:55.504406Z",
     "iopub.status.idle": "2026-10-18T10:09:55.516601Z",
     "shell.execute_reply": "2026-10-18T10:09:55.514447Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "intérprete  : /home/patito/Developments/academy/python/.venv/bin/python3\n",
      "version     : 3.12.3\n",
      "prefijo     : /home/patito/Developments/academy/python/.venv\n",
      "prefijo base: /usr\n",
      "en un venv  : True\n"
     ]
    }
   ],
   "source": [
    "import sys, platform, site\n",
    "print(\"intérprete  :\", sys.executable)\n",
    "print(\"version     :\", platform.python_version())\n",
    "print(\"prefijo     :\", sys.prefix)               # el entorno al que pertenece este intérprete\n",
    "print(\"prefijo base:\", sys.base_prefix)          # la instalación a partir de la que se creó\n",
    "print(\"en un venv  :\", sys.prefix != sys.base_prefix)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9475e45a",
   "metadata": {},
   "source": [
    "## 2. Entornos aislados con `venv`\n",
    "El proyecto A necesita `pandas 1.5` y el proyecto B `pandas 2.2`: instalados globalmente, uno de los dos se\n",
    "rompe. Un **entorno virtual** es una carpeta con su propio `site-packages`, así cada proyecto tiene\n",
    "exactamente las versiones que necesita y nada se mezcla entre ellos. Creado en vivo a continuación:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "d8b55b68",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:09:55.521787Z",
     "iopub.status.busy": "2026-10-18T10:09:55.521348Z",
     "iopub.status.idle": "2026-10-18T10:10:03.714600Z",
     "shell.execute_reply": "2026-10-18T10:10:03.712608Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "prefijo del entorno nuevo: /tmp/tmpmkd0jkxb/demo-env\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "paquetes que contiene    : pip==24.0\n",
      "¿puede importar pandas?  : False\n",
      "este kernel tiene pandas : True\n"
     ]
    }
   ],
   "source": [
    "import subprocess, tempfile, os, venv\n",
    "\n",
    "env_dir = os.path.join(tempfile.mkdtemp(), \"demo-env\")\n",
    "venv.create(env_dir, with_pip=True)                         # equivale a: python -m venv demo-env\n",
    "py = os.path.join(env_dir, \"bin\", \"python\") if os.name != \"nt\" else os.path.join(env_dir, \"Scripts\", \"python.exe\")\n",
    "\n",
    "def in_env(*args):\n",
    "    return subprocess.run([py, *args], capture_output=True, text=True).stdout.strip()\n",
    "\n",
    "print(\"prefijo del entorno nuevo:\", in_env(\"-c\", \"import sys; print(sys.prefix)\"))\n",
    "print(\"paquetes que contiene    :\", in_env(\"-m\", \"pip\", \"list\", \"--format=freeze\") or \"(solo pip)\")\n",
    "print(\"¿puede importar pandas?  :\", in_env(\"-c\", \"import importlib.util as u; print(u.find_spec('pandas') is not None)\"))\n",
    "print(\"este kernel tiene pandas :\", __import__(\"importlib.util\").util.find_spec(\"pandas\") is not None)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f0ab681",
   "metadata": {},
   "source": [
    "El entorno nuevo está vacío: no ve las bibliotecas instaladas para este notebook. Esa es la idea. En el día a\n",
    "día, desde una terminal:\n",
    "\n",
    "```bash\n",
    "python3 -m venv .venv              # crear (una vez por proyecto)\n",
    "source .venv/bin/activate          # activar (Windows: .venv\\Scripts\\activate)\n",
    "pip install -r requirements.txt    # instalar las dependencias del proyecto\n",
    "deactivate                         # salir\n",
    "```\n",
    "\n",
    "Añade `.venv/` a `.gitignore`: los entornos se reconstruyen a partir de la lista de dependencias y nunca se\n",
    "suben al repositorio."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58754356",
   "metadata": {},
   "source": [
    "## 3. Gestores de paquetes\n",
    "| Herramienta | Instala | Úsala para |\n",
    "|---|---|---|\n",
    "| **pip** | paquetes de Python desde PyPI | el estándar; instala en el entorno activo |\n",
    "| **uv** | paquetes de Python y el propio Python | un sustituto mucho más rápido de pip/venv/pip-tools, con archivos de bloqueo (`uv lock`, `uv sync`) |\n",
    "| **pipx** | herramientas de línea de órdenes, cada una en su propio venv oculto | instalar herramientas como `ruff`, `black` o `httpie` sin ensuciar los proyectos |\n",
    "| **conda / mamba** | paquetes de Python *y que no son de Python* (bibliotecas de C/Fortran, CUDA, R) desde conda-forge | entornos científicos con dependencias nativas pesadas, trabajo con GPU |\n",
    "| **Poetry / Hatch / PDM** | gestores de proyectos basados en pyproject.toml | resolución de dependencias, archivos de bloqueo, construir y publicar con una sola herramienta |\n",
    "| **apt / dnf** (Linux) | paquetes del sistema | el software propio del sistema operativo, compiladores, bibliotecas del sistema |\n",
    "| **snap / flatpak** (Linux) | aplicaciones aisladas | aplicaciones de escritorio y herramientas que llevan sus propias dependencias |\n",
    "| **brew** (macOS/Linux), **winget / choco** (Windows) | paquetes del sistema | el mismo papel que apt en otras plataformas |\n",
    "\n",
    "Buenos hábitos:\n",
    "- Nunca hagas `sudo pip install`: puede romper herramientas de las que depende tu sistema operativo. Usa un venv o pipx.\n",
    "- No mezcles pip y conda en el mismo entorno salvo que sepas por qué; quédate con uno.\n",
    "- Instala el propio Python con el gestor de paquetes del sistema, `pyenv`, `uv python install` o el instalador oficial."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "93a8f9de",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:03.718632Z",
     "iopub.status.busy": "2026-10-18T10:10:03.718295Z",
     "iopub.status.idle": "2026-10-18T10:10:05.233249Z",
     "shell.execute_reply": "2026-10-18T10:10:05.231358Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "184 paquetes en el entorno de este kernel, por ejemplo:\n",
      "  numpy      2.5.4\n",
      "  pandas     2.3.3\n",
      "  polars     2.0.0\n",
      "  duckdb     1.5.6\n",
      "  torch      2.14.1+cpu\n",
      "  tensorflow 2.21.0\n"
     ]
    }
   ],
   "source": [
    "r = subprocess.run([sys.executable, \"-m\", \"pip\", \"list\", \"--format=json\"], capture_output=True, text=True)\n",
    "import json\n",
    "pkgs = {p[\"name\"].lower(): p[\"version\"] for p in json.loads(r.stdout)}\n",
    "print(f\"{len(pkgs)} paquetes en el entorno de este kernel, por ejemplo:\")\n",
    "for name in [\"numpy\", \"pandas\", \"polars\", \"duckdb\", \"torch\", \"tensorflow\"]:\n",
    "    print(f\"  {name:<11}{pkgs.get(name, 'no instalado')}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e67d5e8",
   "metadata": {},
   "source": [
    "## 4. Fijar dependencias\n",
    "Un proyecto que funcionaba el año pasado puede no instalarse hoy: una dependencia publicó una versión\n",
    "incompatible. Deja constancia de aquello de lo que dependes.\n",
    "\n",
    "| Archivo | Qué fija | Ejemplo |\n",
    "|---|---|---|\n",
    "| `requirements.txt` con rangos | versiones compatibles | `pandas>=2.2,<3` |\n",
    "| `requirements.lock` / `pip freeze` | las versiones **exactas** que se probaron | `pandas==2.2.3` |\n",
    "| `pyproject.toml` | las dependencias de un paquete (módulo 07) | `dependencies = [\"pandas>=2.2\"]` |\n",
    "| `uv.lock`, `poetry.lock` | versiones exactas más hashes para cada plataforma | los genera la herramienta |\n",
    "\n",
    "Las bibliotecas declaran rangos (para poder combinarse con otras bibliotecas); las aplicaciones se despliegan\n",
    "con versiones exactas (para que producción ejecute lo que se probó)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "baf428cf",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:05.238297Z",
     "iopub.status.busy": "2026-10-18T10:10:05.237596Z",
     "iopub.status.idle": "2026-10-18T10:10:06.591502Z",
     "shell.execute_reply": "2026-10-18T10:10:06.590137Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "absl-py==2.5.1\n",
      "anyio==4.15.1\n",
      "argon2-cffi==25.1.0\n",
      "argon2-cffi-bindings==26.1.0\n",
      "arrow==1.4.0\n",
      "astropy==8.0.2\n",
      "astropy-iers-data==0.2026.10.12.1.3.27\n",
      "asttokens==3.0.2 \n",
      "... 183 líneas en total\n"
     ]
    }
   ],
   "source": [
    "frozen = subprocess.run([sys.executable, \"-m\", \"pip\", \"freeze\"], capture_output=True, text=True).stdout.splitlines()\n",
    "print(\"\\n\".join(frozen[:8]), f\"\\n... {len(frozen)} líneas en total\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "85774425",
   "metadata": {},
   "source": [
    "## 5. Contenedores: enviar el entorno, no solo el código\n",
    "Un entorno virtual aísla los paquetes de Python; un **contenedor** lo aísla todo: las bibliotecas del sistema\n",
    "operativo, la versión de Python, tu código y su configuración. La misma imagen se ejecuta en tu portátil, en\n",
    "la máquina de un compañero y en un servidor en la nube, por eso la mayoría de los servicios web se despliegan\n",
    "así.\n",
    "\n",
    "Un Dockerfile para una pequeña API web:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "93634b65",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:06.596706Z",
     "iopub.status.busy": "2026-10-18T10:10:06.596232Z",
     "iopub.status.idle": "2026-10-18T10:10:06.604221Z",
     "shell.execute_reply": "2026-10-18T10:10:06.602413Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "# small official base image with Python\n",
      "FROM python:3.12-slim\n",
      "\n",
      "WORKDIR /app\n",
      "# dependencies first: this layer is cached between builds while only the code changes\n",
      "COPY requirements.lock .\n",
      "RUN pip install --no-cache-dir -r requirements.lock\n",
      "\n",
      "# then the code, which changes often\n",
      "COPY . .\n",
      "# do not run as root\n",
      "USER 1000\n",
      "EXPOSE 8000\n",
      "CMD [\"uvicorn\", \"api:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n",
      "\n",
      "services:\n",
      "  api:\n",
      "    build: .\n",
      "    ports: [\"127.0.0.1:8000:8000\"]    # only reachable from this machine (a web server proxies to it)\n",
      "    env_file: .env                     # secrets stay out of the image\n",
      "    restart: unless-stopped\n",
      "    mem_limit: 512m                    # a runaway process cannot take the whole server\n",
      "\n"
     ]
    }
   ],
   "source": [
    "dockerfile = \"\"\"\\\n",
    "# small official base image with Python\n",
    "FROM python:3.12-slim\n",
    "\n",
    "WORKDIR /app\n",
    "# dependencies first: this layer is cached between builds while only the code changes\n",
    "COPY requirements.lock .\n",
    "RUN pip install --no-cache-dir -r requirements.lock\n",
    "\n",
    "# then the code, which changes often\n",
    "COPY . .\n",
    "# do not run as root\n",
    "USER 1000\n",
    "EXPOSE 8000\n",
    "CMD [\"uvicorn\", \"api:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n",
    "\"\"\"\n",
    "compose = \"\"\"\\\n",
    "services:\n",
    "  api:\n",
    "    build: .\n",
    "    ports: [\"127.0.0.1:8000:8000\"]    # only reachable from this machine (a web server proxies to it)\n",
    "    env_file: .env                     # secrets stay out of the image\n",
    "    restart: unless-stopped\n",
    "    mem_limit: 512m                    # a runaway process cannot take the whole server\n",
    "\"\"\"\n",
    "print(dockerfile); print(compose)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "52f5eb9b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:06.608450Z",
     "iopub.status.busy": "2026-10-18T10:10:06.607986Z",
     "iopub.status.idle": "2026-10-18T10:10:06.666342Z",
     "shell.execute_reply": "2026-10-18T10:10:06.664802Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Docker version 29.8.1, build 4a63305\n",
      "\n",
      "docker build -t my-api .          # construir la imagen a partir del Dockerfile\n",
      "docker run -p 8000:8000 my-api    # ejecutar un contenedor\n",
      "docker compose up -d              # ejecutar en segundo plano los servicios descritos en compose.yaml\n",
      "docker compose logs -f api        # seguir los registros\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import shutil\n",
    "docker = shutil.which(\"docker\")\n",
    "if docker:\n",
    "    print(subprocess.run([docker, \"--version\"], capture_output=True, text=True).stdout.strip())\n",
    "else:\n",
    "    print(\"docker no está instalado aquí (lo normal en Colab). Las órdenes para probar en tu propia máquina:\")\n",
    "print('''\n",
    "docker build -t my-api .          # construir la imagen a partir del Dockerfile\n",
    "docker run -p 8000:8000 my-api    # ejecutar un contenedor\n",
    "docker compose up -d              # ejecutar en segundo plano los servicios descritos en compose.yaml\n",
    "docker compose logs -f api        # seguir los registros\n",
    "''')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d12378eb",
   "metadata": {},
   "source": [
    "**Buenas prácticas con contenedores:** parte de una imagen `slim`; copia e instala las dependencias antes que\n",
    "el código para que reconstruir sea rápido; ejecuta como usuario sin privilegios de root; guarda los secretos en\n",
    "variables de entorno o archivos montados al arrancar, nunca dentro de la imagen; fija límites de memoria; un\n",
    "proceso por contenedor; guarda los datos en volúmenes, no dentro del contenedor.\n",
    "\n",
    "| | entorno virtual | contenedor |\n",
    "|---|---|---|\n",
    "| aísla | paquetes de Python | bibliotecas del sistema, Python, paquetes, configuración |\n",
    "| tamaño | de decenas a cientos de MB | de cientos de MB a unos pocos GB |\n",
    "| ideal para | desarrollo, scripts | despliegue, servicios, reproducir un sistema completo |\n",
    "\n",
    "## 6. Editores e IDE\n",
    "| Herramienta | Tipo | Puntos fuertes |\n",
    "|---|---|---|\n",
    "| **Visual Studio Code** | editor gratuito con extensiones | extensiones de Python y Jupyter, depurador, desarrollo remoto por SSH y en contenedores, integración con Git. El más usado hoy. |\n",
    "| **PyCharm** | IDE completo (edición Community gratuita) | análisis de código y refactorización profundos, ejecutor de pruebas, herramientas de bases de datos |\n",
    "| **Eclipse + PyDev** | IDE (gratuito) | familiar para equipos de Java; depurador, refactorización, soporte de Django |\n",
    "| **Vim / Neovim** | editor de terminal | disponible en cualquier servidor y muy rápido una vez aprendido; Neovim con un servidor de lenguaje da funciones de IDE |\n",
    "| **JupyterLab** | entorno de notebooks en el navegador | exploración, análisis de datos, docencia (este curso) |\n",
    "| **Spyder** | IDE científico | explorador de variables al estilo de MATLAB, popular en ciencia e ingeniería |\n",
    "| **Visual Studio** (Windows) | IDE completo | soporte de Python para equipos que ya usan herramientas de Microsoft |\n",
    "\n",
    "Elijas el que elijas, aprende tres cosas en él: **usar el depurador** con puntos de interrupción, **saltar a la\n",
    "definición** y **renombrar un símbolo** en todo el proyecto. También merece la pena saber lo básico de Vim: a\n",
    "menudo es el único editor disponible en un servidor remoto.\n",
    "\n",
    "## 7. Una estructura de proyecto para empezar\n",
    "```\n",
    "mi-proyecto/\n",
    "├── .venv/                 # entorno (no se sube al repositorio)\n",
    "├── src/mi_proyecto/       # el código, importable como paquete\n",
    "│   ├── __init__.py\n",
    "│   └── core.py\n",
    "├── tests/                 # pruebas con pytest (módulo 06)\n",
    "├── notebooks/             # exploración, no código de producción\n",
    "├── pyproject.toml         # metadatos, dependencias, ajustes de herramientas (módulo 07)\n",
    "├── requirements.lock      # versiones exactas para el despliegue\n",
    "├── Dockerfile             # si se despliega como servicio\n",
    "├── .gitignore             # .venv/, __pycache__/, .env, archivos de datos\n",
    "├── .env.example           # documenta los ajustes; el .env real nunca se sube\n",
    "└── README.md\n",
    "```\n",
    "\n",
    "Hábitos que compensan: un formateador y un linter (`ruff format`, `ruff check`) ejecutados automáticamente con\n",
    "`pre-commit`; anotaciones de tipo comprobadas con `mypy` o `pyright`; pruebas ejecutadas en cada push por la\n",
    "integración continua (GitHub Actions); secretos solo en variables de entorno.\n",
    "\n",
    "## Ejercicios\n",
    "1. Crea un venv para un proyecto pequeño, instala `requests` y escribe un `requirements.lock` con `pip freeze`. Borra el venv y reconstrúyelo a partir del archivo de bloqueo.\n",
    "2. Instala `ruff` con `pipx` y ejecuta `ruff check` sobre uno de tus scripts.\n",
    "3. Escribe un Dockerfile para un script que imprima la fecha actual; constrúyelo y ejecútalo."
   ]
  }
 ],
 "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
}
