Proyecto con requests python: cliente de la API de GitHub

Cierra la serie juntando todo lo aprendido en una clase reutilizable, y resuelve las dudas más comunes sobre requests.

Te puede interesar: Session, reintentos y archivos con Requests Python

Proyecto final: un cliente de la API de GitHub

Juntamos todo lo anterior en una clase reutilizable: Session, cabeceras, token opcional, timeout, reintentos y errores propios. Es un patrón que puedes copiar para cualquier API.

import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry


class GitHubError(Exception):
    """Error controlado al hablar con la API de GitHub."""


class GitHubClient:
    BASE = "https://api.github.com"

    def __init__(self, token=None, timeout=10):
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({
            "Accept": "application/vnd.github+json",
            "User-Agent": "agusbot-tutorial/1.0",
        })
        if token:
            self.session.headers["Authorization"] = f"Bearer {token}"

        reintentos = Retry(
            total=3,
            backoff_factor=1,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods=["GET"],
        )
        self.session.mount("https://", HTTPAdapter(max_retries=reintentos))

    def _get(self, ruta, **params):
        try:
            r = self.session.get(
                f"{self.BASE}{ruta}", params=params, timeout=self.timeout
            )
            r.raise_for_status()
            return r.json()
        except requests.exceptions.HTTPError as e:
            codigo = e.response.status_code
            if codigo == 404:
                raise GitHubError(f"No encontrado: {ruta}") from e
            raise GitHubError(f"GitHub respondió HTTP {codigo}") from e
        except requests.exceptions.RequestException as e:
            raise GitHubError(f"Fallo de red: {e}") from e

    def repo(self, propietario, nombre):
        return self._get(f"/repos/{propietario}/{nombre}")

    def repos_de_usuario(self, usuario, cantidad=5):
        return self._get(f"/users/{usuario}/repos", per_page=cantidad, sort="updated")


if __name__ == "__main__":
    gh = GitHubClient(token=os.environ.get("GITHUB_TOKEN"))
    try:
        info = gh.repo("psf", "requests")
        print(f"{info['full_name']}: {info['stargazers_count']} estrellas")
        for r in gh.repos_de_usuario("psf"):
            print("-", r["name"])
    except GitHubError as e:
        print(f"Error: {e}")

Sin token, GitHub permite pocas peticiones por hora desde tu IP; con un token personal (variable GITHUB_TOKEN) el límite sube bastante. Para practicar más, prueba a añadir paginación con el parámetro page o a leer la cabecera X-RateLimit-Remaining.

Por qué está estructurado así

El cliente no es más largo de lo necesario, pero cada decisión tiene una razón:

  • La Session se crea una sola vez en el constructor, así todas las peticiones comparten conexiones, cabeceras y reintentos.
  • Un único método _get concentra el timeout, la comprobación de errores y la conversión a JSON. Si mañana quieres registrar cada llamada, lo cambias en un solo lugar.
  • Una excepción propia, GitHubError, evita que el resto de tu programa dependa de requests. Quien use el cliente captura un único tipo de error, sin conocer HTTPError, Timeout ni ConnectionError.
  • El token es opcional, de modo que puedes probar el cliente sin credenciales y añadirlas cuando lo necesites.

Este patrón se traslada casi sin cambios a cualquier otra API: cambia la URL base, las cabeceras de autenticación y los métodos de negocio.

Cómo seguir mejorando el cliente

Con la base lista, estos son los siguientes pasos naturales. Primero, la paginación: GitHub devuelve los resultados por páginas e indica la siguiente en la cabecera Link, que requests ya interpreta en respuesta.links. Segundo, el control del límite de uso, leyendo X-RateLimit-Remaining para pausar antes de recibir un 429. Tercero, las pruebas automáticas sin internet: librerías como responses o requests-mock simulan las respuestas del servidor, y así compruebas que tu manejo de errores funciona sin depender de la red. Por último, considera registrar cada petición con el módulo logging, cuidando de no escribir tokens en los registros.

Preguntas frecuentes

¿requests o httpx? Usa requests si tu código es síncrono y quieres la opción más documentada y extendida. Elige httpx si necesitas async/await o HTTP/2 (pip install httpx[http2]); su API es muy parecida.

¿requests funciona con async? No: es bloqueante. Para muchas peticiones concurrentes puedes usar hilos (concurrent.futures.ThreadPoolExecutor) o pasar a httpx o aiohttp.

¿Qué hago con el error SSL “certificate verify failed”? No pongas verify=False, porque desactiva la protección contra ataques de intermediario. Actualiza los certificados (pip install --upgrade certifi) o apunta a tu certificado corporativo con verify="/ruta/ca.pem".

¿Cómo uso un proxy? Pasa proxies={"https": "http://usuario:clave@host:8080"} o define las variables de entorno HTTP_PROXY y HTTPS_PROXY, que requests respeta automáticamente.

¿Por qué respuesta.json() falla con JSONDecodeError? El servidor devolvió algo que no es JSON: una página de error HTML, un cuerpo vacío o un 204. Revisa respuesta.status_code y respuesta.text antes de decodificar.

¿Puedo hacer web scraping con requests? Requests solo descarga el HTML; para extraer datos combínalo con una librería como BeautifulSoup y revisa antes el robots.txt y los términos de uso del sitio.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *