agrobr 0.1.0__tar.gz → 0.1.2__tar.gz

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 (104) hide show
  1. {agrobr-0.1.0 → agrobr-0.1.2}/PKG-INFO +1 -1
  2. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cache/policies.py +104 -17
  3. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cepea/client.py +1 -1
  4. {agrobr-0.1.0 → agrobr-0.1.2}/pyproject.toml +1 -1
  5. {agrobr-0.1.0 → agrobr-0.1.2}/.github/workflows/health_check.yml +0 -0
  6. {agrobr-0.1.0 → agrobr-0.1.2}/.github/workflows/publish.yml +0 -0
  7. {agrobr-0.1.0 → agrobr-0.1.2}/.github/workflows/structure_monitor.yml +0 -0
  8. {agrobr-0.1.0 → agrobr-0.1.2}/.github/workflows/tests.yml +0 -0
  9. {agrobr-0.1.0 → agrobr-0.1.2}/.gitignore +0 -0
  10. {agrobr-0.1.0 → agrobr-0.1.2}/.pre-commit-config.yaml +0 -0
  11. {agrobr-0.1.0 → agrobr-0.1.2}/.structures/baseline.json +0 -0
  12. {agrobr-0.1.0 → agrobr-0.1.2}/CHANGELOG.md +0 -0
  13. {agrobr-0.1.0 → agrobr-0.1.2}/CODE_OF_CONDUCT.md +0 -0
  14. {agrobr-0.1.0 → agrobr-0.1.2}/CONTRIBUTING.md +0 -0
  15. {agrobr-0.1.0 → agrobr-0.1.2}/LICENSE +0 -0
  16. {agrobr-0.1.0 → agrobr-0.1.2}/README.md +0 -0
  17. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/__init__.py +0 -0
  18. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/alerts/__init__.py +0 -0
  19. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/alerts/notifier.py +0 -0
  20. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cache/__init__.py +0 -0
  21. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cache/duckdb_store.py +0 -0
  22. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cache/history.py +0 -0
  23. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cache/migrations.py +0 -0
  24. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cepea/__init__.py +0 -0
  25. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cepea/api.py +0 -0
  26. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cepea/parsers/__init__.py +0 -0
  27. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cepea/parsers/base.py +0 -0
  28. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cepea/parsers/consensus.py +0 -0
  29. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cepea/parsers/detector.py +0 -0
  30. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cepea/parsers/fingerprint.py +0 -0
  31. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cepea/parsers/v1.py +0 -0
  32. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/cli.py +0 -0
  33. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/conab/__init__.py +0 -0
  34. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/conab/api.py +0 -0
  35. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/conab/client.py +0 -0
  36. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/conab/parsers/__init__.py +0 -0
  37. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/conab/parsers/v1.py +0 -0
  38. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/constants.py +0 -0
  39. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/exceptions.py +0 -0
  40. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/health/__init__.py +0 -0
  41. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/health/checker.py +0 -0
  42. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/health/reporter.py +0 -0
  43. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/http/__init__.py +0 -0
  44. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/http/browser.py +0 -0
  45. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/http/rate_limiter.py +0 -0
  46. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/http/retry.py +0 -0
  47. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/http/user_agents.py +0 -0
  48. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/ibge/__init__.py +0 -0
  49. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/ibge/api.py +0 -0
  50. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/ibge/client.py +0 -0
  51. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/models.py +0 -0
  52. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/normalize/__init__.py +0 -0
  53. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/normalize/dates.py +0 -0
  54. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/normalize/encoding.py +0 -0
  55. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/normalize/regions.py +0 -0
  56. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/normalize/units.py +0 -0
  57. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/noticias_agricolas/__init__.py +0 -0
  58. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/noticias_agricolas/client.py +0 -0
  59. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/noticias_agricolas/parser.py +0 -0
  60. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/sync.py +0 -0
  61. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/telemetry/__init__.py +0 -0
  62. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/telemetry/collector.py +0 -0
  63. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/utils/__init__.py +0 -0
  64. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/utils/logging.py +0 -0
  65. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/validators/__init__.py +0 -0
  66. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/validators/sanity.py +0 -0
  67. {agrobr-0.1.0 → agrobr-0.1.2}/agrobr/validators/structural.py +0 -0
  68. {agrobr-0.1.0 → agrobr-0.1.2}/docs/advanced/resilience.md +0 -0
  69. {agrobr-0.1.0 → agrobr-0.1.2}/docs/advanced/troubleshooting.md +0 -0
  70. {agrobr-0.1.0 → agrobr-0.1.2}/docs/api/cepea.md +0 -0
  71. {agrobr-0.1.0 → agrobr-0.1.2}/docs/api/conab.md +0 -0
  72. {agrobr-0.1.0 → agrobr-0.1.2}/docs/api/ibge.md +0 -0
  73. {agrobr-0.1.0 → agrobr-0.1.2}/docs/index.md +0 -0
  74. {agrobr-0.1.0 → agrobr-0.1.2}/docs/quickstart.md +0 -0
  75. {agrobr-0.1.0 → agrobr-0.1.2}/examples/analise_soja.py +0 -0
  76. {agrobr-0.1.0 → agrobr-0.1.2}/examples/pipeline_async.py +0 -0
  77. {agrobr-0.1.0 → agrobr-0.1.2}/mkdocs.yml +0 -0
  78. {agrobr-0.1.0 → agrobr-0.1.2}/scripts/alert_structure_change.py +0 -0
  79. {agrobr-0.1.0 → agrobr-0.1.2}/scripts/compare_structures.py +0 -0
  80. {agrobr-0.1.0 → agrobr-0.1.2}/scripts/create_workflows.py +0 -0
  81. {agrobr-0.1.0 → agrobr-0.1.2}/scripts/fetch_structures.py +0 -0
  82. {agrobr-0.1.0 → agrobr-0.1.2}/scripts/update_golden.py +0 -0
  83. {agrobr-0.1.0 → agrobr-0.1.2}/tests/__init__.py +0 -0
  84. {agrobr-0.1.0 → agrobr-0.1.2}/tests/conftest.py +0 -0
  85. {agrobr-0.1.0 → agrobr-0.1.2}/tests/golden_data/cepea/soja_sample/expected.json +0 -0
  86. {agrobr-0.1.0 → agrobr-0.1.2}/tests/golden_data/cepea/soja_sample/metadata.json +0 -0
  87. {agrobr-0.1.0 → agrobr-0.1.2}/tests/golden_data/cepea/soja_sample/response.html +0 -0
  88. {agrobr-0.1.0 → agrobr-0.1.2}/tests/golden_data/conab/safra_sample/expected.json +0 -0
  89. {agrobr-0.1.0 → agrobr-0.1.2}/tests/golden_data/conab/safra_sample/metadata.json +0 -0
  90. {agrobr-0.1.0 → agrobr-0.1.2}/tests/golden_data/conab/safra_sample/response.xlsx +0 -0
  91. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_cepea/__init__.py +0 -0
  92. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_cepea/test_api.py +0 -0
  93. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_cepea/test_fingerprint.py +0 -0
  94. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_cepea/test_parser.py +0 -0
  95. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_conab/__init__.py +0 -0
  96. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_conab/test_parser.py +0 -0
  97. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_golden.py +0 -0
  98. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_ibge/__init__.py +0 -0
  99. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_ibge/test_api.py +0 -0
  100. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_ibge/test_client.py +0 -0
  101. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_noticias_agricolas/__init__.py +0 -0
  102. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_noticias_agricolas/test_parser.py +0 -0
  103. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_validators/__init__.py +0 -0
  104. {agrobr-0.1.0 → agrobr-0.1.2}/tests/test_validators/test_sanity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agrobr
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Dados agrícolas brasileiros em uma linha de código
5
5
  Project-URL: Homepage, https://github.com/bruno-portfolio/agrobr
