agrobr 0.1.2__py3-none-any.whl → 0.5.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.
- agrobr/__init__.py +3 -2
- agrobr/benchmark/__init__.py +343 -0
- agrobr/cache/policies.py +3 -8
- agrobr/cepea/api.py +87 -30
- agrobr/cepea/client.py +0 -7
- agrobr/cli.py +141 -5
- agrobr/conab/api.py +72 -6
- agrobr/config.py +137 -0
- agrobr/constants.py +1 -2
- agrobr/contracts/__init__.py +186 -0
- agrobr/contracts/cepea.py +80 -0
- agrobr/contracts/conab.py +181 -0
- agrobr/contracts/ibge.py +146 -0
- agrobr/export.py +251 -0
- agrobr/health/__init__.py +10 -0
- agrobr/health/doctor.py +321 -0
- agrobr/http/browser.py +0 -9
- agrobr/ibge/api.py +104 -25
- agrobr/ibge/client.py +5 -20
- agrobr/models.py +100 -1
- agrobr/noticias_agricolas/client.py +0 -7
- agrobr/noticias_agricolas/parser.py +0 -17
- agrobr/plugins/__init__.py +205 -0
- agrobr/quality.py +319 -0
- agrobr/sla.py +249 -0
- agrobr/snapshots.py +321 -0
- agrobr/stability.py +148 -0
- agrobr/validators/semantic.py +447 -0
- {agrobr-0.1.2.dist-info → agrobr-0.5.0.dist-info}/METADATA +12 -12
- {agrobr-0.1.2.dist-info → agrobr-0.5.0.dist-info}/RECORD +33 -19
- {agrobr-0.1.2.dist-info → agrobr-0.5.0.dist-info}/WHEEL +0 -0
- {agrobr-0.1.2.dist-info → agrobr-0.5.0.dist-info}/entry_points.txt +0 -0
- {agrobr-0.1.2.dist-info → agrobr-0.5.0.dist-info}/licenses/LICENSE +0 -0
agrobr/cepea/client.py
CHANGED
|
@@ -14,9 +14,7 @@ from agrobr.normalize.encoding import decode_content
|
|
|
14
14
|
|
|
15
15
|
logger = structlog.get_logger()
|
|
16
16
|
|
|
17
|
-
# Flag para controlar uso de browser
|
|
18
17
|
_use_browser: bool = False
|
|
19
|
-
# Flag para controlar uso de fonte alternativa (Notícias Agrícolas)
|
|
20
18
|
_use_alternative_source: bool = True
|
|
21
19
|
|
|
22
20
|
|
|
@@ -141,7 +139,6 @@ async def fetch_indicador_page(
|
|
|
141
139
|
Raises:
|
|
142
140
|
SourceUnavailableError: Se não conseguir acessar após todos os fallbacks
|
|
143
141
|
"""
|
|
144
|
-
# Se forçar fonte alternativa, vai direto para Notícias Agrícolas
|
|
145
142
|
if force_alternative:
|
|
146
143
|
return await _fetch_with_alternative_source(produto)
|
|
147
144
|
|
|
@@ -157,7 +154,6 @@ async def fetch_indicador_page(
|
|
|
157
154
|
|
|
158
155
|
last_error: str = ""
|
|
159
156
|
|
|
160
|
-
# Passo 1: Tenta httpx (a menos que force_browser)
|
|
161
157
|
if not force_browser:
|
|
162
158
|
try:
|
|
163
159
|
return await _fetch_with_httpx(url, headers)
|
|
@@ -170,7 +166,6 @@ async def fetch_indicador_page(
|
|
|
170
166
|
error=last_error,
|
|
171
167
|
)
|
|
172
168
|
|
|
173
|
-
# Passo 2: Tenta browser (se habilitado)
|
|
174
169
|
if _use_browser:
|
|
175
170
|
try:
|
|
176
171
|
return await _fetch_with_browser(produto)
|
|
@@ -183,7 +178,6 @@ async def fetch_indicador_page(
|
|
|
183
178
|
error=last_error,
|
|
184
179
|
)
|
|
185
180
|
|
|
186
|
-
# Passo 3: Tenta fonte alternativa (se habilitado)
|
|
187
181
|
if _use_alternative_source:
|
|
188
182
|
try:
|
|
189
183
|
return await _fetch_with_alternative_source(produto)
|
|
@@ -196,7 +190,6 @@ async def fetch_indicador_page(
|
|
|
196
190
|
error=last_error,
|
|
197
191
|
)
|
|
198
192
|
|
|
199
|
-
# Todos os métodos falharam
|
|
200
193
|
logger.error(
|
|
201
194
|
"all_methods_failed",
|
|
202
195
|
source="cepea",
|
agrobr/cli.py
CHANGED
|
@@ -68,6 +68,29 @@ def health(
|
|
|
68
68
|
typer.echo(json.dumps(result, indent=2))
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
@app.command("doctor") # type: ignore[misc]
|
|
72
|
+
def doctor(
|
|
73
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Mostra informacoes detalhadas"),
|
|
74
|
+
json_output: bool = typer.Option(False, "--json", help="Output em formato JSON"),
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Diagnostica saude do sistema, conectividade e cache."""
|
|
77
|
+
import asyncio
|
|
78
|
+
|
|
79
|
+
from agrobr.health.doctor import run_diagnostics
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
result = asyncio.run(run_diagnostics(verbose=verbose))
|
|
83
|
+
|
|
84
|
+
if json_output:
|
|
85
|
+
typer.echo(json.dumps(result.to_dict(), indent=2, ensure_ascii=False))
|
|
86
|
+
else:
|
|
87
|
+
typer.echo(result.to_rich())
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
typer.echo(f"Erro ao executar diagnostico: {e}", err=True)
|
|
91
|
+
raise typer.Exit(1) from None
|
|
92
|
+
|
|
93
|
+
|
|
71
94
|
cache_app = typer.Typer(help="Gerenciamento de cache")
|
|
72
95
|
app.add_typer(cache_app, name="cache")
|
|
73
96
|
|
|
@@ -191,10 +214,6 @@ def conab_produtos() -> None:
|
|
|
191
214
|
typer.echo(f" - {prod}")
|
|
192
215
|
|
|
193
216
|
|
|
194
|
-
# =============================================================================
|
|
195
|
-
# IBGE Commands
|
|
196
|
-
# =============================================================================
|
|
197
|
-
|
|
198
217
|
ibge_app = typer.Typer(help="Dados IBGE - PAM e LSPA")
|
|
199
218
|
app.add_typer(ibge_app, name="ibge")
|
|
200
219
|
|
|
@@ -217,7 +236,6 @@ def ibge_pam(
|
|
|
217
236
|
typer.echo(f"Consultando PAM para {produto}...")
|
|
218
237
|
|
|
219
238
|
try:
|
|
220
|
-
# Parse ano
|
|
221
239
|
ano_param: int | list[int] | None = None
|
|
222
240
|
if ano:
|
|
223
241
|
ano_param = [int(a.strip()) for a in ano.split(",")] if "," in ano else int(ano)
|
|
@@ -298,6 +316,9 @@ def ibge_produtos(
|
|
|
298
316
|
config_app = typer.Typer(help="Configuracoes")
|
|
299
317
|
app.add_typer(config_app, name="config")
|
|
300
318
|
|
|
319
|
+
snapshot_app = typer.Typer(help="Gerenciamento de snapshots para modo deterministico")
|
|
320
|
+
app.add_typer(snapshot_app, name="snapshot")
|
|
321
|
+
|
|
301
322
|
|
|
302
323
|
@config_app.command("show") # type: ignore[misc]
|
|
303
324
|
def config_show() -> None:
|
|
@@ -319,5 +340,120 @@ def config_show() -> None:
|
|
|
319
340
|
typer.echo(f" slack_webhook: {'configured' if alerts.slack_webhook else 'not set'}")
|
|
320
341
|
|
|
321
342
|
|
|
343
|
+
@snapshot_app.command("list") # type: ignore[misc]
|
|
344
|
+
def snapshot_list(
|
|
345
|
+
json_output: bool = typer.Option(False, "--json", help="Output em formato JSON"),
|
|
346
|
+
) -> None:
|
|
347
|
+
"""Lista todos os snapshots disponiveis."""
|
|
348
|
+
from agrobr.snapshots import list_snapshots
|
|
349
|
+
|
|
350
|
+
snapshots = list_snapshots()
|
|
351
|
+
|
|
352
|
+
if not snapshots:
|
|
353
|
+
typer.echo("Nenhum snapshot encontrado.")
|
|
354
|
+
typer.echo("Use 'agrobr snapshot create' para criar um snapshot.")
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
if json_output:
|
|
358
|
+
data = [
|
|
359
|
+
{
|
|
360
|
+
"name": s.name,
|
|
361
|
+
"created_at": s.created_at.isoformat(),
|
|
362
|
+
"size_mb": round(s.size_bytes / 1024 / 1024, 2),
|
|
363
|
+
"sources": s.sources,
|
|
364
|
+
"files": s.file_count,
|
|
365
|
+
}
|
|
366
|
+
for s in snapshots
|
|
367
|
+
]
|
|
368
|
+
typer.echo(json.dumps(data, indent=2))
|
|
369
|
+
else:
|
|
370
|
+
typer.echo("Snapshots disponiveis:")
|
|
371
|
+
typer.echo("-" * 60)
|
|
372
|
+
for s in snapshots:
|
|
373
|
+
size_mb = s.size_bytes / 1024 / 1024
|
|
374
|
+
typer.echo(f" {s.name}")
|
|
375
|
+
typer.echo(f" Criado em: {s.created_at.strftime('%Y-%m-%d %H:%M')}")
|
|
376
|
+
typer.echo(f" Tamanho: {size_mb:.2f} MB")
|
|
377
|
+
typer.echo(f" Fontes: {', '.join(s.sources)}")
|
|
378
|
+
typer.echo(f" Arquivos: {s.file_count}")
|
|
379
|
+
typer.echo()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@snapshot_app.command("create") # type: ignore[misc]
|
|
383
|
+
def snapshot_create(
|
|
384
|
+
name: str | None = typer.Argument(None, help="Nome do snapshot (default: data atual)"),
|
|
385
|
+
sources: str | None = typer.Option(
|
|
386
|
+
None, "--sources", "-s", help="Fontes a incluir (ex: cepea,conab,ibge)"
|
|
387
|
+
),
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Cria um novo snapshot dos dados atuais."""
|
|
390
|
+
import asyncio
|
|
391
|
+
|
|
392
|
+
from agrobr.snapshots import create_snapshot
|
|
393
|
+
|
|
394
|
+
source_list = sources.split(",") if sources else None
|
|
395
|
+
|
|
396
|
+
typer.echo(f"Criando snapshot{f' {name}' if name else ''}...")
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
info = asyncio.run(create_snapshot(name=name, sources=source_list))
|
|
400
|
+
typer.echo("Snapshot criado com sucesso!")
|
|
401
|
+
typer.echo(f" Nome: {info.name}")
|
|
402
|
+
typer.echo(f" Caminho: {info.path}")
|
|
403
|
+
typer.echo(f" Arquivos: {info.file_count}")
|
|
404
|
+
except ValueError as e:
|
|
405
|
+
typer.echo(f"Erro: {e}", err=True)
|
|
406
|
+
raise typer.Exit(1) from None
|
|
407
|
+
except Exception as e:
|
|
408
|
+
typer.echo(f"Erro ao criar snapshot: {e}", err=True)
|
|
409
|
+
raise typer.Exit(1) from None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@snapshot_app.command("delete") # type: ignore[misc]
|
|
413
|
+
def snapshot_delete(
|
|
414
|
+
name: str = typer.Argument(..., help="Nome do snapshot a remover"),
|
|
415
|
+
force: bool = typer.Option(False, "--force", "-f", help="Nao pedir confirmacao"),
|
|
416
|
+
) -> None:
|
|
417
|
+
"""Remove um snapshot."""
|
|
418
|
+
from agrobr.snapshots import delete_snapshot, get_snapshot
|
|
419
|
+
|
|
420
|
+
snapshot = get_snapshot(name)
|
|
421
|
+
if not snapshot:
|
|
422
|
+
typer.echo(f"Snapshot '{name}' nao encontrado.", err=True)
|
|
423
|
+
raise typer.Exit(1)
|
|
424
|
+
|
|
425
|
+
if not force:
|
|
426
|
+
confirm = typer.confirm(f"Remover snapshot '{name}'?")
|
|
427
|
+
if not confirm:
|
|
428
|
+
typer.echo("Operacao cancelada.")
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
if delete_snapshot(name):
|
|
432
|
+
typer.echo(f"Snapshot '{name}' removido com sucesso.")
|
|
433
|
+
else:
|
|
434
|
+
typer.echo("Erro ao remover snapshot.", err=True)
|
|
435
|
+
raise typer.Exit(1)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@snapshot_app.command("use") # type: ignore[misc]
|
|
439
|
+
def snapshot_use(
|
|
440
|
+
name: str = typer.Argument(..., help="Nome do snapshot a usar"),
|
|
441
|
+
) -> None:
|
|
442
|
+
"""Configura agrobr para usar um snapshot (modo deterministico)."""
|
|
443
|
+
from agrobr.config import set_mode
|
|
444
|
+
from agrobr.snapshots import get_snapshot
|
|
445
|
+
|
|
446
|
+
snapshot = get_snapshot(name)
|
|
447
|
+
if not snapshot:
|
|
448
|
+
typer.echo(f"Snapshot '{name}' nao encontrado.", err=True)
|
|
449
|
+
typer.echo("Use 'agrobr snapshot list' para ver snapshots disponiveis.")
|
|
450
|
+
raise typer.Exit(1)
|
|
451
|
+
|
|
452
|
+
set_mode("deterministic", snapshot=name)
|
|
453
|
+
typer.echo(f"Modo deterministico ativado com snapshot '{name}'.")
|
|
454
|
+
typer.echo("Todas as chamadas usarao dados do snapshot.")
|
|
455
|
+
typer.echo("Use 'agrobr config mode normal' para voltar ao modo normal.")
|
|
456
|
+
|
|
457
|
+
|
|
322
458
|
if __name__ == "__main__":
|
|
323
459
|
app()
|
agrobr/conab/api.py
CHANGED
|
@@ -2,25 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Literal, overload
|
|
6
8
|
|
|
7
9
|
import pandas as pd
|
|
8
10
|
import structlog
|
|
9
11
|
|
|
10
12
|
from agrobr import constants
|
|
13
|
+
from agrobr.cache.policies import calculate_expiry
|
|
11
14
|
from agrobr.conab import client
|
|
12
15
|
from agrobr.conab.parsers.v1 import ConabParserV1
|
|
16
|
+
from agrobr.models import MetaInfo
|
|
13
17
|
|
|
14
18
|
logger = structlog.get_logger()
|
|
15
19
|
|
|
16
20
|
|
|
21
|
+
@overload
|
|
17
22
|
async def safras(
|
|
18
23
|
produto: str,
|
|
19
24
|
safra: str | None = None,
|
|
20
25
|
uf: str | None = None,
|
|
21
26
|
levantamento: int | None = None,
|
|
22
27
|
as_polars: bool = False,
|
|
23
|
-
|
|
28
|
+
*,
|
|
29
|
+
return_meta: Literal[False] = False,
|
|
30
|
+
) -> pd.DataFrame: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@overload
|
|
34
|
+
async def safras(
|
|
35
|
+
produto: str,
|
|
36
|
+
safra: str | None = None,
|
|
37
|
+
uf: str | None = None,
|
|
38
|
+
levantamento: int | None = None,
|
|
39
|
+
as_polars: bool = False,
|
|
40
|
+
*,
|
|
41
|
+
return_meta: Literal[True],
|
|
42
|
+
) -> tuple[pd.DataFrame, MetaInfo]: ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def safras(
|
|
46
|
+
produto: str,
|
|
47
|
+
safra: str | None = None,
|
|
48
|
+
uf: str | None = None,
|
|
49
|
+
levantamento: int | None = None,
|
|
50
|
+
as_polars: bool = False,
|
|
51
|
+
return_meta: bool = False,
|
|
52
|
+
) -> pd.DataFrame | tuple[pd.DataFrame, MetaInfo]:
|
|
24
53
|
"""
|
|
25
54
|
Obtém dados de safra por produto.
|
|
26
55
|
|
|
@@ -30,14 +59,23 @@ async def safras(
|
|
|
30
59
|
uf: Filtrar por UF (ex: "MT", "PR")
|
|
31
60
|
levantamento: Número do levantamento (default: mais recente)
|
|
32
61
|
as_polars: Se True, retorna polars.DataFrame
|
|
62
|
+
return_meta: Se True, retorna tupla (DataFrame, MetaInfo)
|
|
33
63
|
|
|
34
64
|
Returns:
|
|
35
|
-
DataFrame com dados de safra por UF
|
|
65
|
+
DataFrame com dados de safra por UF ou tupla (DataFrame, MetaInfo)
|
|
36
66
|
|
|
37
67
|
Example:
|
|
38
68
|
>>> df = await conab.safras('soja', safra='2025/26')
|
|
39
|
-
>>> df = await conab.safras('milho', uf='MT')
|
|
69
|
+
>>> df, meta = await conab.safras('milho', uf='MT', return_meta=True)
|
|
40
70
|
"""
|
|
71
|
+
fetch_start = time.perf_counter()
|
|
72
|
+
meta = MetaInfo(
|
|
73
|
+
source="conab",
|
|
74
|
+
source_url="https://www.conab.gov.br/info-agro/safras/graos",
|
|
75
|
+
source_method="httpx",
|
|
76
|
+
fetched_at=datetime.now(),
|
|
77
|
+
)
|
|
78
|
+
|
|
41
79
|
logger.info(
|
|
42
80
|
"conab_safras_request",
|
|
43
81
|
produto=produto,
|
|
@@ -46,8 +84,17 @@ async def safras(
|
|
|
46
84
|
levantamento=levantamento,
|
|
47
85
|
)
|
|
48
86
|
|
|
87
|
+
parse_start = time.perf_counter()
|
|
49
88
|
xlsx, metadata = await client.fetch_safra_xlsx(safra=safra, levantamento=levantamento)
|
|
50
89
|
|
|
90
|
+
if isinstance(xlsx, bytes):
|
|
91
|
+
meta.raw_content_size = len(xlsx)
|
|
92
|
+
elif hasattr(xlsx, "getbuffer"):
|
|
93
|
+
meta.raw_content_size = len(xlsx.getbuffer())
|
|
94
|
+
else:
|
|
95
|
+
meta.raw_content_size = 0
|
|
96
|
+
meta.source_url = metadata.get("url", meta.source_url)
|
|
97
|
+
|
|
51
98
|
parser = ConabParserV1()
|
|
52
99
|
safra_list = parser.parse_safra_produto(
|
|
53
100
|
xlsx=xlsx,
|
|
@@ -56,6 +103,9 @@ async def safras(
|
|
|
56
103
|
levantamento=metadata.get("levantamento"),
|
|
57
104
|
)
|
|
58
105
|
|
|
106
|
+
meta.parse_duration_ms = int((time.perf_counter() - parse_start) * 1000)
|
|
107
|
+
meta.parser_version = parser.version
|
|
108
|
+
|
|
59
109
|
if uf:
|
|
60
110
|
safra_list = [s for s in safra_list if s.uf == uf.upper()]
|
|
61
111
|
|
|
@@ -66,15 +116,29 @@ async def safras(
|
|
|
66
116
|
safra=safra,
|
|
67
117
|
uf=uf,
|
|
68
118
|
)
|
|
69
|
-
|
|
119
|
+
df = pd.DataFrame()
|
|
120
|
+
if return_meta:
|
|
121
|
+
meta.records_count = 0
|
|
122
|
+
meta.fetch_duration_ms = int((time.perf_counter() - fetch_start) * 1000)
|
|
123
|
+
return df, meta
|
|
124
|
+
return df
|
|
70
125
|
|
|
71
126
|
df = pd.DataFrame([s.model_dump() for s in safra_list])
|
|
72
127
|
|
|
128
|
+
meta.fetch_duration_ms = int((time.perf_counter() - fetch_start) * 1000)
|
|
129
|
+
meta.records_count = len(df)
|
|
130
|
+
meta.columns = df.columns.tolist()
|
|
131
|
+
meta.cache_key = f"conab:safras:{produto}:{safra or 'latest'}"
|
|
132
|
+
meta.cache_expires_at = calculate_expiry(constants.Fonte.CONAB)
|
|
133
|
+
|
|
73
134
|
if as_polars:
|
|
74
135
|
try:
|
|
75
136
|
import polars as pl
|
|
76
137
|
|
|
77
|
-
|
|
138
|
+
result_df = pl.from_pandas(df)
|
|
139
|
+
if return_meta:
|
|
140
|
+
return result_df, meta # type: ignore[return-value,no-any-return]
|
|
141
|
+
return result_df # type: ignore[return-value,no-any-return]
|
|
78
142
|
except ImportError:
|
|
79
143
|
logger.warning("polars_not_installed", fallback="pandas")
|
|
80
144
|
|
|
@@ -84,6 +148,8 @@ async def safras(
|
|
|
84
148
|
records=len(df),
|
|
85
149
|
)
|
|
86
150
|
|
|
151
|
+
if return_meta:
|
|
152
|
+
return df, meta
|
|
87
153
|
return df
|
|
88
154
|
|
|
89
155
|
|
agrobr/config.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Configuracao global do agrobr."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import date
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
_config: AgrobrConfig | None = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class AgrobrConfig:
|
|
15
|
+
"""Configuracao global do agrobr."""
|
|
16
|
+
|
|
17
|
+
mode: Literal["normal", "deterministic"] = "normal"
|
|
18
|
+
snapshot_date: date | None = None
|
|
19
|
+
snapshot_path: Path | None = None
|
|
20
|
+
|
|
21
|
+
cache_enabled: bool = True
|
|
22
|
+
cache_path: Path | None = None
|
|
23
|
+
|
|
24
|
+
network_enabled: bool = True
|
|
25
|
+
timeout_seconds: int = 30
|
|
26
|
+
|
|
27
|
+
browser_fallback: bool = False
|
|
28
|
+
alternative_source: bool = True
|
|
29
|
+
|
|
30
|
+
log_level: str = "INFO"
|
|
31
|
+
|
|
32
|
+
def is_deterministic(self) -> bool:
|
|
33
|
+
"""Verifica se esta em modo deterministico."""
|
|
34
|
+
return self.mode == "deterministic"
|
|
35
|
+
|
|
36
|
+
def get_snapshot_dir(self) -> Path:
|
|
37
|
+
"""Retorna diretorio de snapshots."""
|
|
38
|
+
if self.snapshot_path:
|
|
39
|
+
return self.snapshot_path
|
|
40
|
+
return Path.home() / ".agrobr" / "snapshots"
|
|
41
|
+
|
|
42
|
+
def get_current_snapshot_path(self) -> Path | None:
|
|
43
|
+
"""Retorna caminho do snapshot atual."""
|
|
44
|
+
if not self.snapshot_date:
|
|
45
|
+
return None
|
|
46
|
+
return self.get_snapshot_dir() / self.snapshot_date.isoformat()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def set_mode(
|
|
50
|
+
mode: Literal["normal", "deterministic"],
|
|
51
|
+
snapshot: str | date | None = None,
|
|
52
|
+
snapshot_path: str | Path | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Define modo de operacao do agrobr.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
mode: "normal" ou "deterministic"
|
|
59
|
+
snapshot: Data do snapshot (YYYY-MM-DD ou date)
|
|
60
|
+
snapshot_path: Caminho customizado para snapshots
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
agrobr.set_mode("normal")
|
|
64
|
+
agrobr.set_mode("deterministic", snapshot="2025-12-31")
|
|
65
|
+
"""
|
|
66
|
+
global _config
|
|
67
|
+
|
|
68
|
+
if isinstance(snapshot, str):
|
|
69
|
+
snapshot = date.fromisoformat(snapshot)
|
|
70
|
+
|
|
71
|
+
if isinstance(snapshot_path, str):
|
|
72
|
+
snapshot_path = Path(snapshot_path)
|
|
73
|
+
|
|
74
|
+
_config = AgrobrConfig(
|
|
75
|
+
mode=mode,
|
|
76
|
+
snapshot_date=snapshot,
|
|
77
|
+
snapshot_path=snapshot_path,
|
|
78
|
+
network_enabled=(mode == "normal"),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_config() -> AgrobrConfig:
|
|
83
|
+
"""Retorna configuracao atual."""
|
|
84
|
+
global _config
|
|
85
|
+
if _config is None:
|
|
86
|
+
_config = AgrobrConfig()
|
|
87
|
+
return _config
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def reset_config() -> None:
|
|
91
|
+
"""Reseta para configuracao padrao."""
|
|
92
|
+
global _config
|
|
93
|
+
_config = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def configure(
|
|
97
|
+
cache_enabled: bool | None = None,
|
|
98
|
+
cache_path: str | Path | None = None,
|
|
99
|
+
timeout_seconds: int | None = None,
|
|
100
|
+
browser_fallback: bool | None = None,
|
|
101
|
+
alternative_source: bool | None = None,
|
|
102
|
+
log_level: str | None = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Configura opcoes do agrobr.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
cache_enabled: Habilitar/desabilitar cache
|
|
109
|
+
cache_path: Caminho customizado para cache
|
|
110
|
+
timeout_seconds: Timeout para requisicoes HTTP
|
|
111
|
+
browser_fallback: Usar browser como fallback
|
|
112
|
+
alternative_source: Usar fontes alternativas
|
|
113
|
+
log_level: Nivel de log (DEBUG, INFO, WARNING, ERROR)
|
|
114
|
+
"""
|
|
115
|
+
config = get_config()
|
|
116
|
+
|
|
117
|
+
if cache_enabled is not None:
|
|
118
|
+
config.cache_enabled = cache_enabled
|
|
119
|
+
if cache_path is not None:
|
|
120
|
+
config.cache_path = Path(cache_path) if isinstance(cache_path, str) else cache_path
|
|
121
|
+
if timeout_seconds is not None:
|
|
122
|
+
config.timeout_seconds = timeout_seconds
|
|
123
|
+
if browser_fallback is not None:
|
|
124
|
+
config.browser_fallback = browser_fallback
|
|
125
|
+
if alternative_source is not None:
|
|
126
|
+
config.alternative_source = alternative_source
|
|
127
|
+
if log_level is not None:
|
|
128
|
+
config.log_level = log_level
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = [
|
|
132
|
+
"AgrobrConfig",
|
|
133
|
+
"set_mode",
|
|
134
|
+
"get_config",
|
|
135
|
+
"reset_config",
|
|
136
|
+
"configure",
|
|
137
|
+
]
|
agrobr/constants.py
CHANGED
|
@@ -12,7 +12,7 @@ class Fonte(StrEnum):
|
|
|
12
12
|
CEPEA = "cepea"
|
|
13
13
|
CONAB = "conab"
|
|
14
14
|
IBGE = "ibge"
|
|
15
|
-
NOTICIAS_AGRICOLAS = "noticias_agricolas"
|
|
15
|
+
NOTICIAS_AGRICOLAS = "noticias_agricolas"
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
URLS = {
|
|
@@ -35,7 +35,6 @@ URLS = {
|
|
|
35
35
|
},
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
# Mapeamento de produtos para URLs do Notícias Agrícolas (indicadores CEPEA)
|
|
39
38
|
NOTICIAS_AGRICOLAS_PRODUTOS = {
|
|
40
39
|
"soja": "soja/soja-indicador-cepea-esalq-porto-paranagua",
|
|
41
40
|
"soja_parana": "soja/indicador-cepea-esalq-soja-parana",
|