================================================================================
                    AGROBR - ROADMAP v0.6.0
                    "De biblioteca para infraestrutura"
================================================================================


================================================================================
ROLE & CONTEXTO DO PROJETO
================================================================================

# Role
Desenvolvedor s├¬nior do projeto **agrobr** ÔÇö infraestrutura Python para dados
agr├¡colas brasileiros com camada sem├óntica sobre CEPEA, CONAB e IBGE.

# Contexto do Projeto
- **Stack:** Python 3.11+, httpx (async), beautifulsoup4 + lxml, pandas, polars,
  pydantic v2, DuckDB, structlog, chardet, contextvars
- **Arquitetura:**
  - Camada de fontes: m├│dulos por origem (cepea/, conab/, ibge/)
  - Camada sem├óntica: datasets/ com fallback autom├ítico, contratos versionados
  - Registry de datasets com auto-descoberta
  - Cache DuckDB com hist├│rico permanente
  - Async-first, sync wrapper via agrobr.sync
- **Padr├Áes:** Google Style Guide, pytest + pytest-asyncio, mkdocs-material
- **Fontes:** CEPEA (pre├ºos), CONAB (safras/balan├ºo), IBGE/SIDRA (PAM, LSPA)
- **Resili├¬ncia:** Fingerprinting de layout, valida├º├úo estat├¡stica, golden tests,
  fallback em cascata via callable fetchers, exce├º├Áes tipadas

# Comunica├º├úo (Token Saving Mode)
- Sem pre├ómbulos ("Claro!", "Entendo") ou conclus├Áes gen├®ricas
- Prefira bullet points, tabelas e Markdown estruturado
- Assuma conhecimento de: pandas, polars, scraping, async, agroneg├│cio brasileiro
- Se pedido amb├¡guo: perguntas de esclarecimento antes de processar

# Qualidade de C├│digo (checklist interno, n├úo verbalizar)
- Type hints obrigat├│rios (usar `from __future__ import annotations`)
- Pydantic v2 BaseModel para todos os dados externos (Indicador, Safra, etc.)
- Valida├º├úo de inputs em fun├º├Áes p├║blicas da API
- Try/except espec├¡fico por categoria:
  - `httpx.HTTPError`, `httpx.TimeoutException` ÔåÆ rede
  - `ParseError` ÔåÆ layout mudou
  - `ContractViolationError` ÔåÆ DF n├úo bate com contrato
  - `Exception` gen├®rico apenas em `_try_sources` (loop de fallback resiliente)
- Constantes em `agrobr/constants.py` (URLs, TTLs, mapeamentos)
- Fun├º├Áes > 30 linhas ÔåÆ propor quebra
- Antes de criar helper ÔåÆ verificar se j├í existe em utils/
- Edge cases obrigat├│rios: resposta vazia, layout mudou, timeout, encoding
- Docstring Google style em fun├º├Áes p├║blicas
- structlog com JSON para todos os logs
- SEM COMENT├üRIOS NO C├ôDIGO - c├│digo deve ser autoexplicativo

# Imports
- Preferir `from modulo import submodulo` sobre `from modulo.submodulo import Classe`
- Imports expl├¡citos facilitam mocking e deixam origem clara
- N├úo adicionar imports n├úo utilizados (incluindo __future__ se n├úo usar annotations)
- Lazy imports dentro de fetchers (evitar circular imports entre datasets/ e fontes)

# Cache
- Smart TTL para CEPEA (expira ├ás 18h, n├úo TTL fixo)
- Fontes sem Cloudflare: Not├¡cias Agr├¡colas como fallback padr├úo
- Browser fallback desabilitado por padr├úo (_use_browser = False)

# Testes
- Type hints `-> None` n├úo s├úo necess├írios em m├®todos de teste
- Um teste por cen├írio, m├║ltiplas asser├º├Áes dentro s├úo ok
- setUp() para configura├º├úo compartilhada, n├úo para cada teste
- Nomes descritivos: test_<funcionalidade>_<cenario> ou test_<funcionalidade> se ├║nico

# Padr├Áes Espec├¡ficos agrobr
- HTTP: sempre `httpx.AsyncClient` com timeout, retry exponential, user-agent rotativo
- Cache: DuckDB local com separa├º├úo cache/hist├│rico, verificar antes de request
- Parsing: isolar em `parsers/` com versionamento, fingerprinting, fallback em cascata
- Normaliza├º├úo: usar `normalize/` para unidades, datas safra, UFs, encoding
- Encoding: fallback chain UTF-8 ÔåÆ ISO-8859-1 ÔåÆ Windows-1252 ÔåÆ chardet
- Testes: VCR cassettes + golden data tests para requests; mocks para cache
- Erros: hierarquia tipada (ver agrobr/exceptions.py)
- Valida├º├úo: estat├¡stica (ranges hist├│ricos) + estrutural (fingerprint)
- Datasets: callable fetchers no DatasetSource, auto-registro via registry
- Separa├º├úo fontes/datasets: cepea/, conab/, ibge/ s├úo aut├┤nomos com seus
  pr├│prios parsers/models/contratos internos. datasets/ apenas orquestra,
  normaliza e garante o contrato final. Nunca mover l├│gica de parsing pra datasets/
