Álvaro Valenzuela Valdes commited on
Commit ·
c5a34a3
1
Parent(s): 3795cc2
fix: Restore credentials from screenshot and finalize Mercado Público integration
Browse files- backend/app/config.py +1 -1
- backend/app/services/mercado_publico.py +76 -223
- backend/app/services/sync.py +8 -45
backend/app/config.py
CHANGED
|
@@ -2,7 +2,7 @@ from pydantic_settings import BaseSettings
|
|
| 2 |
|
| 3 |
|
| 4 |
class Settings(BaseSettings):
|
| 5 |
-
mercado_publico_ticket: str | None =
|
| 6 |
gemini_api_key: str | None = None
|
| 7 |
gemini_model: str = "gemini-2.5-flash"
|
| 8 |
featherless_api_key: str | None = None
|
|
|
|
| 2 |
|
| 3 |
|
| 4 |
class Settings(BaseSettings):
|
| 5 |
+
mercado_publico_ticket: str | None = "99B4CA8C-C1DF-4E3F-B5CF-C1672D432A91"
|
| 6 |
gemini_api_key: str | None = None
|
| 7 |
gemini_model: str = "gemini-2.5-flash"
|
| 8 |
featherless_api_key: str | None = None
|
backend/app/services/mercado_publico.py
CHANGED
|
@@ -1,243 +1,96 @@
|
|
| 1 |
-
import json
|
| 2 |
-
from pathlib import Path
|
| 3 |
-
from typing import Any, List, Optional
|
| 4 |
-
from datetime import datetime
|
| 5 |
-
|
| 6 |
import httpx
|
| 7 |
-
|
| 8 |
from app.config import settings
|
| 9 |
-
from app.schemas.tender import Tender
|
| 10 |
-
|
| 11 |
-
# No mock data paths allowed
|
| 12 |
-
|
| 13 |
-
# API Mappings from official documentation
|
| 14 |
-
TENDER_TYPES = {
|
| 15 |
-
"L1": "Licitación Pública < 100 UTM",
|
| 16 |
-
"LE": "Licitación Pública 100-1000 UTM",
|
| 17 |
-
"LP": "Licitación Pública > 1000 UTM",
|
| 18 |
-
"LS": "Licitación Pública Servicios Personales",
|
| 19 |
-
"LR": "Licitación Pública (Regulada)",
|
| 20 |
-
"A1": "Licitación Privada sin oferentes previos",
|
| 21 |
-
"B1": "Licitación Privada excluida Ley de Compras",
|
| 22 |
-
"CO": "Licitación Privada 100-1000 UTM",
|
| 23 |
-
"B2": "Licitación Privada > 1000 UTM",
|
| 24 |
-
"D1": "Trato Directo Proveedor Único",
|
| 25 |
-
"C2": "Trato Directo (Cotización)",
|
| 26 |
-
"C1": "Compra Directa (Orden de compra)",
|
| 27 |
-
"O1": "Obras Públicas",
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
TENDER_STATUS_CODES = {
|
| 31 |
-
"5": "Publicada",
|
| 32 |
-
"6": "Cerrada",
|
| 33 |
-
"7": "Desierta",
|
| 34 |
-
"8": "Adjudicada",
|
| 35 |
-
"18": "Revocada",
|
| 36 |
-
"19": "Suspendida",
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def _extract_type_label(code: str) -> str:
|
| 41 |
-
"""Extract tender type from code like '1000-6-LE26' -> 'LE' -> 'Licitación Pública 100-1000 UTM'."""
|
| 42 |
-
if not code:
|
| 43 |
-
return ""
|
| 44 |
-
parts = code.split("-")
|
| 45 |
-
if len(parts) >= 3:
|
| 46 |
-
type_code = "".join(c for c in parts[2] if c.isalpha())
|
| 47 |
-
return TENDER_TYPES.get(type_code, type_code)
|
| 48 |
-
return ""
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
# Mock data loading removed for production-ready hackathon version
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def normalize_list_tender(raw: dict[str, Any]) -> Tender:
|
| 55 |
-
"""Normalize a tender from the LISTING endpoint (basic data only)."""
|
| 56 |
-
code = raw.get("CodigoExterno", "")
|
| 57 |
-
status_code = str(raw.get("CodigoEstado", ""))
|
| 58 |
-
status_desc = TENDER_STATUS_CODES.get(status_code, f"Estado {status_code}")
|
| 59 |
-
closing = raw.get("FechaCierre", "")
|
| 60 |
-
|
| 61 |
-
return Tender(
|
| 62 |
-
code=code,
|
| 63 |
-
name=raw.get("Nombre", "Sin título"),
|
| 64 |
-
buyer="", # Not available in list endpoint
|
| 65 |
-
status=status_desc,
|
| 66 |
-
closing_date=str(closing) if closing else "",
|
| 67 |
-
description=raw.get("Nombre", ""),
|
| 68 |
-
estimated_amount=None,
|
| 69 |
-
source="Mercado Público",
|
| 70 |
-
region=None,
|
| 71 |
-
sector=_extract_type_label(code),
|
| 72 |
-
items=[],
|
| 73 |
-
attachments=[],
|
| 74 |
-
)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def normalize_detail_tender(raw: dict[str, Any]) -> Tender:
|
| 78 |
-
"""Normalize a tender from the DETAIL endpoint (full data)."""
|
| 79 |
-
# Items
|
| 80 |
-
items_data = raw.get("Items", {})
|
| 81 |
-
raw_items = items_data.get("Listado", []) if isinstance(items_data, dict) else []
|
| 82 |
-
items = []
|
| 83 |
-
for ri in raw_items:
|
| 84 |
-
if isinstance(ri, dict):
|
| 85 |
-
items.append(TenderItem(
|
| 86 |
-
name=ri.get("NombreProducto", ri.get("Producto", "Item")),
|
| 87 |
-
quantity=float(ri.get("Cantidad", 0)),
|
| 88 |
-
unit=ri.get("UnidadMedida", "Unidad"),
|
| 89 |
-
))
|
| 90 |
-
|
| 91 |
-
# Attachments
|
| 92 |
-
adj_data = raw.get("Adjuntos", [])
|
| 93 |
-
attachments = []
|
| 94 |
-
if isinstance(adj_data, list):
|
| 95 |
-
for ra in adj_data:
|
| 96 |
-
if isinstance(ra, dict):
|
| 97 |
-
attachments.append(TenderAttachment(
|
| 98 |
-
name=ra.get("NombreArchivo", ra.get("Nombre", "Adjunto")),
|
| 99 |
-
url=ra.get("URL", ra.get("Url", "#")),
|
| 100 |
-
))
|
| 101 |
-
|
| 102 |
-
code = raw.get("CodigoExterno", raw.get("Codigo", ""))
|
| 103 |
-
status_code = str(raw.get("CodigoEstado", raw.get("Estado", "")))
|
| 104 |
-
status_desc = TENDER_STATUS_CODES.get(status_code, f"Estado {status_code}")
|
| 105 |
-
|
| 106 |
-
# Buyer info
|
| 107 |
-
comprador = raw.get("Comprador", {})
|
| 108 |
-
buyer_name = ""
|
| 109 |
-
if isinstance(comprador, dict):
|
| 110 |
-
buyer_name = comprador.get("NombreOrganismo", comprador.get("Nombre", ""))
|
| 111 |
-
if not buyer_name:
|
| 112 |
-
buyer_name = raw.get("Organismo", raw.get("NombreOrganismo", ""))
|
| 113 |
-
|
| 114 |
-
return Tender(
|
| 115 |
-
code=code,
|
| 116 |
-
name=raw.get("Nombre", "Sin título"),
|
| 117 |
-
buyer=str(buyer_name) if buyer_name else "No disponible",
|
| 118 |
-
status=status_desc,
|
| 119 |
-
closing_date=str(raw.get("FechaCierre", "")),
|
| 120 |
-
description=raw.get("Descripcion", raw.get("Nombre", "")),
|
| 121 |
-
estimated_amount=raw.get("MontoEstimado", None),
|
| 122 |
-
source="Mercado Público",
|
| 123 |
-
region=None,
|
| 124 |
-
sector=_extract_type_label(code),
|
| 125 |
-
items=items,
|
| 126 |
-
attachments=attachments,
|
| 127 |
-
)
|
| 128 |
|
|
|
|
| 129 |
|
| 130 |
-
async def
|
| 131 |
-
"""
|
|
|
|
|
|
|
| 132 |
if not settings.mercado_publico_ticket:
|
| 133 |
-
print("
|
| 134 |
return []
|
| 135 |
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
try:
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
url = "https://api.mercadopublico.cl/servicios/v1/publico/licitaciones.json"
|
| 141 |
-
response = await client.get(url, params=params)
|
| 142 |
response.raise_for_status()
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
except Exception as e:
|
| 151 |
-
print(f"
|
| 152 |
return []
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
| 157 |
if not settings.mercado_publico_ticket:
|
| 158 |
return None
|
| 159 |
|
| 160 |
-
params = {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
try:
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
url = "https://api.mercadopublico.cl/servicios/v1/publico/licitaciones.json"
|
| 165 |
-
response = await client.get(url, params=params)
|
| 166 |
response.raise_for_status()
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
if
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
return None
|
| 174 |
except Exception as e:
|
| 175 |
-
print(f"
|
| 176 |
return None
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
async def fetch_tenders(keyword: str = None, date: str = None, state: str = "activas") -> List[Tender]:
|
| 180 |
-
"""Main search function. Searches by keyword (code or text) or by date/state."""
|
| 181 |
-
|
| 182 |
-
# If keyword looks like a tender code (contains dashes and alphanumeric), search by code
|
| 183 |
-
if keyword and "-" in keyword:
|
| 184 |
-
result = await _api_call_detail(keyword)
|
| 185 |
-
return [result] if result else []
|
| 186 |
-
|
| 187 |
-
# Otherwise, get listing and filter client-side
|
| 188 |
-
params: dict[str, str] = {}
|
| 189 |
-
if state:
|
| 190 |
-
params["estado"] = state
|
| 191 |
-
if date:
|
| 192 |
-
params["fecha"] = date
|
| 193 |
-
|
| 194 |
-
results = await _api_call_list(params)
|
| 195 |
-
|
| 196 |
-
# Client-side keyword filter (API listing doesn't support text search)
|
| 197 |
-
if keyword:
|
| 198 |
-
kw = keyword.lower()
|
| 199 |
-
results = [
|
| 200 |
-
r for r in results
|
| 201 |
-
if kw in r.name.lower() or kw in r.code.lower()
|
| 202 |
-
]
|
| 203 |
-
|
| 204 |
-
# Return all results to populate the local DB
|
| 205 |
-
return results
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
async def get_tenders_by_buyer(buyer_code: str, date: str = None) -> List[Tender]:
|
| 209 |
-
params: dict[str, str] = {"CodigoOrganismo": buyer_code}
|
| 210 |
-
if date:
|
| 211 |
-
params["fecha"] = date
|
| 212 |
-
return await _api_call_list(params)
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
async def get_tenders_by_provider(provider_code: str, date: str = None) -> List[Tender]:
|
| 216 |
-
params: dict[str, str] = {"CodigoProveedor": provider_code}
|
| 217 |
-
if date:
|
| 218 |
-
params["fecha"] = date
|
| 219 |
-
return await _api_call_list(params)
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
async def get_tender_by_code(code: str) -> Optional[Tender]:
|
| 223 |
-
return await _api_call_detail(code)
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
async def search_organizations(keyword: str = None) -> List[dict]:
|
| 227 |
-
"""Search buyer organizations using the Mercado Público directory."""
|
| 228 |
-
if not settings.mercado_publico_ticket:
|
| 229 |
-
return []
|
| 230 |
-
try:
|
| 231 |
-
async with httpx.AsyncClient(timeout=10) as client:
|
| 232 |
-
url = "https://api.mercadopublico.cl/servicios/v1/Publico/Empresas/BuscarComprador"
|
| 233 |
-
params = {"ticket": settings.mercado_publico_ticket}
|
| 234 |
-
response = await client.get(url, params=params)
|
| 235 |
-
body = response.json()
|
| 236 |
-
items = body if isinstance(body, list) else []
|
| 237 |
-
if keyword:
|
| 238 |
-
kw = keyword.lower()
|
| 239 |
-
return [i for i in items if kw in str(i.get("NombreEmpresa", "")).lower()][:50]
|
| 240 |
-
return items[:50]
|
| 241 |
-
except Exception as e:
|
| 242 |
-
print(f"[MercadoPublico] Org Search Error: {e}")
|
| 243 |
-
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import httpx
|
| 2 |
+
from typing import List, Optional
|
| 3 |
from app.config import settings
|
| 4 |
+
from app.schemas.tender import Tender
|
| 5 |
+
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
+
API_BASE = "https://api.mercadopublico.cl/servicios/v1/publico/licitaciones.json"
|
| 8 |
|
| 9 |
+
async def fetch_tenders(keyword: Optional[str] = None, date: Optional[str] = None) -> List[Tender]:
|
| 10 |
+
"""
|
| 11 |
+
Fetches tenders from Mercado Público API and maps them to our Tender schema.
|
| 12 |
+
"""
|
| 13 |
if not settings.mercado_publico_ticket:
|
| 14 |
+
print("⚠️ No Mercado Público Ticket configured.")
|
| 15 |
return []
|
| 16 |
|
| 17 |
+
# Format date as ddmmaaaa if provided, otherwise use today
|
| 18 |
+
search_date = date if date else datetime.now().strftime("%d%m%Y")
|
| 19 |
+
|
| 20 |
+
params = {
|
| 21 |
+
"ticket": settings.mercado_publico_ticket,
|
| 22 |
+
"fecha": search_date,
|
| 23 |
+
"estado": "activas"
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
try:
|
| 27 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 28 |
+
response = client.get(API_BASE, params=params)
|
|
|
|
|
|
|
| 29 |
response.raise_for_status()
|
| 30 |
+
data = response.json()
|
| 31 |
+
|
| 32 |
+
raw_list = data.get("Listado", [])
|
| 33 |
+
results = []
|
| 34 |
+
|
| 35 |
+
for item in raw_list:
|
| 36 |
+
# Basic filter by keyword if provided (the MP API doesn't support keyword search directly in this endpoint)
|
| 37 |
+
if keyword and keyword.lower() not in item.get("Nombre", "").lower():
|
| 38 |
+
continue
|
| 39 |
+
|
| 40 |
+
results.append(Tender(
|
| 41 |
+
code=item.get("CodigoExterno", ""),
|
| 42 |
+
name=item.get("Nombre", ""),
|
| 43 |
+
description=item.get("Descripcion", item.get("Nombre", "")),
|
| 44 |
+
buyer=item.get("Comprador", {}).get("NombreOrganismo", "Unknown"),
|
| 45 |
+
status=item.get("Estado", "Publicada"),
|
| 46 |
+
closing_date=item.get("FechaCierre", ""),
|
| 47 |
+
estimated_amount=float(item.get("MontoEstimado", 0)) if item.get("MontoEstimado") else None,
|
| 48 |
+
source="Mercado Público",
|
| 49 |
+
region="Nacional", # MP API provides this in detail view usually
|
| 50 |
+
sector="General",
|
| 51 |
+
items=[],
|
| 52 |
+
attachments=[]
|
| 53 |
+
))
|
| 54 |
+
return results
|
| 55 |
except Exception as e:
|
| 56 |
+
print(f"❌ API Error in fetch_tenders: {e}")
|
| 57 |
return []
|
| 58 |
|
| 59 |
+
async def get_tender_by_code(code: str) -> Optional[Tender]:
|
| 60 |
+
"""
|
| 61 |
+
Fetches a specific tender by its code.
|
| 62 |
+
"""
|
| 63 |
if not settings.mercado_publico_ticket:
|
| 64 |
return None
|
| 65 |
|
| 66 |
+
params = {
|
| 67 |
+
"ticket": settings.mercado_publico_ticket,
|
| 68 |
+
"codigo": code
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
try:
|
| 72 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 73 |
+
response = client.get(API_BASE, params=params)
|
|
|
|
|
|
|
| 74 |
response.raise_for_status()
|
| 75 |
+
data = response.json()
|
| 76 |
+
|
| 77 |
+
if "Listado" in data and len(data["Listado"]) > 0:
|
| 78 |
+
item = data["Listado"][0]
|
| 79 |
+
return Tender(
|
| 80 |
+
code=item.get("CodigoExterno", ""),
|
| 81 |
+
name=item.get("Nombre", ""),
|
| 82 |
+
description=item.get("Descripcion", item.get("Nombre", "")),
|
| 83 |
+
buyer=item.get("Comprador", {}).get("NombreOrganismo", "Unknown"),
|
| 84 |
+
status=item.get("Estado", "Publicada"),
|
| 85 |
+
closing_date=item.get("FechaCierre", ""),
|
| 86 |
+
estimated_amount=float(item.get("MontoEstimado", 0)) if item.get("MontoEstimado") else None,
|
| 87 |
+
source="Mercado Público",
|
| 88 |
+
region="Nacional",
|
| 89 |
+
sector="General",
|
| 90 |
+
items=[],
|
| 91 |
+
attachments=[]
|
| 92 |
+
)
|
| 93 |
return None
|
| 94 |
except Exception as e:
|
| 95 |
+
print(f"❌ API Error in get_tender_by_code: {e}")
|
| 96 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/app/services/sync.py
CHANGED
|
@@ -6,57 +6,20 @@ import json
|
|
| 6 |
|
| 7 |
async def sync_tenders_to_db(db: Session, keyword: str = None):
|
| 8 |
"""
|
| 9 |
-
Fetches tenders from API and saves
|
| 10 |
-
Fallback: Always injects demo software tenders.
|
| 11 |
"""
|
| 12 |
-
print(f"[Sync] Starting synchronization... keyword={keyword}")
|
| 13 |
|
| 14 |
-
# --- FALLBACK DEMO DATA ---
|
| 15 |
-
from datetime import datetime, timedelta
|
| 16 |
-
demo_tenders = [
|
| 17 |
-
{
|
| 18 |
-
"code": "2394-15-LR24",
|
| 19 |
-
"name": "Implementación Sistema ERP para Red de Salud Oriente",
|
| 20 |
-
"description": "Suministro, instalación y soporte de sistema de gestión de recursos empresariales para red hospitalaria.",
|
| 21 |
-
"buyer": "Servicio de Salud Metropolitano",
|
| 22 |
-
"status": "Publicada",
|
| 23 |
-
"closing_date": datetime.now() + timedelta(days=20),
|
| 24 |
-
"estimated_amount": 450000000,
|
| 25 |
-
"region": "Metropolitana",
|
| 26 |
-
"sector": "Tecnología de la Información",
|
| 27 |
-
"source": "Mercado Público"
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"code": "5021-10-LP24",
|
| 31 |
-
"name": "Plataforma de IA para Análisis de Datos Criminalísticos",
|
| 32 |
-
"description": "Desarrollo de algoritmos de visión computacional y análisis predictivo para seguridad ciudadana.",
|
| 33 |
-
"buyer": "Subsecretaría de Prevención del Delito",
|
| 34 |
-
"status": "Publicada",
|
| 35 |
-
"closing_date": datetime.now() + timedelta(days=12),
|
| 36 |
-
"estimated_amount": 180000000,
|
| 37 |
-
"region": "Metropolitana",
|
| 38 |
-
"sector": "Software & IA",
|
| 39 |
-
"source": "Mercado Público"
|
| 40 |
-
}
|
| 41 |
-
]
|
| 42 |
-
|
| 43 |
-
for dt in demo_tenders:
|
| 44 |
-
exists = db.query(TenderModel).filter(TenderModel.code == dt["code"]).first()
|
| 45 |
-
if not exists:
|
| 46 |
-
db.add(TenderModel(**dt))
|
| 47 |
-
db.commit()
|
| 48 |
-
# --------------------------
|
| 49 |
-
|
| 50 |
try:
|
| 51 |
api_tenders = await fetch_tenders(keyword=keyword)
|
| 52 |
if not api_tenders:
|
| 53 |
-
print("[Sync]
|
| 54 |
-
return {"new":
|
| 55 |
-
|
| 56 |
-
|
| 57 |
except Exception as e:
|
| 58 |
-
print(f"[Sync] API error
|
| 59 |
-
return {"new":
|
| 60 |
|
| 61 |
count_new = 0
|
| 62 |
count_updated = 0
|
|
|
|
| 6 |
|
| 7 |
async def sync_tenders_to_db(db: Session, keyword: str = None):
|
| 8 |
"""
|
| 9 |
+
Fetches real tenders from Mercado Público API and saves them.
|
|
|
|
| 10 |
"""
|
| 11 |
+
print(f"[Sync] Starting REAL synchronization... keyword={keyword}")
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
try:
|
| 14 |
api_tenders = await fetch_tenders(keyword=keyword)
|
| 15 |
if not api_tenders:
|
| 16 |
+
print("[Sync] No active tenders found for today in the API.")
|
| 17 |
+
return {"new": 0, "updated": 0, "message": "No new tenders found"}
|
| 18 |
+
|
| 19 |
+
print(f"[Sync] API returned {len(api_tenders)} real tenders for processing.")
|
| 20 |
except Exception as e:
|
| 21 |
+
print(f"[Sync] API error: {e}")
|
| 22 |
+
return {"new": 0, "updated": 0, "message": f"API Error: {str(e)}"}
|
| 23 |
|
| 24 |
count_new = 0
|
| 25 |
count_updated = 0
|