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.
- agrobr/__init__.py +10 -0
- agrobr/alerts/__init__.py +7 -0
- agrobr/alerts/notifier.py +167 -0
- agrobr/cache/__init__.py +31 -0
- agrobr/cache/duckdb_store.py +433 -0
- agrobr/cache/history.py +317 -0
- agrobr/cache/migrations.py +82 -0
- agrobr/cache/policies.py +240 -0
- agrobr/cepea/__init__.py +7 -0
- agrobr/cepea/api.py +360 -0
- agrobr/cepea/client.py +273 -0
- agrobr/cepea/parsers/__init__.py +37 -0
- agrobr/cepea/parsers/base.py +35 -0
- agrobr/cepea/parsers/consensus.py +300 -0
- agrobr/cepea/parsers/detector.py +108 -0
- agrobr/cepea/parsers/fingerprint.py +226 -0
- agrobr/cepea/parsers/v1.py +305 -0
- agrobr/cli.py +323 -0
- agrobr/conab/__init__.py +21 -0
- agrobr/conab/api.py +239 -0
- agrobr/conab/client.py +219 -0
- agrobr/conab/parsers/__init__.py +7 -0
- agrobr/conab/parsers/v1.py +383 -0
- agrobr/constants.py +205 -0
- agrobr/exceptions.py +104 -0
- agrobr/health/__init__.py +23 -0
- agrobr/health/checker.py +202 -0
- agrobr/health/reporter.py +314 -0
- agrobr/http/__init__.py +9 -0
- agrobr/http/browser.py +214 -0
- agrobr/http/rate_limiter.py +69 -0
- agrobr/http/retry.py +93 -0
- agrobr/http/user_agents.py +67 -0
- agrobr/ibge/__init__.py +19 -0
- agrobr/ibge/api.py +273 -0
- agrobr/ibge/client.py +256 -0
- agrobr/models.py +85 -0
- agrobr/normalize/__init__.py +64 -0
- agrobr/normalize/dates.py +303 -0
- agrobr/normalize/encoding.py +102 -0
- agrobr/normalize/regions.py +308 -0
- agrobr/normalize/units.py +278 -0
- agrobr/noticias_agricolas/__init__.py +6 -0
- agrobr/noticias_agricolas/client.py +222 -0
- agrobr/noticias_agricolas/parser.py +187 -0
- agrobr/sync.py +147 -0
- agrobr/telemetry/__init__.py +17 -0
- agrobr/telemetry/collector.py +153 -0
- agrobr/utils/__init__.py +5 -0
- agrobr/utils/logging.py +59 -0
- agrobr/validators/__init__.py +35 -0
- agrobr/validators/sanity.py +286 -0
- agrobr/validators/structural.py +313 -0
- agrobr-0.1.0.dist-info/METADATA +243 -0
- agrobr-0.1.0.dist-info/RECORD +58 -0
- agrobr-0.1.0.dist-info/WHEEL +4 -0
- agrobr-0.1.0.dist-info/entry_points.txt +2 -0
- agrobr-0.1.0.dist-info/licenses/LICENSE +21 -0
agrobr/ibge/api.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""API pública do módulo IBGE - PAM e LSPA."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
from agrobr.ibge import client
|
|
11
|
+
|
|
12
|
+
logger = structlog.get_logger()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def pam(
|
|
16
|
+
produto: str,
|
|
17
|
+
ano: int | str | list[int] | None = None,
|
|
18
|
+
uf: str | None = None,
|
|
19
|
+
nivel: Literal["brasil", "uf", "municipio"] = "uf",
|
|
20
|
+
variaveis: list[str] | None = None,
|
|
21
|
+
as_polars: bool = False,
|
|
22
|
+
) -> pd.DataFrame:
|
|
23
|
+
"""
|
|
24
|
+
Obtém dados da Produção Agrícola Municipal (PAM).
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
produto: Nome do produto (soja, milho, arroz, feijao, trigo, etc)
|
|
28
|
+
ano: Ano ou lista de anos (default: último disponível)
|
|
29
|
+
uf: Filtrar por UF (ex: "MT", "PR"). Requer nivel="uf" ou "municipio"
|
|
30
|
+
nivel: Nível territorial ("brasil", "uf", "municipio")
|
|
31
|
+
variaveis: Lista de variáveis (area_plantada, area_colhida, producao, rendimento)
|
|
32
|
+
as_polars: Se True, retorna polars.DataFrame
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
DataFrame com dados da PAM
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> df = await ibge.pam('soja', ano=2023)
|
|
39
|
+
>>> df = await ibge.pam('milho', ano=[2020, 2021, 2022], uf='MT')
|
|
40
|
+
"""
|
|
41
|
+
logger.info(
|
|
42
|
+
"ibge_pam_request",
|
|
43
|
+
produto=produto,
|
|
44
|
+
ano=ano,
|
|
45
|
+
uf=uf,
|
|
46
|
+
nivel=nivel,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Mapeia produto para código SIDRA
|
|
50
|
+
produto_lower = produto.lower()
|
|
51
|
+
if produto_lower not in client.PRODUTOS_PAM:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Produto não suportado: {produto}. Disponíveis: {list(client.PRODUTOS_PAM.keys())}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
produto_cod = client.PRODUTOS_PAM[produto_lower]
|
|
57
|
+
|
|
58
|
+
# Mapeia variáveis
|
|
59
|
+
if variaveis is None:
|
|
60
|
+
variaveis = ["area_plantada", "area_colhida", "producao", "rendimento"]
|
|
61
|
+
|
|
62
|
+
var_codes = []
|
|
63
|
+
for var in variaveis:
|
|
64
|
+
if var in client.VARIAVEIS:
|
|
65
|
+
var_codes.append(client.VARIAVEIS[var])
|
|
66
|
+
else:
|
|
67
|
+
logger.warning(f"Variável desconhecida: {var}")
|
|
68
|
+
|
|
69
|
+
# Mapeia nível territorial
|
|
70
|
+
nivel_map = {
|
|
71
|
+
"brasil": "1",
|
|
72
|
+
"uf": "3",
|
|
73
|
+
"municipio": "6",
|
|
74
|
+
}
|
|
75
|
+
territorial_level = nivel_map.get(nivel, "3")
|
|
76
|
+
|
|
77
|
+
# Define código territorial
|
|
78
|
+
ibge_code = "all"
|
|
79
|
+
if uf and nivel in ("uf", "municipio"):
|
|
80
|
+
ibge_code = client.uf_to_ibge_code(uf)
|
|
81
|
+
|
|
82
|
+
# Define período
|
|
83
|
+
if ano is None:
|
|
84
|
+
period = "last"
|
|
85
|
+
elif isinstance(ano, list):
|
|
86
|
+
period = ",".join(str(a) for a in ano)
|
|
87
|
+
else:
|
|
88
|
+
period = str(ano)
|
|
89
|
+
|
|
90
|
+
# Busca dados
|
|
91
|
+
df = await client.fetch_sidra(
|
|
92
|
+
table_code=client.TABELAS["pam_nova"],
|
|
93
|
+
territorial_level=territorial_level,
|
|
94
|
+
ibge_territorial_code=ibge_code,
|
|
95
|
+
variable=",".join(var_codes) if var_codes else None,
|
|
96
|
+
period=period,
|
|
97
|
+
classifications={"782": produto_cod},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Processa resposta
|
|
101
|
+
df = client.parse_sidra_response(df)
|
|
102
|
+
|
|
103
|
+
# Pivota para ter variáveis como colunas
|
|
104
|
+
if "variavel" in df.columns and "valor" in df.columns:
|
|
105
|
+
df_pivot = df.pivot_table(
|
|
106
|
+
index=["localidade", "ano"] if "localidade" in df.columns else ["ano"],
|
|
107
|
+
columns="variavel",
|
|
108
|
+
values="valor",
|
|
109
|
+
aggfunc="first",
|
|
110
|
+
).reset_index()
|
|
111
|
+
|
|
112
|
+
# Renomeia colunas para nomes mais simples
|
|
113
|
+
rename_map = {
|
|
114
|
+
"Área plantada": "area_plantada",
|
|
115
|
+
"Área colhida": "area_colhida",
|
|
116
|
+
"Quantidade produzida": "producao",
|
|
117
|
+
"Rendimento médio da produção": "rendimento",
|
|
118
|
+
"Valor da produção": "valor_producao",
|
|
119
|
+
}
|
|
120
|
+
df_pivot = df_pivot.rename(columns=rename_map)
|
|
121
|
+
df = df_pivot
|
|
122
|
+
|
|
123
|
+
df["produto"] = produto_lower
|
|
124
|
+
df["fonte"] = "ibge_pam"
|
|
125
|
+
|
|
126
|
+
if as_polars:
|
|
127
|
+
try:
|
|
128
|
+
import polars as pl
|
|
129
|
+
|
|
130
|
+
return pl.from_pandas(df) # type: ignore[no-any-return]
|
|
131
|
+
except ImportError:
|
|
132
|
+
logger.warning("polars_not_installed", fallback="pandas")
|
|
133
|
+
|
|
134
|
+
logger.info(
|
|
135
|
+
"ibge_pam_success",
|
|
136
|
+
produto=produto,
|
|
137
|
+
records=len(df),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return df
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def lspa(
|
|
144
|
+
produto: str,
|
|
145
|
+
ano: int | str | None = None,
|
|
146
|
+
mes: int | str | None = None,
|
|
147
|
+
uf: str | None = None,
|
|
148
|
+
as_polars: bool = False,
|
|
149
|
+
) -> pd.DataFrame:
|
|
150
|
+
"""
|
|
151
|
+
Obtém dados do Levantamento Sistemático da Produção Agrícola (LSPA).
|
|
152
|
+
|
|
153
|
+
O LSPA fornece estimativas mensais de safra para os principais produtos.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
produto: Nome do produto (soja, milho_1, milho_2, arroz, feijao_1, etc)
|
|
157
|
+
ano: Ano de referência (default: atual)
|
|
158
|
+
mes: Mês de referência (1-12). Se None, retorna todos os meses do ano.
|
|
159
|
+
uf: Filtrar por UF (ex: "MT", "PR")
|
|
160
|
+
as_polars: Se True, retorna polars.DataFrame
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
DataFrame com estimativas LSPA
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> df = await ibge.lspa('soja', ano=2024)
|
|
167
|
+
>>> df = await ibge.lspa('milho_1', ano=2024, mes=6, uf='PR')
|
|
168
|
+
"""
|
|
169
|
+
logger.info(
|
|
170
|
+
"ibge_lspa_request",
|
|
171
|
+
produto=produto,
|
|
172
|
+
ano=ano,
|
|
173
|
+
mes=mes,
|
|
174
|
+
uf=uf,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Mapeia produto para código SIDRA
|
|
178
|
+
produto_lower = produto.lower()
|
|
179
|
+
if produto_lower not in client.PRODUTOS_LSPA:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"Produto não suportado: {produto}. Disponíveis: {list(client.PRODUTOS_LSPA.keys())}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
produto_cod = client.PRODUTOS_LSPA[produto_lower]
|
|
185
|
+
|
|
186
|
+
# Define período
|
|
187
|
+
if ano is None:
|
|
188
|
+
from datetime import date
|
|
189
|
+
|
|
190
|
+
ano = date.today().year
|
|
191
|
+
|
|
192
|
+
# Define período
|
|
193
|
+
period = f"{ano}{int(mes):02d}" if mes else ",".join(f"{ano}{m:02d}" for m in range(1, 13))
|
|
194
|
+
|
|
195
|
+
# Define nível territorial
|
|
196
|
+
territorial_level = "3" if uf else "1"
|
|
197
|
+
ibge_code = client.uf_to_ibge_code(uf) if uf else "all"
|
|
198
|
+
|
|
199
|
+
# Busca dados (não especifica variáveis - retorna todas)
|
|
200
|
+
df = await client.fetch_sidra(
|
|
201
|
+
table_code=client.TABELAS["lspa"],
|
|
202
|
+
territorial_level=territorial_level,
|
|
203
|
+
ibge_territorial_code=ibge_code,
|
|
204
|
+
period=period,
|
|
205
|
+
classifications={"48": produto_cod},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Processa resposta
|
|
209
|
+
df = client.parse_sidra_response(df)
|
|
210
|
+
|
|
211
|
+
# Adiciona período da consulta
|
|
212
|
+
df["ano"] = ano
|
|
213
|
+
if mes:
|
|
214
|
+
df["mes"] = mes
|
|
215
|
+
|
|
216
|
+
df["produto"] = produto_lower
|
|
217
|
+
df["fonte"] = "ibge_lspa"
|
|
218
|
+
|
|
219
|
+
if as_polars:
|
|
220
|
+
try:
|
|
221
|
+
import polars as pl
|
|
222
|
+
|
|
223
|
+
return pl.from_pandas(df) # type: ignore[no-any-return]
|
|
224
|
+
except ImportError:
|
|
225
|
+
logger.warning("polars_not_installed", fallback="pandas")
|
|
226
|
+
|
|
227
|
+
logger.info(
|
|
228
|
+
"ibge_lspa_success",
|
|
229
|
+
produto=produto,
|
|
230
|
+
records=len(df),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return df
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
async def produtos_pam() -> list[str]:
|
|
237
|
+
"""
|
|
238
|
+
Lista produtos disponíveis na PAM.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Lista de nomes de produtos
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
>>> prods = await ibge.produtos_pam()
|
|
245
|
+
>>> print(prods)
|
|
246
|
+
['soja', 'milho', 'arroz', ...]
|
|
247
|
+
"""
|
|
248
|
+
return list(client.PRODUTOS_PAM.keys())
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
async def produtos_lspa() -> list[str]:
|
|
252
|
+
"""
|
|
253
|
+
Lista produtos disponíveis no LSPA.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Lista de nomes de produtos
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
>>> prods = await ibge.produtos_lspa()
|
|
260
|
+
>>> print(prods)
|
|
261
|
+
['soja', 'milho_1', 'milho_2', ...]
|
|
262
|
+
"""
|
|
263
|
+
return list(client.PRODUTOS_LSPA.keys())
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def ufs() -> list[str]:
|
|
267
|
+
"""
|
|
268
|
+
Lista UFs disponíveis.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Lista de siglas de UF
|
|
272
|
+
"""
|
|
273
|
+
return list(client.get_uf_codes().keys())
|
agrobr/ibge/client.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Cliente para API SIDRA do IBGE usando sidrapy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import sidrapy
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
from agrobr import constants
|
|
12
|
+
from agrobr.http.rate_limiter import RateLimiter
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Códigos das tabelas SIDRA
|
|
18
|
+
TABELAS = {
|
|
19
|
+
# PAM - Produção Agrícola Municipal
|
|
20
|
+
"pam_temporarias": "1612", # Lavouras temporárias (1974-2018)
|
|
21
|
+
"pam_permanentes": "1613", # Lavouras permanentes (1974-2018)
|
|
22
|
+
"pam_nova": "5457", # Nova série PAM (2018+)
|
|
23
|
+
# LSPA - Levantamento Sistemático da Produção Agrícola
|
|
24
|
+
"lspa": "6588", # Série mensal (2006+)
|
|
25
|
+
"lspa_safra": "1618", # Por ano de safra
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Variáveis disponíveis
|
|
29
|
+
VARIAVEIS = {
|
|
30
|
+
# PAM 5457
|
|
31
|
+
"area_plantada": "214",
|
|
32
|
+
"area_colhida": "215",
|
|
33
|
+
"producao": "216",
|
|
34
|
+
"rendimento": "112",
|
|
35
|
+
"valor_producao": "215",
|
|
36
|
+
# PAM 1612 (lavouras temporárias)
|
|
37
|
+
"area_plantada_1612": "109",
|
|
38
|
+
"area_colhida_1612": "1000109",
|
|
39
|
+
"producao_1612": "214",
|
|
40
|
+
"rendimento_1612": "112",
|
|
41
|
+
"valor_1612": "215",
|
|
42
|
+
# LSPA 6588
|
|
43
|
+
"area_lspa": "109",
|
|
44
|
+
"producao_lspa": "216",
|
|
45
|
+
"rendimento_lspa": "112",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Níveis territoriais
|
|
49
|
+
NIVEIS_TERRITORIAIS = {
|
|
50
|
+
"brasil": "1",
|
|
51
|
+
"regiao": "2",
|
|
52
|
+
"uf": "3",
|
|
53
|
+
"mesorregiao": "7",
|
|
54
|
+
"microrregiao": "8",
|
|
55
|
+
"municipio": "6",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Códigos de produtos agrícolas (classificação 782 para tabela 5457)
|
|
59
|
+
PRODUTOS_PAM = {
|
|
60
|
+
"soja": "40124",
|
|
61
|
+
"milho": "40126",
|
|
62
|
+
"arroz": "40117",
|
|
63
|
+
"feijao": "40120",
|
|
64
|
+
"trigo": "40145",
|
|
65
|
+
"algodao": "40109",
|
|
66
|
+
"cafe": "40112",
|
|
67
|
+
"cana": "40114",
|
|
68
|
+
"mandioca": "40127",
|
|
69
|
+
"laranja": "40125",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Códigos para LSPA (classificação 48 para tabela 6588)
|
|
73
|
+
PRODUTOS_LSPA = {
|
|
74
|
+
"soja": "39443",
|
|
75
|
+
"milho_1": "39441",
|
|
76
|
+
"milho_2": "39442",
|
|
77
|
+
"arroz": "39432",
|
|
78
|
+
"feijao_1": "39436",
|
|
79
|
+
"feijao_2": "39437",
|
|
80
|
+
"feijao_3": "39438",
|
|
81
|
+
"trigo": "39447",
|
|
82
|
+
"algodao": "39433",
|
|
83
|
+
"cafe": "109194",
|
|
84
|
+
"amendoim_1": "109180",
|
|
85
|
+
"amendoim_2": "109181",
|
|
86
|
+
"aveia": "109179",
|
|
87
|
+
"batata_1": "39434",
|
|
88
|
+
"batata_2": "39435",
|
|
89
|
+
"cevada": "109182",
|
|
90
|
+
"mamona": "109183",
|
|
91
|
+
"sorgo": "109184",
|
|
92
|
+
"triticale": "109185",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def fetch_sidra(
|
|
97
|
+
table_code: str,
|
|
98
|
+
territorial_level: str = "1",
|
|
99
|
+
ibge_territorial_code: str = "all",
|
|
100
|
+
variable: str | list[str] | None = None,
|
|
101
|
+
period: str | list[str] | None = None,
|
|
102
|
+
classifications: dict[str, str | list[str]] | None = None,
|
|
103
|
+
header: str = "n",
|
|
104
|
+
) -> pd.DataFrame:
|
|
105
|
+
"""
|
|
106
|
+
Busca dados do SIDRA/IBGE usando sidrapy.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
table_code: Código da tabela SIDRA
|
|
110
|
+
territorial_level: Nível territorial (1=Brasil, 3=UF, 6=Município)
|
|
111
|
+
ibge_territorial_code: Código territorial IBGE ou "all"
|
|
112
|
+
variable: Código(s) da variável
|
|
113
|
+
period: Período (ex: "2023", "last 5", "2019-2023")
|
|
114
|
+
classifications: Classificações/filtros adicionais
|
|
115
|
+
header: "n" para header numérico, "y" para descritivo
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
DataFrame com dados do SIDRA
|
|
119
|
+
"""
|
|
120
|
+
logger.info(
|
|
121
|
+
"ibge_fetch_start",
|
|
122
|
+
table=table_code,
|
|
123
|
+
level=territorial_level,
|
|
124
|
+
period=period,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async with RateLimiter.acquire(constants.Fonte.IBGE):
|
|
128
|
+
# sidrapy é síncrono, então apenas chamamos diretamente
|
|
129
|
+
kwargs: dict[str, Any] = {
|
|
130
|
+
"table_code": table_code,
|
|
131
|
+
"territorial_level": territorial_level,
|
|
132
|
+
"ibge_territorial_code": ibge_territorial_code,
|
|
133
|
+
"header": header,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if variable:
|
|
137
|
+
if isinstance(variable, list):
|
|
138
|
+
kwargs["variable"] = ",".join(variable)
|
|
139
|
+
else:
|
|
140
|
+
kwargs["variable"] = variable
|
|
141
|
+
|
|
142
|
+
if period:
|
|
143
|
+
if isinstance(period, list):
|
|
144
|
+
kwargs["period"] = ",".join(period)
|
|
145
|
+
else:
|
|
146
|
+
kwargs["period"] = period
|
|
147
|
+
|
|
148
|
+
if classifications:
|
|
149
|
+
kwargs["classifications"] = classifications
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
df = sidrapy.get_table(**kwargs)
|
|
153
|
+
|
|
154
|
+
# Remove primeira linha que é o header descritivo
|
|
155
|
+
if header == "n" and len(df) > 1:
|
|
156
|
+
df = df.iloc[1:].reset_index(drop=True)
|
|
157
|
+
|
|
158
|
+
logger.info(
|
|
159
|
+
"ibge_fetch_success",
|
|
160
|
+
table=table_code,
|
|
161
|
+
rows=len(df),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return pd.DataFrame(df)
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(
|
|
168
|
+
"ibge_fetch_error",
|
|
169
|
+
table=table_code,
|
|
170
|
+
error=str(e),
|
|
171
|
+
)
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def parse_sidra_response(
|
|
176
|
+
df: pd.DataFrame,
|
|
177
|
+
rename_columns: dict[str, str] | None = None,
|
|
178
|
+
) -> pd.DataFrame:
|
|
179
|
+
"""
|
|
180
|
+
Processa resposta do SIDRA para formato mais legível.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
df: DataFrame retornado pelo sidrapy
|
|
184
|
+
rename_columns: Mapeamento de renomeação de colunas
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
DataFrame processado
|
|
188
|
+
"""
|
|
189
|
+
# Mapeamento padrão de colunas SIDRA
|
|
190
|
+
default_rename = {
|
|
191
|
+
"NC": "nivel_territorial_cod",
|
|
192
|
+
"NN": "nivel_territorial",
|
|
193
|
+
"MC": "localidade_cod",
|
|
194
|
+
"MN": "localidade",
|
|
195
|
+
"V": "valor",
|
|
196
|
+
"D1C": "ano_cod",
|
|
197
|
+
"D1N": "ano",
|
|
198
|
+
"D2C": "variavel_cod",
|
|
199
|
+
"D2N": "variavel",
|
|
200
|
+
"D3C": "produto_cod",
|
|
201
|
+
"D3N": "produto",
|
|
202
|
+
"D4C": "classificacao_cod",
|
|
203
|
+
"D4N": "classificacao",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if rename_columns:
|
|
207
|
+
default_rename.update(rename_columns)
|
|
208
|
+
|
|
209
|
+
# Renomeia apenas colunas que existem
|
|
210
|
+
rename_map = {k: v for k, v in default_rename.items() if k in df.columns}
|
|
211
|
+
df = df.rename(columns=rename_map)
|
|
212
|
+
|
|
213
|
+
# Converte valor para numérico
|
|
214
|
+
if "valor" in df.columns:
|
|
215
|
+
df["valor"] = pd.to_numeric(df["valor"], errors="coerce")
|
|
216
|
+
|
|
217
|
+
return df
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_uf_codes() -> dict[str, str]:
|
|
221
|
+
"""Retorna mapeamento de sigla UF para código IBGE."""
|
|
222
|
+
return {
|
|
223
|
+
"RO": "11",
|
|
224
|
+
"AC": "12",
|
|
225
|
+
"AM": "13",
|
|
226
|
+
"RR": "14",
|
|
227
|
+
"PA": "15",
|
|
228
|
+
"AP": "16",
|
|
229
|
+
"TO": "17",
|
|
230
|
+
"MA": "21",
|
|
231
|
+
"PI": "22",
|
|
232
|
+
"CE": "23",
|
|
233
|
+
"RN": "24",
|
|
234
|
+
"PB": "25",
|
|
235
|
+
"PE": "26",
|
|
236
|
+
"AL": "27",
|
|
237
|
+
"SE": "28",
|
|
238
|
+
"BA": "29",
|
|
239
|
+
"MG": "31",
|
|
240
|
+
"ES": "32",
|
|
241
|
+
"RJ": "33",
|
|
242
|
+
"SP": "35",
|
|
243
|
+
"PR": "41",
|
|
244
|
+
"SC": "42",
|
|
245
|
+
"RS": "43",
|
|
246
|
+
"MS": "50",
|
|
247
|
+
"MT": "51",
|
|
248
|
+
"GO": "52",
|
|
249
|
+
"DF": "53",
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def uf_to_ibge_code(uf: str) -> str:
|
|
254
|
+
"""Converte sigla UF para código IBGE."""
|
|
255
|
+
codes = get_uf_codes()
|
|
256
|
+
return codes.get(uf.upper(), uf)
|
agrobr/models.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Modelos Pydantic v2 do agrobr."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field, field_validator
|
|
10
|
+
|
|
11
|
+
from .constants import Fonte
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Indicador(BaseModel):
|
|
15
|
+
fonte: Fonte
|
|
16
|
+
produto: str = Field(..., min_length=2)
|
|
17
|
+
praca: str | None = None
|
|
18
|
+
data: date
|
|
19
|
+
valor: Decimal = Field(..., gt=0)
|
|
20
|
+
unidade: str
|
|
21
|
+
metodologia: str | None = None
|
|
22
|
+
revisao: int = Field(default=0, ge=0)
|
|
23
|
+
meta: dict[str, Any] = Field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
parsed_at: datetime = Field(default_factory=datetime.utcnow)
|
|
26
|
+
parser_version: int = Field(default=1)
|
|
27
|
+
anomalies: list[str] = Field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
@field_validator("produto")
|
|
30
|
+
@classmethod
|
|
31
|
+
def lowercase_produto(cls, v: str) -> str:
|
|
32
|
+
if isinstance(v, str):
|
|
33
|
+
return v.lower().strip()
|
|
34
|
+
return v
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Safra(BaseModel):
|
|
38
|
+
fonte: Fonte
|
|
39
|
+
produto: str
|
|
40
|
+
safra: str = Field(..., pattern=r"^\d{4}/\d{2}$")
|
|
41
|
+
uf: str | None = Field(None, min_length=2, max_length=2)
|
|
42
|
+
area_plantada: Decimal | None = Field(None, ge=0)
|
|
43
|
+
producao: Decimal | None = Field(None, ge=0)
|
|
44
|
+
produtividade: Decimal | None = Field(None, ge=0)
|
|
45
|
+
unidade_area: str = Field(default="mil_ha")
|
|
46
|
+
unidade_producao: str = Field(default="mil_ton")
|
|
47
|
+
levantamento: int = Field(..., ge=1, le=12)
|
|
48
|
+
data_publicacao: date
|
|
49
|
+
meta: dict[str, Any] = Field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
parsed_at: datetime = Field(default_factory=datetime.utcnow)
|
|
52
|
+
parser_version: int = Field(default=1)
|
|
53
|
+
anomalies: list[str] = Field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CacheEntry(BaseModel):
|
|
57
|
+
key: str
|
|
58
|
+
data: bytes
|
|
59
|
+
created_at: datetime
|
|
60
|
+
expires_at: datetime
|
|
61
|
+
source: Fonte
|
|
62
|
+
version: int = 1
|
|
63
|
+
stale: bool = False
|
|
64
|
+
hit_count: int = 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HistoryEntry(BaseModel):
|
|
68
|
+
key: str
|
|
69
|
+
data: bytes
|
|
70
|
+
source: Fonte
|
|
71
|
+
data_date: date
|
|
72
|
+
collected_at: datetime
|
|
73
|
+
parser_version: int
|
|
74
|
+
fingerprint_hash: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Fingerprint(BaseModel):
|
|
78
|
+
source: Fonte
|
|
79
|
+
url: str
|
|
80
|
+
collected_at: datetime
|
|
81
|
+
table_classes: list[list[str]]
|
|
82
|
+
key_ids: list[str]
|
|
83
|
+
structure_hash: str
|
|
84
|
+
table_headers: list[list[str]]
|
|
85
|
+
element_counts: dict[str, int]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Normalização de dados - unidades, datas, regiões, encoding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .dates import (
|
|
6
|
+
anos_para_safra,
|
|
7
|
+
lista_safras,
|
|
8
|
+
normalizar_safra,
|
|
9
|
+
periodo_safra,
|
|
10
|
+
safra_anterior,
|
|
11
|
+
safra_atual,
|
|
12
|
+
safra_para_anos,
|
|
13
|
+
safra_posterior,
|
|
14
|
+
validar_safra,
|
|
15
|
+
)
|
|
16
|
+
from .encoding import decode_content, detect_encoding
|
|
17
|
+
from .regions import (
|
|
18
|
+
ibge_para_uf,
|
|
19
|
+
listar_regioes,
|
|
20
|
+
listar_ufs,
|
|
21
|
+
normalizar_municipio,
|
|
22
|
+
normalizar_praca,
|
|
23
|
+
normalizar_uf,
|
|
24
|
+
uf_para_ibge,
|
|
25
|
+
uf_para_nome,
|
|
26
|
+
uf_para_regiao,
|
|
27
|
+
validar_uf,
|
|
28
|
+
)
|
|
29
|
+
from .units import (
|
|
30
|
+
converter,
|
|
31
|
+
preco_saca_para_tonelada,
|
|
32
|
+
preco_tonelada_para_saca,
|
|
33
|
+
sacas_para_toneladas,
|
|
34
|
+
toneladas_para_sacas,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__: list[str] = [
|
|
38
|
+
"decode_content",
|
|
39
|
+
"detect_encoding",
|
|
40
|
+
"converter",
|
|
41
|
+
"preco_saca_para_tonelada",
|
|
42
|
+
"preco_tonelada_para_saca",
|
|
43
|
+
"sacas_para_toneladas",
|
|
44
|
+
"toneladas_para_sacas",
|
|
45
|
+
"safra_atual",
|
|
46
|
+
"validar_safra",
|
|
47
|
+
"normalizar_safra",
|
|
48
|
+
"safra_para_anos",
|
|
49
|
+
"anos_para_safra",
|
|
50
|
+
"safra_anterior",
|
|
51
|
+
"safra_posterior",
|
|
52
|
+
"lista_safras",
|
|
53
|
+
"periodo_safra",
|
|
54
|
+
"normalizar_uf",
|
|
55
|
+
"uf_para_nome",
|
|
56
|
+
"uf_para_regiao",
|
|
57
|
+
"uf_para_ibge",
|
|
58
|
+
"ibge_para_uf",
|
|
59
|
+
"listar_ufs",
|
|
60
|
+
"listar_regioes",
|
|
61
|
+
"normalizar_municipio",
|
|
62
|
+
"normalizar_praca",
|
|
63
|
+
"validar_uf",
|
|
64
|
+
]
|