- Contratos: schema versionado, MetaInfo padronizado com proveni├¬ncia
- Valores monet├írios: float64 no DataFrame (n├úo Decimal ÔÇö evita dtype object)
- Determinismo: via contextvars (thread/async-safe), nunca global state

# Entregas
- Scripts completos e funcionais na primeira tentativa
- Corre├º├Áes: apenas bloco corrigido + contexto m├¡nimo
- Fontes: seguir estrutura existente (client ÔåÆ parser ÔåÆ models ÔåÆ API p├║blica)
- Datasets: seguir fluxo (fetcher ÔåÆ base ÔåÆ registry ÔåÆ API p├║blica)
- Mencionar depend├¬ncias quando adicionar nova lib


================================================================================
PARTE 1: VIS├âO ESTRAT├ëGICA
================================================================================

CONTEXTO
--------
O agrobr 0.5.0 ultrapassou a fase "wrapper ├║til" e entrou na fase mais
promissora: quando pessoas come├ºam a confiar nele sem ler o c├│digo.

INSIGHT CENTRAL
---------------
"Ningu├®m quer tr├¬s fontes; querem uma tabela boa."

Pesquisador, trader, jornalista ou gestor n├úo pensa "vou consultar o CEPEA",
pensa "preciso do pre├ºo di├írio da soja confi├ível, cont├¡nuo e audit├ível".

DECIS├âO ESTRAT├ëGICA
-------------------
Crescer para CIMA (abstra├º├úo sem├óntica), n├úo para os LADOS (mais fontes).

| Mais fontes              | Crescer pra cima              |
|--------------------------|-------------------------------|
| +Complexidade            | +Valor                        |
| +Manuten├º├úo              | +Diferencia├º├úo                |
| Usu├írio escolhe fonte    | Usu├írio pede o que quer       |
| Compete com scraping     | Compete com servi├ºo de dados  |
| F├ícil de copiar          | Dif├¡cil de copiar             |

Mais fontes qualquer um adiciona depois (inclusive contribuidores).
Camada sem├óntica bem feita ├® vantagem competitiva permanente.


================================================================================
PARTE 2: OBJETIVO v0.6.0
================================================================================

TEMA: "De biblioteca para infraestrutura"

ANTES (v0.5.0)
--------------
- Usu├írio precisa saber qual fonte usar
- Contratos existem mas n├úo s├úo vis├¡veis
- Reprodutibilidade existe mas n├úo ├® marketada
- Health check roda mas usu├írio n├úo v├¬

DEPOIS (v0.6.0)
---------------
- Usu├írio pede o que quer, agrobr resolve
- Contratos s├úo p├║blicos e documentados
- Reprodutibilidade ├® feature de marketing (context manager)
- Notebook demo Colab: funciona sem instalar nada
- Health badge mostra sa├║de no README


ANTES vs DEPOIS (C├ôDIGO)
------------------------

# Antes (v0.5.0)
from agrobr import cepea
df = await cepea.indicador("soja")  # Usu├írio precisa saber que ├® CEPEA

# Depois (v0.6.0)
from agrobr import datasets
df = await datasets.preco_diario("soja")  # Pede o que quer, fonte ├® detalhe


================================================================================
PARTE 3: ENTREGAS v0.6.0
================================================================================

| Entrega                          | Esfor├ºo   | Impacto   |
|----------------------------------|-----------|-----------|
| preco_diario MVP (end-to-end)    | 3 dias    | Cr├¡tico   |
| Notebook demo Colab              | 3 horas   | Alto      |
| Demais 3 datasets                | 4 dias    | Alto      |
| Contratos p├║blicos documentados  | 3 horas   | Alto      |
| Docs: reprodutibilidade          | 2 horas   | Alto      |
| Docs: integra├º├úo pipelines       | 2 horas   | M├®dio     |
| Health badge no README           | 30 min    | Baixo     |

Total estimado: ~3 semanas

NOTA: A status page HTML customizada foi removida do escopo.
O repo j├í possui Daily Health Check badge (workflow existente).
Uma p├ígina HTML est├ítica atualizada a cada 6h n├úo ├® "tempo real" e o
custo/benef├¡cio n├úo justifica. O badge existente resolve.


================================================================================
PARTE 4: CAMADA SEM├éNTICA (agrobr/datasets/)
================================================================================

CONCEITO
--------
Camada de abstra├º├úo acima das fontes individuais. Usu├írio pede um "dataset"
(ex: pre├ºo di├írio), agrobr resolve internamente qual fonte usar, com fallback
autom├ítico e metadados de proveni├¬ncia.

