{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Cuando los datos no vienen en un archivo\n",
    "\n",
    "Una base de datos y una API desde Python, con la API montada aquí mismo para que corra sin internet.\n",
    "\n",
    "Cuaderno de práctica del capítulo 14 de **Python desde cero**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/python-desde-cero/traer-datos-de-fuera/\n",
    "\n",
    "Los ejercicios están al final y traen una celda vacía debajo de cada uno. Las\n",
    "respuestas viven en el cuaderno de soluciones, y merece la pena pelearse un\n",
    "rato antes de abrirlo 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# Este capitulo abre el archivo por su nombre, sin URL, que es lo que se hace\n",
    "# cuando lo tienes al lado. Aqui se baja primero para que exista.\n",
    "for nombre in ['tienda.db']:\n",
    "    urllib.request.urlretrieve(f\"https://missyera.com/static/datasets/{nombre}\", nombre)\n",
    "    print(nombre, \"lista\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hasta aquí los datos llegaban en un archivo. En un trabajo eso pasa poco: lo\n",
    "normal es que estén en una base de datos o detrás de una API 🔌\n",
    "\n",
    "Una pregunta antes de empezar: **¿de dónde salen los archivos que\n",
    "alguien te manda por correo?** Casi siempre de una de estas dos cosas, y\n",
    "alguien los exportó a mano. Este capítulo es para saltarte a esa persona 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Una base de datos, en cuatro líneas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vamos con `tienda.db`, la misma base del\n",
    "[libro de SQL desde cero](https://missyera.com/guias/sql-desde-cero/). Descárgala y ponla\n",
    "al lado de tu cuaderno."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sqlite3\n",
    "import pandas as pd\n",
    "\n",
    "con = sqlite3.connect('tienda.db')\n",
    "ticket = pd.read_sql_query('''\n",
    "    SELECT c.ciudad, COUNT(*) AS pedidos, ROUND(AVG(p.monto), 2) AS ticket\n",
    "    FROM pedidos p\n",
    "    JOIN clientes c ON c.id = p.id_cliente\n",
    "    GROUP BY c.ciudad\n",
    "    ORDER BY ticket DESC\n",
    "''', con)\n",
    "print(ticket.to_string(index=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eso es todo: te conectas, escribes SQL y te devuelve un DataFrame 🎉\n",
    "\n",
    "Y fíjate en el reparto del trabajo, que es la decisión importante:\n",
    "**agrupar lo hizo la base, no pandas**. Con seiscientas filas da\n",
    "igual, con seis millones no: traértelas todas para agrupar en tu portátil es\n",
    "mover seis millones de filas por la red para quedarte con seis 🚚\n",
    "\n",
    "La regla que uso: **filtra y agrupa en la base, y trae a Python lo que\n",
    "ya está resumido**. Con lo que sabes del libro de SQL te alcanza de\n",
    "sobra."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuando el filtro viene de fuera"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si la ciudad la elige quien consulta, hay una forma de escribirlo y una sola:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "CIUDAD = 'Piura'\n",
    "sub = pd.read_sql_query(\n",
    "    'SELECT COUNT(*) AS pedidos FROM pedidos p '\n",
    "    'JOIN clientes c ON c.id = p.id_cliente WHERE c.ciudad = ?',\n",
    "    con, params=(CIUDAD,))\n",
    "print(f'pedidos de {CIUDAD}:', int(sub['pedidos'][0]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El `?` con `params` aparte, nunca una f-string dentro\n",
    "del SQL. Eso es **inyección SQL**, tiene capítulo propio en el libro\n",
    "de SQL y es el error de base de datos que más caro sale 🔒"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora una API"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una API es una dirección web que devuelve datos en vez de una página. Le\n",
    "pides algo, te contesta con JSON, y eso en Python es un diccionario.\n",
    "\n",
    "Para que este capítulo corra en tu máquina aunque no tengas internet,\n",
    "**vamos a montar la API aquí mismo**. Son quince líneas y no hace\n",
    "falta que las entiendas todas: lo que importa viene después."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "import threading\n",
    "from http.server import BaseHTTPRequestHandler, HTTPServer\n",
    "\n",
    "PEDIDOS = [{'id': 1, 'ciudad': 'Lima', 'monto': 892.06},\n",
    "           {'id': 2, 'ciudad': 'Arequipa', 'monto': 731.09},\n",
    "           {'id': 3, 'ciudad': 'Cusco', 'monto': 407.39}]\n",
    "\n",
    "class ApiDeMentira(BaseHTTPRequestHandler):\n",
    "    def do_GET(self):\n",
    "        if self.path.startswith('/pedidos'):\n",
    "            cuerpo, codigo, tipo = json.dumps({'total': 3, 'datos': PEDIDOS}), 200, 'application/json'\n",
    "        elif self.path.startswith('/mantenimiento'):\n",
    "            cuerpo, codigo, tipo = '<html>Volvemos en un rato</html>', 200, 'text/html'\n",
    "        else:\n",
    "            cuerpo, codigo, tipo = json.dumps({'error': 'no existe'}), 404, 'application/json'\n",
    "        cuerpo = cuerpo.encode()\n",
    "        self.send_response(codigo)\n",
    "        self.send_header('Content-Type', tipo)\n",
    "        self.send_header('Content-Length', str(len(cuerpo)))\n",
    "        self.end_headers()\n",
    "        self.wfile.write(cuerpo)\n",
    "    def log_message(self, *args):\n",
    "        pass\n",
    "\n",
    "servidor = HTTPServer(('127.0.0.1', 0), ApiDeMentira)\n",
    "threading.Thread(target=servidor.serve_forever, daemon=True).start()\n",
    "BASE = f'http://127.0.0.1:{servidor.server_address[1]}'\n",
    "print('API de mentira levantada')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto que acabamos de hacer tiene nombre y se usa en serio: **montar un\n",
    "servidor de mentira para probar código que llama a una API**. Si tus\n",
    "pruebas dependen de que internet funcione y de que el proveedor no cambie nada,\n",
    "no son pruebas 🧪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pedirle datos, que es una línea"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import requests\n",
    "\n",
    "r = requests.get(f'{BASE}/pedidos', timeout=5)\n",
    "print('codigo :', r.status_code)\n",
    "print('tipo   :', r.headers['Content-Type'])\n",
    "datos = r.json()\n",
    "print('total  :', datos['total'])\n",
    "print('primero:', datos['datos'][0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Contra una API de verdad se escribe exactamente igual, cambiando\n",
    "`BASE` por su dirección 🌐\n",
    "\n",
    "Tres cosas de esa línea, y las tres son costumbres que se pagan:\n",
    "\n",
    "- ⏱️ **El `timeout` no es opcional.** Sin él, si el\n",
    "otro lado no contesta, tu programa se queda esperando *para siempre*. Es\n",
    "el fallo más tonto y el que más veces he visto colgar un proceso de madrugada.\n",
    "\n",
    "- 🔢 **El `status_code`** dice si salió bien. 200 es\n",
    "bien, 404 no existe, 401 no tienes permiso, 429 estás pidiendo demasiado rápido\n",
    "y 500 se rompió el otro lado.\n",
    "\n",
    "- 📦 **`.json()`** convierte el cuerpo en diccionario.\n",
    "No hace falta `json.loads`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que devuelve cuando sale mal"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mala = requests.get(f'{BASE}/no-existe', timeout=5)\n",
    "print('codigo:', mala.status_code, ' ok:', mala.ok)\n",
    "print('y aun asi trae cuerpo:', mala.json())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí está la trampa que hay que ver: **una respuesta con error\n",
    "*no* lanza una excepción** 😳\n",
    "\n",
    "`requests` te devuelve el objeto tan tranquilo, con su 404 y su\n",
    "cuerpo. Si tú no miras el código, tu programa sigue como si nada y guarda\n",
    "`{'error': 'no existe'}` creyendo que son datos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una API que responde no es una API que funcionó. Hay que mirar el código de estado, siempre."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La forma corta de no olvidarse es `r.raise_for_status()`, que\n",
    "convierte cualquier código de error en una excepción y te obliga a tratarla."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## De JSON a tabla"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame(datos['datos'])\n",
    "print(df.to_string(index=False))\n",
    "print('tipos:')\n",
    "print(df.dtypes.to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una línea y ya estás en el capítulo 12 🐼\n",
    "\n",
    "Eso funciona porque el JSON venía plano: una lista de diccionarios con las\n",
    "mismas claves. Cuando venga anidado, con diccionarios dentro de diccionarios,\n",
    "está `pd.json_normalize`, que los aplana en columnas con puntos.\n",
    "\n",
    "Y mira los tipos, que es la comprobación de siempre: si `monto`\n",
    "hubiera llegado como texto, ahí se vería, igual que en el capítulo\n",
    "10 👀"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los tres errores que hay que saber leer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    requests.get('http://127.0.0.1:1/pedidos', timeout=0.3)\n",
    "except Exception as e:\n",
    "    print('1. no se pudo ni conectar ->', type(e).__name__)\n",
    "\n",
    "try:\n",
    "    requests.get(f'{BASE}/no-existe', timeout=5).raise_for_status()\n",
    "except Exception as e:\n",
    "    print('2. contesto con error     ->', type(e).__name__)\n",
    "\n",
    "try:\n",
    "    requests.get(f'{BASE}/mantenimiento', timeout=5).json()\n",
    "except Exception as e:\n",
    "    print('3. contesto algo que no es JSON ->', type(e).__name__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Son tres cosas distintas y conviene no confundirlas 🩺\n",
    "\n",
    "El primero es de red: no llegaste. El segundo llegó y el otro lado dijo que\n",
    "no. Y el tercero es el más traicionero: contestó **200**, todo\n",
    "parecía bien, y lo que mandó fue una página de mantenimiento en HTML.\n",
    "\n",
    "Ese tercero, sin el `try`, es así:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    requests.get(f'{BASE}/mantenimiento', timeout=5).json()\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "JSONDecodeError: Expecting value: line 1 column 1 (char 0)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Expecting value: line 1 column 1* quiere decir \"lo primero que me\n",
    "diste ya no era JSON\". Cuando veas eso, imprime `r.text[:200]` antes\n",
    "de nada: casi siempre es una página de error o de inicio de sesión 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que falta y no cabe aquí"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Si te toca | Qué buscar |\n",
    "|---|---|\n",
    "| La API pide una clave | Va en las cabeceras: `headers={'Authorization': f'Bearer {clave}'}`. Y la clave NUNCA en el código, se lee del entorno |\n",
    "| Vienen 10.000 registros de a 100 | Paginación. Un bucle que pide páginas hasta que la respuesta viene vacía |\n",
    "| Te devuelve 429 | Estás pidiendo muy rápido. Se espera y se reintenta, doblando la espera cada vez |\n",
    "| Muchas peticiones seguidas | `requests.Session()`, que reutiliza la conexión y va bastante más rápido |\n",
    "| Escribir en vez de leer | `requests.post`, igual pero con `json=` para mandar el cuerpo |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la costumbre que más disgustos ahorra: **guarda la respuesta cruda\n",
    "antes de tocarla**. Si el proveedor cambia algo mañana, tienes lo de hoy\n",
    "para comparar; y si no, hay que volver a pedirlo todo 💾"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Un proceso trae los pedidos de una API cada noche y los guarda. Lleva meses funcionando. Una noche el proveedor se cae y a la mañana siguiente el informe sale en cero, sin ningún error en el registro.\n",
    "\n",
    "```\n",
    "import requests, pandas as pd\n",
    "\n",
    "r = requests.get('https://api.proveedor.com/pedidos')\n",
    "datos = r.json().get('datos', [])\n",
    "\n",
    "df = pd.DataFrame(datos)\n",
    "df.to_csv('pedidos.csv', index=False)\n",
    "print(f'guardados {len(df)} pedidos')\n",
    "```\n",
    "\n",
    "**¿Qué está mal?** La respuesta está en el cuaderno de soluciones. Míralo tú primero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seis. El 5 es el que de verdad se parece a un encargo 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Deja que la base haga el trabajo\n",
    "\n",
    "Trae los cinco clientes que más compraron, con una sola\n",
    "consulta, sin agrupar en pandas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Mira lo que llegó antes de creerlo\n",
    "\n",
    "Antes de `.json()`, imprime los primeros\n",
    "caracteres de la respuesta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Ponle una clave\n",
    "\n",
    "Añade una cabecera de autorización a la petición y\n",
    "comprueba que la API de mentira la recibe."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Paginar\n",
    "\n",
    "Escribe el bucle que pide páginas hasta que no vengan más.\n",
    "La API de mentira devuelve siempre lo mismo, así que ponle un tope."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. El proceso de la noche, bien hecho\n",
    "\n",
    "Arregla la trampa del capítulo: timeout, comprobación del\n",
    "código, y que no pise el archivo bueno si no llegó nada."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Una API de verdad\n",
    "\n",
    "Sin dataset y con internet. Busca una API pública que no\n",
    "pida clave y tráete algo a un DataFrame."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Tu proceso trae datos de una API cada noche. ¿Cuál de estas cuatro es la que no puede faltar?\n",
    "\n",
    "a) El timeout, el raise_for_status y no pisar el archivo si viene vacío\n",
    "\n",
    "b) Guardar la respuesta cruda por si acaso\n",
    "\n",
    "c) Reintentar tres veces si falla\n",
    "\n",
    "d) Avisar por correo cuando termine"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🗄️ `sqlite3` más `pd.read_sql_query` y ya tienes la\n",
    "base en un DataFrame.\n",
    "\n",
    "- 🚚 Filtra y agrupa en la base. Trae a Python lo que ya está resumido.\n",
    "\n",
    "- 🔒 Si el filtro viene de fuera, va con `?` y `params`.\n",
    "Nunca pegado.\n",
    "\n",
    "- ⏱️ `requests.get` siempre con `timeout`.\n",
    "\n",
    "- 🔢 Una respuesta con error no lanza excepción. Hay que mirar el código o\n",
    "llamar a `raise_for_status()`.\n",
    "\n",
    "- 🩺 Y los tres errores son distintos: no llegué, llegué y dijo que no, o\n",
    "llegué y me dio algo que no era JSON.\n",
    "\n",
    "Si lo que quieres es escribir tú las consultas en vez de copiarlas, eso es el\n",
    "[libro de SQL desde cero](https://missyera.com/guias/sql-desde-cero/) 🗃️\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 14 de **Python desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/python-desde-cero/traer-datos-de-fuera/\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
