agrobr 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. agrobr/__init__.py +10 -0
  2. agrobr/alerts/__init__.py +7 -0
  3. agrobr/alerts/notifier.py +167 -0
  4. agrobr/cache/__init__.py +31 -0
  5. agrobr/cache/duckdb_store.py +433 -0
  6. agrobr/cache/history.py +317 -0
  7. agrobr/cache/migrations.py +82 -0
  8. agrobr/cache/policies.py +240 -0
  9. agrobr/cepea/__init__.py +7 -0
  10. agrobr/cepea/api.py +360 -0
  11. agrobr/cepea/client.py +273 -0
  12. agrobr/cepea/parsers/__init__.py +37 -0
  13. agrobr/cepea/parsers/base.py +35 -0
  14. agrobr/cepea/parsers/consensus.py +300 -0
  15. agrobr/cepea/parsers/detector.py +108 -0
  16. agrobr/cepea/parsers/fingerprint.py +226 -0
  17. agrobr/cepea/parsers/v1.py +305 -0
  18. agrobr/cli.py +323 -0
  19. agrobr/conab/__init__.py +21 -0
  20. agrobr/conab/api.py +239 -0
  21. agrobr/conab/client.py +219 -0
  22. agrobr/conab/parsers/__init__.py +7 -0
  23. agrobr/conab/parsers/v1.py +383 -0
  24. agrobr/constants.py +205 -0
  25. agrobr/exceptions.py +104 -0
  26. agrobr/health/__init__.py +23 -0
  27. agrobr/health/checker.py +202 -0
  28. agrobr/health/reporter.py +314 -0
  29. agrobr/http/__init__.py +9 -0
  30. agrobr/http/browser.py +214 -0
  31. agrobr/http/rate_limiter.py +69 -0
  32. agrobr/http/retry.py +93 -0
  33. agrobr/http/user_agents.py +67 -0
  34. agrobr/ibge/__init__.py +19 -0
  35. agrobr/ibge/api.py +273 -0
  36. agrobr/ibge/client.py +256 -0
  37. agrobr/models.py +85 -0
  38. agrobr/normalize/__init__.py +64 -0
  39. agrobr/normalize/dates.py +303 -0
  40. agrobr/normalize/encoding.py +102 -0
  41. agrobr/normalize/regions.py +308 -0
  42. agrobr/normalize/units.py +278 -0
  43. agrobr/noticias_agricolas/__init__.py +6 -0
  44. agrobr/noticias_agricolas/client.py +222 -0
  45. agrobr/noticias_agricolas/parser.py +187 -0
  46. agrobr/sync.py +147 -0
  47. agrobr/telemetry/__init__.py +17 -0
  48. agrobr/telemetry/collector.py +153 -0
  49. agrobr/utils/__init__.py +5 -0
  50. agrobr/utils/logging.py +59 -0
  51. agrobr/validators/__init__.py +35 -0
  52. agrobr/validators/sanity.py +286 -0
  53. agrobr/validators/structural.py +313 -0
  54. agrobr-0.1.0.dist-info/METADATA +243 -0
  55. agrobr-0.1.0.dist-info/RECORD +58 -0
  56. agrobr-0.1.0.dist-info/WHEEL +4 -0
  57. agrobr-0.1.0.dist-info/entry_points.txt +2 -0
  58. agrobr-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,305 @@