6
6
  Project-URL: Documentation, https://agrobr.dev
@@ -1,10 +1,6 @@
1
- """
2
- Políticas de cache e TTL por fonte.
3
- """
4
-
5
1
  from __future__ import annotations
6
2
 
7
- from datetime import datetime, timedelta
3
+ from datetime import datetime, time, timedelta
8
4
  from enum import Enum
9
5
  from typing import NamedTuple
10
6
 
@@ -17,6 +13,7 @@ class CachePolicy(NamedTuple):
17
13
  ttl_seconds: int
18
14
  stale_max_seconds: int
19
15
  description: str
16
+ smart_expiry: bool = False # Se True, usa horário fixo de expiração
20
17
 
21
18
 
22
19
  class TTL(Enum):
@@ -33,41 +30,53 @@ class TTL(Enum):
33
30
  DAYS_90 = 90 * 24 * 60 * 60
34
31
 
35
32
 
33
+ # Horário de atualização do CEPEA (18:00 Brasília)
34
+ CEPEA_UPDATE_HOUR = 18
35
+ CEPEA_UPDATE_MINUTE = 0
36
+
37
+
36
38
  POLICIES: dict[str, CachePolicy] = {
37
39
  "cepea_diario": CachePolicy(
38
- ttl_seconds=TTL.HOURS_4.value,
40
+ ttl_seconds=TTL.HOURS_24.value, # Fallback se smart_expiry falhar
39
41
  stale_max_seconds=TTL.HOURS_24.value * 2,
40
- description="CEPEA indicador diário (atualiza ~18h)",
42
+ description="CEPEA indicador diário (expira às 18h)",
43
+ smart_expiry=True,
41
44
  ),
42
45
  "cepea_semanal": CachePolicy(
43
46
  ttl_seconds=TTL.HOURS_24.value,
44
47
  stale_max_seconds=TTL.DAYS_7.value,
45
48
  description="CEPEA indicador semanal (atualiza sexta)",
49
+ smart_expiry=False,
46
50
  ),
47
51
  "conab_safras": CachePolicy(
48
52
  ttl_seconds=TTL.HOURS_24.value,
49
53
  stale_max_seconds=TTL.DAYS_30.value,
50
54
  description="CONAB safras (atualiza mensalmente)",
55
+ smart_expiry=False,
51
56
  ),
52
57
  "conab_balanco": CachePolicy(
53
58
  ttl_seconds=TTL.HOURS_24.value,
54
59
  stale_max_seconds=TTL.DAYS_30.value,
55
60
  description="CONAB balanço (atualiza mensalmente)",
61
+ smart_expiry=False,
56
62
  ),
57
63
  "ibge_pam": CachePolicy(
58
64
  ttl_seconds=TTL.DAYS_7.value,
59
65
  stale_max_seconds=TTL.DAYS_90.value,
60
66
  description="IBGE PAM (atualiza anualmente)",
67
+ smart_expiry=False,
61
68
  ),
62
69
  "ibge_lspa": CachePolicy(
63
70
  ttl_seconds=TTL.HOURS_24.value,
64
71
  stale_max_seconds=TTL.DAYS_30.value,
65
72
  description="IBGE LSPA (atualiza mensalmente)",
73
+ smart_expiry=False,
66
74
  ),
67
75
  "noticias_agricolas": CachePolicy(
68
- ttl_seconds=TTL.HOURS_4.value,
76
+ ttl_seconds=TTL.HOURS_24.value, # Fallback
69
77
  stale_max_seconds=TTL.HOURS_24.value * 2,
70
- description="Notícias Agrícolas (mirror CEPEA)",
78
+ description="Notícias Agrícolas (expira às 18h, mirror CEPEA)",
79
+ smart_expiry=True,
71
80
  ),
72
81
  }