EXTENSIBILIDADE DE FONTES
--------------------------
Novas fontes (Not├¡cias Agr├¡colas, B3, USDA, CME, etc.) s├úo adicionadas
sem modificar o core ÔÇö basta criar um fetcher async e registrar como
DatasetSource com prioridade adequada:

    async def _fetch_b3(produto: str, **kwargs) -> tuple[pd.DataFrame, Any]:
        ...

    DatasetSource(name="b3", priority=3, fetch_fn=_fetch_b3)

Isso permite que contribuidores adicionem fontes sem entender a
arquitetura interna (base.py, registry, etc.).


ESTRUTURA DE DIRET├ôRIOS
-----------------------

agrobr/
Ôö£ÔöÇÔöÇ exceptions.py            # Hierarquia de exce├º├Áes
Ôö£ÔöÇÔöÇ datasets/
Ôöé   Ôö£ÔöÇÔöÇ __init__.py          # Exports p├║blicos
Ôöé   Ôö£ÔöÇÔöÇ base.py              # Classe base Dataset
Ôöé   Ôö£ÔöÇÔöÇ registry.py          # Registro de datasets
Ôöé   Ôö£ÔöÇÔöÇ deterministic.py     # Context manager determin├¡stico
Ôöé   Ôö£ÔöÇÔöÇ preco_diario.py      # Dataset de pre├ºos
Ôöé   Ôö£ÔöÇÔöÇ producao_anual.py    # Dataset de produ├º├úo
Ôöé   Ôö£ÔöÇÔöÇ estimativa_safra.py  # Dataset de estimativas
Ôöé   ÔööÔöÇÔöÇ balanco.py           # Dataset de balan├ºo O&D


HIERARQUIA DE EXCE├ç├òES
----------------------

# agrobr/exceptions.py

from __future__ import annotations


class AgroBRError(Exception):
    """Base para todas as exce├º├Áes do agrobr."""


class SourceUnavailableError(AgroBRError):
    """Todas as fontes falharam para um dataset/produto."""


class NetworkError(AgroBRError):
    """Erro de rede (timeout, HTTP error, DNS)."""


class ParseError(AgroBRError):
    """Layout da fonte mudou ou parsing falhou."""


class ContractViolationError(AgroBRError):
    """DataFrame n├úo atende o contrato do dataset (colunas, tipos, ranges)."""


class StaleDataWarning(UserWarning):
    """Dados podem estar desatualizados (ex: cache antigo sem refresh)."""


MODELO METAINFO PADRONIZADO
----------------------------

# agrobr/models.py (expandir MetaInfo existente)

from __future__ import annotations

from datetime import datetime
from typing import Any

from pydantic import BaseModel, Field


class MetaInfo(BaseModel):
    """Metadados de proveni├¬ncia de um dataset fetch."""

    dataset: str = ""
    source: str = ""
    source_url: str = ""
    source_method: str = ""
    fetched_at: datetime = Field(default_factory=lambda: datetime.now())
    records_count: int = 0
    columns: list[str] = Field(default_factory=list)
    cache_hit: bool = False
    parser_version: str = ""
    contract_version: str = ""
    snapshot: str | None = None
    warnings: list[str] = Field(default_factory=list)


DATASETS INICIAIS
-----------------

| Dataset           | Descri├º├úo                | Fontes (prioridade)               |
|-------------------|--------------------------|-----------------------------------|
| preco_diario      | Pre├ºo spot commodities   | CEPEA ÔåÆ Not├¡cias Agr├¡colas ÔåÆ cache |
| producao_anual    | Produ├º├úo por UF/munic├¡pio| IBGE PAM ÔåÆ CONAB                  |
| estimativa_safra  | Safra corrente           | CONAB ÔåÆ IBGE LSPA                 |
| balanco           | Oferta/demanda           | CONAB                             |


USO DA API
----------

from agrobr import datasets

# Pre├ºo di├írio - fonte ├® detalhe interno
df = await datasets.preco_diario("soja")
df = await datasets.preco_diario("soja", return_meta=True)

# Produ├º├úo anual consolidada
df = await datasets.producao_anual("milho", ano=2024)
df = await datasets.producao_anual("milho", ano=2024, nivel="municipio")

# Estimativa de safra corrente
df = await datasets.estimativa_safra("soja", safra="2024/25")

# Balan├ºo oferta e demanda
df = await datasets.balanco("soja", safra="2024/25")

# Listar datasets dispon├¡veis
datasets.list_datasets()

# Listar produtos de um dataset
datasets.list_products("preco_diario")  # ['algodao', 'boi', 'cafe', ...]

# Info sobre um dataset
datasets.info("preco_diario")


IMPLEMENTA├ç├âO: BASE
-------------------

# agrobr/datasets/base.py

from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any, TYPE_CHECKING

import httpx
import structlog

if TYPE_CHECKING:
    import pandas as pd
    from agrobr.models import MetaInfo

from agrobr.exceptions import (
    ContractViolationError,
    NetworkError,
    ParseError,
    SourceUnavailableError,
)

logger = structlog.get_logger()


@dataclass
class DatasetSource:
    name: str
    priority: int
    fetch_fn: Callable[..., Awaitable[tuple[pd.DataFrame, Any]]]
    enabled: bool = True
    description: str = ""


