{
 "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 soluciones 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",
    "Este es el cuaderno de **soluciones**. Trae el código de cada ejercicio, la\n",
    "explicación de la trampa y la respuesta del quiz. Si vienes del cuaderno de\n",
    "práctica sin haberlo intentado, vuelve 🙂"
   ]
  },
  {
   "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**\n",
    "\n",
    "Tres cosas y ninguna da error. La primera: **no hay `timeout`**, así que si el proveedor tarda en contestar el proceso se queda colgado hasta que alguien lo mate. La segunda: no se mira el `status_code`, así que un 500 o un 401 pasan de largo. Y la tercera es la que causó el cero: ese `.get('datos', [])` parece prudente y es justo lo que esconde el problema, porque cuando la respuesta no trae 'datos' devuelve una lista vacía sin quejarse. Después el `to_csv` **pisa el archivo bueno del día anterior con uno vacío**, así que además de no traer nada, se perdió lo que ya había. Se arregla con timeout, con `raise_for_status()`, y sobre todo escribiendo en un archivo temporal y renombrándolo solo si llegaron filas. Guardar nada encima de algo es la parte cara 🚩"
   ]
  },
  {
   "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": [
    "top = pd.read_sql_query('''\n",
    "    SELECT c.nombre, ROUND(SUM(p.monto), 2) AS total\n",
    "    FROM pedidos p JOIN clientes c ON c.id = p.id_cliente\n",
    "    GROUP BY c.nombre ORDER BY total DESC LIMIT 5\n",
    "''', con)\n",
    "print(top.to_string(index=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Compara con hacerlo trayendo las 900 filas y agrupando con\n",
    "`groupby`. El resultado es el mismo y lo que viaja por la red no."
   ]
  },
  {
   "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": [
    "r = requests.get(f'{BASE}/mantenimiento', timeout=5)\n",
    "print('codigo:', r.status_code)\n",
    "print('tipo  :', r.headers['Content-Type'])\n",
    "print('texto :', r.text[:60])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Código 200 y contenido HTML. Esta es la comprobación de tres líneas que\n",
    "convierte \"no entiendo por qué falla\" en \"ah, está en mantenimiento\"."
   ]
  },
  {
   "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": [
    "r = requests.get(f'{BASE}/pedidos', timeout=5,\n",
    "                 headers={'Authorization': 'Bearer una-clave-de-mentira'})\n",
    "print(r.status_code)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La API de mentira no la mira, y da igual: lo que se practica es dónde va. Y\n",
    "la regla que no se negocia: **la clave se lee de una variable de entorno\n",
    "con `os.environ`**, nunca escrita en el archivo, porque el\n",
    "archivo termina en algún sitio compartido."
   ]
  },
  {
   "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": [
    "todos = []\n",
    "for pagina in range(1, 4):\n",
    "    r = requests.get(f'{BASE}/pedidos?page={pagina}', timeout=5)\n",
    "    r.raise_for_status()\n",
    "    lote = r.json()['datos']\n",
    "    if not lote:\n",
    "        break\n",
    "    todos.extend(lote)\n",
    "print('trajimos', len(todos), 'registros en', pagina, 'paginas')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en el `break`: la condición de salida es que llegue vacío,\n",
    "no un número que te inventes. Un bucle de paginación sin salida clara es la\n",
    "forma más rápida de que te bloqueen la clave."
   ]
  },
  {
   "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": [
    "import os\n",
    "\n",
    "r = requests.get(f'{BASE}/pedidos', timeout=10)\n",
    "r.raise_for_status()\n",
    "datos = r.json()['datos']\n",
    "if not datos:\n",
    "    raise ValueError('la API contesto bien pero no trajo filas')\n",
    "\n",
    "pd.DataFrame(datos).to_csv('pedidos.tmp', index=False)\n",
    "os.replace('pedidos.tmp', 'pedidos.csv')\n",
    "print('guardados', len(datos), 'pedidos')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El `os.replace` del final es la pieza clave: cambia el nombre de\n",
    "golpe, así que o está el archivo viejo entero o el nuevo entero, nunca uno a\n",
    "medias. Es la misma idea que la transacción del libro de SQL."
   ]
  },
  {
   "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": "markdown",
   "metadata": {},
   "source": [
    "Sirve cualquiera: el tipo de cambio, los feriados de Perú, el clima. Lo que\n",
    "vas a descubrir sola es que **lo difícil no es pedirlo**: es que el\n",
    "JSON viene anidado de una forma que nadie te avisó, y ahí es donde se va la\n",
    "tarde 🧩\n",
    "\n",
    "Cuando pase, imprime `datos.keys()` y ve bajando nivel por nivel.\n",
    "Es lo mismo que mirar antes de tocar del capítulo 16."
   ]
  },
  {
   "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\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Es buenísima costumbre y no evita el desastre de esta noche. Piensa qué pasa si el proveedor contesta 500.\n",
    "\n",
    "*c)* Ayuda con las caídas cortas. Pero si la respuesta llega con un 401, reintentar tres veces te da tres 401.\n",
    "\n",
    "*d)* Te enteras después. Lo que hace falta es que no llegue a guardar basura encima de lo bueno.\n",
    "\n",
    "Y el orden importa: primero que no rompa nada, después que avise."
   ]
  },
  {
   "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
}