73
82
 
@@ -106,6 +115,37 @@ def get_policy(source: Fonte | str, endpoint: str | None = None) -> CachePolicy:
106
115
  return POLICIES[default_key]
107
116
 
108
117
 
118
+ def _get_smart_expiry_time() -> datetime:
119
+ """
120
+ Calcula próximo horário de expiração para CEPEA (18h).
121
+
122
+ CEPEA atualiza dados por volta das 17-18h.
123
+ Cache expira às 18h para pegar dados novos.
124
+
125
+ Returns:
126
+ Datetime da próxima expiração
127
+ """
128
+ now = datetime.now()
129
+ today_expiry = datetime.combine(now.date(), time(CEPEA_UPDATE_HOUR, CEPEA_UPDATE_MINUTE))
130
+
131
+ if now < today_expiry:
132
+ # Ainda não chegou às 18h hoje → expira hoje às 18h
133
+ return today_expiry
134
+ else:
135
+ # Já passou das 18h → expira amanhã às 18h
136
+ return today_expiry + timedelta(days=1)
137
+
138
+
139
+ def _get_last_expiry_time() -> datetime:
140
+ """
141
+ Retorna o último horário de expiração (18h anterior).
142
+
143
+ Returns:
144
+ Datetime da última expiração
145
+ """
146
+ return _get_smart_expiry_time() - timedelta(days=1)
147
+
148
+
109
149
  def get_ttl(source: Fonte | str, endpoint: str | None = None) -> int:
110
150
  """
111
151
  Retorna TTL em segundos para uma fonte.
@@ -134,20 +174,31 @@ def get_stale_max(source: Fonte | str, endpoint: str | None = None) -> int:
134
174
  return get_policy(source, endpoint).stale_max_seconds
135
175
 
136
176
 
137
- def is_expired(created_at: datetime, source: Fonte | str) -> bool:
177
+ def is_expired(created_at: datetime, source: Fonte | str, endpoint: str | None = None) -> bool:
138
178
  """
139
179
  Verifica se entrada de cache está expirada.
140
180
 
181
+ Para fontes com smart_expiry (CEPEA), expira às 18h.
182
+ Para outras fontes, usa TTL fixo.
183
+
141
184
  Args:
142
185
  created_at: Data de criação
143
186
  source: Fonte de dados
187
+ endpoint: Endpoint específico
144
188
 
145
189
  Returns:
146
190
  True se expirado