@dataclass
class DatasetInfo:
    name: str
    description: str
    sources: list[DatasetSource]
    products: list[str]
    contract_version: str
    update_frequency: str
    typical_latency: str
    
    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "description": self.description,
            "sources": [s.name for s in self.sources],
            "products": self.products,
            "contract_version": self.contract_version,
            "update_frequency": self.update_frequency,
            "typical_latency": self.typical_latency,
        }


class BaseDataset(ABC):
    info: DatasetInfo
    
    @abstractmethod
    async def fetch(
        self,
        produto: str,
        return_meta: bool = False,
        **kwargs: Any,
    ) -> pd.DataFrame | tuple[pd.DataFrame, MetaInfo]:
        pass

    def _validate_produto(self, produto: str) -> None:
        if produto not in self.info.products:
            raise ValueError(
                f"Produto '{produto}' n├úo suportado por {self.info.name}. "
                f"V├ílidos: {self.info.products}"
            )

    async def _try_sources(
        self,
        produto: str,
        **kwargs: Any,
    ) -> tuple[pd.DataFrame, str, Any]:
        self._validate_produto(produto)
        errors: list[tuple[str, str, str]] = []
        
        for source in sorted(self.info.sources, key=lambda s: s.priority):
            if not source.enabled:
                continue
            
            try:
                df, meta = await source.fetch_fn(produto, **kwargs)
                logger.info(
                    "source_success",
                    dataset=self.info.name,
                    source=source.name,
                    rows=len(df),
                )
                return df, source.name, meta

            except (httpx.HTTPError, httpx.TimeoutException, OSError) as e:
                logger.warning(
                    "source_network_error",
                    dataset=self.info.name,
                    source=source.name,
                    error_type="network",
                    error=str(e),
                )
                errors.append((source.name, "network", str(e)))

            except ParseError as e:
                logger.warning(
                    "source_parse_error",
                    dataset=self.info.name,
                    source=source.name,
                    error_type="parse",
                    error=str(e),
                )
                errors.append((source.name, "parse", str(e)))

            except ContractViolationError as e:
                logger.warning(
                    "source_contract_error",
                    dataset=self.info.name,
                    source=source.name,
                    error_type="contract",
                    error=str(e),
                )
                errors.append((source.name, "contract", str(e)))

            except Exception as e:
                logger.warning(
                    "source_unexpected_error",
                    dataset=self.info.name,
                    source=source.name,
                    error_type="unexpected",
                    error=str(e),
                )
                errors.append((source.name, "unexpected", str(e)))
        
        raise SourceUnavailableError(
            f"All sources failed for {self.info.name}/{produto}: {errors}"
        )


IMPLEMENTA├ç├âO: PRECO_DIARIO
---------------------------

# agrobr/datasets/preco_diario.py

from __future__ import annotations

from datetime import date, datetime, timezone
from typing import Any, TYPE_CHECKING

import structlog

from agrobr.datasets.base import BaseDataset, DatasetInfo, DatasetSource
from agrobr.models import MetaInfo

if TYPE_CHECKING:
    import pandas as pd

logger = structlog.get_logger()


async def _fetch_cepea(produto: str, **kwargs: Any) -> tuple[pd.DataFrame, Any]:
    from agrobr import cepea
    return await cepea.indicador(produto, return_meta=True, **kwargs)


async def _fetch_cache(produto: str, **kwargs: Any) -> tuple[pd.DataFrame, Any]:
    from agrobr.cache import duckdb_store
    store = duckdb_store.get_store()
    df = store.get_historico("cepea", produto)
    return df, None


PRECO_DIARIO_INFO = DatasetInfo(
    name="preco_diario",
    description="Pre├ºo di├írio spot de commodities agr├¡colas brasileiras",
    sources=[
        DatasetSource(
            name="cepea",
            priority=1,
            fetch_fn=_fetch_cepea,
            description="CEPEA/ESALQ via Not├¡cias Agr├¡colas",
        ),
        DatasetSource(
            name="cache",
            priority=99,
            fetch_fn=_fetch_cache,
            description="Cache local DuckDB",
        ),
    ],
    products=["soja", "milho", "boi", "cafe", "trigo", "algodao"],
    contract_version="1.0",
    update_frequency="daily",
    typical_latency="D+0",
)


class PrecoDiarioDataset(BaseDataset):
    info = PRECO_DIARIO_INFO

    async def fetch(
        self,
        produto: str,
        inicio: str | date | None = None,
        fim: str | date | None = None,
        return_meta: bool = False,
        **kwargs: Any,
    ) -> pd.DataFrame | tuple[pd.DataFrame, MetaInfo]:
        logger.info("dataset_fetch", dataset="preco_diario", produto=produto)

        df, source_name, source_meta = await self._try_sources(
            produto, inicio=inicio, fim=fim, **kwargs
        )

        df = self._normalize(df, produto)

        if return_meta:
            from agrobr.datasets.deterministic import get_snapshot
            meta = MetaInfo(
                dataset="preco_diario",
                source=f"datasets.preco_diario/{source_name}",
                source_url=source_meta.source_url if source_meta else "",
                source_method="dataset",
                fetched_at=source_meta.fetched_at if source_meta else datetime.now(timezone.utc),
                records_count=len(df),
                columns=df.columns.tolist(),
                cache_hit=source_name == "cache",
                parser_version=source_meta.parser_version if source_meta else "",
                contract_version=self.info.contract_version,
                snapshot=get_snapshot(),
            )
            return df, meta

        return df

    def _normalize(self, df: pd.DataFrame, produto: str) -> pd.DataFrame:
        required = ["data", "produto", "valor", "unidade"]

        for col in required:
            if col not in df.columns:
                if col == "produto":
                    df["produto"] = produto
                else:
                    raise ValueError(f"Missing required column: {col}")

        return df.sort_values("data", ascending=False)


_preco_diario = PrecoDiarioDataset()

# Auto-registro
from agrobr.datasets.registry import register
register(_preco_diario)


async def preco_diario(
    produto: str,
    inicio: str | date | None = None,
    fim: str | date | None = None,
    return_meta: bool = False,
    **kwargs: Any,
) -> pd.DataFrame | tuple[pd.DataFrame, MetaInfo]:
    """Busca pre├ºo di├írio de um produto agr├¡cola.

    Fontes (em ordem de prioridade):
      1. CEPEA/ESALQ via Not├¡cias Agr├¡colas
      2. Cache local

    Args:
        produto: soja, milho, boi, cafe, trigo, algodao
        inicio: Data inicial (opcional)
        fim: Data final (opcional)
        return_meta: Se True, retorna tupla (DataFrame, MetaInfo)

    Returns:
        DataFrame com colunas: data, produto, valor, unidade, variacao
    """
    return await _preco_diario.fetch(
        produto, inicio=inicio, fim=fim, return_meta=return_meta, **kwargs
    )


IMPLEMENTA├ç├âO: __init__.py
--------------------------

# agrobr/datasets/__init__.py

from agrobr.datasets.preco_diario import preco_diario
from agrobr.datasets.producao_anual import producao_anual
from agrobr.datasets.estimativa_safra import estimativa_safra
from agrobr.datasets.balanco import balanco
from agrobr.datasets.registry import list_datasets, list_products, info, get_dataset

__all__ = [
    "preco_diario",
    "producao_anual",
    "estimativa_safra",
    "balanco",
    "list_datasets",
    "list_products",
    "info",
    "get_dataset",
]


IMPLEMENTA├ç├âO: REGISTRY
------------------------

# agrobr/datasets/registry.py

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from agrobr.datasets.base import BaseDataset, DatasetInfo

_REGISTRY: dict[str, BaseDataset] = {}


def register(dataset: BaseDataset) -> BaseDataset:
    """Registra um dataset no registry global."""
    _REGISTRY[dataset.info.name] = dataset
    return dataset


def get_dataset(name: str) -> BaseDataset:
    """Retorna inst├óncia de um dataset pelo nome."""
    if name not in _REGISTRY:
        raise KeyError(
            f"Dataset '{name}' n├úo encontrado. "
            f"Dispon├¡veis: {list(_REGISTRY.keys())}"
        )
    return _REGISTRY[name]


def list_datasets() -> list[str]:
    """Lista nomes de todos os datasets registrados."""
    return sorted(_REGISTRY.keys())


def list_products(name: str) -> list[str]:
    """Lista produtos dispon├¡veis para um dataset."""
    return get_dataset(name).info.products


def info(name: str) -> dict:
    """Retorna metadados de um dataset."""
    return get_dataset(name).info.to_dict()


# Auto-registro ao importar os m├│dulos de dataset.
# Cada m├│dulo chama register() no n├¡vel de m├│dulo.
# Ex: em preco_diario.py ÔåÆ register(_preco_diario)


================================================================================
PARTE 5: CONTRATOS P├ÜBLICOS
================================================================================

OBJETIVO
--------
Publicar contratos de forma vis├¡vel e acess├¡vel. Usu├írio sabe exatamente
o que esperar e pode confiar que n├úo vai quebrar.


docs/contracts/index.md
-----------------------

# Contratos de Dados

O agrobr garante estabilidade de schema. Seu pipeline n├úo vai quebrar.

## Garantias Globais

| Garantia | Descri├º├úo |
|----------|-----------|
| **Nomes est├íveis** | Colunas nunca mudam de nome (s├│ adicionam) |
| **Tipos s├│ alargam** | intÔåÆfloat ok, floatÔåÆint nunca |
| **Datas ISO-8601** | Sempre YYYY-MM-DD |
| **Unidades expl├¡citas** | Coluna dedicada |
| **Breaking = Major** | Quebras s├│ em vers├úo major |

## Datasets