1
+ """Parser v1 para indicadores CEPEA - Layout 2024."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from datetime import date, datetime
7
+ from decimal import Decimal, InvalidOperation
8
+ from typing import Any
9
+
10
+ import structlog
11
+ from bs4 import BeautifulSoup
12
+
13
+ from agrobr.constants import Fonte
14
+ from agrobr.exceptions import ParseError
15
+ from agrobr.models import Indicador
16
+
17
+ from .base import BaseParser
18
+ from .fingerprint import extract_fingerprint
19
+
20
+ logger = structlog.get_logger()
21
+
22
+
23
+ class CepeaParserV1(BaseParser):
24
+ """Parser para layout CEPEA 2024."""
25
+
26
+ version = 1
27
+ source = "cepea"
28
+ valid_from = date(2024, 1, 1)
29
+ valid_until = None
30
+
31
+ def can_parse(self, html: str) -> tuple[bool, float]:
32
+ """Verifica se este parser consegue processar o HTML."""
33
+ soup = BeautifulSoup(html, "lxml")
34
+
35
+ confidence = 0.0
36
+ checks_passed = 0
37
+ total_checks = 5
38
+
39
+ tables = soup.find_all("table")
40
+ if tables:
41
+ checks_passed += 1
42
+
43
+ indicador_table = soup.find("table", id=re.compile(r"indicador|preco|cotacao", re.I))
44
+ if not indicador_table:
45
+ indicador_table = soup.find(
46
+ "table", class_=re.compile(r"indicador|preco|cotacao", re.I)
47
+ )
48
+ if indicador_table:
49
+ checks_passed += 1
50
+
51
+ headers = soup.find_all("th")
52
+ header_texts = [th.get_text(strip=True).lower() for th in headers]
53
+ date_keywords = ["data", "dia", "date"]
54
+ value_keywords = ["valor", "preço", "preco", "price", "r$"]
55
+
56
+ if any(kw in " ".join(header_texts) for kw in date_keywords):
57
+ checks_passed += 1
58
+ if any(kw in " ".join(header_texts) for kw in value_keywords):
59
+ checks_passed += 1
60
+
61
+ cepea_indicators = soup.find_all(string=re.compile(r"cepea|esalq|indicador", re.I))
62
+ if cepea_indicators:
63
+ checks_passed += 1
64
+
65
+ confidence = checks_passed / total_checks
66
+
67
+ can_parse = confidence >= 0.4
68
+ logger.debug(
69
+ "can_parse_check",
70
+ parser_version=self.version,
71
+ confidence=confidence,
72
+ checks_passed=checks_passed,
73
+ total_checks=total_checks,
74
+ )
75
+
76
+ return can_parse, confidence
77
+
78
+ def parse(self, html: str, produto: str) -> list[Indicador]:
79
+ """Parseia HTML e retorna lista de indicadores."""
80
+ soup = BeautifulSoup(html, "lxml")
81
+ indicadores: list[Indicador] = []
82
+
83
+ tables = soup.find_all("table")
84
+ if not tables:
85
+ raise ParseError(
86
+ source=self.source,
87
+ parser_version=self.version,
88
+ reason="No tables found in HTML",
89
+ html_snippet=html[:500],
90
+ )
91
+
92
+ data_table = self._find_data_table(soup)
93
+ if not data_table:
94
+ raise ParseError(
95
+ source=self.source,
96
+ parser_version=self.version,
97
+ reason="Could not identify data table",
98
+ html_snippet=html[:500],
99
+ )
100
+
101
+ headers = self._extract_headers(data_table)
102
+ rows = data_table.find_all("tr")[1:]
103
+
104
+ for row in rows:
105
+ cells = row.find_all(["td", "th"])
106
+ if len(cells) < 2:
107
+ continue
108
+
109
+ try:
110
+ indicador = self._parse_row(cells, headers, produto)
111
+ if indicador:
112
+ indicadores.append(indicador)
113
+ except (ValueError, InvalidOperation) as e:
114
+ logger.debug(
115
+ "row_parse_failed",
116
+ error=str(e),
117
+ cells=[c.get_text(strip=True) for c in cells],
118
+ )
119
+ continue
120
+
121
+ if not indicadores:
122
+ raise ParseError(
123
+ source=self.source,
124
+ parser_version=self.version,
125
+ reason="No valid indicators extracted",
126
+ html_snippet=html[:500],
127
+ )
128
+
129
+ logger.info(
130
+ "parse_success",
131
+ source=self.source,
132
+ parser_version=self.version,
133
+ records_count=len(indicadores),
134
+ )
135
+
136
+ return indicadores
137
+
138
+ def extract_fingerprint(self, html: str) -> dict[str, Any]:
139
+ """Extrai assinatura estrutural do HTML."""
140
+ fp = extract_fingerprint(html, Fonte.CEPEA, "internal")
141
+ return fp.model_dump()
142
+
143
+ def _find_data_table(self, soup: BeautifulSoup) -> Any | None:
144
+ """Encontra a tabela com dados de indicadores."""
145
+ table = soup.find("table", id=re.compile(r"indicador|preco|cotacao|dados", re.I))
146
+ if table:
147
+ return table
148
+
149
+ table = soup.find("table", class_=re.compile(r"indicador|preco|cotacao|dados|table", re.I))
150
+ if table:
151
+ return table
152
+
153
+ tables = soup.find_all("table")
154
+ for table in tables:
155
+ headers = table.find_all("th")
156
+ header_text = " ".join(th.get_text(strip=True).lower() for th in headers)
157
+ if "data" in header_text and ("valor" in header_text or "r$" in header_text):
158
+ return table
159
+
160
+ if tables:
161
+ largest_table = max(tables, key=lambda t: len(t.find_all("tr")))
162
+ if len(largest_table.find_all("tr")) >= 3:
163
+ return largest_table
164
+
165
+ return None
166
+
167
+ def _extract_headers(self, table: Any) -> list[str]:
168
+ """Extrai headers da tabela."""
169
+ headers: list[str] = []
170
+ header_row = table.find("tr")
171
+
172
+ if header_row:
173
+ for cell in header_row.find_all(["th", "td"]):
174
+ text = cell.get_text(strip=True).lower()
175
+ text = re.sub(r"\s+", " ", text)
176
+ headers.append(text)
177
+
178
+ return headers
179
+
180
+ def _parse_row(self, cells: list[Any], headers: list[str], produto: str) -> Indicador | None:
181
+ """Parseia uma linha da tabela."""
182
+ cell_texts = [c.get_text(strip=True) for c in cells]
183
+
184
+ data_value = None
185
+ valor_value = None
186
+ variacao_value = None
187
+
188
+ for _i, (header, cell_text) in enumerate(zip(headers, cell_texts)):
189
+ header_lower = header.lower()
190
+
191
+ if any(kw in header_lower for kw in ["data", "dia", "date"]):
192
+ data_value = self._parse_date(cell_text)
193
+ elif any(kw in header_lower for kw in ["valor", "preço", "preco", "r$", "price"]):
194
+ valor_value = self._parse_decimal(cell_text)
195
+ elif "var" in header_lower or "%" in header_lower:
196
+ variacao_value = cell_text
197
+
198
+ if not data_value and cell_texts:
199
+ data_value = self._parse_date(cell_texts[0])
200
+
201
+ if not valor_value and len(cell_texts) > 1:
202
+ for text in cell_texts[1:]:
203
+ parsed = self._parse_decimal(text)
204
+ if parsed and parsed > 0:
205
+ valor_value = parsed
206
+ break
207
+
208
+ if not data_value or not valor_value:
209
+ return None
210
+
211
+ unidade = self._detect_unidade(produto, headers)
212
+
213
+ return Indicador(
214
+ fonte=Fonte.CEPEA,
215
+ produto=produto,
216
+ praca=None,
217
+ data=data_value,
218
+ valor=valor_value,
219
+ unidade=unidade,
220
+ metodologia="indicador_esalq",
221
+ revisao=0,
222
+ meta={"variacao": variacao_value} if variacao_value else {},
223
+ parser_version=self.version,
224
+ )
225
+
226
+ def _parse_date(self, text: str) -> date | None:
227
+ """Parseia data de diferentes formatos."""
228
+ text = text.strip()
229
+
230
+ patterns = [
231
+ (r"(\d{2})/(\d{2})/(\d{4})", "%d/%m/%Y"),
232
+ (r"(\d{2})-(\d{2})-(\d{4})", "%d-%m-%Y"),
233
+ (r"(\d{4})-(\d{2})-(\d{2})", "%Y-%m-%d"),
234
+ (r"(\d{2})/(\d{2})/(\d{2})", "%d/%m/%y"),
235
+ ]
236
+
237
+ for pattern, date_format in patterns:
238
+ match = re.search(pattern, text)
239
+ if match:
240
+ try:
241
+ return datetime.strptime(match.group(), date_format).date()
242
+ except ValueError:
243
+ continue
244
+
245
+ return None
246
+
247
+ def _parse_decimal(self, text: str) -> Decimal | None:
248
+ """Parseia valor decimal."""
249
+ text = text.strip()
250
+
251
+ text = re.sub(r"[R$\s]", "", text)
252
+
253
+ if "," in text and "." in text:
254
+ text = text.replace(".", "").replace(",", ".")
255
+ elif "," in text:
256
+ text = text.replace(",", ".")
257
+
258
+ text = re.sub(r"[^\d.\-]", "", text)
259
+
260
+ if not text or text == "." or text == "-":
261
+ return None
262
+
263
+ try:
264
+ value = Decimal(text)
265
+ return value if value > 0 else None
266
+ except InvalidOperation:
267
+ return None
268
+
269
+ def _detect_unidade(self, produto: str, headers: list[str]) -> str:
270
+ """Detecta unidade baseado no produto e headers."""
271
+ produto_lower = produto.lower()
272
+
273
+ unidades_produto = {
274
+ "soja": "BRL/sc60kg",
275
+ "milho": "BRL/sc60kg",
276
+ "cafe": "BRL/sc60kg",
277
+ "trigo": "BRL/sc60kg",
278
+ "arroz": "BRL/sc50kg",
279
+ "boi": "BRL/@",
280
+ "boi_gordo": "BRL/@",
281
+ "boi-gordo": "BRL/@",
282
+ "algodao": "BRL/@",
283
+ "frango": "BRL/kg",
284
+ "suino": "BRL/kg",
285
+ "acucar": "BRL/sc50kg",
286
+ "etanol": "BRL/L",
287
+ }
288
+
289
+ for key, unidade in unidades_produto.items():
290
+ if key in produto_lower:
291
+ return unidade
292
+
293
+ header_text = " ".join(headers).lower()
294
+ if "sc" in header_text or "saca" in header_text:
295
+ if "50" in header_text:
296
+ return "BRL/sc50kg"
297
+ return "BRL/sc60kg"
298
+ if "@" in header_text or "arroba" in header_text:
299
+ return "BRL/@"
300
+ if "kg" in header_text:
301
+ return "BRL/kg"
302
+ if "litro" in header_text or "/l" in header_text:
303
+ return "BRL/L"
304
+
305
+ return "BRL/sc60kg"
agrobr/cli.py ADDED
@@ -0,0 +1,323 @@
1
+ """CLI do agrobr usando Typer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ import typer
9
+
10
+ from agrobr import __version__, constants
11
+
12
+ app = typer.Typer(
13
+ name="agrobr",
14
+ help="Dados agricolas brasileiros em uma linha de codigo",
15
+ add_completion=False,
16
+ )
17
+
18
+
19
+ def version_callback(value: bool) -> None:
20
+ if value:
21
+ typer.echo(f"agrobr version {__version__}")
22
+ raise typer.Exit()
23
+
24
+
25
+ @app.callback() # type: ignore[misc]
26
+ def main(
27
+ _version: bool = typer.Option(
28
+ None,
29
+ "--version",
30
+ "-v",
31
+ help="Mostra a versao e sai",
32
+ callback=version_callback,
33
+ is_eager=True,
34
+ ),
35
+ ) -> None:
36
+ """agrobr - Dados agricolas brasileiros."""
37
+ pass
38
+
39
+
40
+ cepea_app = typer.Typer(help="Indicadores CEPEA")
41
+ app.add_typer(cepea_app, name="cepea")
42
+
43
+
44
+ @cepea_app.command("indicador") # type: ignore[misc]
45
+ def cepea_indicador(
46
+ produto: str = typer.Argument(..., help="Produto (soja, milho, cafe, boi, etc)"),
47
+ _inicio: str | None = typer.Option(None, "--inicio", "-i", help="Data inicio (YYYY-MM-DD)"),
48
+ _fim: str | None = typer.Option(None, "--fim", "-f", help="Data fim (YYYY-MM-DD)"),
49
+ _ultimo: bool = typer.Option(False, "--ultimo", "-u", help="Apenas ultimo valor"),
50
+ _formato: str = typer.Option("table", "--formato", "-o", help="Formato: table, csv, json"),
51
+ ) -> None:
52
+ """Consulta indicador CEPEA."""
53
+ typer.echo(f"Consultando {produto}...")
54
+ typer.echo("Funcionalidade em desenvolvimento")
55
+
56
+
57
+ @app.command("health") # type: ignore[misc]
58
+ def health(
59
+ _all_sources: bool = typer.Option(False, "--all", "-a", help="Verifica todas as fontes"),
60
+ _source: str | None = typer.Option(None, "--source", "-s", help="Fonte especifica"),
61
+ output: str = typer.Option("text", "--output", "-o", help="Formato: text, json"),
62
+ ) -> None:
63
+ """Executa health checks."""
64
+ typer.echo("Health check em desenvolvimento")
65
+
66
+ if output == "json":
67
+ result = {"status": "ok", "checks": []}
68
+ typer.echo(json.dumps(result, indent=2))
69
+
70
+
71
+ cache_app = typer.Typer(help="Gerenciamento de cache")
72
+ app.add_typer(cache_app, name="cache")
73
+
74
+
75
+ @cache_app.command("status") # type: ignore[misc]
76
+ def cache_status() -> None:
77
+ """Mostra status do cache."""
78
+ typer.echo("Status do cache em desenvolvimento")
79
+
80
+
81
+ @cache_app.command("clear") # type: ignore[misc]
82
+ def cache_clear(
83
+ _source: str | None = typer.Option(None, "--source", "-s", help="Limpar apenas fonte"),
84
+ _older_than: str | None = typer.Option(None, "--older-than", help="Ex: 30d"),
85
+ ) -> None:
86
+ """Limpa o cache."""
87
+ typer.echo("Limpeza de cache em desenvolvimento")
88
+
89
+
90
+ conab_app = typer.Typer(help="Dados CONAB - Safras e balanco")
91
+ app.add_typer(conab_app, name="conab")
92
+
93
+
94
+ @conab_app.command("safras") # type: ignore[misc]
95
+ def conab_safras(
96
+ produto: str = typer.Argument(..., help="Produto (soja, milho, arroz, feijao, etc)"),
97
+ safra: str | None = typer.Option(None, "--safra", "-s", help="Safra (ex: 2025/26)"),
98
+ uf: str | None = typer.Option(None, "--uf", "-u", help="Filtrar por UF"),
99
+ formato: str = typer.Option("table", "--formato", "-o", help="Formato: table, csv, json"),
100
+ ) -> None:
101
+ """Consulta dados de safra por produto."""
102
+ import asyncio
103
+
104
+ from agrobr import conab
105
+
106
+ typer.echo(f"Consultando safras de {produto}...")
107
+
108
+ try:
109
+ df = asyncio.run(conab.safras(produto=produto, safra=safra, uf=uf))
110
+
111
+ if df.empty:
112
+ typer.echo("Nenhum dado encontrado")
113
+ return
114
+
115
+ if formato == "json":
116
+ typer.echo(df.to_json(orient="records", indent=2))
117
+ elif formato == "csv":
118
+ typer.echo(df.to_csv(index=False))
119
+ else:
120
+ typer.echo(df.to_string(index=False))
121
+
122
+ except Exception as e:
123
+ typer.echo(f"Erro: {e}", err=True)
124
+ raise typer.Exit(1) from None
125
+
126
+
127
+ @conab_app.command("balanco") # type: ignore[misc]
128
+ def conab_balanco(
129
+ produto: str | None = typer.Argument(None, help="Produto (opcional)"),
130
+ formato: str = typer.Option("table", "--formato", "-o", help="Formato: table, csv, json"),
131
+ ) -> None:
132
+ """Consulta balanco de oferta e demanda."""
133
+ import asyncio
134
+
135
+ from agrobr import conab
136
+
137
+ typer.echo("Consultando balanco oferta/demanda...")
138
+
139
+ try:
140
+ df = asyncio.run(conab.balanco(produto=produto))
141
+
142
+ if df.empty:
143
+ typer.echo("Nenhum dado encontrado")
144
+ return
145
+
146
+ if formato == "json":
147
+ typer.echo(df.to_json(orient="records", indent=2))
148
+ elif formato == "csv":
149
+ typer.echo(df.to_csv(index=False))
150
+ else:
151
+ typer.echo(df.to_string(index=False))
152
+
153
+ except Exception as e:
154
+ typer.echo(f"Erro: {e}", err=True)
155
+ raise typer.Exit(1) from None
156
+
157
+
158
+ @conab_app.command("levantamentos") # type: ignore[misc]
159
+ def conab_levantamentos() -> None:
160
+ """Lista levantamentos disponiveis."""
161
+ import asyncio
162
+
163
+ from agrobr import conab
164
+
165
+ typer.echo("Listando levantamentos...")
166
+
167
+ try:
168
+ levs = asyncio.run(conab.levantamentos())
169
+
170
+ for lev in levs[:10]:
171
+ typer.echo(f" {lev['safra']} - {lev['levantamento']}o levantamento")
172
+
173
+ if len(levs) > 10:
174
+ typer.echo(f" ... e mais {len(levs) - 10} levantamentos")
175
+
176
+ except Exception as e:
177
+ typer.echo(f"Erro: {e}", err=True)
178
+ raise typer.Exit(1) from None
179
+
180
+
181
+ @conab_app.command("produtos") # type: ignore[misc]
182
+ def conab_produtos() -> None:
183
+ """Lista produtos disponiveis."""
184
+ import asyncio
185
+
186
+ from agrobr import conab
187
+
188
+ prods = asyncio.run(conab.produtos())
189
+ typer.echo("Produtos disponiveis:")
190
+ for prod in prods:
191
+ typer.echo(f" - {prod}")
192
+
193
+
194
+ # =============================================================================
195
+ # IBGE Commands
196
+ # =============================================================================
197
+
198
+ ibge_app = typer.Typer(help="Dados IBGE - PAM e LSPA")
199
+ app.add_typer(ibge_app, name="ibge")
200
+
201
+
202
+ @ibge_app.command("pam") # type: ignore[misc]
203
+ def ibge_pam(
204
+ produto: str = typer.Argument(..., help="Produto (soja, milho, arroz, etc)"),
205
+ ano: str | None = typer.Option(
206
+ None, "--ano", "-a", help="Ano ou anos (ex: 2023 ou 2020,2021,2022)"
207
+ ),
208
+ uf: str | None = typer.Option(None, "--uf", "-u", help="Filtrar por UF"),
209
+ nivel: str = typer.Option("uf", "--nivel", "-n", help="Nivel: brasil, uf, municipio"),
210
+ formato: str = typer.Option("table", "--formato", "-o", help="Formato: table, csv, json"),
211
+ ) -> None:
212
+ """Consulta dados da Producao Agricola Municipal (PAM)."""
213
+ import asyncio
214
+
215
+ from agrobr import ibge
216
+
217
+ typer.echo(f"Consultando PAM para {produto}...")
218
+
219
+ try:
220
+ # Parse ano
221
+ ano_param: int | list[int] | None = None
222
+ if ano:
223
+ ano_param = [int(a.strip()) for a in ano.split(",")] if "," in ano else int(ano)
224
+
225
+ nivel_typed: Any = nivel # type validated by ibge.pam at runtime
226
+ df = asyncio.run(ibge.pam(produto=produto, ano=ano_param, uf=uf, nivel=nivel_typed))
227
+
228
+ if df.empty:
229
+ typer.echo("Nenhum dado encontrado")
230
+ return
231
+
232
+ if formato == "json":
233
+ typer.echo(df.to_json(orient="records", indent=2))
234
+ elif formato == "csv":
235
+ typer.echo(df.to_csv(index=False))
236
+ else:
237
+ typer.echo(df.to_string(index=False))
238
+
239
+ except Exception as e:
240
+ typer.echo(f"Erro: {e}", err=True)
241
+ raise typer.Exit(1) from None
242
+
243
+
244
+ @ibge_app.command("lspa") # type: ignore[misc]
245
+ def ibge_lspa(
246
+ produto: str = typer.Argument(..., help="Produto (soja, milho_1, milho_2, etc)"),
247
+ ano: int | None = typer.Option(None, "--ano", "-a", help="Ano de referencia"),
248
+ mes: int | None = typer.Option(None, "--mes", "-m", help="Mes (1-12)"),
249
+ uf: str | None = typer.Option(None, "--uf", "-u", help="Filtrar por UF"),
250
+ formato: str = typer.Option("table", "--formato", "-o", help="Formato: table, csv, json"),
251
+ ) -> None:
252
+ """Consulta dados do Levantamento Sistematico da Producao Agricola (LSPA)."""
253
+ import asyncio
254
+
255
+ from agrobr import ibge
256
+
257
+ typer.echo(f"Consultando LSPA para {produto}...")
258
+
259
+ try:
260
+ df = asyncio.run(ibge.lspa(produto=produto, ano=ano, mes=mes, uf=uf))
261
+
262
+ if df.empty:
263
+ typer.echo("Nenhum dado encontrado")
264
+ return
265
+
266
+ if formato == "json":
267
+ typer.echo(df.to_json(orient="records", indent=2))
268
+ elif formato == "csv":
269
+ typer.echo(df.to_csv(index=False))
270
+ else:
271
+ typer.echo(df.to_string(index=False))
272
+
273
+ except Exception as e:
274
+ typer.echo(f"Erro: {e}", err=True)
275
+ raise typer.Exit(1) from None
276
+
277
+
278
+ @ibge_app.command("produtos") # type: ignore[misc]
279
+ def ibge_produtos(
280
+ pesquisa: str = typer.Option("pam", "--pesquisa", "-p", help="Pesquisa: pam ou lspa"),
281
+ ) -> None:
282
+ """Lista produtos disponiveis."""
283
+ import asyncio
284
+
285
+ from agrobr import ibge
286
+
287
+ if pesquisa == "pam":
288
+ prods = asyncio.run(ibge.produtos_pam())
289
+ typer.echo("Produtos disponiveis na PAM:")
290
+ else:
291
+ prods = asyncio.run(ibge.produtos_lspa())
292
+ typer.echo("Produtos disponiveis no LSPA:")
293
+
294
+ for prod in prods:
295
+ typer.echo(f" - {prod}")
296
+
297
+
298
+ config_app = typer.Typer(help="Configuracoes")
299
+ app.add_typer(config_app, name="config")
300
+
301
+
302
+ @config_app.command("show") # type: ignore[misc]
303
+ def config_show() -> None:
304
+ """Mostra configuracoes atuais."""
305
+ typer.echo("=== Cache Settings ===")
306
+ settings = constants.CacheSettings()
307
+ typer.echo(f" cache_dir: {settings.cache_dir}")
308
+ typer.echo(f" db_name: {settings.db_name}")
309
+ typer.echo(f" ttl_cepea_diario: {settings.ttl_cepea_diario}s")
310
+
311
+ typer.echo("\n=== HTTP Settings ===")
312
+ http = constants.HTTPSettings()
313
+ typer.echo(f" timeout_read: {http.timeout_read}s")
314
+ typer.echo(f" max_retries: {http.max_retries}")
315
+
316
+ typer.echo("\n=== Alert Settings ===")
317
+ alerts = constants.AlertSettings()
318
+ typer.echo(f" enabled: {alerts.enabled}")
319
+ typer.echo(f" slack_webhook: {'configured' if alerts.slack_webhook else 'not set'}")
320
+
321
+
322
+ if __name__ == "__main__":
323
+ app()
@@ -0,0 +1,21 @@
1
+ """Modulo CONAB - Dados de safras e balanco oferta/demanda."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agrobr.conab.api import (
6
+ balanco,
7
+ brasil_total,
8
+ levantamentos,
9
+ produtos,
10
+ safras,
11
+ ufs,
12
+ )
13
+
14
+ __all__ = [
15
+ "safras",
16
+ "balanco",
17
+ "brasil_total",
18
+ "levantamentos",
19
+ "produtos",
20
+ "ufs",
21
+ ]