147
191
  """
148
- ttl = get_ttl(source)
149
- expires_at = created_at + timedelta(seconds=ttl)
150
- return datetime.utcnow() > expires_at
192
+ policy = get_policy(source, endpoint)
193
+
194
+ if policy.smart_expiry:
195
+ # Smart expiry: cache válido se criado após última expiração (18h)
196
+ last_expiry = _get_last_expiry_time()
197
+ return created_at < last_expiry
198
+
199
+ # TTL fixo tradicional
200
+ expires_at = created_at + timedelta(seconds=policy.ttl_seconds)
201
+ return datetime.now() > expires_at
151
202
 
152
203
 
153
204
  def is_stale_acceptable(created_at: datetime, source: Fonte | str) -> bool:
@@ -163,13 +214,16 @@ def is_stale_acceptable(created_at: datetime, source: Fonte | str) -> bool:
163
214
  """
164
215
  stale_max = get_stale_max(source)
165
216
  max_acceptable = created_at + timedelta(seconds=stale_max)
166
- return datetime.utcnow() <= max_acceptable
217
+ return datetime.now() <= max_acceptable
167
218
 
168
219
 
169
220
  def calculate_expiry(source: Fonte | str, endpoint: str | None = None) -> datetime:
170
221
  """
171
222
  Calcula data de expiração para nova entrada.
172
223
 
224
+ Para fontes com smart_expiry (CEPEA), retorna próximas 18h.
225
+ Para outras fontes, usa TTL fixo.
226
+
173
227
  Args:
174
228
  source: Fonte de dados
175
229
  endpoint: Endpoint específico
@@ -177,8 +231,12 @@ def calculate_expiry(source: Fonte | str, endpoint: str | None = None) -> dateti
177
231
  Returns:
178
232
  Data de expiração
179
233
  """
180
- ttl = get_ttl(source, endpoint)
181
- return datetime.utcnow() + timedelta(seconds=ttl)
234
+ policy = get_policy(source, endpoint)
235
+
236
+ if policy.smart_expiry:
237
+ return _get_smart_expiry_time()
238
+
239
+ return datetime.now() + timedelta(seconds=policy.ttl_seconds)
182
240
 
183
241
 
184
242
  class InvalidationReason(Enum):
@@ -196,6 +254,7 @@ def should_refresh(
196
254
  created_at: datetime,
197
255
  source: Fonte | str,
198
256
  force: bool = False,
257
+ endpoint: str | None = None,
199
258
  ) -> tuple[bool, str]:
200
259
  """
201
260
  Determina se cache deve ser atualizado.
@@ -204,6 +263,7 @@ def should_refresh(
204
263
  created_at: Data de criação do cache
205
264
  source: Fonte de dados
206
265
  force: Forçar atualização
266
+ endpoint: Endpoint específico
207
267
 
208
268
  Returns:
209
269
  Tupla (deve_atualizar, razão)
@@ -211,7 +271,7 @@ def should_refresh(
211
271
  if force:
212
272
  return True, "force_refresh"
213
273
 
214
- if is_expired(created_at, source):
274
+ if is_expired(created_at, source, endpoint):
215
275
  return True, "expired"
216
276
 
217
277
  return False, "fresh"
@@ -238,3 +298,30 @@ def format_ttl(seconds: int) -> str:
238
298
 
239
299
  days = seconds // 86400
240
300
  return f"{days} dia{'s' if days > 1 else ''}"
301
+
302
+
303
+ def get_next_update_info(source: Fonte | str) -> dict[str, str]:
304
+ """
305
+ Retorna informações sobre próxima atualização.
306
+
307
+ Args:
308
+ source: Fonte de dados
309
+
310
+ Returns:
311
+ Dict com info de expiração
312
+ """
313
+ policy = get_policy(source)
314
+
315
+ if policy.smart_expiry:
316
+ next_expiry = _get_smart_expiry_time()
317
+ return {
318
+ "type": "smart",
319
+ "expires_at": next_expiry.strftime("%Y-%m-%d %H:%M"),
320
+ "description": f"Expira às {CEPEA_UPDATE_HOUR}h (atualização CEPEA)",
321
+ }
322
+
323
+ return {
324
+ "type": "ttl",
325
+ "ttl": format_ttl(policy.ttl_seconds),
326
+ "description": policy.description,
327
+ }
@@ -15,7 +15,7 @@ from agrobr.normalize.encoding import decode_content
15
15
  logger = structlog.get_logger()
16
16
 
17
17
  # Flag para controlar uso de browser
18
- _use_browser: bool = True
18
+ _use_browser: bool = False
19
19
  # Flag para controlar uso de fonte alternativa (Notícias Agrícolas)
20
20
  _use_alternative_source: bool = True
21
21
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agrobr"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Dados agrícolas brasileiros em uma linha de código"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes