Skill: olegantonov/camara-deputados (version 1.0.2)

--- name: camara-deputados description: Monitor and research the Brazilian Chamber of Deputies (Câmara dos Deputados) legislative activity. Use when: (1) searching for bills/proposições by author, keyword, type or year, (2) checking today's or upcoming agenda/events, (3) looking up deputies (deputados) by name, party or state, (4) tracking voting results (votações) in plenary or committees, (5) checking committee (comissão) schedules and membership, (6) monitoring specific bills' tramitação/status, (7) retrieving deputy expenses (CEAP/cota parlamentar), (8) searching for frentes parlamentares, grupos de trabalho, (9) any question about the Câmara dos Deputados, deputies, or federal bills. Base URL: https://dadosabertos.camara.leg.br/api/v2 — no auth required, returns JSON. version: "1.0.1" author: "Daniel Marques" license: "MIT" --- # Câmara dos Deputados — API de Dados Abertos Base URL: `https://dadosabertos.camara.leg.br/api/v2` Docs/Swagger: `https://dadosabertos.camara.leg.br/swagger/api.html` No authentication required. All endpoints return JSON. ## Common query params - `itens` — page size (default 15, max 100) - `pagina` — page number - `ordem` — `ASC` or `DESC` - `ordenarPor` — field name for sorting --- ## Key Endpoints ### Deputies (Deputados) ``` GET /deputados ?nome= # name search ?siglaPartido= # party (e.g. PT, PL, MDB) ?siglaUf= # state (e.g. SP, RJ, AM) ?idLegislatura=57 # current legislature ?ordem=ASC&ordenarPor=nome GET /deputados/{id} # full profile GET /deputados/{id}/discursos # speeches ?dataInicio=YYYY-MM-DD&dataFim=YYYY-MM-DD GET /deputados/{id}/despesas # CEAP expenses ?ano=YYYY&mes=MM GET /deputados/{id}/frentes # parliamentary fronts GET /deputados/{id}/ocupacoes # professional history GET /deputados/{id}/orgaos # committee memberships ``` ### Bills / Proposições ``` GET /proposicoes ?siglaTipo=PL # type: PL, PEC, MPV, PDC, PLP, etc. ?numero=123 ?ano=2026 ?autor=Nome # author name (partial match) ?tema= # topic id (see /referencias/temas) ?keywords= # keywords ?dataApresentacaoInicio=YYYY-MM-DD ?dataApresentacaoFim=YYYY-MM-DD ?codSituacao= # status code ?tramitacaoSenado=true # bills currently in Senate ?ordem=DESC&ordenarPor=id GET /proposicoes/{id} # full detail GET /proposicoes/{id}/autores # authorship GET /proposicoes/{id}/relacionadas# related bills GET /proposicoes/{id}/temas # topics GET /proposicoes/{id}/tramitacoes # full history/status GET /proposicoes/{id}/votacoes # votes on this bill ``` ### Votações (Voting) ``` GET /votacoes ?dataInicio=YYYY-MM-DD&dataFim=YYYY-MM-DD ?idOrgao=180 # 180 = Plenário ?siglaPartido= ?ordem=DESC&ordenarPor=dataHoraRegistro GET /votacoes/{id} # vote detail GET /votacoes/{id}/votos # individual votes per deputy GET /votacoes/{id}/orientacoes # party orientations ``` ### Events / Agenda (Eventos) ``` GET /eventos ?dataInicio=YYYY-MM-DD ?dataFim=YYYY-MM-DD ?siglaOrgao= # committee sigla or PLEN for plenary ?codTipoEvento= # see /referencias/tiposEvento ?codSituacao= # see /referencias/situacoesEvento ?ordem=ASC&ordenarPor=dataHoraInicio GET /eventos/{id} # event detail GET /eventos/{id}/deputados # attending deputies GET /eventos/{id}/orgaos # organizing bodies GET /eventos/{id}/pauta # agenda items GET /eventos/{id}/votacoes # votes in this session ``` ### Committees (Órgãos / Comissões) ``` GET /orgaos ?sigla=CCJC # committee abbreviation ?codTipoOrgao= # type (see /referencias/tiposOrgao) ?nome= GET /orgaos/{id} # detail GET /orgaos/{id}/eventos # committee agenda GET /orgaos/{id}/membros # current membership GET /orgaos/{id}/votacoes # votes by committee ``` ### Parties & Blocs (Partidos / Blocos) ``` GET /partidos?ordem=ASC&ordenarPor=sigla GET /partidos/{id} GET /partidos/{id}/membros GET /blocos GET /blocos/{id} ``` ### Parliamentary Fronts (Frentes) ``` GET /frentes GET /frentes/{id} GET /frentes/{id}/membros ``` ### Working Groups (Grupos de Trabalho) ``` GET /gruposTrabalho GET /gruposTrabalho/{id} GET /gruposTrabalho/{id}/membros ``` ### Legislative Sessions (Legislaturas) ``` GET /legislaturas GET /legislaturas/{id} GET /legislaturas/{id}/mesa # presiding board ``` ### Reference Data ``` GET /referencias/deputados/codSituacao GET /referencias/proposicoes/codSituacaoProposicao GET /referencias/proposicoes/siglaTipo GET /referencias/proposicoes/tema GET /referencias/tiposEvento GET /referencias/tiposOrgao GET /referencias/uf ``` --- ## Common Tasks ### Today's agenda ```bash DATE=$(date +%Y-%m-%d) curl "https://dadosabertos.camara.leg.br/api/v2/eventos?dataInicio=$DATE&dataFim=$DATE&ordem=ASC&ordenarPor=dataHoraInicio" ``` ### Search bills by keyword ```bash curl "https://dadosabertos.camara.leg.br/api/v2/proposicoes?keywords=transporte+público&ano=2026&ordem=DESC&ordenarPor=id&itens=20" ``` ### Find a deputy ```bash curl "https://dadosabertos.camara.leg.br/api/v2/deputados?nome=Marcos+Pontes&idLegislatura=57" ``` ### Recent plenary votes ```bash curl "https://dadosabertos.camara.leg.br/api/v2/votacoes?idOrgao=180&dataInicio=2026-03-01&ordem=DESC&ordenarPor=dataHoraRegistro&itens=10" ``` ### Bill status / tramitação ```bash curl "https://dadosabertos.camara.leg.br/api/v2/proposicoes/{id}/tramitacoes" ``` --- ## Notes - Legislature 57 = current (started Feb 2023) - Plenário orgão id = 180 - CEAP data also available as bulk download: `https://www.camara.leg.br/cotas/Ano-{ano}.json.zip` - Full API reference: see `references/api-endpoints.md` for parameter details - Swagger UI: `https://dadosabertos.camara.leg.br/swagger/api.html` --- ## API Quirks & Tips - **Standard response wrapper**: All endpoints return `{dados: [...], links: [...]}`. The Python client's `_get()` automatically extracts `dados`. - **Pagination**: Use `itens` (page size, max 100) and `pagina` (page number). The Python client's `_get_list()` auto-paginates — if `dados` has fewer items than `itens`, it's the last page. - **Date format**: Always ISO format `YYYY-MM-DD` (unlike Senado which uses `YYYYMMDD`). - **Legislature 57**: Current legislature (2023-2027). Most endpoints default to current if not specified. - **Plenário ID**: orgão id = 180 for plenary hall. - **tramitacaoSenado**: Use `?tramitacaoSenado=true` on proposições endpoint to find bills currently being reviewed by the Senate. - **CEAP bulk data**: Deputy expenses also available as bulk download at `https://www.camara.leg.br/cotas/Ano-{ano}.json.zip`. --- ## Python Client (async) You can use the async Python client for programmatic access: ```python import asyncio from camara_client import get_camara_client async def main(): client = get_camara_client() # Listar deputados deps = await client.lista_deputados() # Buscar por nome resultado = await client.buscar_deputado_por_nome("Lula") # Pesquisar proposições props = await client.pesquisar_proposicoes(keywords="transporte", ano=2026) # Eventos de hoje from datetime import date eventos = await client.get_eventos_dia(date.today()) await client.close() asyncio.run(main()) ``` ### Available Methods **Deputados:** - `lista_deputados(legislatura)` — List deputies - `buscar_deputado_por_nome(nome, legislatura)` — Search by name - `get_deputado_detalhe(id)` — Full profile - `get_despesas_deputado(id, ano)` — CEAP expenses - `get_frentes_deputado(id)` — Parliamentary fronts - `get_discursos_deputado(id, data_inicio, data_fim)` — Speeches - `get_presenca_deputado(id, data_inicio, data_fim)` — Attendance - `get_deputado_orgaos(id)` — Committee memberships - `get_deputado_ocupacoes(id)` — Professional history **Proposições:** - `pesquisar_proposicoes(keywords, sigla_tipo, numero, ano, autor, tramitando, tema, tramitacao_senado)` — Search bills - `get_proposicao_detalhe(id)` — Full detail - `get_proposicao_tramitacao(id)` — Status history - `get_proposicao_votacoes(id)` — Votes on bill - `get_proposicao_autores(id)` — Authors - `get_proposicao_temas(id)` — Topics/tags - `get_proposicao_relacionadas(id)` — Related bills **Eventos:** - `get_eventos_dia(data)` — Day agenda - `get_eventos_periodo(data_inicio, data_fim)` — Events in period - `get_evento_detalhe(id)` — Event detail - `get_evento_deputados(id)` — Deputies in event - `get_evento_orgaos(id)` — Organizing bodies - `get_evento_votacoes(id)` — Votes in event **Votações:** - `get_votacoes_periodo(data_inicio, data_fim, id_orgao)` — Votes in period - `get_votacao_detalhe(id)` — Vote detail - `get_votos_votacao(id)` — Individual votes - `get_orientacoes_votacao(id)` — Party orientations **Órgãos/Comissões:** - `lista_orgaos()` — List committees - `get_orgao_detalhe(id)` — Committee detail - `get_eventos_orgao_periodo(id, data_inicio, data_fim)` — Committee agenda - `get_membros_orgao(id)` — Current members - `get_orgao_membros_periodo(id, data_inicio, data_fim)` — Members in period - `get_votacoes_orgao(id, data_inicio, data_fim)` — Committee votes **Legislaturas:** - `lista_legislaturas()` — List legislatures - `get_legislatura_detalhe(id)` — Legislature detail - `get_legislatura_mesa(id)` — Presiding board **Partidos:** - `lista_partidos()` — List parties - `get_partido_detalhe(id)` — Party detail - `get_partido_membros(id)` — Party members **Blocos:** - `lista_blocos()` — List parliamentary blocs - `get_bloco_detalhe(id)` — Bloc detail **Frentes:** - `lista_frentes(id_legislatura)` — List parliamentary fronts - `get_frente_detalhe(id)` — Front detail - `get_frente_membros(id)` — Front members **Referências:** - `get_referencias_situacao_deputado()` — Deputy status codes - `get_referencias_situacao_proposicao()` — Bill status codes - `get_referencias_tipo_proposicao()` — Bill type codes (PL, PEC, etc) - `get_referencias_temas()` — Topic/theme list - `get_referencias_tipos_evento()` — Event types - `get_referencias_tipos_orgao()` — Committee types - `get_referencias_uf()` — Brazilian states **Utilitários:** - `get_proposicoes_recentes(dias)` — Recent bills - `get_votacoes_semana()` — This week's votes - `get_eventos_semana()` — This week's events - `get_pauta_evento(id)` — Event agenda items Requires: `pip install httpx`

