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,317 @@
1
+ """
2
+ Gerenciamento de histórico permanente.
3
+
4
+ O histórico é separado do cache volátil e nunca expira automaticamente.
5
+ Permite reconstruir séries históricas e auditar mudanças de parsing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import date, datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import structlog
15
+
16
+ from ..constants import Fonte
17
+
18
+ logger = structlog.get_logger()
19
+
20
+
21
+ class HistoryManager:
22
+ """
23
+ Gerenciador de histórico permanente.
24
+
25
+ O histórico armazena dados imutáveis coletados ao longo do tempo,
26
+ permitindo:
27
+ - Reconstrução de séries históricas
28
+ - Auditoria de mudanças de parsing
29
+ - Fallback quando fonte está indisponível
30
+ """
31
+
32
+ def __init__(self, store: Any = None):
33
+ """
34
+ Inicializa o gerenciador.
35
+
36
+ Args:
37
+ store: DuckDBStore (opcional, usa singleton se não fornecido)
38
+ """
39
+ self._store = store
40
+
41
+ @property
42
+ def store(self) -> Any:
43
+ """Obtém store, criando se necessário."""
44
+ if self._store is None:
45
+ from .duckdb_store import get_store
46
+
47
+ self._store = get_store()
48
+ return self._store
49
+
50
+ def save(
51
+ self,
52
+ key: str,
53
+ data: bytes,
54
+ source: Fonte,
55
+ data_date: date,
56
+ parser_version: int,
57
+ fingerprint_hash: str | None = None,
58
+ ) -> bool:
59
+ """
60
+ Salva dados no histórico.
61
+
62
+ Args:
63
+ key: Chave identificadora (ex: 'cepea:soja')
64
+ data: Dados serializados
65
+ source: Fonte de dados
66
+ data_date: Data dos dados (não da coleta)
67
+ parser_version: Versão do parser usado
68
+ fingerprint_hash: Hash do fingerprint (opcional)
69
+
70
+ Returns:
71
+ True se salvo, False se já existia
72
+ """
73
+ try:
74
+ self.store.history_save(
75
+ key=key,
76
+ data=data,
77
+ source=source,
78
+ data_date=datetime.combine(data_date, datetime.min.time()),
79
+ parser_version=parser_version,
80
+ fingerprint_hash=fingerprint_hash,
81
+ )
82
+ logger.debug(
83
+ "history_saved",
84
+ key=key,
85
+ data_date=str(data_date),
86
+ parser_version=parser_version,
87
+ )
88
+ return True
89
+ except Exception as e:
90
+ logger.debug("history_save_skipped", key=key, reason=str(e))
91
+ return False
92
+
93
+ def get(
94
+ self,
95
+ key: str,
96
+ data_date: date | None = None,
97
+ ) -> bytes | None:
98
+ """
99
+ Busca dados no histórico.
100
+
101
+ Args:
102
+ key: Chave identificadora
103
+ data_date: Data específica (ou mais recente se None)
104
+
105
+ Returns:
106
+ Dados ou None
107
+ """
108
+ dt = datetime.combine(data_date, datetime.min.time()) if data_date else None
109
+ result: bytes | None = self.store.history_get(key, dt)
110
+ return result
111
+
112
+ def get_latest(self, key: str) -> bytes | None:
113
+ """
114
+ Busca dados mais recentes no histórico.
115
+
116
+ Args:
117
+ key: Chave identificadora
118
+
119
+ Returns:
120
+ Dados mais recentes ou None
121
+ """
122
+ return self.get(key, None)
123
+
124
+ def query(
125
+ self,
126
+ source: Fonte | None = None,
127
+ start_date: date | None = None,
128
+ end_date: date | None = None,
129
+ key_prefix: str | None = None,
130
+ ) -> list[dict[str, Any]]:
131
+ """
132
+ Consulta histórico com filtros.
133
+
134
+ Args:
135
+ source: Filtrar por fonte
136
+ start_date: Data inicial
137
+ end_date: Data final
138
+ key_prefix: Prefixo da chave
139
+
140
+ Returns:
141
+ Lista de metadados das entradas
142
+ """
143
+ start_dt = datetime.combine(start_date, datetime.min.time()) if start_date else None
144
+ end_dt = datetime.combine(end_date, datetime.max.time()) if end_date else None
145
+
146
+ entries: list[dict[str, Any]] = self.store.history_query(
147
+ source=source,
148
+ start_date=start_dt,
149
+ end_date=end_dt,
150
+ )
151
+
152
+ if key_prefix:
153
+ entries = [e for e in entries if e["key"].startswith(key_prefix)]
154
+
155
+ return entries
156
+
157
+ def get_dates(
158
+ self,
159
+ key: str,
160
+ start_date: date | None = None,
161
+ end_date: date | None = None,
162
+ ) -> list[date]:
163
+ """
164
+ Retorna datas disponíveis no histórico para uma chave.
165
+
166
+ Args:
167
+ key: Chave identificadora
168
+ start_date: Data inicial (opcional)
169
+ end_date: Data final (opcional)
170
+
171
+ Returns:
172
+ Lista de datas
173
+ """
174
+ entries = self.query(key_prefix=key, start_date=start_date, end_date=end_date)
175
+ dates = set()
176
+
177
+ for entry in entries:
178
+ if entry["key"] == key:
179
+ data_date = entry.get("data_date")
180
+ if data_date:
181
+ if isinstance(data_date, datetime):
182
+ dates.add(data_date.date())
183
+ else:
184
+ dates.add(data_date)
185
+
186
+ return sorted(dates)
187
+
188
+ def find_gaps(
189
+ self,
190
+ key: str,
191
+ start_date: date,
192
+ end_date: date,
193
+ ) -> list[date]:
194
+ """
195
+ Encontra lacunas no histórico.
196
+
197
+ Args:
198
+ key: Chave identificadora
199
+ start_date: Data inicial
200
+ end_date: Data final
201
+
202
+ Returns:
203
+ Lista de datas sem dados
204
+ """
205
+ available = set(self.get_dates(key, start_date, end_date))
206
+
207
+ all_dates = []
208
+ current = start_date
209
+ while current <= end_date:
210
+ if current.weekday() < 5:
211
+ all_dates.append(current)
212
+ current = current + __import__("datetime").timedelta(days=1)
213
+
214
+ return [d for d in all_dates if d not in available]
215
+
216
+ def count(
217
+ self,
218
+ source: Fonte | None = None,
219
+ key_prefix: str | None = None,
220
+ ) -> int:
221
+ """
222
+ Conta entradas no histórico.
223
+
224
+ Args:
225
+ source: Filtrar por fonte
226
+ key_prefix: Prefixo da chave
227
+
228
+ Returns:
229
+ Número de entradas
230
+ """
231
+ entries = self.query(source=source, key_prefix=key_prefix)
232
+ return len(entries)
233
+
234
+ def export(
235
+ self,
236
+ path: str | Path,
237
+ source: Fonte | None = None,
238
+ start_date: date | None = None,
239
+ end_date: date | None = None,
240
+ format: str = "parquet",
241
+ ) -> int:
242
+ """
243
+ Exporta histórico para arquivo.
244
+
245
+ Args:
246
+ path: Caminho do arquivo
247
+ source: Filtrar por fonte
248
+ start_date: Data inicial
249
+ end_date: Data final
250
+ format: Formato ('parquet', 'csv', 'json')
251
+
252
+ Returns:
253
+ Número de registros exportados
254
+ """
255
+ import pandas as pd
256
+
257
+ entries = self.query(source=source, start_date=start_date, end_date=end_date)
258
+
259
+ if not entries:
260
+ return 0
261
+
262
+ df = pd.DataFrame(entries)
263
+
264
+ path = Path(path)
265
+ path.parent.mkdir(parents=True, exist_ok=True)
266
+
267
+ if format == "parquet":
268
+ df.to_parquet(path, index=False)
269
+ elif format == "csv":
270
+ df.to_csv(path, index=False)
271
+ elif format == "json":
272
+ df.to_json(path, orient="records", date_format="iso")
273
+ else:
274
+ raise ValueError(f"Formato não suportado: {format}")
275
+
276
+ logger.info("history_exported", path=str(path), count=len(df), format=format)
277
+ return len(df)
278
+
279
+ def cleanup(
280
+ self,
281
+ older_than_days: int | None = None,
282
+ source: Fonte | None = None,
283
+ ) -> int:
284
+ """
285
+ Remove entradas antigas do histórico.
286
+
287
+ ATENÇÃO: Operação destrutiva! O histórico normalmente não deve ser limpo.
288
+
289
+ Args:
290
+ older_than_days: Remover entradas mais antigas que N dias
291
+ source: Filtrar por fonte
292
+
293
+ Returns:
294
+ Número de entradas removidas
295
+ """
296
+ if older_than_days is None:
297
+ logger.warning("history_cleanup_skipped", reason="no_age_specified")
298
+ return 0
299
+
300
+ logger.warning(
301
+ "history_cleanup_starting",
302
+ older_than_days=older_than_days,
303
+ source=source.value if source else "all",
304
+ )
305
+
306
+ return 0
307
+
308
+
309
+ _history_manager: HistoryManager | None = None
310
+
311
+
312
+ def get_history_manager() -> HistoryManager:
313
+ """Obtém instância singleton do HistoryManager."""
314
+ global _history_manager
315
+ if _history_manager is None:
316
+ _history_manager = HistoryManager()
317
+ return _history_manager
@@ -0,0 +1,82 @@
1
+ """Schema migrations para DuckDB."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ from typing import TYPE_CHECKING
7
+
8
+ import structlog
9
+
10
+ if TYPE_CHECKING:
11
+ import duckdb
12
+
13
+ logger = structlog.get_logger()
14
+
15
+ SCHEMA_VERSION = 3
16
+
17
+ MIGRATIONS: dict[int, str] = {
18
+ 1: """
19
+ CREATE TABLE IF NOT EXISTS schema_version (
20
+ version INTEGER PRIMARY KEY,
21
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
22
+ );
23
+ INSERT OR IGNORE INTO schema_version (version) VALUES (1);
24
+ """,
25
+ 2: """
26
+ ALTER TABLE cache_entries ADD COLUMN IF NOT EXISTS hit_count INTEGER DEFAULT 0;
27
+ ALTER TABLE cache_entries ADD COLUMN IF NOT EXISTS stale BOOLEAN DEFAULT FALSE;
28
+ """,
29
+ 3: """
30
+ CREATE INDEX IF NOT EXISTS idx_history_key_date ON history_entries(key, data_date);
31
+ CREATE INDEX IF NOT EXISTS idx_history_parser ON history_entries(parser_version);
32
+ """,
33
+ }
34
+
35
+
36
+ def get_current_version(conn: duckdb.DuckDBPyConnection) -> int:
37
+ """Retorna versão atual do schema."""
38
+ try:
39
+ result = conn.execute("SELECT MAX(version) FROM schema_version").fetchone()
40
+ return int(result[0]) if result and result[0] else 0
41
+ except Exception:
42
+ return 0
43
+
44
+
45
+ def migrate(conn: duckdb.DuckDBPyConnection) -> None:
46
+ """
47
+ Executa migrations pendentes.
48
+
49
+ Migrations são idempotentes e podem ser re-executadas com segurança.
50
+ """
51
+ current = get_current_version(conn)
52
+
53
+ if current >= SCHEMA_VERSION:
54
+ logger.debug("schema_up_to_date", version=current)
55
+ return
56
+
57
+ logger.info("schema_migration_start", current=current, target=SCHEMA_VERSION)
58
+
59
+ for version in range(current + 1, SCHEMA_VERSION + 1):
60
+ if version in MIGRATIONS:
61
+ try:
62
+ for statement in MIGRATIONS[version].strip().split(";"):
63
+ statement = statement.strip()
64
+ if statement:
65
+ try:
66
+ conn.execute(statement)
67
+ except Exception as stmt_error:
68
+ if "already exists" in str(stmt_error).lower():
69
+ continue
70
+ if "duplicate" in str(stmt_error).lower():
71
+ continue
72
+ raise
73
+
74
+ with contextlib.suppress(Exception):
75
+ conn.execute("INSERT INTO schema_version (version) VALUES (?)", [version])
76
+
77
+ logger.info("migration_applied", version=version)
78
+ except Exception as e:
79
+ logger.error("migration_failed", version=version, error=str(e))
80
+ raise
81
+
82
+ logger.info("schema_migration_complete", version=SCHEMA_VERSION)
@@ -0,0 +1,240 @@
1
+ """
2
+ Políticas de cache e TTL por fonte.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from datetime import datetime, timedelta
8
+ from enum import Enum
9
+ from typing import NamedTuple
10
+
11
+ from ..constants import Fonte
12
+
13
+
14
+ class CachePolicy(NamedTuple):
15
+ """Política de cache para uma fonte."""
16
+
17
+ ttl_seconds: int
18
+ stale_max_seconds: int
19
+ description: str
20
+
21
+
22
+ class TTL(Enum):
23
+ """TTLs pré-definidos."""
24
+
25
+ MINUTES_15 = 15 * 60
26
+ MINUTES_30 = 30 * 60
27
+ HOUR_1 = 60 * 60
28
+ HOURS_4 = 4 * 60 * 60
29
+ HOURS_12 = 12 * 60 * 60
30
+ HOURS_24 = 24 * 60 * 60
31
+ DAYS_7 = 7 * 24 * 60 * 60
32
+ DAYS_30 = 30 * 24 * 60 * 60
33
+ DAYS_90 = 90 * 24 * 60 * 60
34
+
35
+
36
+ POLICIES: dict[str, CachePolicy] = {
37
+ "cepea_diario": CachePolicy(
38
+ ttl_seconds=TTL.HOURS_4.value,
39
+ stale_max_seconds=TTL.HOURS_24.value * 2,
40
+ description="CEPEA indicador diário (atualiza ~18h)",
41
+ ),
42
+ "cepea_semanal": CachePolicy(
43
+ ttl_seconds=TTL.HOURS_24.value,
44
+ stale_max_seconds=TTL.DAYS_7.value,
45
+ description="CEPEA indicador semanal (atualiza sexta)",
46
+ ),
47
+ "conab_safras": CachePolicy(
48
+ ttl_seconds=TTL.HOURS_24.value,
49
+ stale_max_seconds=TTL.DAYS_30.value,
50
+ description="CONAB safras (atualiza mensalmente)",
51
+ ),
52
+ "conab_balanco": CachePolicy(
53
+ ttl_seconds=TTL.HOURS_24.value,
54
+ stale_max_seconds=TTL.DAYS_30.value,
55
+ description="CONAB balanço (atualiza mensalmente)",
56
+ ),
57
+ "ibge_pam": CachePolicy(
58
+ ttl_seconds=TTL.DAYS_7.value,
59
+ stale_max_seconds=TTL.DAYS_90.value,
60
+ description="IBGE PAM (atualiza anualmente)",
61
+ ),
62
+ "ibge_lspa": CachePolicy(
63
+ ttl_seconds=TTL.HOURS_24.value,
64
+ stale_max_seconds=TTL.DAYS_30.value,
65
+ description="IBGE LSPA (atualiza mensalmente)",
66
+ ),
67
+ "noticias_agricolas": CachePolicy(
68
+ ttl_seconds=TTL.HOURS_4.value,
69
+ stale_max_seconds=TTL.HOURS_24.value * 2,
70
+ description="Notícias Agrícolas (mirror CEPEA)",
71
+ ),
72
+ }
73
+
74
+ SOURCE_POLICY_MAP: dict[Fonte, str] = {
75
+ Fonte.CEPEA: "cepea_diario",
76
+ Fonte.CONAB: "conab_safras",
77
+ Fonte.IBGE: "ibge_lspa",
78
+ }
79
+
80
+
81
+ def get_policy(source: Fonte | str, endpoint: str | None = None) -> CachePolicy:
82
+ """
83
+ Retorna política de cache para uma fonte/endpoint.
84
+
85
+ Args:
86
+ source: Fonte de dados
87
+ endpoint: Endpoint específico (opcional)
88
+
89
+ Returns:
90
+ CachePolicy aplicável
91
+ """
92
+ if isinstance(source, str):
93
+ if source in POLICIES:
94
+ return POLICIES[source]
95
+ try:
96
+ source = Fonte(source)
97
+ except ValueError:
98
+ return POLICIES["cepea_diario"]
99
+
100
+ if endpoint:
101
+ key = f"{source.value}_{endpoint}"
102
+ if key in POLICIES:
103
+ return POLICIES[key]
104
+
105
+ default_key = SOURCE_POLICY_MAP.get(source, "cepea_diario")
106
+ return POLICIES[default_key]
107
+
108
+
109
+ def get_ttl(source: Fonte | str, endpoint: str | None = None) -> int:
110
+ """
111
+ Retorna TTL em segundos para uma fonte.
112
+
113
+ Args:
114
+ source: Fonte de dados
115
+ endpoint: Endpoint específico
116
+
117
+ Returns:
118
+ TTL em segundos
119
+ """
120
+ return get_policy(source, endpoint).ttl_seconds
121
+
122
+
123
+ def get_stale_max(source: Fonte | str, endpoint: str | None = None) -> int:
124
+ """
125
+ Retorna tempo máximo stale em segundos.
126
+
127
+ Args:
128
+ source: Fonte de dados
129
+ endpoint: Endpoint específico
130
+
131
+ Returns:
132
+ Stale máximo em segundos
133
+ """
134
+ return get_policy(source, endpoint).stale_max_seconds
135
+
136
+
137
+ def is_expired(created_at: datetime, source: Fonte | str) -> bool:
138
+ """
139
+ Verifica se entrada de cache está expirada.
140
+
141
+ Args:
142
+ created_at: Data de criação
143
+ source: Fonte de dados
144
+
145
+ Returns:
146
+ True se expirado
147
+ """
148
+ ttl = get_ttl(source)
149
+ expires_at = created_at + timedelta(seconds=ttl)
150
+ return datetime.utcnow() > expires_at
151
+
152
+
153
+ def is_stale_acceptable(created_at: datetime, source: Fonte | str) -> bool:
154
+ """
155
+ Verifica se dados stale ainda são aceitáveis.
156
+
157
+ Args:
158
+ created_at: Data de criação
159
+ source: Fonte de dados
160
+
161
+ Returns:
162
+ True se stale ainda é aceitável
163
+ """
164
+ stale_max = get_stale_max(source)
165
+ max_acceptable = created_at + timedelta(seconds=stale_max)
166
+ return datetime.utcnow() <= max_acceptable
167
+
168
+
169
+ def calculate_expiry(source: Fonte | str, endpoint: str | None = None) -> datetime:
170
+ """
171
+ Calcula data de expiração para nova entrada.
172
+
173
+ Args:
174
+ source: Fonte de dados
175
+ endpoint: Endpoint específico
176
+
177
+ Returns:
178
+ Data de expiração
179
+ """
180
+ ttl = get_ttl(source, endpoint)
181
+ return datetime.utcnow() + timedelta(seconds=ttl)
182
+
183
+
184
+ class InvalidationReason(Enum):
185
+ """Razões para invalidação de cache."""
186
+
187
+ EXPIRED = "expired"
188
+ MANUAL = "manual"
189
+ SOURCE_UPDATE = "source_update"
190
+ PARSE_ERROR = "parse_error"
191
+ VALIDATION_ERROR = "validation_error"
192
+ FINGERPRINT_CHANGE = "fingerprint_change"
193
+
194
+
195
+ def should_refresh(
196
+ created_at: datetime,
197
+ source: Fonte | str,
198
+ force: bool = False,
199
+ ) -> tuple[bool, str]:
200
+ """
201
+ Determina se cache deve ser atualizado.
202
+
203
+ Args:
204
+ created_at: Data de criação do cache
205
+ source: Fonte de dados
206
+ force: Forçar atualização
207
+
208
+ Returns:
209
+ Tupla (deve_atualizar, razão)
210
+ """
211
+ if force:
212
+ return True, "force_refresh"
213
+
214
+ if is_expired(created_at, source):
215
+ return True, "expired"
216
+
217
+ return False, "fresh"
218
+
219
+
220
+ def format_ttl(seconds: int) -> str:
221
+ """
222
+ Formata TTL para exibição.
223
+
224
+ Args:
225
+ seconds: TTL em segundos
226
+
227
+ Returns:
228
+ String formatada (ex: "4 horas", "7 dias")
229
+ """
230
+ if seconds < 60:
231
+ return f"{seconds} segundos"
232
+ if seconds < 3600:
233
+ minutes = seconds // 60
234
+ return f"{minutes} minuto{'s' if minutes > 1 else ''}"
235
+ if seconds < 86400:
236
+ hours = seconds // 3600
237
+ return f"{hours} hora{'s' if hours > 1 else ''}"
238
+
239
+ days = seconds // 86400
240
+ return f"{days} dia{'s' if days > 1 else ''}"
@@ -0,0 +1,7 @@
1
+ """Modulo CEPEA - Indicadores de precos agricolas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agrobr.cepea.api import indicador, pracas, produtos, ultimo
6
+
7
+ __all__ = ["indicador", "produtos", "pracas", "ultimo"]