- [preco_diario](./preco_diario.md) - Pre├ºos di├írios
- [producao_anual](./producao_anual.md) - Produ├º├úo anual
- [estimativa_safra](./estimativa_safra.md) - Estimativas
- [balanco](./balanco.md) - Oferta/demanda


docs/contracts/preco_diario.md
------------------------------

# preco_diario v1.0

Pre├ºo di├írio spot de commodities agr├¡colas brasileiras.

## Fontes

| Prioridade | Fonte | Descri├º├úo |
|------------|-------|-----------|
| 1 | CEPEA/ESALQ | Via Not├¡cias Agr├¡colas |
| 2 | Cache local | DuckDB |

## Produtos

`soja`, `milho`, `boi`, `cafe`, `trigo`, `algodao`

## Schema

| Coluna | Tipo | Nullable | Unidade | Descri├º├úo |
|--------|------|----------|---------|-----------|
| `data` | date | ÔØî | - | Data do indicador |
| `produto` | str | ÔØî | - | Nome do produto |
| `praca` | str | Ô£à | - | Pra├ºa de refer├¬ncia |
| `valor` | float64 | ÔØî | BRL | Pre├ºo em reais (2 casas decimais) |
| `unidade` | str | ÔØî | - | Ex: "BRL/sc60kg" |
| `variacao` | float64 | Ô£à | % | Varia├º├úo vs dia anterior |
| `fonte` | str | ÔØî | - | Origem dos dados |

**Nota sobre precis├úo:** `valor` usa `float64` (n├úo `Decimal`) para
compatibilidade com pandas/polars e performance em pipelines. Precis├úo
IEEE 754 ├® suficiente para pre├ºos agr├¡colas (m├íx ~R$ 999.999,99).
Para uso cont├íbil que exija precis├úo exata, converter com
`df["valor"].apply(Decimal)` ap├│s o fetch.

## Garantias

- `data` ├® sempre dia ├║til
- `valor` ├® sempre positivo
- Ordenado por `data` decrescente

## Exemplo

```python
from agrobr import datasets

df = await datasets.preco_diario("soja")
df, meta = await datasets.preco_diario("soja", return_meta=True)
```


================================================================================
PARTE 6: STATUS ÔÇö HEALTH BADGE (SIMPLIFICADO)
================================================================================

DECIS├âO
-------
O repo j├í possui workflow "Daily Health Check" com badge no README.
Uma p├ígina HTML est├ítica atualizada a cada 6h n├úo ├® "tempo real" e tem
custo/benef├¡cio baixo comparado ├ás outras entregas.

ABORDAGEM: Reaproveitar o health_check existente + badge din├ómico.

Caso queira expandir no futuro, o health_check.yml j├í coleta dados
suficientes para gerar um status.json. Por ora, o badge resolve.


README.md ÔÇö Adicionar se├º├úo de status
--------------------------------------

## Status das Fontes

