# HTTP Client primitivas

Gap identificado en el registry. Reimplementar desde cero.

Hay fetch_ohlcv y stream_ticks pero no un HTTP client generico reutilizable.
Necesario para integraciones con APIs externas sin reimplementar retry/headers cada vez.

## Funciones a implementar

### http_get_json

- **Dominio:** infra
- **Lang:** Python
- **Purity:** impure (I/O de red)
- **Signature:** `http_get_json(url: str, headers: dict[str, str] | None = None, params: dict[str, str] | None = None, timeout: float = 30.0) -> dict`
- **Retorno:** Response body parseado como dict/list
- **Descripcion:** GET request que espera JSON. Agrega `Accept: application/json` automaticamente. Lanza error descriptivo si status >= 400 (incluye status code, url truncada, y primeros 200 chars del body).
- **Deps:** `urllib.request`, `urllib.parse`, `json` (solo stdlib, sin requests)
- **Tests:**
  - Mock de respuesta 200 con JSON
  - Mock de respuesta 404 → error con status code
  - Mock de respuesta con JSON invalido → error descriptivo
  - Params serializados como query string
  - Headers custom enviados

### http_post_json

- **Dominio:** infra
- **Lang:** Python
- **Purity:** impure
- **Signature:** `http_post_json(url: str, body: dict, headers: dict[str, str] | None = None, timeout: float = 30.0) -> dict`
- **Retorno:** Response body parseado como dict/list
- **Descripcion:** POST request con body JSON. Agrega `Content-Type: application/json` y `Accept: application/json`. Misma politica de errores que http_get_json.
- **Deps:** `urllib.request`, `json` (solo stdlib)
- **Tests:**
  - Mock de POST con body serializado correctamente
  - Mock de respuesta 201
  - Mock de respuesta 500 → error
  - Body con unicode

### http_download_file

- **Dominio:** infra
- **Lang:** Python
- **Purity:** impure
- **Signature:** `http_download_file(url: str, dest_path: str, headers: dict[str, str] | None = None, timeout: float = 120.0, chunk_size: int = 8192) -> dict`
- **Retorno:** `{"path": str, "size_bytes": int, "content_type": str}`
- **Descripcion:** Descarga un archivo por HTTP en streaming (sin cargar todo en memoria). Crea directorios intermedios si no existen. Si el archivo destino ya existe, lo sobreescribe.
- **Deps:** `urllib.request`, `os` (solo stdlib)
- **Tests:**
  - Mock de descarga con contenido binario
  - Directorio destino creado automaticamente
  - Retorno con size correcto
  - Timeout configurado en el request

### http_get_json (Go)

- **Dominio:** infra
- **Lang:** Go
- **Purity:** impure
- **Signature:** `HttpGetJSON(url string, headers map[string]string, timeout time.Duration) (map[string]any, error)`
- **Descripcion:** Misma semantica que Python. Usa `net/http` + `encoding/json`. Retorna error con status code si >= 400. Cierra body siempre (defer).
- **Deps:** `net/http`, `encoding/json`, `time` (solo stdlib)
- **Tests:**
  - httptest.Server con respuesta JSON
  - Status 404 → error
  - Timeout → error
  - Headers custom

### http_post_json (Go)

- **Dominio:** infra
- **Lang:** Go
- **Purity:** impure
- **Signature:** `HttpPostJSON(url string, body any, headers map[string]string, timeout time.Duration) (map[string]any, error)`
- **Descripcion:** Misma semantica que Python. Serializa body con json.Marshal, POST con Content-Type application/json.
- **Deps:** `net/http`, `encoding/json`, `bytes`, `time`
- **Tests:**
  - httptest.Server recibe body correcto
  - Status 201 → exito
  - Status 500 → error con body parcial

### http_download_file (Go)

- **Dominio:** infra
- **Lang:** Go
- **Purity:** impure
- **Signature:** `HttpDownloadFile(url, destPath string, headers map[string]string, timeout time.Duration) (int64, error)`
- **Retorno:** bytes escritos, error
- **Descripcion:** Descarga en streaming con io.Copy. Crea directorios con os.MkdirAll. Usa archivo temporal + rename para atomicidad.
- **Deps:** `net/http`, `os`, `io`, `path/filepath`
- **Tests:**
  - httptest.Server sirve archivo binario
  - Directorio creado automaticamente
  - Archivo temporal + rename (no deja basura si falla)
  - Size retornado coincide