path: README.md
content: # Câmara dos Deputados - Cliente Python

[![Python](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

Cliente Python assíncrono para a [API de Dados Abertos da Câmara dos Deputados](https://dadosabertos.camara.leg.br).

## 📋 Características

- ✅ Cliente assíncrono com `httpx`
- ✅ Tratamento robusto de erros com retry automático
- ✅ Logging estruturado
- ✅ Type hints completos
- ✅ Paginação automática
- ✅ Sem necessidade de autenticação
- ✅ Suporte a todas as principais funcionalidades da API v2

## 🚀 Instalação

```bash
# Clone o repositório
git clone https://github.com/olegantonov/camara-deputados.git
cd camara-deputados

# Instale as dependências
pip install -r requirements.txt

# Para desenvolvimento
pip install -r requirements-dev.txt
```

## 📖 Uso Básico

### Buscar Deputados

```python
import asyncio
from camara_client import get_camara_client

async def main():
    client = get_camara_client()
    try:
        # Listar todos os deputados (legislatura 57)
        deputados = await client.lista_deputados()
        
        # Buscar por nome
        resultado = await client.buscar_deputado_por_nome("Lula")
        
        for dep in resultado:
            print(f"{dep['nome']} ({dep['siglaPartido']}-{dep['siglaUf']})")
            
        # Detalhes de um deputado
        detalhe = await client.get_deputado_detalhe(dep['id'])
        print(f"Email: {detalhe['ultimoStatus']['email']}")
    finally:
        await client.close()

asyncio.run(main())
```

### Pesquisar Proposições

```python
# Buscar PLs de 2026 com palavras-chave
proposicoes = await client.pesquisar_proposicoes(
    keywords="transporte público",
    sigla_tipo="PL",
    ano=2026,
    tramitando=True
)

for prop in proposicoes:
    print(f"{prop['siglaTipo']} {prop['numero']}/{prop['ano']}")
    print(f"Ementa: {prop['ementa']}")
```

### Eventos e Agenda

```python
from datetime import date, timedelta

# Eventos de hoje
eventos = await client.get_eventos_dia()

# Próximos 7 dias
eventos_semana = await client.get_eventos_periodo(
    date.today(),
    date.today() + timedelta(days=7)
)

for evento in eventos_semana:
    print(f"{evento['dataHoraInicio']}: {evento['descricaoTipo']}")
    print(f"Local: {evento['localExterno']['nome']}")
```

### Votações

```python
from datetime import date, timedelta

# Votações da última semana
votacoes = await client.get_votacoes_periodo(
    date.today() - timedelta(days=7),
    date.today(),
    id_orgao=180  # 180 = Plenário
)

for votacao in votacoes:
    print(f"{votacao['data']}: {votacao['descricao']}")
    
# Votos individuais
votos = await client.get_votos_votacao(votacao['id'])
for voto in votos:
    print(f"{voto['deputado_']['nome']}: {voto['tipoVoto']}")
```

### Despesas Parlamentares (CEAP)

```python
# Despesas de um deputado em 2026
despesas = await client.get_despesas_deputado(
    id_deputado=204554,
    ano=2026
)

total = sum(float(d['valorDocumento']) for d in despesas)
print(f"Total gasto: R$ {total:,.2f}")

# Agrupar por tipo
from collections import Counter
tipos = Counter(d['tipoDespesa'] for d in despesas)
for tipo, count in tipos.most_common(5):
    print(f"{tipo}: {count} despesas")
```

### Tramitação de Proposição

```python
# Histórico completo de tramitação
tramitacoes = await client.get_proposicao_tramitacao(id_proposicao=2366661)

for tram in tramitacoes:
    print(f"{tram['dataHora']}: {tram['descricaoTramitacao']}")
    print(f"Situação: {tram['descricaoSituacao']}")
```

## 🔧 Funcionalidades Principais

### Deputados
- `lista_deputados(legislatura)` - Lista de deputados
- `buscar_deputado_por_nome(nome)` - Busca por nome
- `get_deputado_detalhe(id)` - Perfil completo
- `get_despesas_deputado(id, ano)` - CEAP/despesas
- `get_discursos_deputado(id)` - Discursos
- `get_frentes_deputado(id)` - Frentes parlamentares
- `get_deputado_orgaos(id)` - Comissões do deputado
- `get_deputado_ocupacoes(id)` - Histórico profissional

### Proposições
- `pesquisar_proposicoes(keywords, tipo, ano, tema, tramitacao_senado)` - Busca de bills
- `get_proposicao_detalhe(id)` - Detalhes completos
- `get_proposicao_tramitacao(id)` - Histórico de tramitação
- `get_proposicao_votacoes(id)` - Votações da proposição
- `get_proposicao_autores(id)` - Autores
- `get_proposicao_temas(id)` - Temas/tags
- `get_proposicao_relacionadas(id)` - Proposições relacionadas

### Eventos
- `get_eventos_dia(data)` - Agenda do dia
- `get_eventos_periodo(inicio, fim)` - Eventos em período
- `get_evento_detalhe(id)` - Detalhes do evento
- `get_evento_deputados(id)` - Deputados no evento
- `get_evento_orgaos(id)` - Órgãos organizadores
- `get_evento_votacoes(id)` - Votações do evento

### Votações
- `get_votacoes_periodo(inicio, fim, orgao)` - Votações em período
- `get_votacao_detalhe(id)` - Detalhes da votação
- `get_votos_votacao(id)` - Votos individuais por deputado
- `get_orientacoes_votacao(id)` - Orientações de bancada

### Órgãos/Comissões
- `lista_orgaos()` - Lista de comissões e órgãos
- `get_orgao_detalhe(id)` - Detalhes do órgão
- `get_eventos_orgao_periodo(id, inicio, fim)` - Agenda da comissão
- `get_membros_orgao(id)` - Membros atuais
- `get_votacoes_orgao(id, inicio, fim)` - Votações da comissão

### Legislaturas
- `lista_legislaturas()` - Lista de legislaturas
- `get_legislatura_detalhe(id)` - Detalhes da legislatura
- `get_legislatura_mesa(id)` - Mesa diretora

### Partidos
- `lista_partidos()` - Lista de partidos
- `get_partido_detalhe(id)` - Detalhes do partido
- `get_partido_membros(id)` - Deputados do partido

### Blocos Parlamentares
- `lista_blocos()` - Lista de blocos
- `get_bloco_detalhe(id)` - Detalhes do bloco

### Frentes Parlamentares
- `lista_frentes(legislatura)` - Lista de frentes
- `get_frente_detalhe(id)` - Detalhes da frente
- `get_frente_membros(id)` - Membros da frente

### Referências
- `get_referencias_situacao_deputado()` - Códigos de situação
- `get_referencias_situacao_proposicao()` - Status de proposições
- `get_referencias_tipo_proposicao()` - Tipos (PL, PEC, etc)
- `get_referencias_temas()` - Temas/assuntos
- `get_referencias_tipos_evento()` - Tipos de evento
- `get_referencias_tipos_orgao()` - Tipos de órgão
- `get_referencias_uf()` - Unidades federativas

## ⚠️ Tratamento de Erros

O cliente possui exceções específicas:

```python
from camara_client import (
    CamaraAPIError,         # Erro genérico
    CamaraTimeoutError,     # Timeout
    CamaraNotFoundError,    # 404
    CamaraConnectionError,  # Erro de conexão
    CamaraValidationError   # Validação de parâmetros
)

try:
    deputados = await client.lista_deputados()
except CamaraTimeoutError:
    print("A API está demorando para responder")
except CamaraNotFoundError:
    print("Recurso não encontrado")
except CamaraAPIError as e:
    print(f"Erro na API: {e}")
```

## 📝 Logging

Para habilitar logs:

```python
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
```

## 🔗 Recursos

- [Documentação da API](https://dadosabertos.camara.leg.br)
- [Swagger UI](https://dadosabertos.camara.leg.br/swagger/api.html)
- [SKILL.md](SKILL.md) - Referência completa de endpoints
- [Portal de Dados Abertos](https://www2.camara.leg.br/transparencia/dados-abertos)

## 🧪 Testes

```bash
# Executar testes
pytest

# Com coverage
pytest --cov=camara_client --cov-report=html

# Apenas testes unitários
pytest tests/unit/

# Apenas testes de integração
pytest tests/integration/
```

## 💡 Exemplos Avançados

### Análise de Gastos por Partido

```python
from collections import defaultdict

deputados = await client.lista_deputados()
gastos_partido = defaultdict(float)

for dep in deputados[:10]:  # Top 10 para exemplo
    despesas = await client.get_despesas_deputado(dep['id'], ano=2026)
    total = sum(float(d['valorDocumento']) for d in despesas)
    gastos_partido[dep['siglaPartido']] += total

for partido, total in sorted(gastos_partido.items(), key=lambda x: x[1], reverse=True):
    print(f"{partido}: R$ {total:,.2f}")
```

### Monitor de Votações em Tempo Real

```python
import asyncio
from datetime import date

async def monitor_votacoes():
    client = get_camara_client()
    try:
        while True:
            votacoes = await client.get_votacoes_periodo(
                date.today(),
                date.today(),
                id_orgao=180  # Plenário
            )
            
            print(f"Votações hoje: {len(votacoes)}")
            for v in votacoes[-5:]:  # Últimas 5
                print(f"  - {v['dataHoraRegistro']}: {v['descricao']}")
            
            await asyncio.sleep(300)  # Check a cada 5 minutos
    finally:
        await client.close()

asyncio.run(monitor_votacoes())
```

### Blocos e Frentes Parlamentares

```python
# Listar blocos parlamentares
blocos = await client.lista_blocos()
for bloco in blocos:
    print(f"{bloco['id']}: {bloco['nome']}")

# Frentes parlamentares da legislatura atual
frentes = await client.lista_frentes(id_legislatura=57)
for frente in frentes[:10]:
    print(f"{frente['titulo']}")

# Membros de uma frente
membros = await client.get_frente_membros(frente['id'])
for m in membros:
    print(f"  {m['nome']} ({m.get('titulo', 'membro')})")
```

### Mesa Diretora

```python
# Mesa da legislatura atual
mesa = await client.get_legislatura_mesa(57)
for membro in mesa:
    print(f"{membro['titulo']}: {membro['nome']} ({membro['siglaPartido']}-{membro['siglaUf']})")
```

### Dados de Referência

```python
# Listar todos os temas disponíveis para filtro
temas = await client.get_referencias_temas()
for tema in temas:
    print(f"  {tema['cod']}: {tema['nome']}")

# Listar UFs
ufs = await client.get_referencias_uf()
```

### Proposições em Tramitação no Senado

```python
# Buscar projetos originados na Câmara que estão no Senado
no_senado = await client.pesquisar_proposicoes(
    sigla_tipo="PL",
    tramitacao_senado=True
)
for prop in no_senado:
    print(f"PL {prop['numero']}/{prop['ano']}: {prop['ementa'][:60]}")
```

## 📄 Licença

MIT License

## 🤝 Contribuindo

Contribuições são bem-vindas! Por favor:

1. Fork o projeto
2. Crie uma branch para sua feature (`git checkout -b feature/AmazingFeature`)
3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
4. Push para a branch (`git push origin feature/AmazingFeature`)
5. Abra um Pull Request

## 👨💻 Autor

Daniel Marques - [@olegantonov](https://github.com/olegantonov)

## 🙏 Agradecimentos

- Câmara dos Deputados pela disponibilização da API de Dados Abertos
- Comunidade Python Brasil


path: camara_client.py
content: """
Cliente async para API de Dados Abertos da Câmara dos Deputados.
Base: https://dadosabertos.camara.leg.br/api/v2

Uso:
    from camara_client import get_camara_client
    
    async def main():
        client = get_camara_client()
        try:
            # Listar deputados
            deps = await client.lista_deputados()
            
            # Pesquisar proposições
            props = await client.pesquisar_proposicoes(
                keywords="transporte",
                ano=2026
            )
            
            # Eventos de hoje
            from datetime import date, timedelta
            eventos = await client.get_eventos_periodo(
                date.today(),
                date.today() + timedelta(days=7)
            )
        finally:
            await client.close()
"""
import asyncio
import logging
from datetime import date, timedelta
from typing import Any

import httpx


# Configurar logger
logger = logging.getLogger(__name__)


# Exceções customizadas
class CamaraAPIError(Exception):
    """Erro base para erros da API da Câmara."""
    pass


class CamaraConnectionError(CamaraAPIError):
    """Erro de conexão com a API."""
    pass


class CamaraTimeoutError(CamaraAPIError):
    """Timeout ao conectar com a API."""
    pass


class CamaraNotFoundError(CamaraAPIError):
    """Recurso não encontrado."""
    pass


class CamaraValidationError(CamaraAPIError):
    """Erro de validação de parâmetros."""
    pass


BASE_URL = "https://dadosabertos.camara.leg.br/api/v2"

# Timeout padrão para requisições (45s)
DEFAULT_TIMEOUT = 45.0


class CamaraClient:
    def __init__(self, timeout: float = DEFAULT_TIMEOUT):
        self.client: httpx.AsyncClient | None = None
        self.timeout = timeout

    async def _get_client(self) -> httpx.AsyncClient:
        if self.client is None or self.client.is_closed:
            self.client = httpx.AsyncClient(
                base_url=BASE_URL,
                timeout=httpx.Timeout(self.timeout),
                follow_redirects=True
            )
        return self.client

    async def close(self):
        if self.client and not self.client.is_closed:
            await self.client.aclose()

    async def _get(self, path: str, params: dict | None = None, retries: int = 3) -> dict:
        """
        Executa GET com retry logic e tratamento de erros.
        
        Args:
            path: Caminho do endpoint
            params: Parâmetros da query
            retries: Número de tentativas em caso de falha
            
        Raises:
            CamaraTimeoutError: Timeout na requisição
            CamaraNotFoundError: Recurso não encontrado (404)
            CamaraConnectionError: Erro de conexão
            CamaraAPIError: Outros erros da API
        """
        client = await self._get_client()
        last_error = None
        
        for attempt in range(retries):
            try:
                logger.debug(f"Requisição para {path}, tentativa {attempt + 1}/{retries}")
                response = await client.get(path, params=params)
                response.raise_for_status()
                data = response.json()
                # API v2 retorna { dados: [...] }
                return data.get("dados", data)
                
            except httpx.TimeoutException as e:
                last_error = CamaraTimeoutError(f"Timeout ao acessar {path}: {e}")
                logger.warning(f"Timeout na tentativa {attempt + 1}/{retries}")
                if attempt < retries - 1:
                    await asyncio.sleep(2 ** attempt)  # Backoff exponencial
                    
            except httpx.HTTPStatusError as e:
                if e.response.status_code == 404:
                    raise CamaraNotFoundError(f"Recurso não encontrado: {path}")
                last_error = CamaraAPIError(f"Erro HTTP {e.response.status_code}: {e}")
                if e.response.status_code >= 500:
                    logger.warning(f"Erro {e.response.status_code} na tentativa {attempt + 1}/{retries}")
                    if attempt < retries - 1:
                        await asyncio.sleep(2 ** attempt)
                        continue
                logger.error(f"Erro HTTP: {e}")
                break
                
            except httpx.ConnectError as e:
                last_error = CamaraConnectionError(f"Erro de conexão: {e}")
                logger.warning(f"Erro de conexão na tentativa {attempt + 1}/{retries}")
                if attempt < retries - 1:
                    await asyncio.sleep(2 ** attempt)
                    
            except Exception as e:
                last_error = CamaraAPIError(f"Erro inesperado: {e}")
                logger.error(f"Erro inesperado: {e}")
                break
        
        if last_error:
            raise last_error
        
        raise CamaraAPIError("Falha após todas as tentativas")

    async def _get_raw(self, path: str, params: dict | None = None, retries: int = 3) -> dict:
        """GET com retry que retorna o JSON completo (sem extrair 'dados')."""
        client = await self._get_client()
        last_error = None

        for attempt in range(retries):
            try:
                logger.debug(f"Requisição raw para {path}, tentativa {attempt + 1}/{retries}")
                response = await client.get(path, params=params)
                response.raise_for_status()
                return response.json()

            except httpx.TimeoutException as e:
                last_error = CamaraTimeoutError(f"Timeout ao acessar {path}: {e}")
                logger.warning(f"Timeout na tentativa {attempt + 1}/{retries}")
                if attempt < retries - 1:
                    await asyncio.sleep(2 ** attempt)

            except httpx.HTTPStatusError as e:
                if e.response.status_code == 404:
                    raise CamaraNotFoundError(f"Recurso não encontrado: {path}")
                last_error = CamaraAPIError(f"Erro HTTP {e.response.status_code}: {e}")
                if e.response.status_code >= 500:
                    logger.warning(f"Erro {e.response.status_code} na tentativa {attempt + 1}/{retries}")
                    if attempt < retries - 1:
                        await asyncio.sleep(2 ** attempt)
                        continue
                logger.error(f"Erro HTTP: {e}")
                break

            except httpx.ConnectError as e:
                last_error = CamaraConnectionError(f"Erro de conexão: {e}")
                logger.warning(f"Erro de conexão na tentativa {attempt + 1}/{retries}")
                if attempt < retries - 1:
                    await asyncio.sleep(2 ** attempt)

            except Exception as e:
                last_error = CamaraAPIError(f"Erro inesperado: {e}")
                logger.error(f"Erro inesperado: {e}")
                break

        if last_error:
            raise last_error
        raise CamaraAPIError("Falha após todas as tentativas")

    async def _get_list(self, path: str, params: dict | None = None, max_pages: int = 50) -> list:
        """Retorna lista de resultados com paginação automática e retry.

        Args:
            max_pages: limite de páginas para evitar loops infinitos (default 50).
        """
        params = params or {}
        params.setdefault("itens", 100)

        results = []
        pagina = 1
        while pagina <= max_pages:
            params["pagina"] = pagina
            data = await self._get_raw(path, params)
            dados = data.get("dados", [])
            if not dados:
                break
            results.extend(dados)
            if len(dados) < params["itens"]:
                break
            pagina += 1
        return results

    # ========== DEPUTADOS ==========

    async def lista_deputados(self, legislatura: int | None = None) -> list[dict]:
        """Lista de deputados. Default: legislatura atual (57ª, 2023-2027)."""
        params: dict[str, Any] = {}
        if legislatura is not None:
            params["idLegislatura"] = str(legislatura)
        return await self._get_list("/deputados", params)

    async def buscar_deputado_por_nome(self, nome: str, legislatura: int | None = None) -> list[dict]:
        """Busca deputados pelo nome (contém)."""
        params: dict[str, Any] = {"nome": nome, "itens": 10}
        if legislatura is not None:
            params["idLegislatura"] = str(legislatura)
        return await self._get_list("/deputados", params)

    async def get_deputado_detalhe(self, id_deputado: str | int) -> dict:
        """Detalhe de um deputado pelo ID."""
        return await self._get(f"/deputados/{id_deputado}")

    async def get_despesas_deputado(self, id_deputado: str | int, ano: int | None = None) -> list[dict]:
        """Despesas de um deputado (CEAP)."""
        params: dict[str, Any] = {"ordem": "ASC", "ordenarPor": "ano"}
        if ano:
            params["ano"] = str(ano)
        return await self._get_list(f"/deputados/{id_deputado}/despesas", params)

    async def get_frentes_deputado(self, id_deputado: str | int) -> list[dict]:
        """Frentes parlamentares que o deputado participa."""
        return await self._get_list(f"/deputados/{id_deputado}/frentes")

    async def get_discursos_deputado(self, id_deputado: str | int, data_inicio: date | None = None, data_fim: date | None = None) -> list[dict]:
        """Discursos de um deputado."""
        params = {}
        if data_inicio:
            params["dataIni"] = data_inicio.isoformat()
        if data_fim:
            params["dataFim"] = data_fim.isoformat()
        return await self._get_list(f"/deputados/{id_deputado}/discursos", params)

    async def get_presenca_deputado(self, id_deputado: str | int, data_inicio: date, data_fim: date) -> dict:
        """Presença em sessões."""
        return await self._get(f"/deputados/{id_deputado}/presenca", {
            "dataIni": data_inicio.isoformat(),
            "dataFim": data_fim.isoformat()
        })

    # ========== PROPOSIÇÕES ==========

    async def pesquisar_proposicoes(
        self,
        keywords: str | None = None,
        sigla_tipo: str | None = None,
        numero: str | None = None,
        ano: int | None = None,
        autor: str | None = None,
        tramitando: bool = False,
        tema: int | None = None,
        tramitacao_senado: bool = False,
    ) -> list[dict]:
        """Pesquisa proposições."""
        params: dict[str, Any] = {
            "ordem": "DESC",
            "ordenarPor": "id",
            "itens": 20
        }
        if keywords:
            params["keywords"] = keywords
        if sigla_tipo:
            params["siglaTipo"] = sigla_tipo
        if numero:
            params["numero"] = str(numero)
        if ano:
            params["ano"] = str(ano)
        if autor:
            params["autor"] = autor
        if tramitando:
            # Código 903 = "Aguardando Deliberação" (em tramitação)
            params["codSituacao"] = "903"
        if tema:
            params["tema"] = str(tema)
        if tramitacao_senado:
            params["tramitacaoSenado"] = "true"

        return await self._get_list("/proposicoes", params)

    async def get_proposicao_detalhe(self, id_proposicao: str | int) -> dict:
        """Detalhe de uma proposição."""
        return await self._get(f"/proposicoes/{id_proposicao}")

    async def get_proposicao_tramitacao(self, id_proposicao: str | int) -> list[dict]:
        """Tramitações de uma proposição."""
        return await self._get_list(f"/proposicoes/{id_proposicao}/tramitacoes")

    async def get_proposicao_votacoes(self, id_proposicao: str | int) -> list[dict]:
        """Votações de uma proposição."""
        return await self._get_list(f"/proposicoes/{id_proposicao}/votacoes")

    async def get_proposicao_autores(self, id_proposicao: str | int) -> list[dict]:
        """Autores de uma proposição."""
        return await self._get_list(f"/proposicoes/{id_proposicao}/autores")

    async def get_proposicao_temas(self, id_proposicao: str | int) -> list[dict]:
        """Temas de uma proposição."""
        return await self._get_list(f"/proposicoes/{id_proposicao}/temas")

    async def get_proposicao_relacionadas(self, id_proposicao: str | int) -> list[dict]:
        """Proposicoes relacionadas a uma proposicao."""
        return await self._get_list(f"/proposicoes/{id_proposicao}/relacionadas")

    # ========== EVENTOS / SESSÕES ==========

    async def get_eventos_dia(self, data: date | None = None) -> list[dict]:
        """Eventos do dia (sessões, audiências, etc)."""
        if data is None:
            data = date.today()
        return await self._get_list("/eventos", {
            "dataInicio": data.isoformat(),
            "dataFim": data.isoformat(),
            "ordem": "ASC",
            "ordenarPor": "dataHoraInicio"
        })

    async def get_eventos_periodo(self, data_inicio: date, data_fim: date) -> list[dict]:
        """Eventos em um período."""
        return await self._get_list("/eventos", {
            "dataInicio": data_inicio.isoformat(),
            "dataFim": data_fim.isoformat(),
            "ordem": "ASC",
            "ordenarPor": "dataHoraInicio"
        })

    async def get_evento_detalhe(self, id_evento: str | int) -> dict:
        """Detalhe de um evento."""
        return await self._get(f"/eventos/{id_evento}")

    async def get_evento_deputados(self, id_evento: str | int) -> list[dict]:
        """Deputados presentes em um evento."""
        return await self._get_list(f"/eventos/{id_evento}/deputados")

    async def get_evento_orgaos(self, id_evento: str | int) -> list[dict]:
        """Orgaos organizadores de um evento."""
        return await self._get_list(f"/eventos/{id_evento}/orgaos")

    async def get_evento_votacoes(self, id_evento: str | int) -> list[dict]:
        """Votacoes realizadas em um evento."""
        return await self._get_list(f"/eventos/{id_evento}/votacoes")

    # ========== VOTAÇÕES ==========

    async def get_votacoes_periodo(self, data_inicio: date, data_fim: date, id_orgao: int | None = None) -> list[dict]:
        """Votações em um período."""
        params: dict[str, Any] = {
            "dataInicio": data_inicio.isoformat(),
            "dataFim": data_fim.isoformat(),
            "ordem": "DESC",
            "ordenarPor": "dataHoraRegistro"
        }
        if id_orgao:
            params["idOrgao"] = str(id_orgao)
        return await self._get_list("/votacoes", params)

    async def get_votacao_detalhe(self, id_votacao: str | int) -> dict:
        """Detalhe de uma votação."""
        return await self._get(f"/votacoes/{id_votacao}")

    async def get_votos_votacao(self, id_votacao: str | int) -> list[dict]:
        """Votos de uma votação."""
        return await self._get_list(f"/votacoes/{id_votacao}/votos")

    # ========== ÓRGÃOS / COMISSÕES ==========

    async def lista_orgaos(self) -> list[dict]:
        """Lista de órgãos da Câmara."""
        return await self._get_list("/orgaos")

    async def get_orgao_detalhe(self, id_orgao: str | int) -> dict:
        """Detalhe de um órgão."""
        return await self._get(f"/orgaos/{id_orgao}")

    async def get_eventos_orgao_periodo(self, id_orgao: str | int, data_inicio: date, data_fim: date) -> list[dict]:
        """Eventos de um órgão em período."""
        return await self._get_list(f"/orgaos/{id_orgao}/eventos", {
            "dataInicio": data_inicio.isoformat(),
            "dataFim": data_fim.isoformat(),
            "ordem": "ASC",
            "ordenarPor": "dataHoraInicio"
        })

    # ========== LEGISLATURAS ==========

    async def lista_legislaturas(self) -> list[dict]:
        """Lista de legislaturas."""
        return await self._get_list("/legislaturas")

    async def get_legislatura_detalhe(self, id_legislatura: str | int) -> dict:
        """Detalhe de uma legislatura."""
        return await self._get(f"/legislaturas/{id_legislatura}")

    async def get_legislatura_mesa(self, id_legislatura: str | int) -> list[dict]:
        """Mesa diretora de uma legislatura."""
        return await self._get_list(f"/legislaturas/{id_legislatura}/mesa")

    # ========== PARTIDOS ==========

    async def lista_partidos(self) -> list[dict]:
        """Lista de partidos."""
        return await self._get_list("/partidos")

    async def get_partido_detalhe(self, id_partido: str | int) -> dict:
        """Detalhes de um partido."""
        return await self._get(f"/partidos/{id_partido}")

    async def get_partido_membros(self, id_partido: str | int) -> list[dict]:
        """Membros de um partido (deputados)."""
        return await self._get_list(f"/partidos/{id_partido}/membros")

    # ========== BLOCOS ==========

    async def lista_blocos(self) -> list[dict]:
        """Lista de blocos parlamentares."""
        return await self._get_list("/blocos")

    async def get_bloco_detalhe(self, id_bloco: str | int) -> dict:
        """Detalhes de um bloco parlamentar."""
        return await self._get(f"/blocos/{id_bloco}")

    # ========== FRENTES ==========

    async def lista_frentes(self, id_legislatura: int | None = None) -> list[dict]:
        """Lista de frentes parlamentares."""
        params: dict[str, Any] = {}
        if id_legislatura:
            params["idLegislatura"] = str(id_legislatura)
        return await self._get_list("/frentes", params)

    async def get_frente_detalhe(self, id_frente: str | int) -> dict:
        """Detalhes de uma frente parlamentar."""
        return await self._get(f"/frentes/{id_frente}")

    async def get_frente_membros(self, id_frente: str | int) -> list[dict]:
        """Membros de uma frente parlamentar."""
        return await self._get_list(f"/frentes/{id_frente}/membros")

    # ========== NOVOS ENDPOINTS ==========

    async def get_orientacoes_votacao(self, id_votacao: str | int) -> list[dict]:
        """Orientações dos partidos em uma votação."""
        return await self._get_list(f"/votacoes/{id_votacao}/orientacoes")

    async def get_membros_orgao(self, id_orgao: str | int) -> list[dict]:
        """Membros de um órgão/comissão."""
        return await self._get_list(f"/orgaos/{id_orgao}/membros")

    async def get_pauta_evento(self, id_evento: str | int) -> list[dict]:
        """Pauta de um evento (sessão/audiência)."""
        return await self._get_list(f"/eventos/{id_evento}/pauta")

    async def get_orgao_membros_periodo(self, id_orgao: str | int, data_inicio: date, data_fim: date) -> list[dict]:
        """Membros de uma comissão em um período."""
        return await self._get_list(f"/orgaos/{id_orgao}/membros", {
            "dataInicio": data_inicio.isoformat(),
            "dataFim": data_fim.isoformat()
        })

    async def get_deputado_orgaos(self, id_deputado: str | int) -> list[dict]:
        """Órgãos/comissões em que o deputado participa."""
        return await self._get_list(f"/deputados/{id_deputado}/orgaos")

    async def get_deputado_ocupacoes(self, id_deputado: str | int) -> list[dict]:
        """Historico de ocupacoes/profissoes de um deputado."""
        return await self._get_list(f"/deputados/{id_deputado}/ocupacoes")

    async def get_votacoes_orgao(self, id_orgao: str | int, data_inicio: date | None = None, data_fim: date | None = None) -> list[dict]:
        """Votacoes de um orgao/comissao."""
        params: dict[str, Any] = {}
        if data_inicio:
            params["dataInicio"] = data_inicio.isoformat()
        if data_fim:
            params["dataFim"] = data_fim.isoformat()
        return await self._get_list(f"/orgaos/{id_orgao}/votacoes", params)

    # ========== REFERÊNCIAS ==========

    async def get_referencias_situacao_deputado(self) -> list[dict]:
        """Codigos de situacao de deputados."""
        return await self._get_list("/referencias/deputados/codSituacao")

    async def get_referencias_situacao_proposicao(self) -> list[dict]:
        """Codigos de situacao de proposicoes."""
        return await self._get_list("/referencias/proposicoes/codSituacaoProposicao")

    async def get_referencias_tipo_proposicao(self) -> list[dict]:
        """Siglas de tipo de proposicao (PL, PEC, etc)."""
        return await self._get_list("/referencias/proposicoes/siglaTipo")

    async def get_referencias_temas(self) -> list[dict]:
        """Lista de temas/assuntos."""
        return await self._get_list("/referencias/proposicoes/tema")

    async def get_referencias_tipos_evento(self) -> list[dict]:
        """Tipos de evento."""
        return await self._get_list("/referencias/tiposEvento")

    async def get_referencias_tipos_orgao(self) -> list[dict]:
        """Tipos de orgao."""
        return await self._get_list("/referencias/tiposOrgao")

    async def get_referencias_uf(self) -> list[dict]:
        """Lista de UFs."""
        return await self._get_list("/referencias/uf")

    # ========== UTILITÁRIOS ==========

    async def get_proposicoes_recentes(self, dias: int = 7) -> list[dict]:
        """Busca proposições apresentadas nos últimos N dias."""
        hoje = date.today()
        inicio = hoje - timedelta(days=dias)

        params: dict[str, Any] = {
            "dataApresentacaoInicio": inicio.isoformat(),
            "dataApresentacaoFim": hoje.isoformat(),
            "ordem": "DESC",
            "ordenarPor": "id",
            "itens": 50,
        }
        return await self._get_list("/proposicoes", params)

    async def get_votacoes_semana(self) -> list[dict]:
        """Votações da última semana."""
        return await self.get_votacoes_periodo(
            date.today() - timedelta(days=7),
            date.today()
        )

    async def get_eventos_semana(self) -> list[dict]:
        """Eventos da semana atual."""
        hoje = date.today()
        return await self.get_eventos_periodo(hoje, hoje + timedelta(days=7))


# Instância global
_client: CamaraClient | None = None


def get_camara_client() -> CamaraClient:
    """Retorna instância singleton do cliente."""
    global _client
    if _client is None:
        _client = CamaraClient()
    return _client


async def close_camara_client():
    """Fecha o cliente global."""
    global _client
    if _client:
        await _client.close()
        _client = None


# ========== FACADE FUNCTIONS (uso direto) ==========

async def lista_deputados() -> list[dict]:
    """Facade: lista de deputados."""
    client = get_camara_client()
    return await client.lista_deputados()


async def buscar_deputado(nome: str) -> list[dict]:
    """Facade: busca deputados por nome."""
    client = get_camara_client()
    return await client.buscar_deputado_por_nome(nome)


async def pesquisar_proposicoes(keywords: str | None = None, ano: int | None = None, sigla_tipo: str | None = None) -> list[dict]:
    """Facade: pesquisa proposições."""
    client = get_camara_client()
    return await client.pesquisar_proposicoes(keywords=keywords, ano=ano, sigla_tipo=sigla_tipo)


async def get_votacoes_semana() -> list[dict]:
    """Facade: votações da semana."""
    client = get_camara_client()
    return await client.get_votacoes_semana()


async def get_eventos_semana() -> list[dict]:
    """Facade: eventos da semana."""
    client = get_camara_client()
    return await client.get_eventos_semana()


async def get_proposicoes_recentes(dias: int = 7) -> list[dict]:
    """Facade: proposições recentes."""
    client = get_camara_client()
    return await client.get_proposicoes_recentes(dias)


path: pyproject.toml
content: [build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "camara-deputados"
version = "1.0.0"
description = "Cliente assíncrono para a API de Dados Abertos da Câmara dos Deputados"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.11"
authors = [{name = "olegantonov"}]
keywords = ["câmara", "deputados", "api", "brasil", "dados-abertos"]

dependencies = [
    "httpx>=0.27.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "pytest-asyncio>=0.23",
    "pytest-mock>=3.14",
    "respx>=0.21",
]

[tool.pytest.ini_options]
asyncio_mode = "auto"
markers = [
    "integration: marks tests as integration tests (deselect with '-m \"not integration\"')",
]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]

[tool.ruff]
target-version = "py311"
line-length = 120


path: references/api-endpoints.md
content: # Câmara dos Deputados — Referência Completa de Endpoints

Base: `https://dadosabertos.camara.leg.br/api/v2`
Swagger: `https://dadosabertos.camara.leg.br/swagger/api.html`

## Deputados

| Endpoint | Params principais |
|---|---|
| `GET /deputados` | nome, siglaPartido, siglaUf, idLegislatura, siglaSexo, codSituacao |
| `GET /deputados/{id}` | — |
| `GET /deputados/{id}/discursos` | dataInicio, dataFim, idTipoDiscurso, ordenarPor, ordem |
| `GET /deputados/{id}/despesas` | ano, mes, cnpjCpfFornecedor, pagina, itens |
| `GET /deputados/{id}/documentos` | dataInicio, dataFim, codTipoDocumento |
| `GET /deputados/{id}/eventos` | dataInicio, dataFim, codTipoEvento |
| `GET /deputados/{id}/frentes` | — |
| `GET /deputados/{id}/mandatos` | — |
| `GET /deputados/{id}/ocupacoes` | — |
| `GET /deputados/{id}/orgaos` | dataInicio, dataFim |
| `GET /deputados/{id}/profissoes` | — |

## Proposições

| Endpoint | Params principais |
|---|---|
| `GET /proposicoes` | siglaTipo, numero, ano, autor, tema, keywords, dataApresentacaoInicio, dataApresentacaoFim, codSituacao, tramitacaoSenado |
| `GET /proposicoes/{id}` | — |
| `GET /proposicoes/{id}/autores` | — |
| `GET /proposicoes/{id}/relacionadas` | — |
| `GET /proposicoes/{id}/temas` | — |
| `GET /proposicoes/{id}/tramitacoes` | dataInicio, dataFim |
| `GET /proposicoes/{id}/votacoes` | — |

### Tipos de proposição (siglaTipo)
PL, PEC, PLP, MPV, PDC, PRC, REC, REQ, REP, REO, MSC, PLN, PDS, PFC, PDN, EMC, EMP, EMS, SBT, SCR, SGM, SMP

### Situações de proposição (codSituacao)
- `1` = Tramitando
- `2` = Arquivada
- `3` = Transformada em norma jurídica
- Consultar `/referencias/proposicoes/codSituacaoProposicao`

## Votações

| Endpoint | Params principais |
|---|---|
| `GET /votacoes` | dataInicio, dataFim, idOrgao, siglaPartido, idDeputado, ordem, ordenarPor |
| `GET /votacoes/{id}` | — |
| `GET /votacoes/{id}/votos` | — |
| `GET /votacoes/{id}/orientacoes` | — |

### Órgãos principais
- `180` = Plenário

## Eventos / Agenda

| Endpoint | Params principais |
|---|---|
| `GET /eventos` | dataInicio, dataFim, siglaOrgao, codTipoEvento, codSituacao, ordem, ordenarPor, itens |
| `GET /eventos/{id}` | — |
| `GET /eventos/{id}/deputados` | — |
| `GET /eventos/{id}/orgaos` | — |
| `GET /eventos/{id}/pauta` | — |
| `GET /eventos/{id}/votacoes` | — |

### Situações de evento (codSituacao)
- `1` = Convocada / Prevista
- `2` = Realizada
- `3` = Em andamento
- `4` = Cancelada

## Órgãos / Comissões

| Endpoint | Params principais |
|---|---|
| `GET /orgaos` | sigla, codTipoOrgao, nome, dataInicio, dataFim |
| `GET /orgaos/{id}` | — |
| `GET /orgaos/{id}/eventos` | dataInicio, dataFim, codTipoEvento |
| `GET /orgaos/{id}/membros` | dataInicio, dataFim |
| `GET /orgaos/{id}/votacoes` | dataInicio, dataFim |

### Tipos de órgão (codTipoOrgao)
- `2` = Comissão Permanente
- `3` = Comissão Temporária
- `4` = Comissão Especial
- `5` = Comissão Externa
- `6` = CPI
- `26` = Plenário Virtual
- Consultar `/referencias/tiposOrgao`

## Comissões principais (sigla)
CCJC, CFT, CDE, CSAUDE, CTRAB, CAPADR, CDC, CDU, CMADS, CREDN, CME, CTUR, CCULT, CDH, CCOM, CESPO, CVT, CSPCCO

## Partidos e Blocos

| Endpoint | Params |
|---|---|
| `GET /partidos` | sigla, dataInicio, dataFim, ordem, ordenarPor |
| `GET /partidos/{id}` | — |
| `GET /partidos/{id}/membros` | dataInicio, dataFim |
| `GET /blocos` | idLegislatura |
| `GET /blocos/{id}` | — |

## Frentes Parlamentares

| Endpoint |
|---|
| `GET /frentes` |
| `GET /frentes/{id}` |
| `GET /frentes/{id}/membros` |

## Grupos de Trabalho

| Endpoint |
|---|
| `GET /gruposTrabalho` |
| `GET /gruposTrabalho/{id}` |
| `GET /gruposTrabalho/{id}/membros` |

## Legislaturas

| Endpoint |
|---|
| `GET /legislaturas` |
| `GET /legislaturas/{id}` |
| `GET /legislaturas/{id}/mesa` |

- Legislatura atual: **57** (iniciada em 01/02/2023)

## Referências (dados auxiliares)

```
GET /referencias/deputados/codSituacao
GET /referencias/deputados/siglaSexo
GET /referencias/proposicoes/codSituacaoProposicao
GET /referencias/proposicoes/siglaTipo
GET /referencias/proposicoes/tema
GET /referencias/proposicoes/codTema
GET /referencias/tiposEvento
GET /referencias/tiposSituacaoEvento
GET /referencias/tiposOrgao
GET /referencias/uf
GET /referencias/partidos
```

## Bulk Data (despesas CEAP)
- `https://www.camara.leg.br/cotas/Ano-{ano}.json.zip`
- Anos disponíveis: 2008–atual
- Atualização diária


path: requirements-dev.txt
content: -r requirements.txt
pytest>=8.0.0
pytest-asyncio>=0.23.0
pytest-cov>=4.1.0
pytest-mock>=3.12.0
httpx-mock>=0.15.0
black>=24.0.0
mypy>=1.8.0
ruff>=0.2.0


path: requirements.txt
content: httpx>=0.27.0


path: scripts/camara.py
content: #!/usr/bin/env python3
"""
camara.py - CLI para a API de Dados Abertos da Câmara dos Deputados
Uso: python3 camara.py <comando> [opções]

Comandos:
  agenda [data]           Agenda do dia (default: hoje). Data: YYYY-MM-DD
  deputado <nome>         Busca deputado por nome
  deputado-id <id>        Perfil completo de um deputado
  ocupacoes <id>          Histórico de ocupações/profissões de um deputado
  proposicao <id>         Detalhes de uma proposição
  relacionadas <id>       Proposições relacionadas a uma proposição
  buscar-pl <keywords>    Busca proposições por palavras-chave
  votacoes [data]         Votações no plenário (default: hoje)
  votos <votacao_id>      Votos individuais de uma votação
  comissao <sigla>        Busca comissão por sigla
  blocos                  Lista blocos parlamentares
  frentes [legislatura]   Lista frentes parlamentares (default: legislatura 57)
  frente <id>             Detalhes e membros de uma frente parlamentar
  partido <id>            Detalhes e membros de um partido
  mesa [legislatura]      Mesa diretora (default: legislatura 57)
  referencias <tipo>      Dados de referência (situacao-dep, situacao-prop,
                          tipo-prop, temas, tipos-evento, tipos-orgao, uf)
"""

import sys
import json
import urllib.request
import urllib.parse
from datetime import date

BASE = "https://dadosabertos.camara.leg.br/api/v2"

def get(path, params=None):
    url = f"{BASE}{path}"
    if params:
        url += "?" + urllib.parse.urlencode({k: v for k, v in params.items() if v is not None})
    with urllib.request.urlopen(url, timeout=15) as r:
        return json.loads(r.read())

def fmt(obj):
    print(json.dumps(obj, ensure_ascii=False, indent=2))

def agenda(data=None):
    d = data or date.today().isoformat()
    r = get("/eventos", {"dataInicio": d, "dataFim": d, "ordem": "ASC", "ordenarPor": "dataHoraInicio", "itens": 50})
    eventos = r.get("dados", [])
    if not eventos:
        print(f"Sem eventos para {d}.")
        return
    print(f"\n📅 Agenda da Câmara — {d}\n{'='*50}")
    for e in eventos:
        hora = e.get("dataHoraInicio", "")[-5:] if e.get("dataHoraInicio") else "?"
        orgaos = ", ".join(o.get("sigla","") for o in e.get("orgaos",[]))
        local = e.get("localCamara", {}).get("nome") or e.get("localExterno") or ""
        print(f"  {hora} | {orgaos:10} | {e.get('descricaoTipo',''):30} | {e.get('descricao','')[:60]}")
        if local:
            print(f"         Local: {local}")
    print()

def buscar_deputado(nome):
    r = get("/deputados", {"nome": nome, "idLegislatura": 57, "ordem": "ASC", "ordenarPor": "nome"})
    deps = r.get("dados", [])
    if not deps:
        print(f"Nenhum deputado encontrado para '{nome}'.")
        return
    print(f"\n👤 Deputados — '{nome}'\n{'='*50}")
    for d in deps:
        print(f"  ID {d['id']} | {d['nome']:40} | {d['siglaPartido']:12} | {d['siglaUf']}")
    print()

def deputado_id(dep_id):
    r = get(f"/deputados/{dep_id}")
    d = r.get("dados", {})
    nome = d.get("nomeCivil", d.get("ultimoStatus", {}).get("nome", ""))
    status = d.get("ultimoStatus", {})
    print(f"\n👤 {nome}")
    print(f"  Partido: {status.get('siglaPartido')} | UF: {status.get('siglaUf')}")
    print(f"  Situação: {status.get('situacao')}")
    print(f"  Gabinete: {status.get('gabinete', {}).get('nome')} sala {status.get('gabinete', {}).get('sala')}")
    print(f"  Email: {status.get('email')}")
    print(f"  URL foto: {status.get('urlFoto')}")
    print()

def proposicao(prop_id):
    r = get(f"/proposicoes/{prop_id}")
    p = r.get("dados", {})
    print(f"\n📄 {p.get('siglaTipo')} {p.get('numero')}/{p.get('ano')}")
    print(f"  Ementa: {p.get('ementa')}")
    print(f"  Apresentação: {p.get('dataApresentacao')}")
    print(f"  Status: {p.get('statusProposicao', {}).get('descricaoSituacao')}")
    print(f"  Órgão atual: {p.get('statusProposicao', {}).get('siglaOrgao')}")
    print(f"  Relator: {p.get('statusProposicao', {}).get('uriRelator')}")
    # tramitação
    tr = get(f"/proposicoes/{prop_id}/tramitacoes")
    trams = tr.get("dados", [])[-5:]
    if trams:
        print(f"\n  📋 Últimas 5 tramitações:")
        for t in trams:
            print(f"    {t.get('dataHora','')[:10]} | {t.get('siglaOrgao',''):8} | {t.get('descricaoSituacao','')[:50]}")
    print()

def buscar_pl(keywords, tipo="PL", ano=None):
    params = {"keywords": keywords, "siglaTipo": tipo, "ordem": "DESC", "ordenarPor": "id", "itens": 15}
    if ano:
        params["ano"] = ano
    r = get("/proposicoes", params)
    pls = r.get("dados", [])
    if not pls:
        print(f"Nenhuma proposição encontrada para '{keywords}'.")
        return
    print(f"\n📑 Proposições — '{keywords}'\n{'='*50}")
    for p in pls:
        print(f"  ID {p['id']} | {p['siglaTipo']} {p['numero']}/{p['ano']} | {p.get('dataApresentacao','')[:10]} | {p['ementa'][:70]}")
    print()

def votacoes(data=None):
    d = data or date.today().isoformat()
    r = get("/votacoes", {"dataInicio": d, "dataFim": d, "idOrgao": 180, "ordem": "DESC", "ordenarPor": "dataHoraRegistro", "itens": 20})
    vots = r.get("dados", [])
    if not vots:
        print(f"Sem votações no plenário em {d}.")
        return
    print(f"\n🗳️  Votações — Plenário — {d}\n{'='*50}")
    for v in vots:
        ap = "✅ APROVADA" if v.get("aprovacao") == 1 else "❌ REJEITADA" if v.get("aprovacao") == 0 else "⏳"
        print(f"  {v['id']} | {ap} | {v.get('descricao','')[:70]}")
    print()

def votos(votacao_id):
    r = get(f"/votacoes/{votacao_id}/votos")
    votos_list = r.get("dados", [])
    print(f"\n🗳️  Votos na votação {votacao_id}\n{'='*50}")
    contagem = {}
    for v in votos_list:
        voto = v.get("tipoVoto", "?")
        contagem[voto] = contagem.get(voto, 0) + 1
    for tipo, qtd in sorted(contagem.items(), key=lambda x: -x[1]):
        print(f"  {tipo:10}: {qtd}")
    print(f"  Total: {len(votos_list)}")
    print()

def comissao(sigla):
    r = get("/orgaos", {"sigla": sigla})
    orgs = r.get("dados", [])
    if not orgs:
        print(f"Comissão '{sigla}' não encontrada.")
        return
    org = orgs[0]
    print(f"\n🏛️  {org.get('nome')} ({org.get('sigla')})")
    print(f"  ID: {org['id']} | Tipo: {org.get('tipoOrgao')}")
    # Membros
    try:
        mr = get(f"/orgaos/{org['id']}/membros")
        membros = mr.get("dados", [])
        print(f"  Membros: {len(membros)}")
        for m in membros[:5]:
            print(f"    {m.get('nome',''):40} | {m.get('siglaPartido',''):8} | {m.get('titulo','')}")
        if len(membros) > 5:
            print(f"    ... e mais {len(membros)-5}")
    except Exception:
        pass
    print()

def ocupacoes(dep_id):
    r = get(f"/deputados/{dep_id}/ocupacoes")
    ocups = r.get("dados", [])
    if not ocups:
        print(f"Sem ocupações registradas para deputado {dep_id}.")
        return
    print(f"\n💼 Ocupações — Deputado {dep_id}\n{'='*50}")
    for o in ocups:
        periodo = f"{o.get('anoInicio', '?')}-{o.get('anoFim', 'atual')}"
        print(f"  {periodo:12} | {o.get('titulo', ''):30} | {o.get('entidade', '')}")
    print()

def relacionadas(prop_id):
    r = get(f"/proposicoes/{prop_id}/relacionadas")
    rels = r.get("dados", [])
    if not rels:
        print(f"Sem proposições relacionadas para {prop_id}.")
        return
    print(f"\n🔗 Proposições Relacionadas — {prop_id}\n{'='*50}")
    for p in rels:
        print(f"  ID {p.get('id','')} | {p.get('siglaTipo','')} {p.get('numero','')}/{p.get('ano','')} | {p.get('ementa','')[:60]}")
    print()

def blocos():
    r = get("/blocos")
    blocs = r.get("dados", [])
    if not blocs:
        print("Nenhum bloco parlamentar encontrado.")
        return
    print(f"\n🏛️  Blocos Parlamentares\n{'='*50}")
    for b in blocs:
        print(f"  ID {b.get('id','')} | {b.get('nome',''):50} | Leg. {b.get('idLegislatura','')}")
    print()

def frentes(legislatura=None):
    params = {"itens": 50}
    if legislatura:
        params["idLegislatura"] = legislatura
    else:
        params["idLegislatura"] = 57
    r = get("/frentes", params)
    frs = r.get("dados", [])
    if not frs:
        print("Nenhuma frente parlamentar encontrada.")
        return
    print(f"\n🤝 Frentes Parlamentares\n{'='*50}")
    for f in frs:
        print(f"  ID {f.get('id','')} | {f.get('titulo','')[:70]}")
    print(f"  Total: {len(frs)}")
    print()

def frente(id_frente):
    r = get(f"/frentes/{id_frente}")
    f = r.get("dados", {})
    print(f"\n🤝 {f.get('titulo', '')}")
    print(f"  ID: {id_frente}")
    print(f"  Coordenador: {f.get('coordenador', {}).get('nome', '?')}")
    # Membros
    try:
        mr = get(f"/frentes/{id_frente}/membros")
        membros = mr.get("dados", [])
        print(f"  Membros: {len(membros)}")
        for m in membros[:10]:
            print(f"    {m.get('nome',''):40} | {m.get('siglaPartido',''):8} | {m.get('titulo','')}")
        if len(membros) > 10:
            print(f"    ... e mais {len(membros)-10}")
    except Exception:
        pass
    print()

def partido(id_partido):
    r = get(f"/partidos/{id_partido}")
    p = r.get("dados", {})
    status = p.get("status", {})
    print(f"\n🏛️  {p.get('nome', '')} ({status.get('sigla', '')})")
    print(f"  ID: {id_partido}")
    print(f"  Líder: {status.get('lider', {}).get('nome', '?')}")
    print(f"  Total membros: {status.get('totalMembros', '?')}")
    # Membros
    try:
        mr = get(f"/partidos/{id_partido}/membros", {"itens": 50})
        membros = mr.get("dados", [])
        print(f"\n  Deputados ({len(membros)}):")
        for m in membros[:10]:
            print(f"    {m.get('nome',''):40} | {m.get('siglaUf','')}")
        if len(membros) > 10:
            print(f"    ... e mais {len(membros)-10}")
    except Exception:
        pass
    print()

def mesa(legislatura=None):
    leg = legislatura or "57"
    r = get(f"/legislaturas/{leg}/mesa")
    membros = r.get("dados", [])
    if not membros:
        print(f"Sem dados da mesa para legislatura {leg}.")
        return
    print(f"\n🏛️  Mesa Diretora — Legislatura {leg}\n{'='*50}")
    for m in membros:
        print(f"  {m.get('titulo',''):30} | {m.get('nome',''):30} | {m.get('siglaPartido',''):8} | {m.get('siglaUf','')}")
    print()

def referencias(tipo):
    REF_MAP = {
        "situacao-dep": "/referencias/deputados/codSituacao",
        "situacao-prop": "/referencias/proposicoes/codSituacaoProposicao",
        "tipo-prop": "/referencias/proposicoes/siglaTipo",
        "temas": "/referencias/proposicoes/tema",
        "tipos-evento": "/referencias/tiposEvento",
        "tipos-orgao": "/referencias/tiposOrgao",
        "uf": "/referencias/uf",
    }
    if not tipo or tipo not in REF_MAP:
        print(f"Tipos disponíveis: {', '.join(REF_MAP.keys())}")
        return
    r = get(REF_MAP[tipo])
    dados = r.get("dados", [])
    if not dados:
        print(f"Sem dados para '{tipo}'.")
        return
    print(f"\n📋 Referências — {tipo}\n{'='*50}")
    for item in dados:
        # Formatar de forma genérica
        parts = [str(v) for v in item.values()]
        print(f"  {' | '.join(parts)}")
    print(f"  Total: {len(dados)}")
    print()

COMMANDS = {
    "agenda": lambda args: agenda(args[0] if args else None),
    "deputado": lambda args: buscar_deputado(" ".join(args)),
    "deputado-id": lambda args: deputado_id(args[0]),
    "ocupacoes": lambda args: ocupacoes(args[0]),
    "proposicao": lambda args: proposicao(args[0]),
    "relacionadas": lambda args: relacionadas(args[0]),
    "buscar-pl": lambda args: buscar_pl(" ".join(args)),
    "votacoes": lambda args: votacoes(args[0] if args else None),
    "votos": lambda args: votos(args[0]),
    "comissao": lambda args: comissao(args[0]),
    "blocos": lambda args: blocos(),
    "frentes": lambda args: frentes(args[0] if args else None),
    "frente": lambda args: frente(args[0]),
    "partido": lambda args: partido(args[0]),
    "mesa": lambda args: mesa(args[0] if args else None),
    "referencias": lambda args: referencias(args[0] if args else None),
}

if __name__ == "__main__":
    if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
        print(__doc__)
        sys.exit(1)
    cmd = sys.argv[1]
    args = sys.argv[2:]
    try:
        COMMANDS[cmd](args)
    except Exception as e:
        print(f"Erro: {e}", file=sys.stderr)
        sys.exit(1)


path: test_manual.py
content: #!/usr/bin/env python3
"""Script de teste manual para validar o cliente da Câmara."""
import asyncio
import sys
from datetime import date, timedelta


async def test_client():
    """Testa funcionalidades básicas do cliente."""
    try:
        from camara_client import get_camara_client
        
        print("🧪 Testando Cliente da Câmara dos Deputados\n")
        print("=" * 60)
        
        client = get_camara_client()
        
        # Teste 1: Listar deputados
        print("\n✓ Teste 1: Listando deputados da legislatura 57...")
        try:
            deputados = await client.lista_deputados()
            print(f"  ✓ Encontrados {len(deputados)} deputados")
            if len(deputados) > 0:
                d = deputados[0]
                print(f"  ✓ Exemplo: {d['nome']} ({d['siglaPartido']}-{d['siglaUf']})")
        except Exception as e:
            print(f"  ✗ Erro: {e}")
            return False
        
        # Teste 2: Buscar deputado por nome
        print("\n✓ Teste 2: Buscando deputado por nome...")
        try:
            resultado = await client.buscar_deputado_por_nome("Lula")
            print(f"  ✓ Encontrados {len(resultado)} resultados para 'Lula'")
        except Exception as e:
            print(f"  ✗ Erro: {e}")
            return False
        
        # Teste 3: Pesquisar proposições
        print("\n✓ Teste 3: Pesquisando proposições (PL 2024)...")
        try:
            proposicoes = await client.pesquisar_proposicoes(
                sigla_tipo="PL",
                ano=2024,
                tramitando=True
            )
            print(f"  ✓ Encontradas {len(proposicoes)} proposições")
            if len(proposicoes) > 0:
                p = proposicoes[0]
                print(f"  ✓ Exemplo: {p['siglaTipo']} {p['numero']}/{p['ano']}")
        except Exception as e:
            print(f"  ✗ Erro: {e}")
            return False
        
        # Teste 4: Eventos de hoje
        print("\n✓ Teste 4: Verificando eventos de hoje...")
        try:
            eventos = await client.get_eventos_dia()
            print(f"  ✓ Encontrados {len(eventos)} eventos")
        except Exception as e:
            print(f"  ✗ Erro: {e}")
            return False
        
        # Teste 5: Votações recentes
        print("\n✓ Teste 5: Buscando votações dos últimos 30 dias...")
        try:
            fim = date.today()
            inicio = fim - timedelta(days=30)
            votacoes = await client.get_votacoes_periodo(inicio, fim)
            print(f"  ✓ Encontradas {len(votacoes)} votações")
        except Exception as e:
            print(f"  ✗ Erro: {e}")
            return False
        
        # Teste 6: Listar órgãos
        print("\n✓ Teste 6: Listando órgãos/comissões...")
        try:
            orgaos = await client.lista_orgaos()
            print(f"  ✓ Encontrados {len(orgaos)} órgãos")
        except Exception as e:
            print(f"  ✗ Erro: {e}")
            return False
        
        await client.close()
        
        print("\n" + "=" * 60)
        print("✅ Todos os testes passaram com sucesso!")
        print("=" * 60)
        return True
        
    except ImportError as e:
        print(f"✗ Erro ao importar módulo: {e}")
        print("  Certifique-se de que httpx está instalado: python3 -m pip install httpx")
        return False
    except Exception as e:
        print(f"✗ Erro inesperado: {e}")
        return False


if __name__ == "__main__":
    success = asyncio.run(test_client())
    sys.exit(0 if success else 1)


path: tests/conftest.py
content: """Configuração do pytest para testes do camara_client."""
import pytest


@pytest.fixture
def mock_camara_response():
    """Mock de resposta típica da API da Câmara."""
    return {
        "dados": [
            {
                "id": 204554,
                "nome": "Marcos Pontes",
                "siglaPartido": "PL",
                "siglaUf": "SP",
                "urlFoto": "https://...",
                "email": "[REDACTED_SECRET]"
            }
        ]
    }


@pytest.fixture
def mock_proposicao_response():
    """Mock de resposta de pesquisa de proposições."""
    return {
        "dados": [
            {
                "id": 2366661,
                "siglaTipo": "PL",
                "numero": 1234,
                "ano": 2026,
                "ementa": "Dispõe sobre..."
            }
        ]
    }


path: tests/integration/test_camara_integration.py
content: """Testes de integração (requerem conexão com a API real)."""
import pytest
from datetime import date, timedelta

from camara_client import get_camara_client


@pytest.mark.integration
@pytest.mark.asyncio
class TestCamaraIntegration:
    """Testes de integração com a API real."""

    async def test_lista_deputados_real(self):
        """Testa listagem real de deputados."""
        client = get_camara_client()
        try:
            deputados = await client.lista_deputados()
            
            assert isinstance(deputados, list)
            assert len(deputados) > 400  # Brasil tem 513 deputados
            
            # Verifica estrutura
            deputado = deputados[0]
            assert 'id' in deputado
            assert 'nome' in deputado
            assert 'siglaPartido' in deputado
        finally:
            await client.close()

    async def test_pesquisar_proposicoes_real(self):
        """Testa pesquisa real de proposições."""
        client = get_camara_client()
        try:
            # Busca específica para evitar paginação excessiva
            proposicoes = await client.pesquisar_proposicoes(
                sigla_tipo="PL",
                ano=2024,
                numero="1"
            )

            assert isinstance(proposicoes, list)
            if len(proposicoes) > 0:
                prop = proposicoes[0]
                assert 'siglaTipo' in prop
                assert prop['siglaTipo'] == "PL"
        finally:
            await client.close()

    async def test_get_eventos_dia_real(self):
        """Testa busca real de eventos."""
        client = get_camara_client()
        try:
            eventos = await client.get_eventos_dia()
            
            assert isinstance(eventos, list)
            # Lista pode estar vazia em dias sem eventos
        finally:
            await client.close()

    async def test_get_votacoes_periodo_real(self):
        """Testa busca real de votações."""
        client = get_camara_client()
        try:
            fim = date.today()
            inicio = fim - timedelta(days=30)
            
            votacoes = await client.get_votacoes_periodo(inicio, fim)
            
            assert isinstance(votacoes, list)
            # Pode estar vazio se não houver votações no período
        finally:
            await client.close()

    async def test_buscar_deputado_especifico_real(self):
        """Testa busca de deputado específico."""
        client = get_camara_client()
        try:
            resultado = await client.buscar_deputado_por_nome("Lula")
            
            assert isinstance(resultado, list)
            # Verifica se encontrou algo
            if len(resultado) > 0:
                assert any("lula" in dep['nome'].lower() for dep in resultado)
        finally:
            await client.close()


path: tests/unit/test_camara_client.py
content: """Testes unitários para camara_client."""
import pytest
from unittest.mock import AsyncMock, Mock, patch, PropertyMock
from datetime import date

from camara_client import (
    CamaraClient,
    get_camara_client,
    CamaraAPIError,
    CamaraTimeoutError,
    CamaraNotFoundError,
    CamaraConnectionError,
)


def _make_mock_client(responses):
    """Helper: cria mock httpx client com lista de respostas sequenciais."""
    mock_client = AsyncMock()
    type(mock_client).is_closed = PropertyMock(return_value=False)
    if isinstance(responses, list):
        mock_client.get.side_effect = responses
    else:
        mock_client.get.return_value = responses
    return mock_client


def _make_response(json_data, status_code=200):
    """Helper: cria mock de resposta HTTP."""
    resp = Mock()
    resp.json.return_value = json_data
    resp.raise_for_status = Mock()
    resp.status_code = status_code
    return resp


@pytest.mark.asyncio
class TestCamaraClient:
    """Testes do cliente da Câmara."""

    async def test_client_initialization(self):
        """Testa inicialização do cliente."""
        client = CamaraClient()
        assert client.client is None
        assert client.timeout == 45.0

    async def test_custom_timeout(self):
        """Testa timeout customizado."""
        client = CamaraClient(timeout=10.0)
        assert client.timeout == 10.0

    async def test_get_singleton(self):
        """Testa padrão singleton."""
        import camara_client as mod
        mod._client = None
        client1 = get_camara_client()
        client2 = get_camara_client()
        assert client1 is client2
        mod._client = None

    @patch('camara_client.httpx.AsyncClient')
    async def test_lista_deputados_success(self, mock_httpx, mock_camara_response):
        """Testa listagem de deputados com sucesso."""
        mock_client = _make_mock_client(_make_response(mock_camara_response))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.lista_deputados()

        assert isinstance(result, list)
        assert len(result) == 1
        assert result[0]['nome'] == "Marcos Pontes"

    @patch('camara_client.httpx.AsyncClient')
    async def test_lista_deputados_sem_legislatura(self, mock_httpx, mock_camara_response):
        """Testa listagem sem especificar legislatura."""
        mock_client = _make_mock_client(_make_response(mock_camara_response))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        await client.lista_deputados()

        call_kwargs = mock_client.get.call_args
        params = call_kwargs.kwargs.get('params', call_kwargs[1].get('params', {}))
        assert 'idLegislatura' not in params

    @patch('camara_client.httpx.AsyncClient')
    async def test_buscar_deputado_por_nome(self, mock_httpx, mock_camara_response):
        """Testa busca de deputado por nome."""
        mock_client = _make_mock_client(_make_response(mock_camara_response))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.buscar_deputado_por_nome("Marcos")

        assert len(result) == 1
        assert "marcos" in result[0]['nome'].lower()

    @patch('camara_client.httpx.AsyncClient')
    async def test_pesquisar_proposicoes_success(self, mock_httpx, mock_proposicao_response):
        """Testa pesquisa de proposições."""
        mock_client = _make_mock_client(_make_response(mock_proposicao_response))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.pesquisar_proposicoes(sigla_tipo="PL", ano=2026)

        assert isinstance(result, list)
        assert result[0]['siglaTipo'] == "PL"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_with_timeout_and_retry(self, mock_httpx):
        """Testa retry após timeout."""
        import httpx
        success_resp = _make_response({"dados": {"id": 1}})
        mock_client = _make_mock_client([
            httpx.TimeoutException("Timeout"),
            success_resp,
        ])
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client._get("/test")
        assert result == {"id": 1}
        assert mock_client.get.call_count == 2

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_timeout_exhausts_retries(self, mock_httpx):
        """Testa que timeout esgota tentativas."""
        import httpx
        mock_client = AsyncMock()
        type(mock_client).is_closed = PropertyMock(return_value=False)
        mock_client.get.side_effect = httpx.TimeoutException("Timeout")
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        with pytest.raises(CamaraTimeoutError):
            await client._get("/test", retries=2)
        assert mock_client.get.call_count == 2

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_with_404_error(self, mock_httpx):
        """Testa tratamento de 404 (sem retry)."""
        import httpx
        mock_response = Mock()
        mock_response.status_code = 404
        mock_client = AsyncMock()
        type(mock_client).is_closed = PropertyMock(return_value=False)
        mock_client.get.side_effect = httpx.HTTPStatusError(
            "Not found", request=Mock(), response=mock_response
        )
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        with pytest.raises(CamaraNotFoundError):
            await client._get("/test")
        assert mock_client.get.call_count == 1

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_with_connection_error_and_retry(self, mock_httpx):
        """Testa retry após erro de conexão."""
        import httpx
        mock_client = AsyncMock()
        type(mock_client).is_closed = PropertyMock(return_value=False)
        mock_client.get.side_effect = httpx.ConnectError("Connection failed")
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        with pytest.raises(CamaraConnectionError):
            await client._get("/test", retries=2)
        assert mock_client.get.call_count == 2

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_list_pagination(self, mock_httpx):
        """Testa paginação automática em _get_list."""
        page1 = _make_response({"dados": [{"id": i} for i in range(100)]})
        page2 = _make_response({"dados": [{"id": 100}]})
        mock_client = _make_mock_client([page1, page2])
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client._get_list("/test")

        assert len(result) == 101
        assert mock_client.get.call_count == 2

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_list_uses_retry(self, mock_httpx):
        """Testa que _get_list tem retry via _get_raw."""
        import httpx
        success_resp = _make_response({"dados": [{"id": 1}]})
        mock_client = _make_mock_client([
            httpx.TimeoutException("Timeout"),
            success_resp,
        ])
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client._get_list("/test")

        assert len(result) == 1

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_eventos_dia(self, mock_httpx):
        """Testa busca de eventos do dia."""
        mock_client = _make_mock_client(_make_response({"dados": []}))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_eventos_dia(date(2026, 4, 2))

        assert isinstance(result, list)
        assert len(result) == 0

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_orientacoes_votacao(self, mock_httpx):
        """Testa novo endpoint: orientações de votação."""
        orientacoes = {"dados": [{"siglaPartido": "PT", "orientacao": "Sim"}]}
        mock_client = _make_mock_client(_make_response(orientacoes))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_orientacoes_votacao("12345")

        assert isinstance(result, list)
        assert result[0]['siglaPartido'] == "PT"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_membros_orgao(self, mock_httpx):
        """Testa novo endpoint: membros de órgão."""
        membros = {"dados": [{"nome": "Deputado A"}]}
        mock_client = _make_mock_client(_make_response(membros))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_membros_orgao(123)

        assert len(result) == 1

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_deputado_orgaos(self, mock_httpx):
        """Testa novo endpoint: órgãos do deputado."""
        orgaos = {"dados": [{"sigla": "CCJC", "nome": "Comissão de Constituição e Justiça"}]}
        mock_client = _make_mock_client(_make_response(orgaos))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_deputado_orgaos(204554)

        assert len(result) == 1
        assert result[0]['sigla'] == "CCJC"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_proposicoes_recentes_uses_date_filter(self, mock_httpx):
        """Testa que get_proposicoes_recentes passa filtro de data."""
        mock_client = _make_mock_client(_make_response({"dados": []}))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        await client.get_proposicoes_recentes(dias=7)

        call_kwargs = mock_client.get.call_args
        params = call_kwargs.kwargs.get('params', call_kwargs[1].get('params', {}))
        assert 'dataApresentacaoInicio' in params
        assert 'dataApresentacaoFim' in params

    async def test_close_client(self):
        """Testa fechamento do cliente."""
        client = CamaraClient()
        client.client = AsyncMock()
        client.client.is_closed = False

        await client.close()

        client.client.aclose.assert_called_once()

    async def test_close_when_already_closed(self):
        """Testa close quando já fechado."""
        client = CamaraClient()
        client.client = AsyncMock()
        client.client.is_closed = True

        await client.close()
        client.client.aclose.assert_not_called()

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_deputado_ocupacoes(self, mock_httpx):
        """Testa endpoint: ocupações de deputado."""
        ocupacoes = {"dados": [{"titulo": "Engenheiro", "entidade": "NASA"}]}
        mock_client = _make_mock_client(_make_response(ocupacoes))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_deputado_ocupacoes(204554)

        assert isinstance(result, list)
        assert result[0]['titulo'] == "Engenheiro"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_proposicao_relacionadas(self, mock_httpx):
        """Testa endpoint: proposições relacionadas."""
        relacionadas = {"dados": [{"id": 111, "siglaTipo": "PL", "numero": 99}]}
        mock_client = _make_mock_client(_make_response(relacionadas))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_proposicao_relacionadas(2366661)

        assert len(result) == 1
        assert result[0]['siglaTipo'] == "PL"

    @patch('camara_client.httpx.AsyncClient')
    async def test_pesquisar_proposicoes_com_tema(self, mock_httpx, mock_proposicao_response):
        """Testa pesquisa de proposições com parâmetro tema."""
        mock_client = _make_mock_client(_make_response(mock_proposicao_response))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        await client.pesquisar_proposicoes(tema=62)

        call_kwargs = mock_client.get.call_args
        params = call_kwargs.kwargs.get('params', call_kwargs[1].get('params', {}))
        assert params.get('tema') == '62'

    @patch('camara_client.httpx.AsyncClient')
    async def test_pesquisar_proposicoes_tramitacao_senado(self, mock_httpx, mock_proposicao_response):
        """Testa pesquisa de proposições com tramitacaoSenado."""
        mock_client = _make_mock_client(_make_response(mock_proposicao_response))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        await client.pesquisar_proposicoes(tramitacao_senado=True)

        call_kwargs = mock_client.get.call_args
        params = call_kwargs.kwargs.get('params', call_kwargs[1].get('params', {}))
        assert params.get('tramitacaoSenado') == 'true'

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_evento_deputados(self, mock_httpx):
        """Testa endpoint: deputados em evento."""
        deps = {"dados": [{"id": 204554, "nome": "Marcos Pontes"}]}
        mock_client = _make_mock_client(_make_response(deps))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_evento_deputados(12345)

        assert len(result) == 1
        assert result[0]['nome'] == "Marcos Pontes"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_evento_votacoes(self, mock_httpx):
        """Testa endpoint: votações de evento."""
        vots = {"dados": [{"id": "v1", "descricao": "Votação teste"}]}
        mock_client = _make_mock_client(_make_response(vots))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_evento_votacoes(12345)

        assert len(result) == 1
        assert result[0]['id'] == "v1"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_votacoes_orgao(self, mock_httpx):
        """Testa endpoint: votações de órgão."""
        vots = {"dados": [{"id": "v2", "descricao": "Votação comissão"}]}
        mock_client = _make_mock_client(_make_response(vots))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_votacoes_orgao(180, data_inicio=date(2026, 1, 1), data_fim=date(2026, 1, 31))

        assert len(result) == 1

    @patch('camara_client.httpx.AsyncClient')
    async def test_lista_blocos(self, mock_httpx):
        """Testa endpoint: lista de blocos parlamentares."""
        blocos = {"dados": [{"id": 1, "nome": "Bloco X"}]}
        mock_client = _make_mock_client(_make_response(blocos))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.lista_blocos()

        assert len(result) == 1
        assert result[0]['nome'] == "Bloco X"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_bloco_detalhe(self, mock_httpx):
        """Testa endpoint: detalhe de bloco parlamentar."""
        bloco = {"dados": {"id": 1, "nome": "Bloco X", "idLegislatura": 57}}
        mock_client = _make_mock_client(_make_response(bloco))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_bloco_detalhe(1)

        assert result['nome'] == "Bloco X"

    @patch('camara_client.httpx.AsyncClient')
    async def test_lista_frentes(self, mock_httpx):
        """Testa endpoint: lista de frentes parlamentares."""
        frentes = {"dados": [{"id": 100, "titulo": "Frente da Educação"}]}
        mock_client = _make_mock_client(_make_response(frentes))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.lista_frentes(id_legislatura=57)

        assert len(result) == 1
        assert result[0]['titulo'] == "Frente da Educação"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_frente_membros(self, mock_httpx):
        """Testa endpoint: membros de frente parlamentar."""
        membros = {"dados": [{"nome": "Deputado A", "titulo": "Coordenador"}]}
        mock_client = _make_mock_client(_make_response(membros))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_frente_membros(100)

        assert len(result) == 1
        assert result[0]['titulo'] == "Coordenador"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_partido_detalhe(self, mock_httpx):
        """Testa endpoint: detalhe de partido."""
        partido = {"dados": {"id": 36899, "sigla": "PL", "nome": "Partido Liberal"}}
        mock_client = _make_mock_client(_make_response(partido))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_partido_detalhe(36899)

        assert result['sigla'] == "PL"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_partido_membros(self, mock_httpx):
        """Testa endpoint: membros de partido."""
        membros = {"dados": [{"id": 1, "nome": "Deputado X", "siglaUf": "SP"}]}
        mock_client = _make_mock_client(_make_response(membros))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_partido_membros(36899)

        assert len(result) == 1
        assert result[0]['siglaUf'] == "SP"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_legislatura_mesa(self, mock_httpx):
        """Testa endpoint: mesa diretora da legislatura."""
        mesa = {"dados": [{"nome": "Arthur Lira", "titulo": "Presidente"}]}
        mock_client = _make_mock_client(_make_response(mesa))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_legislatura_mesa(57)

        assert len(result) == 1
        assert result[0]['titulo'] == "Presidente"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_referencias_temas(self, mock_httpx):
        """Testa endpoint: referências de temas."""
        temas = {"dados": [{"cod": 62, "nome": "Educação"}]}
        mock_client = _make_mock_client(_make_response(temas))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_referencias_temas()

        assert len(result) == 1
        assert result[0]['nome'] == "Educação"

    @patch('camara_client.httpx.AsyncClient')
    async def test_get_referencias_uf(self, mock_httpx):
        """Testa endpoint: referências de UFs."""
        ufs = {"dados": [{"sigla": "SP", "nome": "São Paulo"}]}
        mock_client = _make_mock_client(_make_response(ufs))
        mock_httpx.return_value = mock_client

        client = CamaraClient()
        result = await client.get_referencias_uf()

        assert len(result) == 1
        assert result[0]['sigla'] == "SP"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)