| Fonte | Status |
|-------|--------|
| CEPEA | [![Health](https://github.com/bruno-portfolio/agrobr/actions/workflows/health_check.yml/badge.svg)](https://github.com/bruno-portfolio/agrobr/actions/workflows/health_check.yml) |
| Testes | [![Tests](https://github.com/bruno-portfolio/agrobr/actions/workflows/tests.yml/badge.svg)](https://github.com/bruno-portfolio/agrobr/actions/workflows/tests.yml) |

O agrobr monitora automaticamente a disponibilidade das fontes.
Use `agrobr health --all` para verificar localmente.


================================================================================
PARTE 7: DOCUMENTA├ç├âO - REPRODUTIBILIDADE
================================================================================

# docs/advanced/reproducibility.md

# Reprodutibilidade

O agrobr permite an├ílises 100% reproduz├¡veis.

## Modo Determin├¡stico

```python
import agrobr

# Context manager ÔÇö seguro para uso concorrente (async e threads)
async with agrobr.deterministic(snapshot="2025-12-31"):
    # Todas as chamadas filtram data <= 2025-12-31
    # NUNCA acessa internet, usa apenas cache local
    # SEMPRE retorna o mesmo resultado
    df = await datasets.preco_diario("soja")

# Fora do bloco, comportamento normal ├® restaurado automaticamente
```

Tamb├®m dispon├¡vel como decorator para pipelines:

```python
@agrobr.deterministic(snapshot="2025-12-31")
async def meu_pipeline():
    df = await datasets.preco_diario("soja")
    return df
```

## Sem├óntica do Snapshot

| Aspecto | Defini├º├úo |
|---------|-----------|
| **Formato** | `"YYYY-MM-DD"` ÔÇö data m├íxima de corte |
| **Filtro** | Todos os DataFrames filtrados por `data <= snapshot` |
| **Rede** | Bloqueada ÔÇö apenas cache DuckDB local |
| **Escopo** | Isolado por contexto async (contextvars) ÔÇö n├úo afeta outras tasks |
| **MetaInfo** | Campo `snapshot` preenchido automaticamente |

## Casos de Uso

- **Papers acad├¬micos** ÔÇö Resultados replic├íveis
- **Backtests** ÔÇö Simular com dados hist├│ricos exatos
- **Auditoria** ÔÇö Provar quais dados foram usados
- **Relat├│rios** ÔÇö Data de refer├¬ncia fixa


IMPLEMENTA├ç├âO: DETERMINISTIC CONTEXT MANAGER
----------------------------------------------

# agrobr/datasets/deterministic.py

from __future__ import annotations

import contextvars
from contextlib import asynccontextmanager
from datetime import date
from functools import wraps
from typing import Any

_snapshot_var: contextvars.ContextVar[str | None] = contextvars.ContextVar(
    "agrobr_snapshot", default=None
)


def get_snapshot() -> str | None:
    """Retorna snapshot ativo no contexto atual (ou None)."""
    return _snapshot_var.get()


def is_deterministic() -> bool:
    """Retorna True se estamos em modo determin├¡stico."""
    return _snapshot_var.get() is not None


@asynccontextmanager
async def deterministic(snapshot: str):
    """Context manager para modo determin├¡stico.

    Args:
        snapshot: Data de corte no formato "YYYY-MM-DD".
            Todas as consultas filtram data <= snapshot.
            Rede ├® bloqueada, apenas cache local ├® usado.
    """
    date.fromisoformat(snapshot)
    token = _snapshot_var.set(snapshot)
    try:
        yield
    finally:
        _snapshot_var.reset(token)


================================================================================
PARTE 7.5: NOTEBOOK DEMO (COLAB) + GIF NO README
================================================================================

OBJETIVO
--------
Quem chega no repo quer ver output antes de instalar.
Um notebook Colab elimina toda fric├º├úo e um GIF no README mostra
que o projeto funciona de verdade.

PRIORIDADE: Alta ÔÇö move mais o ponteiro de ado├º├úo do que status page.


examples/demo_colab.ipynb
-------------------------

Notebook com c├®lulas execut├íveis:

1. Instala├º├úo (1 c├®lula)
   !pip install agrobr -q

2. CEPEA ÔÇö Pre├ºo di├írio (2 c├®lulas)
   from agrobr.sync import cepea
   df = cepea.indicador("soja", periodo="2024")
   df.head(10)
   # + gr├ífico com matplotlib

3. CONAB ÔÇö Safras (2 c├®lulas)
   from agrobr.sync import conab
   df = conab.safras("soja", safra="2024/25")
   df[["uf", "area_plantada", "producao", "produtividade"]]

4. IBGE ÔÇö PAM (2 c├®lulas)
   from agrobr.sync import ibge
   df = ibge.pam("soja", ano=2023, nivel="uf")
   # + mapa ou bar chart por UF

5. Camada sem├óntica (v0.6.0) (2 c├®lulas)
   from agrobr.sync import datasets
   df = datasets.preco_diario("soja")
   df, meta = datasets.preco_diario("soja", return_meta=True)
   print(meta)

TOTAL: ~10 c├®lulas, execu├º├úo em <30s


GIF para README
---------------

Grava├º├úo com asciinema ou terminalizer:

$ pip install agrobr
$ python -c "
from agrobr.sync import cepea
df = cepea.indicador('soja', periodo='2024')
print(df.head())
print(f'Total: {len(df)} registros')
"

Converter para GIF e adicionar no README:

## Demo

![agrobr demo](docs/assets/demo.gif)

> [Abrir no Google Colab](https://colab.research.google.com/github/bruno-portfolio/agrobr/blob/main/examples/demo_colab.ipynb)


README.md ÔÇö Adicionar bot├úo Colab
----------------------------------

Ap├│s os badges existentes, adicionar:

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/bruno-portfolio/agrobr/blob/main/examples/demo_colab.ipynb)


================================================================================
PARTE 8: DOCUMENTA├ç├âO - INTEGRA├ç├âO PIPELINES
================================================================================

# docs/advanced/pipelines.md

# Integra├º├úo com Pipelines

O agrobr oferece API s├¡ncrona via `agrobr.sync` para uso em orquestradores
que j├í possuem event loop pr├│prio (onde `asyncio.run()` quebraria).

## Airflow

```python
from airflow.decorators import task

@task
def extract_precos():
    from agrobr.sync import datasets
    df = datasets.preco_diario("soja")
    df.to_parquet("/data/soja_precos.parquet")

@task
def extract_safra():
    from agrobr.sync import datasets
    df = datasets.estimativa_safra("soja", safra="2024/25")
    df.to_parquet("/data/soja_safra.parquet")
```

## Prefect

```python
from prefect import task, flow

@task
def fetch_precos(produto: str):
    from agrobr.sync import datasets
    return datasets.preco_diario(produto)

@flow
def pipeline_agro():
    for produto in ["soja", "milho", "cafe"]:
        fetch_precos(produto)
```

## Dagster

```python
from dagster import asset

@asset
def soja_precos():
    from agrobr.sync import datasets
    return datasets.preco_diario("soja")

@asset
def soja_producao():
    from agrobr.sync import datasets
    return datasets.producao_anual("soja", ano=2024)
```

## Uso direto (async)

Se voc├¬ controla o event loop, pode usar a API async diretamente:

```python
import asyncio
from agrobr import datasets

async def main():
    df = await datasets.preco_diario("soja")
    df.to_parquet("/data/soja.parquet")

asyncio.run(main())
```


================================================================================
PARTE 9: CHECKLIST DE IMPLEMENTA├ç├âO
================================================================================

SEMANA 1: MVP ÔÇö PRECO_DIARIO END-TO-END
----------------------------------------

[x] agrobr/exceptions.py
[x] agrobr/models.py (expandir MetaInfo)
[x] agrobr/datasets/__init__.py
[x] agrobr/datasets/base.py
[x] agrobr/datasets/registry.py
[x] agrobr/datasets/deterministic.py
[x] agrobr/datasets/preco_diario.py
[x] tests/test_datasets/test_preco_diario.py
[x] tests/test_datasets/test_registry.py
[x] tests/test_datasets/test_deterministic.py
[x] tests/test_exceptions.py
[x] docs/contracts/preco_diario.md
[x] examples/demo_colab.ipynb
[ ] Tag v0.5.1-alpha (publicar no TestPyPI para validar)


SEMANA 2: DATASETS RESTANTES + CONTRATOS
-----------------------------------------

[x] agrobr/datasets/producao_anual.py
[x] agrobr/datasets/estimativa_safra.py
[x] agrobr/datasets/balanco.py
[x] tests/test_datasets/test_producao_anual.py
[x] tests/test_datasets/test_estimativa_safra.py
[x] tests/test_datasets/test_balanco.py
[x] docs/contracts/index.md
[x] docs/contracts/producao_anual.md
[x] docs/contracts/estimativa_safra.md
[x] docs/contracts/balanco.md


SEMANA 3: DOCS + DEMO + RELEASE
---------------------------------

[x] docs/advanced/reproducibility.md
[x] docs/advanced/pipelines.md
[x] mkdocs.yml atualizado
[ ] GIF demo (asciinema/terminalizer) - OPCIONAL
[x] README.md atualizado (badge Colab, se├º├úo status, se├º├úo datasets)
[x] CHANGELOG.md
[x] pyproject.toml version = "0.6.0"
[ ] Release GitHub
[ ] Publicar PyPI


================================================================================
PARTE 10: RELEASE NOTES
================================================================================

## v0.6.0 - Semantic Layer & Trust

O agrobr agora oferece datasets padronizados, n├úo apenas acesso a fontes.

### Ô£¿ Novidades

- ­ƒÄ» **Camada Sem├óntica** - 4 datasets com fallback autom├ítico entre fontes
- ­ƒô£ **Contratos P├║blicos** - Garantias formais de schema versionado
- ­ƒöÆ **Modo Determin├¡stico** - Context manager para reprodutibilidade total
- ­ƒöº **Integra├º├úo Pipelines** - Guias Airflow, Prefect, Dagster (via sync API)
- ­ƒôô **Notebook Demo** - Google Colab com exemplos execut├íveis

### ­ƒÅù´©Å Melhorias Internas

- Dataset sources agora usam callable direto (extens├¡vel sem if/elif)
- Registry de datasets com auto-descoberta
- Valida├º├úo de produto antes de tentar fontes
- Logging estruturado em fallback de fontes com categoriza├º├úo de erro
- Hierarquia de exce├º├Áes tipadas (NetworkError, ParseError, ContractViolationError)
- MetaInfo expandido com proveni├¬ncia completa (cache_hit, parser_version, snapshot)
- Modo determin├¡stico via contextvars (thread/async-safe)
- Valores monet├írios padronizados como float64

### Upgrade

```bash
pip install --upgrade agrobr
```


================================================================================
PARTE 11: TIMELINE
================================================================================

| Semana | Entregas                                            | Status     |
|--------|-----------------------------------------------------|------------|
| 1      | MVP preco_diario + contrato + notebook Colab        | [x] 13/14  |
| 2      | 3 datasets restantes + contratos                    | [x] 10/10  |
| 3      | Docs (reprodutibilidade, pipelines) + GIF + release | [x] 6/9    |


================================================================================
PARTE 12: BACKLOG P├ôS v0.6.0
================================================================================

Itens v├ílidos mas que N├âO devem atrasar a v0.6.0.

| Item | Vers├úo alvo | Notas |
|------|-------------|-------|
| Aliases de produto (soyÔåÆsoja, caf├®ÔåÆcafe) | v0.6.1 | Normalizar em `_validate_produto`, logar alias usado. Contrato mant├®m nome can├┤nico |
| Mais fontes via comunidade (B3, USDA, CME) | v0.7+ | Padr├úo callable j├í suporta. Documentar CONTRIBUTING com exemplo de DatasetSource |
| Status page HTML rica | v0.7+ | Reavaliar se health badge n├úo ├® suficiente |


================================================================================
                              FIM DO DOCUMENTO
================================================================================
