| 
							
							
							
						 |  |  | @ -1,4 +1,5 @@ | 
		
	
		
			
				|  |  |  |  | #!/usr/bin/env python3 | 
		
	
		
			
				|  |  |  |  | # -*- coding: utf-8 -*- | 
		
	
		
			
				|  |  |  |  | """ | 
		
	
		
			
				|  |  |  |  | Rate Update Script | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
	
		
			
				
					|  |  |  | @ -18,15 +19,22 @@ import requests | 
		
	
		
			
				|  |  |  |  | import sys | 
		
	
		
			
				|  |  |  |  | import subprocess | 
		
	
		
			
				|  |  |  |  | import importlib.util | 
		
	
		
			
				|  |  |  |  | import time | 
		
	
		
			
				|  |  |  |  | from pathlib import Path | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | # Configuración de logging | 
		
	
		
			
				|  |  |  |  | logging.basicConfig( | 
		
	
		
			
				|  |  |  |  |     level=logging.INFO, | 
		
	
		
			
				|  |  |  |  |     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | 
		
	
		
			
				|  |  |  |  |     handlers=[logging.StreamHandler()] | 
		
	
		
			
				|  |  |  |  | ) | 
		
	
		
			
				|  |  |  |  | # Por defecto, establecemos nivel WARNING para la consola (solo errores y advertencias) | 
		
	
		
			
				|  |  |  |  | console_handler = logging.StreamHandler() | 
		
	
		
			
				|  |  |  |  | console_handler.setLevel(logging.WARNING) | 
		
	
		
			
				|  |  |  |  | console_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | # Configuración global del logger | 
		
	
		
			
				|  |  |  |  | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[]) | 
		
	
		
			
				|  |  |  |  | logger = logging.getLogger('rate_update') | 
		
	
		
			
				|  |  |  |  | logger.addHandler(console_handler) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | # Variable para controlar si se muestra el resultado en la consola | 
		
	
		
			
				|  |  |  |  | SHOW_RESULTS = True | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | # Verificar si pycountry está instalado, si no, instalarlo | 
		
	
		
			
				|  |  |  |  | def check_install_pycountry(): | 
		
	
	
		
			
				
					|  |  |  | @ -77,30 +85,96 @@ def check_install_pycountry(): | 
		
	
		
			
				|  |  |  |  | # Directorio base del proyecto | 
		
	
		
			
				|  |  |  |  | BASE_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | 
		
	
		
			
				|  |  |  |  | CONFIG_DIR = BASE_DIR / 'bin' / 'config' | 
		
	
		
			
				|  |  |  |  | RATES_DIR = CONFIG_DIR / 'rates'  # Nueva ubicación para archivos de tarifas | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | # Archivo de configuración para el modelo de IA | 
		
	
		
			
				|  |  |  |  | # Archivos de configuración para la API de Perplexity | 
		
	
		
			
				|  |  |  |  | MODEL_CONFIG_FILE = CONFIG_DIR / 'rate_model.ai' | 
		
	
		
			
				|  |  |  |  | TEMPERATURE_CONFIG_FILE = CONFIG_DIR / 'rate_temperature.ai' | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def get_ai_model(): | 
		
	
		
			
				|  |  |  |  |     """Obtener el modelo de IA configurado.""" | 
		
	
		
			
				|  |  |  |  |     try: | 
		
	
		
			
				|  |  |  |  |         with open(MODEL_CONFIG_FILE, 'r') as f: | 
		
	
		
			
				|  |  |  |  |             return f.read().strip() | 
		
	
		
			
				|  |  |  |  |         with open(MODEL_CONFIG_FILE, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |             model = f.read().strip() | 
		
	
		
			
				|  |  |  |  |             # Si hay un valor específico configurado, usarlo | 
		
	
		
			
				|  |  |  |  |             if model and model.strip(): | 
		
	
		
			
				|  |  |  |  |                 return model | 
		
	
		
			
				|  |  |  |  |     except FileNotFoundError: | 
		
	
		
			
				|  |  |  |  |         logger.warning(f"Archivo de configuración {MODEL_CONFIG_FILE} no encontrado. Usando modelo predeterminado 'o1'.") | 
		
	
		
			
				|  |  |  |  |         return "o1" | 
		
	
		
			
				|  |  |  |  |         logger.warning(f"Archivo de configuración {MODEL_CONFIG_FILE} no encontrado. Usando modelo predeterminado 'sonar'.") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Usar 'sonar' como modelo predeterminado (el más adecuado para la API de Perplexity) | 
		
	
		
			
				|  |  |  |  |     return "sonar" | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def get_ai_temperature(): | 
		
	
		
			
				|  |  |  |  |     """Obtener la temperatura configurada para la API de Perplexity.""" | 
		
	
		
			
				|  |  |  |  |     try: | 
		
	
		
			
				|  |  |  |  |         with open(TEMPERATURE_CONFIG_FILE, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |             temp_str = f.read().strip() | 
		
	
		
			
				|  |  |  |  |             # Si hay un valor específico configurado, intentar convertirlo a float | 
		
	
		
			
				|  |  |  |  |             if temp_str and temp_str.strip(): | 
		
	
		
			
				|  |  |  |  |                 try: | 
		
	
		
			
				|  |  |  |  |                     temp = float(temp_str) | 
		
	
		
			
				|  |  |  |  |                     # Validar que está en el rango correcto (0.0 a 1.0) | 
		
	
		
			
				|  |  |  |  |                     if 0.0 <= temp <= 1.0: | 
		
	
		
			
				|  |  |  |  |                         return temp | 
		
	
		
			
				|  |  |  |  |                     else: | 
		
	
		
			
				|  |  |  |  |                         logger.warning(f"Temperatura fuera de rango en {TEMPERATURE_CONFIG_FILE}: {temp}. Usando valor predeterminado 0.3.") | 
		
	
		
			
				|  |  |  |  |                 except ValueError: | 
		
	
		
			
				|  |  |  |  |                     logger.warning(f"Valor no numérico en {TEMPERATURE_CONFIG_FILE}: {temp_str}. Usando valor predeterminado 0.3.") | 
		
	
		
			
				|  |  |  |  |     except FileNotFoundError: | 
		
	
		
			
				|  |  |  |  |         logger.info(f"Archivo de configuración {TEMPERATURE_CONFIG_FILE} no encontrado. Usando temperatura predeterminada 0.3.") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Usar 0.3 como temperatura predeterminada | 
		
	
		
			
				|  |  |  |  |     return 0.3 | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def get_perplexity_api_key(): | 
		
	
		
			
				|  |  |  |  |     """Obtener la clave API de Perplexity desde una variable de entorno.""" | 
		
	
		
			
				|  |  |  |  |     """Obtener la clave API de Perplexity desde una variable de entorno o archivo.""" | 
		
	
		
			
				|  |  |  |  |     # Intentar obtener la clave de la variable de entorno | 
		
	
		
			
				|  |  |  |  |     api_key = os.environ.get('PERPLEXITY_API_KEY') | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Si no está en la variable de entorno, intentar cargarla desde un archivo | 
		
	
		
			
				|  |  |  |  |     if not api_key: | 
		
	
		
			
				|  |  |  |  |         logger.error("No se encontró la clave API de Perplexity. Establezca la variable de entorno PERPLEXITY_API_KEY.") | 
		
	
		
			
				|  |  |  |  |         # Rutas posibles para el archivo de API key | 
		
	
		
			
				|  |  |  |  |         possible_paths = [ | 
		
	
		
			
				|  |  |  |  |             os.path.expanduser('~/.perplexity/api_key'), | 
		
	
		
			
				|  |  |  |  |             os.path.expanduser('~/.config/perplexity/api_key'), | 
		
	
		
			
				|  |  |  |  |             os.path.join(BASE_DIR, 'bin', 'config', 'perplexity_api_key') | 
		
	
		
			
				|  |  |  |  |         ] | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         for path in possible_paths: | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 if os.path.exists(path): | 
		
	
		
			
				|  |  |  |  |                     logger.info(f"Intentando cargar API key desde: {path}") | 
		
	
		
			
				|  |  |  |  |                     with open(path, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |                         api_key = f.read().strip() | 
		
	
		
			
				|  |  |  |  |                         if api_key: | 
		
	
		
			
				|  |  |  |  |                             logger.info("API key de Perplexity cargada desde archivo.") | 
		
	
		
			
				|  |  |  |  |                             break | 
		
	
		
			
				|  |  |  |  |             except Exception as e: | 
		
	
		
			
				|  |  |  |  |                 logger.warning(f"Error al leer el archivo de API key {path}: {e}") | 
		
	
		
			
				|  |  |  |  |                 continue | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Si seguimos sin tener API key, mostrar error y salir | 
		
	
		
			
				|  |  |  |  |     if not api_key: | 
		
	
		
			
				|  |  |  |  |         logger.error("No se encontró la clave API de Perplexity.") | 
		
	
		
			
				|  |  |  |  |         logger.error("Opciones para configurarla:") | 
		
	
		
			
				|  |  |  |  |         logger.error("1. Establecer la variable de entorno PERPLEXITY_API_KEY") | 
		
	
		
			
				|  |  |  |  |         logger.error("2. Crear un archivo ~/.perplexity/api_key con la clave") | 
		
	
		
			
				|  |  |  |  |         logger.error("3. Crear un archivo ~/.config/perplexity/api_key con la clave") | 
		
	
		
			
				|  |  |  |  |         logger.error("4. Crear un archivo bin/config/perplexity_api_key en el directorio del proyecto") | 
		
	
		
			
				|  |  |  |  |         sys.exit(1) | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |     # Validar formato básico de la API key | 
		
	
		
			
				|  |  |  |  |     if not api_key.startswith('pplx-'): | 
		
	
		
			
				|  |  |  |  |         logger.warning("El formato de la API key de Perplexity parece incorrecto.") | 
		
	
		
			
				|  |  |  |  |         logger.warning("Las API keys de Perplexity suelen comenzar con 'pplx-'.") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |     return api_key | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def query_perplexity(prompt, model="o1"): | 
		
	
		
			
				|  |  |  |  | def query_perplexity(prompt, model="sonar"): | 
		
	
		
			
				|  |  |  |  |     """Realizar una consulta a la API de Perplexity.""" | 
		
	
		
			
				|  |  |  |  |     api_key = get_perplexity_api_key() | 
		
	
		
			
				|  |  |  |  |     temperature = get_ai_temperature()  # Obtener temperatura configurada | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     url = "https://api.perplexity.ai/chat/completions" | 
		
	
		
			
				|  |  |  |  |     headers = { | 
		
	
	
		
			
				
					|  |  |  | @ -108,38 +182,62 @@ def query_perplexity(prompt, model="o1"): | 
		
	
		
			
				|  |  |  |  |         "Content-Type": "application/json" | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Configuración simple y directa para la API | 
		
	
		
			
				|  |  |  |  |     data = { | 
		
	
		
			
				|  |  |  |  |         "model": model, | 
		
	
		
			
				|  |  |  |  |         "messages": [ | 
		
	
		
			
				|  |  |  |  |             { | 
		
	
		
			
				|  |  |  |  |                 "role": "system", | 
		
	
		
			
				|  |  |  |  |                 "content": "You are a helpful assistant that provides accurate salary information for developers worldwide. Respond only with numeric values in USD with 2 decimal places." | 
		
	
		
			
				|  |  |  |  |                 "content": "You are a helpful assistant that provides accurate salary information for developers worldwide. Respond only with numeric values in USD with 2 decimal places. Do not include any text, currency symbols, or other characters." | 
		
	
		
			
				|  |  |  |  |             }, | 
		
	
		
			
				|  |  |  |  |             { | 
		
	
		
			
				|  |  |  |  |                 "role": "user", | 
		
	
		
			
				|  |  |  |  |                 "content": prompt | 
		
	
		
			
				|  |  |  |  |             } | 
		
	
		
			
				|  |  |  |  |         ], | 
		
	
		
			
				|  |  |  |  |         "max_tokens": 100 | 
		
	
		
			
				|  |  |  |  |         "max_tokens": 100, | 
		
	
		
			
				|  |  |  |  |         "temperature": temperature  # Usar temperatura configurada para respuestas | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     try: | 
		
	
		
			
				|  |  |  |  |         logger.info(f"Enviando consulta a Perplexity usando modelo: {model}") | 
		
	
		
			
				|  |  |  |  |         logger.info(f"Temperatura configurada: {temperature}") | 
		
	
		
			
				|  |  |  |  |         logger.info(f"Prompt: {prompt}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         response = requests.post(url, headers=headers, json=data) | 
		
	
		
			
				|  |  |  |  |         response.raise_for_status() | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si la respuesta no es 200, mostrar el error | 
		
	
		
			
				|  |  |  |  |         if response.status_code != 200: | 
		
	
		
			
				|  |  |  |  |             logger.error(f"Error de API Perplexity ({response.status_code}): {response.text}") | 
		
	
		
			
				|  |  |  |  |             return None | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Extraer el contenido de la respuesta | 
		
	
		
			
				|  |  |  |  |         result = response.json() | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Extraer solo el valor numérico de la respuesta | 
		
	
		
			
				|  |  |  |  |         content = result.get('choices', [{}])[0].get('message', {}).get('content', '') | 
		
	
		
			
				|  |  |  |  |         # Intentar encontrar un número con 2 decimales en la respuesta | 
		
	
		
			
				|  |  |  |  |         match = re.search(r'\$?(\d+\.\d{2})', content) | 
		
	
		
			
				|  |  |  |  |         if match: | 
		
	
		
			
				|  |  |  |  |             return float(match.group(1)) | 
		
	
		
			
				|  |  |  |  |         logger.info(f"Contenido de la respuesta: {content}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si no encuentra un formato exacto, intentar convertir toda la respuesta a float | 
		
	
		
			
				|  |  |  |  |         # Extraer solo el valor numérico, limpiando cualquier formato | 
		
	
		
			
				|  |  |  |  |         content_clean = content.replace(',', '') | 
		
	
		
			
				|  |  |  |  |         match = re.search(r'(\d+\.\d+|\d+)', content_clean) | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         if match: | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 value = float(match.group(1)) | 
		
	
		
			
				|  |  |  |  |                 logger.info(f"Valor extraído: {value}") | 
		
	
		
			
				|  |  |  |  |                 return round(value, 2)  # Asegurar 2 decimales | 
		
	
		
			
				|  |  |  |  |             except ValueError: | 
		
	
		
			
				|  |  |  |  |                 pass | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si no encontramos un número con el patrón regular, intenta limpiar todo | 
		
	
		
			
				|  |  |  |  |         try: | 
		
	
		
			
				|  |  |  |  |             # Eliminar cualquier símbolo de moneda y espacios | 
		
	
		
			
				|  |  |  |  |             cleaned_content = re.sub(r'[^\d.]', '', content) | 
		
	
		
			
				|  |  |  |  |             return round(float(cleaned_content), 2) | 
		
	
		
			
				|  |  |  |  |             if cleaned_content: | 
		
	
		
			
				|  |  |  |  |                 value = float(cleaned_content) | 
		
	
		
			
				|  |  |  |  |                 logger.info(f"Valor limpiado y extraído: {value}") | 
		
	
		
			
				|  |  |  |  |                 return round(value, 2) | 
		
	
		
			
				|  |  |  |  |             else: | 
		
	
		
			
				|  |  |  |  |                 logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}") | 
		
	
		
			
				|  |  |  |  |                 return None | 
		
	
		
			
				|  |  |  |  |         except ValueError: | 
		
	
		
			
				|  |  |  |  |             logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}") | 
		
	
		
			
				|  |  |  |  |             return None | 
		
	
	
		
			
				
					|  |  |  | @ -147,6 +245,9 @@ def query_perplexity(prompt, model="o1"): | 
		
	
		
			
				|  |  |  |  |     except requests.exceptions.RequestException as e: | 
		
	
		
			
				|  |  |  |  |         logger.error(f"Error al conectar con la API de Perplexity: {e}") | 
		
	
		
			
				|  |  |  |  |         return None | 
		
	
		
			
				|  |  |  |  |     except Exception as e: | 
		
	
		
			
				|  |  |  |  |         logger.error(f"Error inesperado en la consulta a Perplexity: {e}") | 
		
	
		
			
				|  |  |  |  |         return None | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | # Variable global para indicar si pycountry está disponible | 
		
	
		
			
				|  |  |  |  | pycountry_available = False | 
		
	
	
		
			
				
					|  |  |  | @ -249,9 +350,10 @@ def generate_prompt(programmer_type, region_code): | 
		
	
		
			
				|  |  |  |  |         'it': 'Italia' | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Archivos especiales como kdevs.rate se manejan de forma diferente | 
		
	
		
			
				|  |  |  |  |     if programmer_type == 'kdevs': | 
		
	
		
			
				|  |  |  |  |         # Simplemente mantener el valor actual para kdevs.rate | 
		
	
		
			
				|  |  |  |  |     # Ya no tenemos archivos especiales como kdevs.rate | 
		
	
		
			
				|  |  |  |  |     # Este bloque solo debe ejecutarse para tipos especiales que no siguen el patrón normal | 
		
	
		
			
				|  |  |  |  |     # Como ya no tenemos kdevs.rate, esta sección no se ejecutará | 
		
	
		
			
				|  |  |  |  |     if programmer_type in ['kdevs', 'special_case']: | 
		
	
		
			
				|  |  |  |  |         return None | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     programmer_map = { | 
		
	
	
		
			
				
					|  |  |  | @ -275,69 +377,728 @@ def generate_prompt(programmer_type, region_code): | 
		
	
		
			
				|  |  |  |  |     region_name = region_map.get(region_code, f'región con código {region_code}') | 
		
	
		
			
				|  |  |  |  |     programmer_description = programmer_map.get(programmer_type, f'desarrollador {programmer_type}') | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     prompt = f"¿Cuál es la tarifa por hora promedio en dólares estadounidenses (USD) para un {programmer_description} en {region_name} en 2025? Responde solo con el valor numérico con dos decimales." | 
		
	
		
			
				|  |  |  |  |     # Usamos el año actual para la consulta principal | 
		
	
		
			
				|  |  |  |  |     current_year = time.strftime("%Y") | 
		
	
		
			
				|  |  |  |  |     prompt = f"""¿Cuál es la tarifa POR HORA promedio en dólares estadounidenses (USD) para un {programmer_description} en {region_name} en {current_year}?  | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  | Si no tienes datos del {current_year}, usa la información más reciente y haz una estimación aproximada.  | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | IMPORTANTE: Si encuentras información en otros períodos de tiempo (mensual, anual, etc.), conviértela a tarifa POR HORA. | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | Responde solo con el valor numérico con dos decimales.""" | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     return prompt | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def update_rate_files(): | 
		
	
		
			
				|  |  |  |  |     """Actualiza los archivos de tarifas con datos de Perplexity.""" | 
		
	
		
			
				|  |  |  |  | def get_default_rate(programmer_type): | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     Proporciona una tarifa por defecto simple para cuando la API falla. | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     # Tarifas base por tipo de programador (valores estimados en USD) | 
		
	
		
			
				|  |  |  |  |     base_rates = { | 
		
	
		
			
				|  |  |  |  |         'python': 35.00, | 
		
	
		
			
				|  |  |  |  |         'fullstack': 45.00, | 
		
	
		
			
				|  |  |  |  |         'frontend': 40.00, | 
		
	
		
			
				|  |  |  |  |         'backend': 42.00, | 
		
	
		
			
				|  |  |  |  |         'devops': 50.00, | 
		
	
		
			
				|  |  |  |  |         'mobile': 45.00, | 
		
	
		
			
				|  |  |  |  |         'java': 40.00, | 
		
	
		
			
				|  |  |  |  |         'php': 30.00, | 
		
	
		
			
				|  |  |  |  |         'ruby': 40.00, | 
		
	
		
			
				|  |  |  |  |         'dotnet': 45.00, | 
		
	
		
			
				|  |  |  |  |         'data': 55.00, | 
		
	
		
			
				|  |  |  |  |         'ml': 65.00, | 
		
	
		
			
				|  |  |  |  |         'cloud': 60.00, | 
		
	
		
			
				|  |  |  |  |         'odoo': 40.00 | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     # Cálculo especial para bash: 0.4 veces la tarifa de devops | 
		
	
		
			
				|  |  |  |  |     if programmer_type == 'bash': | 
		
	
		
			
				|  |  |  |  |         return 0.4 * base_rates['devops'] | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Valor por defecto si el tipo no está en la lista | 
		
	
		
			
				|  |  |  |  |     return base_rates.get(programmer_type, 35.00) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def show_result(message): | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     Función para mostrar resultados en la consola. | 
		
	
		
			
				|  |  |  |  |     Solo muestra mensajes si SHOW_RESULTS es True. | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     if SHOW_RESULTS: | 
		
	
		
			
				|  |  |  |  |         print(message) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def get_programmer_types(): | 
		
	
		
			
				|  |  |  |  |     """Retorna una lista de todos los tipos de programadores soportados.""" | 
		
	
		
			
				|  |  |  |  |     return [ | 
		
	
		
			
				|  |  |  |  |         'bash', | 
		
	
		
			
				|  |  |  |  |         'python', | 
		
	
		
			
				|  |  |  |  |         'fullstack', | 
		
	
		
			
				|  |  |  |  |         'frontend', | 
		
	
		
			
				|  |  |  |  |         'backend', | 
		
	
		
			
				|  |  |  |  |         'devops', | 
		
	
		
			
				|  |  |  |  |         'mobile', | 
		
	
		
			
				|  |  |  |  |         'java', | 
		
	
		
			
				|  |  |  |  |         'php', | 
		
	
		
			
				|  |  |  |  |         'ruby', | 
		
	
		
			
				|  |  |  |  |         'dotnet', | 
		
	
		
			
				|  |  |  |  |         'data', | 
		
	
		
			
				|  |  |  |  |         'ml', | 
		
	
		
			
				|  |  |  |  |         'cloud', | 
		
	
		
			
				|  |  |  |  |         'odoo' | 
		
	
		
			
				|  |  |  |  |     ] | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def update_single_rate(programmer_type): | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     Actualiza la tarifa para un tipo específico de programador. | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     # Verificar que el tipo de programador sea válido | 
		
	
		
			
				|  |  |  |  |     if programmer_type not in get_programmer_types(): | 
		
	
		
			
				|  |  |  |  |         logger.error(f"Tipo de programador no válido: {programmer_type}") | 
		
	
		
			
				|  |  |  |  |         print(f"ERROR: Tipo de programador no válido: {programmer_type}") | 
		
	
		
			
				|  |  |  |  |         print(f"Tipos válidos: {', '.join(get_programmer_types())}") | 
		
	
		
			
				|  |  |  |  |         return False | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Obtener modelo configurado | 
		
	
		
			
				|  |  |  |  |     model = get_ai_model() | 
		
	
		
			
				|  |  |  |  |     logger.info(f"Usando modelo de IA: {model}") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Buscar todos los archivos .rate | 
		
	
		
			
				|  |  |  |  |     rate_files = glob.glob(str(CONFIG_DIR / '*.rate')) | 
		
	
		
			
				|  |  |  |  |     logger.info(f"Encontrados {len(rate_files)} archivos de tarifas para actualizar.") | 
		
	
		
			
				|  |  |  |  |     # Verificar disponibilidad de la API | 
		
	
		
			
				|  |  |  |  |     api_available = True | 
		
	
		
			
				|  |  |  |  |     try: | 
		
	
		
			
				|  |  |  |  |         # Intentar obtener la clave API (si falla, saltará una excepción) | 
		
	
		
			
				|  |  |  |  |         get_perplexity_api_key() | 
		
	
		
			
				|  |  |  |  |     except: | 
		
	
		
			
				|  |  |  |  |         api_available = False | 
		
	
		
			
				|  |  |  |  |         logger.warning("API de Perplexity no disponible. Se usarán valores predeterminados.") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     for rate_file in rate_files: | 
		
	
		
			
				|  |  |  |  |         programmer_type, region_code = parse_rate_filename(rate_file) | 
		
	
		
			
				|  |  |  |  |     # Crear la carpeta rates si no existe | 
		
	
		
			
				|  |  |  |  |     os.makedirs(RATES_DIR, exist_ok=True) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |         if programmer_type is None: | 
		
	
		
			
				|  |  |  |  |             logger.warning(f"No se pudo analizar el nombre del archivo: {rate_file}, saltando.") | 
		
	
		
			
				|  |  |  |  |     # Diccionario para almacenar las tarifas calculadas y existentes | 
		
	
		
			
				|  |  |  |  |     rates_dict = {} | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Si es fullstack, necesitamos primero obtener el valor más alto de todos los tipos | 
		
	
		
			
				|  |  |  |  |     if programmer_type == 'fullstack': | 
		
	
		
			
				|  |  |  |  |         # Leer las tarifas existentes | 
		
	
		
			
				|  |  |  |  |         for prog_type in get_programmer_types(): | 
		
	
		
			
				|  |  |  |  |             if prog_type != 'fullstack': | 
		
	
		
			
				|  |  |  |  |                 rate_file = RATES_DIR / f"{prog_type}.rate" | 
		
	
		
			
				|  |  |  |  |                 if rate_file.exists(): | 
		
	
		
			
				|  |  |  |  |                     try: | 
		
	
		
			
				|  |  |  |  |                         with open(rate_file, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |                             rates_dict[prog_type] = float(f.read().strip()) | 
		
	
		
			
				|  |  |  |  |                             logger.info(f"Tarifa leída para {prog_type}: {rates_dict[prog_type]:.2f}") | 
		
	
		
			
				|  |  |  |  |                     except (FileNotFoundError, ValueError) as e: | 
		
	
		
			
				|  |  |  |  |                         logger.warning(f"Error al leer tarifa de {prog_type}: {e}") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Si es bash, necesitamos primero obtener o actualizar la tarifa de devops | 
		
	
		
			
				|  |  |  |  |     devops_rate = None | 
		
	
		
			
				|  |  |  |  |     if programmer_type == 'bash': | 
		
	
		
			
				|  |  |  |  |         logger.info("Actualizando tarifa de devops primero para calcular bash") | 
		
	
		
			
				|  |  |  |  |         devops_rate_file = RATES_DIR / "devops.rate" | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Comprobar si existe un archivo de devops y si debemos actualizarlo | 
		
	
		
			
				|  |  |  |  |         if devops_rate_file.exists(): | 
		
	
		
			
				|  |  |  |  |             # Leer la tarifa guardada de devops | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 with open(devops_rate_file, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |                     devops_rate = float(f.read().strip()) | 
		
	
		
			
				|  |  |  |  |                     rates_dict['devops'] = devops_rate | 
		
	
		
			
				|  |  |  |  |                     logger.info(f"Tarifa leída para devops: {devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |             except (FileNotFoundError, ValueError) as e: | 
		
	
		
			
				|  |  |  |  |                 logger.error(f"Error al leer tarifa de devops: {e}") | 
		
	
		
			
				|  |  |  |  |                 # Si hay error de lectura, intentamos actualizar la tarifa | 
		
	
		
			
				|  |  |  |  |                 devops_rate = None | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si no existe o hubo error de lectura, actualizamos la tarifa de devops | 
		
	
		
			
				|  |  |  |  |         if devops_rate is None: | 
		
	
		
			
				|  |  |  |  |             prompt = generate_prompt_base('devops') | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             # Si la API está disponible, intentar consultarla | 
		
	
		
			
				|  |  |  |  |             if api_available: | 
		
	
		
			
				|  |  |  |  |                 logger.info("Consultando tarifa para devops") | 
		
	
		
			
				|  |  |  |  |                  | 
		
	
		
			
				|  |  |  |  |                 try: | 
		
	
		
			
				|  |  |  |  |                     # Consultar a Perplexity | 
		
	
		
			
				|  |  |  |  |                     devops_rate = query_perplexity(prompt, model) | 
		
	
		
			
				|  |  |  |  |                      | 
		
	
		
			
				|  |  |  |  |                     if devops_rate is not None: | 
		
	
		
			
				|  |  |  |  |                         logger.info(f"Tarifa obtenida correctamente para devops: {devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |                     else: | 
		
	
		
			
				|  |  |  |  |                         logger.error("No se pudo obtener la tarifa para devops") | 
		
	
		
			
				|  |  |  |  |                 except Exception as e: | 
		
	
		
			
				|  |  |  |  |                     logger.error(f"Error al consultar tarifa para devops: {e}") | 
		
	
		
			
				|  |  |  |  |                     time.sleep(2) | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             # Si la API falló o no está disponible, usar valor predeterminado | 
		
	
		
			
				|  |  |  |  |             if devops_rate is None: | 
		
	
		
			
				|  |  |  |  |                 logger.warning("Usando valor predeterminado para devops") | 
		
	
		
			
				|  |  |  |  |                 devops_rate = get_default_rate('devops') | 
		
	
		
			
				|  |  |  |  |                 logger.info(f"Valor predeterminado para devops: {devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             # Limitar a tarifas menores de 200 USD/hora | 
		
	
		
			
				|  |  |  |  |             max_rate = 200.00 | 
		
	
		
			
				|  |  |  |  |             if devops_rate > max_rate: | 
		
	
		
			
				|  |  |  |  |                 logger.warning(f"Ajustando tarifa {devops_rate:.2f} al máximo permitido de {max_rate}") | 
		
	
		
			
				|  |  |  |  |                 devops_rate = max_rate | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             # Guardar el resultado en el archivo | 
		
	
		
			
				|  |  |  |  |             with open(devops_rate_file, 'w', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |                 f.write(f"{devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             # Añadir al diccionario | 
		
	
		
			
				|  |  |  |  |             rates_dict['devops'] = devops_rate | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             # Mostrar el resultado en la consola | 
		
	
		
			
				|  |  |  |  |             result_message = f"Tarifa para devops: {devops_rate:.2f} USD/hora" | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Actualizado archivo {devops_rate_file} con valor: {devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |             show_result(result_message) | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             # Pequeña pausa para no sobrecargar la API | 
		
	
		
			
				|  |  |  |  |             time.sleep(1) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Ahora procesamos el tipo de programador solicitado | 
		
	
		
			
				|  |  |  |  |     rate_file = RATES_DIR / f"{programmer_type}.rate" | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Caso especial para bash: calculamos como 0.4 veces la tarifa de devops | 
		
	
		
			
				|  |  |  |  |     if programmer_type == 'bash' and devops_rate is not None: | 
		
	
		
			
				|  |  |  |  |         logger.info(f"Calculando tarifa para bash como 0.4 * {devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |         rate = 0.4 * devops_rate | 
		
	
		
			
				|  |  |  |  |         logger.info(f"Tarifa calculada para bash: {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |     # Caso especial para fullstack | 
		
	
		
			
				|  |  |  |  |     elif programmer_type == 'fullstack': | 
		
	
		
			
				|  |  |  |  |         # Primero calculamos el valor más alto entre todos los tipos | 
		
	
		
			
				|  |  |  |  |         max_rate_value = 0.0 | 
		
	
		
			
				|  |  |  |  |         for prog_type, rate_value in rates_dict.items(): | 
		
	
		
			
				|  |  |  |  |             if prog_type != 'fullstack' and rate_value > max_rate_value: | 
		
	
		
			
				|  |  |  |  |                 max_rate_value = rate_value | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         logger.info(f"Valor más alto encontrado: {max_rate_value:.2f}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Obtener tarifa para fullstack vía API | 
		
	
		
			
				|  |  |  |  |         api_rate = None | 
		
	
		
			
				|  |  |  |  |         if api_available: | 
		
	
		
			
				|  |  |  |  |             prompt = generate_prompt_base(programmer_type) | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Consultando tarifa para {programmer_type}") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 # Consultar a Perplexity | 
		
	
		
			
				|  |  |  |  |                 api_rate = query_perplexity(prompt, model) | 
		
	
		
			
				|  |  |  |  |                  | 
		
	
		
			
				|  |  |  |  |                 if api_rate is not None: | 
		
	
		
			
				|  |  |  |  |                     logger.info(f"Tarifa obtenida correctamente: {api_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |                 else: | 
		
	
		
			
				|  |  |  |  |                     logger.error(f"No se pudo obtener la tarifa para {programmer_type}") | 
		
	
		
			
				|  |  |  |  |             except Exception as e: | 
		
	
		
			
				|  |  |  |  |                 logger.error(f"Error al consultar tarifa para {programmer_type}: {e}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si la API falló, usar el valor predeterminado | 
		
	
		
			
				|  |  |  |  |         if api_rate is None: | 
		
	
		
			
				|  |  |  |  |             logger.warning(f"Usando valor predeterminado para {programmer_type}") | 
		
	
		
			
				|  |  |  |  |             api_rate = get_default_rate(programmer_type) | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Valor predeterminado: {api_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Calcular el valor de fullstack | 
		
	
		
			
				|  |  |  |  |         # Si el valor de la API es menor o igual al valor más alto, usar 1.5 veces el valor más alto | 
		
	
		
			
				|  |  |  |  |         if api_rate <= max_rate_value: | 
		
	
		
			
				|  |  |  |  |             rate = 1.5 * max_rate_value | 
		
	
		
			
				|  |  |  |  |             logger.info(f"API rate ({api_rate:.2f}) <= max rate ({max_rate_value:.2f}), ajustando fullstack a 1.5 * {max_rate_value:.2f} = {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |         else: | 
		
	
		
			
				|  |  |  |  |             rate = api_rate | 
		
	
		
			
				|  |  |  |  |             logger.info(f"API rate ({api_rate:.2f}) > max rate ({max_rate_value:.2f}), manteniendo valor API") | 
		
	
		
			
				|  |  |  |  |     else: | 
		
	
		
			
				|  |  |  |  |         # Para otros tipos, procedemos como antes | 
		
	
		
			
				|  |  |  |  |         prompt = generate_prompt_base(programmer_type) | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Variable para almacenar la tarifa | 
		
	
		
			
				|  |  |  |  |         rate = None | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si la API está disponible, intentar consultarla | 
		
	
		
			
				|  |  |  |  |         if api_available: | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Consultando tarifa para {programmer_type}") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 # Consultar a Perplexity | 
		
	
		
			
				|  |  |  |  |                 rate = query_perplexity(prompt, model) | 
		
	
		
			
				|  |  |  |  |                  | 
		
	
		
			
				|  |  |  |  |                 if rate is not None: | 
		
	
		
			
				|  |  |  |  |                     logger.info(f"Tarifa obtenida correctamente: {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |                 else: | 
		
	
		
			
				|  |  |  |  |                     logger.error(f"No se pudo obtener la tarifa para {programmer_type}") | 
		
	
		
			
				|  |  |  |  |             except Exception as e: | 
		
	
		
			
				|  |  |  |  |                 logger.error(f"Error al consultar tarifa para {programmer_type}: {e}") | 
		
	
		
			
				|  |  |  |  |                 # Pequeña pausa tras un error para evitar sobrecargar la API | 
		
	
		
			
				|  |  |  |  |                 time.sleep(2) | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si la API falló o no está disponible, usar valor predeterminado | 
		
	
		
			
				|  |  |  |  |         if rate is None: | 
		
	
		
			
				|  |  |  |  |             logger.warning(f"Usando valor predeterminado para {programmer_type}") | 
		
	
		
			
				|  |  |  |  |             rate = get_default_rate(programmer_type) | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Valor predeterminado: {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Limitar a tarifas menores de 200 USD/hora | 
		
	
		
			
				|  |  |  |  |     max_rate = 200.00 | 
		
	
		
			
				|  |  |  |  |     if rate > max_rate: | 
		
	
		
			
				|  |  |  |  |         logger.warning(f"Ajustando tarifa {rate:.2f} al máximo permitido de {max_rate}") | 
		
	
		
			
				|  |  |  |  |         rate = max_rate | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Guardar el resultado en el archivo - solo el valor numérico con dos decimales, sin salto de línea | 
		
	
		
			
				|  |  |  |  |     with open(rate_file, 'w', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |         f.write(f"{rate:.2f}") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Mostrar el resultado en la consola | 
		
	
		
			
				|  |  |  |  |     result_message = f"Tarifa para {programmer_type}: {rate:.2f} USD/hora" | 
		
	
		
			
				|  |  |  |  |     logger.info(f"Actualizado archivo {rate_file} con valor: {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |     show_result(result_message) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     return True | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def update_rate_files(force_update=False): | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     Actualiza los archivos de tarifas con datos de Perplexity. | 
		
	
		
			
				|  |  |  |  |     Crea un archivo por tipo de programador con la tarifa por hora general. | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     Parámetros: | 
		
	
		
			
				|  |  |  |  |     force_update (bool): Si es True, actualiza todos los tipos incluso si ya existen archivos. | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     # Obtener modelo configurado | 
		
	
		
			
				|  |  |  |  |     model = get_ai_model() | 
		
	
		
			
				|  |  |  |  |     logger.info(f"Usando modelo de IA: {model}") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Verificar disponibilidad de la API | 
		
	
		
			
				|  |  |  |  |     api_available = True | 
		
	
		
			
				|  |  |  |  |     try: | 
		
	
		
			
				|  |  |  |  |         # Intentar obtener la clave API (si falla, saltará una excepción) | 
		
	
		
			
				|  |  |  |  |         get_perplexity_api_key() | 
		
	
		
			
				|  |  |  |  |     except: | 
		
	
		
			
				|  |  |  |  |         api_available = False | 
		
	
		
			
				|  |  |  |  |         logger.warning("API de Perplexity no disponible. Se usarán valores predeterminados.") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Crear la carpeta rates si no existe | 
		
	
		
			
				|  |  |  |  |     os.makedirs(RATES_DIR, exist_ok=True) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Obtener la lista de tipos de programadores | 
		
	
		
			
				|  |  |  |  |     programmer_types = get_programmer_types() | 
		
	
		
			
				|  |  |  |  |     logger.info(f"Procesando {len(programmer_types)} tipos de programadores.") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Diccionario para almacenar las tarifas ya calculadas | 
		
	
		
			
				|  |  |  |  |     rates_dict = {} | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Leer las tarifas existentes primero | 
		
	
		
			
				|  |  |  |  |     if 'fullstack' in programmer_types: | 
		
	
		
			
				|  |  |  |  |         # Primero leer los valores existentes de tipos que no vamos a actualizar | 
		
	
		
			
				|  |  |  |  |         for prog_type in get_programmer_types(): | 
		
	
		
			
				|  |  |  |  |             if prog_type not in programmer_types: | 
		
	
		
			
				|  |  |  |  |                 rate_file = RATES_DIR / f"{prog_type}.rate" | 
		
	
		
			
				|  |  |  |  |                 if rate_file.exists(): | 
		
	
		
			
				|  |  |  |  |                     try: | 
		
	
		
			
				|  |  |  |  |                         with open(rate_file, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |                             rates_dict[prog_type] = float(f.read().strip()) | 
		
	
		
			
				|  |  |  |  |                             logger.info(f"Tarifa leída para {prog_type}: {rates_dict[prog_type]:.2f}") | 
		
	
		
			
				|  |  |  |  |                     except (FileNotFoundError, ValueError) as e: | 
		
	
		
			
				|  |  |  |  |                         logger.warning(f"Error al leer tarifa de {prog_type}: {e}") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Procesamos primero devops siempre que force_update sea True o bash esté pendiente | 
		
	
		
			
				|  |  |  |  |     devops_rate = None | 
		
	
		
			
				|  |  |  |  |     need_devops_for_bash = ('bash' in programmer_types and (not (RATES_DIR / "bash.rate").exists() or force_update)) | 
		
	
		
			
				|  |  |  |  |     need_process_devops = ('devops' in programmer_types and (not (RATES_DIR / "devops.rate").exists() or force_update)) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Si necesitamos la tarifa de devops para bash, la procesamos primero | 
		
	
		
			
				|  |  |  |  |     if need_devops_for_bash and need_process_devops: | 
		
	
		
			
				|  |  |  |  |         logger.info("Procesando tarifa de devops primero para calcular bash") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Obtener tarifa para devops | 
		
	
		
			
				|  |  |  |  |         devops_rate_file = RATES_DIR / "devops.rate" | 
		
	
		
			
				|  |  |  |  |         prompt = generate_prompt_base('devops') | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Variable para almacenar la tarifa | 
		
	
		
			
				|  |  |  |  |         if api_available: | 
		
	
		
			
				|  |  |  |  |             logger.info("Consultando tarifa para devops") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 # Consultar a Perplexity | 
		
	
		
			
				|  |  |  |  |                 devops_rate = query_perplexity(prompt, model) | 
		
	
		
			
				|  |  |  |  |                  | 
		
	
		
			
				|  |  |  |  |                 if devops_rate is not None: | 
		
	
		
			
				|  |  |  |  |                     logger.info(f"Tarifa obtenida correctamente para devops: {devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |                 else: | 
		
	
		
			
				|  |  |  |  |                     logger.error("No se pudo obtener la tarifa para devops") | 
		
	
		
			
				|  |  |  |  |             except Exception as e: | 
		
	
		
			
				|  |  |  |  |                 logger.error(f"Error al consultar tarifa para devops: {e}") | 
		
	
		
			
				|  |  |  |  |                 time.sleep(2) | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si la API falló o no está disponible, usar valor predeterminado | 
		
	
		
			
				|  |  |  |  |         if devops_rate is None: | 
		
	
		
			
				|  |  |  |  |             logger.warning("Usando valor predeterminado para devops") | 
		
	
		
			
				|  |  |  |  |             devops_rate = get_default_rate('devops') | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Valor predeterminado para devops: {devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Limitar a tarifas menores de 200 USD/hora | 
		
	
		
			
				|  |  |  |  |         max_rate = 200.00 | 
		
	
		
			
				|  |  |  |  |         if devops_rate > max_rate: | 
		
	
		
			
				|  |  |  |  |             logger.warning(f"Ajustando tarifa {devops_rate:.2f} al máximo permitido de {max_rate}") | 
		
	
		
			
				|  |  |  |  |             devops_rate = max_rate | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Guardar el resultado en el archivo | 
		
	
		
			
				|  |  |  |  |         with open(devops_rate_file, 'w', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |             f.write(f"{devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Guardar en el diccionario de tarifas | 
		
	
		
			
				|  |  |  |  |         rates_dict['devops'] = devops_rate | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Mostrar el resultado en la consola | 
		
	
		
			
				|  |  |  |  |         result_message = f"Tarifa para devops: {devops_rate:.2f} USD/hora" | 
		
	
		
			
				|  |  |  |  |         logger.info(f"{'Actualizado' if force_update else 'Creado'} archivo {devops_rate_file} con valor: {devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |         show_result(result_message) | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Actualizamos la lista de tipos pendientes | 
		
	
		
			
				|  |  |  |  |         if 'devops' in programmer_types: | 
		
	
		
			
				|  |  |  |  |             programmer_types.remove('devops') | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Pequeña pausa para no sobrecargar la API | 
		
	
		
			
				|  |  |  |  |         time.sleep(1) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Si ya existe un archivo para devops pero necesitamos la tarifa para bash | 
		
	
		
			
				|  |  |  |  |     elif need_devops_for_bash and not need_process_devops: | 
		
	
		
			
				|  |  |  |  |         # Leer la tarifa guardada de devops | 
		
	
		
			
				|  |  |  |  |         devops_rate_file = RATES_DIR / "devops.rate" | 
		
	
		
			
				|  |  |  |  |         try: | 
		
	
		
			
				|  |  |  |  |             with open(devops_rate_file, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |                 devops_rate = float(f.read().strip()) | 
		
	
		
			
				|  |  |  |  |                 rates_dict['devops'] = devops_rate | 
		
	
		
			
				|  |  |  |  |                 logger.info(f"Tarifa leída para devops: {devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |         except (FileNotFoundError, ValueError) as e: | 
		
	
		
			
				|  |  |  |  |             logger.error(f"Error al leer tarifa de devops: {e}") | 
		
	
		
			
				|  |  |  |  |             devops_rate = get_default_rate('devops') | 
		
	
		
			
				|  |  |  |  |             rates_dict['devops'] = devops_rate | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Procesar los demás tipos de programador antes de fullstack | 
		
	
		
			
				|  |  |  |  |     fullstack_type = None | 
		
	
		
			
				|  |  |  |  |     if 'fullstack' in programmer_types: | 
		
	
		
			
				|  |  |  |  |         # Guardar para procesarlo después | 
		
	
		
			
				|  |  |  |  |         fullstack_type = 'fullstack' | 
		
	
		
			
				|  |  |  |  |         # Eliminarlo temporalmente de la lista | 
		
	
		
			
				|  |  |  |  |         programmer_types.remove('fullstack') | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Procesar cada tipo de programador (excepto fullstack) | 
		
	
		
			
				|  |  |  |  |     for programmer_type in programmer_types: | 
		
	
		
			
				|  |  |  |  |         # Comprobar si ya existe el archivo para este tipo de programador | 
		
	
		
			
				|  |  |  |  |         rate_file = RATES_DIR / f"{programmer_type}.rate" | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si el archivo ya existe y no estamos forzando la actualización, saltamos este tipo | 
		
	
		
			
				|  |  |  |  |         if rate_file.exists() and not force_update: | 
		
	
		
			
				|  |  |  |  |             # Leer el valor para almacenarlo en el diccionario | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 with open(rate_file, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |                     rates_dict[programmer_type] = float(f.read().strip()) | 
		
	
		
			
				|  |  |  |  |             except (FileNotFoundError, ValueError) as e: | 
		
	
		
			
				|  |  |  |  |                 logger.warning(f"Error al leer archivo {rate_file}: {e}") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             logger.info(f"El archivo {rate_file} ya existe. Saltando.") | 
		
	
		
			
				|  |  |  |  |             continue | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si es un archivo especial como kdevs.rate | 
		
	
		
			
				|  |  |  |  |         if region_code is None: | 
		
	
		
			
				|  |  |  |  |             prompt = generate_prompt(programmer_type, None) | 
		
	
		
			
				|  |  |  |  |             # Saltamos los archivos especiales que no necesitan actualización | 
		
	
		
			
				|  |  |  |  |             if prompt is None: | 
		
	
		
			
				|  |  |  |  |                 logger.info(f"Saltando archivo especial: {os.path.basename(rate_file)}") | 
		
	
		
			
				|  |  |  |  |                 continue | 
		
	
		
			
				|  |  |  |  |         # Caso especial para bash: calculamos como 0.4 veces la tarifa de devops | 
		
	
		
			
				|  |  |  |  |         if programmer_type == 'bash' and devops_rate is not None: | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Calculando tarifa para bash como 0.4 * {devops_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |             rate = 0.4 * devops_rate | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Tarifa calculada para bash: {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |         else: | 
		
	
		
			
				|  |  |  |  |             prompt = generate_prompt(programmer_type, region_code) | 
		
	
		
			
				|  |  |  |  |             # Para otros tipos, procedemos como antes | 
		
	
		
			
				|  |  |  |  |             prompt = generate_prompt_base(programmer_type) | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |         logger.info(f"Consultando tarifa para {programmer_type}" +  | 
		
	
		
			
				|  |  |  |  |                    (f" en región {region_code}" if region_code else "")) | 
		
	
		
			
				|  |  |  |  |             # Variable para almacenar la tarifa | 
		
	
		
			
				|  |  |  |  |             rate = None | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |         # Consultar a Perplexity | 
		
	
		
			
				|  |  |  |  |         rate = query_perplexity(prompt, model) | 
		
	
		
			
				|  |  |  |  |             # Si la API está disponible, intentar consultarla | 
		
	
		
			
				|  |  |  |  |             if api_available: | 
		
	
		
			
				|  |  |  |  |                 logger.info(f"Consultando tarifa para {programmer_type}") | 
		
	
		
			
				|  |  |  |  |                  | 
		
	
		
			
				|  |  |  |  |         if rate is not None: | 
		
	
		
			
				|  |  |  |  |             # Guardar el resultado en el archivo | 
		
	
		
			
				|  |  |  |  |             with open(rate_file, 'w') as f: | 
		
	
		
			
				|  |  |  |  |                 f.write(f"{rate:.2f}\n") | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Actualizado {os.path.basename(rate_file)} con valor: {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |                 try: | 
		
	
		
			
				|  |  |  |  |                     # Consultar a Perplexity | 
		
	
		
			
				|  |  |  |  |                     rate = query_perplexity(prompt, model) | 
		
	
		
			
				|  |  |  |  |                      | 
		
	
		
			
				|  |  |  |  |                     if rate is not None: | 
		
	
		
			
				|  |  |  |  |                         logger.info(f"Tarifa obtenida correctamente: {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |                     else: | 
		
	
		
			
				|  |  |  |  |                         logger.error(f"No se pudo obtener la tarifa para {programmer_type}") | 
		
	
		
			
				|  |  |  |  |                 except Exception as e: | 
		
	
		
			
				|  |  |  |  |                     logger.error(f"Error al consultar tarifa para {programmer_type}: {e}") | 
		
	
		
			
				|  |  |  |  |                     # Pequeña pausa tras un error para evitar sobrecargar la API | 
		
	
		
			
				|  |  |  |  |                     time.sleep(2) | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             # Si la API falló o no está disponible, usar valor predeterminado | 
		
	
		
			
				|  |  |  |  |             if rate is None: | 
		
	
		
			
				|  |  |  |  |                 logger.warning(f"Usando valor predeterminado para {programmer_type}") | 
		
	
		
			
				|  |  |  |  |                 rate = get_default_rate(programmer_type) | 
		
	
		
			
				|  |  |  |  |                 logger.info(f"Valor predeterminado: {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Limitar a tarifas menores de 200 USD/hora | 
		
	
		
			
				|  |  |  |  |         max_rate = 200.00 | 
		
	
		
			
				|  |  |  |  |         if rate > max_rate: | 
		
	
		
			
				|  |  |  |  |             logger.warning(f"Ajustando tarifa {rate:.2f} al máximo permitido de {max_rate}") | 
		
	
		
			
				|  |  |  |  |             rate = max_rate | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Guardar la tarifa en el diccionario | 
		
	
		
			
				|  |  |  |  |         rates_dict[programmer_type] = rate | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Guardar el resultado en el archivo - solo el valor numérico con dos decimales, sin salto de línea | 
		
	
		
			
				|  |  |  |  |         with open(rate_file, 'w', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |             f.write(f"{rate:.2f}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Mostrar el resultado en la consola | 
		
	
		
			
				|  |  |  |  |         result_message = f"Tarifa para {programmer_type}: {rate:.2f} USD/hora" | 
		
	
		
			
				|  |  |  |  |         logger.info(f"{'Actualizado' if force_update and rate_file.exists() else 'Creado'} archivo {rate_file} con valor: {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |         show_result(result_message) | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Pequeña pausa para no sobrecargar la API | 
		
	
		
			
				|  |  |  |  |         time.sleep(1) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Procesar fullstack al final si está en la lista | 
		
	
		
			
				|  |  |  |  |     if fullstack_type: | 
		
	
		
			
				|  |  |  |  |         rate_file = RATES_DIR / f"{fullstack_type}.rate" | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si el archivo ya existe y no estamos forzando la actualización, saltamos | 
		
	
		
			
				|  |  |  |  |         if rate_file.exists() and not force_update: | 
		
	
		
			
				|  |  |  |  |             # Leer el valor para mostrarlo | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 with open(rate_file, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |                     fullstack_rate = float(f.read().strip()) | 
		
	
		
			
				|  |  |  |  |                     logger.info(f"El archivo {rate_file} ya existe con valor {fullstack_rate:.2f}. Saltando.") | 
		
	
		
			
				|  |  |  |  |             except (FileNotFoundError, ValueError) as e: | 
		
	
		
			
				|  |  |  |  |                 logger.warning(f"Error al leer archivo {rate_file}: {e}") | 
		
	
		
			
				|  |  |  |  |             return | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Primero calculamos el valor más alto entre todos los tipos | 
		
	
		
			
				|  |  |  |  |         max_rate_value = 0.0 | 
		
	
		
			
				|  |  |  |  |         for prog_type, rate_value in rates_dict.items(): | 
		
	
		
			
				|  |  |  |  |             if prog_type != 'fullstack' and rate_value > max_rate_value: | 
		
	
		
			
				|  |  |  |  |                 max_rate_value = rate_value | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         logger.info(f"Valor más alto encontrado: {max_rate_value:.2f}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Obtener tarifa para fullstack vía API | 
		
	
		
			
				|  |  |  |  |         api_rate = None | 
		
	
		
			
				|  |  |  |  |         if api_available: | 
		
	
		
			
				|  |  |  |  |             prompt = generate_prompt_base(fullstack_type) | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Consultando tarifa para {fullstack_type}") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 # Consultar a Perplexity | 
		
	
		
			
				|  |  |  |  |                 api_rate = query_perplexity(prompt, model) | 
		
	
		
			
				|  |  |  |  |                  | 
		
	
		
			
				|  |  |  |  |                 if api_rate is not None: | 
		
	
		
			
				|  |  |  |  |                     logger.info(f"Tarifa obtenida correctamente: {api_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |                 else: | 
		
	
		
			
				|  |  |  |  |                     logger.error(f"No se pudo obtener la tarifa para {fullstack_type}") | 
		
	
		
			
				|  |  |  |  |             except Exception as e: | 
		
	
		
			
				|  |  |  |  |                 logger.error(f"Error al consultar tarifa para {fullstack_type}: {e}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si la API falló, usar el valor predeterminado | 
		
	
		
			
				|  |  |  |  |         if api_rate is None: | 
		
	
		
			
				|  |  |  |  |             logger.warning(f"Usando valor predeterminado para {fullstack_type}") | 
		
	
		
			
				|  |  |  |  |             api_rate = get_default_rate(fullstack_type) | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Valor predeterminado: {api_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Calcular el valor de fullstack | 
		
	
		
			
				|  |  |  |  |         # Si el valor de la API es menor o igual al valor más alto, usar 1.5 veces el valor más alto | 
		
	
		
			
				|  |  |  |  |         if api_rate <= max_rate_value: | 
		
	
		
			
				|  |  |  |  |             fullstack_rate = 1.5 * max_rate_value | 
		
	
		
			
				|  |  |  |  |             logger.info(f"API rate ({api_rate:.2f}) <= max rate ({max_rate_value:.2f}), ajustando fullstack a 1.5 * {max_rate_value:.2f} = {fullstack_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |         else: | 
		
	
		
			
				|  |  |  |  |             logger.error(f"No se pudo obtener la tarifa para {programmer_type}" +  | 
		
	
		
			
				|  |  |  |  |                         (f" en región {region_code}" if region_code else "")) | 
		
	
		
			
				|  |  |  |  |             fullstack_rate = api_rate | 
		
	
		
			
				|  |  |  |  |             logger.info(f"API rate ({api_rate:.2f}) > max rate ({max_rate_value:.2f}), manteniendo valor API") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Limitar a tarifas menores de 200 USD/hora | 
		
	
		
			
				|  |  |  |  |         max_limit = 200.00 | 
		
	
		
			
				|  |  |  |  |         if fullstack_rate > max_limit: | 
		
	
		
			
				|  |  |  |  |             logger.warning(f"Ajustando tarifa {fullstack_rate:.2f} al máximo permitido de {max_limit}") | 
		
	
		
			
				|  |  |  |  |             fullstack_rate = max_limit | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Guardar el resultado en el archivo | 
		
	
		
			
				|  |  |  |  |         with open(rate_file, 'w', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |             f.write(f"{fullstack_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Mostrar el resultado en la consola | 
		
	
		
			
				|  |  |  |  |         result_message = f"Tarifa para {fullstack_type}: {fullstack_rate:.2f} USD/hora" | 
		
	
		
			
				|  |  |  |  |         logger.info(f"{'Actualizado' if force_update and rate_file.exists() else 'Creado'} archivo {rate_file} con valor: {fullstack_rate:.2f}") | 
		
	
		
			
				|  |  |  |  |         show_result(result_message) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def generate_prompt_base(programmer_type): | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     Genera un prompt para consultar la tarifa base por hora para un tipo de programador. | 
		
	
		
			
				|  |  |  |  |     Utiliza el prompt definido en el archivo rates.prompt. | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     # Mapa para traducir tipos de programador a términos en inglés | 
		
	
		
			
				|  |  |  |  |     programmer_map = { | 
		
	
		
			
				|  |  |  |  |         'bash': 'Bash/Shell', | 
		
	
		
			
				|  |  |  |  |         'python': 'Python', | 
		
	
		
			
				|  |  |  |  |         'fullstack': 'Full Stack', | 
		
	
		
			
				|  |  |  |  |         'frontend': 'Frontend', | 
		
	
		
			
				|  |  |  |  |         'backend': 'Backend', | 
		
	
		
			
				|  |  |  |  |         'devops': 'DevOps', | 
		
	
		
			
				|  |  |  |  |         'mobile': 'Mobile App', | 
		
	
		
			
				|  |  |  |  |         'java': 'Java', | 
		
	
		
			
				|  |  |  |  |         'php': 'PHP', | 
		
	
		
			
				|  |  |  |  |         'ruby': 'Ruby', | 
		
	
		
			
				|  |  |  |  |         'dotnet': '.NET', | 
		
	
		
			
				|  |  |  |  |         'data': 'Data Science', | 
		
	
		
			
				|  |  |  |  |         'ml': 'Machine Learning', | 
		
	
		
			
				|  |  |  |  |         'cloud': 'Cloud', | 
		
	
		
			
				|  |  |  |  |         'odoo': 'Odoo' | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     programmer_description = programmer_map.get(programmer_type, programmer_type) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Cargar el prompt desde el archivo | 
		
	
		
			
				|  |  |  |  |     prompt_template = "" | 
		
	
		
			
				|  |  |  |  |     prompt_file = CONFIG_DIR / 'rates.prompt' | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     try: | 
		
	
		
			
				|  |  |  |  |         with open(prompt_file, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |             prompt_template = f.read().strip() | 
		
	
		
			
				|  |  |  |  |     except FileNotFoundError: | 
		
	
		
			
				|  |  |  |  |         # Si no se encuentra el archivo, usar un prompt predeterminado | 
		
	
		
			
				|  |  |  |  |         prompt_template = "What is the average hourly rate in USD for a [developer type] developer, expressed as a numerical value with two decimal places?" | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Reemplazar [developer type] con el tipo de programador específico | 
		
	
		
			
				|  |  |  |  |     prompt = prompt_template.replace('[developer type]', programmer_description) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     return prompt | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | def list_rate_files(): | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     Lista todas las tarifas por tipo de programador. | 
		
	
		
			
				|  |  |  |  |     Si el archivo no existe, utiliza el valor por defecto. | 
		
	
		
			
				|  |  |  |  |     """ | 
		
	
		
			
				|  |  |  |  |     # Crear la carpeta rates si no existe | 
		
	
		
			
				|  |  |  |  |     os.makedirs(RATES_DIR, exist_ok=True) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Obtener la lista de tipos de programadores | 
		
	
		
			
				|  |  |  |  |     programmer_types = get_programmer_types() | 
		
	
		
			
				|  |  |  |  |     programmer_types.sort()  # Ordenar alfabéticamente | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     print("Tarifas por tipo de programador:") | 
		
	
		
			
				|  |  |  |  |     print("--------------------------------") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Primero procesamos devops para tener su tarifa actualizada para bash | 
		
	
		
			
				|  |  |  |  |     devops_rate = None | 
		
	
		
			
				|  |  |  |  |     devops_rate_file = RATES_DIR / "devops.rate" | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     if devops_rate_file.exists(): | 
		
	
		
			
				|  |  |  |  |         try: | 
		
	
		
			
				|  |  |  |  |             with open(devops_rate_file, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |                 devops_rate = float(f.read().strip()) | 
		
	
		
			
				|  |  |  |  |         except (FileNotFoundError, ValueError) as e: | 
		
	
		
			
				|  |  |  |  |             logger.warning(f"Error al leer tarifa de devops: {e}") | 
		
	
		
			
				|  |  |  |  |             devops_rate = get_default_rate('devops') | 
		
	
		
			
				|  |  |  |  |     else: | 
		
	
		
			
				|  |  |  |  |         devops_rate = get_default_rate('devops') | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Procesar cada tipo de programador | 
		
	
		
			
				|  |  |  |  |     for programmer_type in programmer_types: | 
		
	
		
			
				|  |  |  |  |         rate_file = RATES_DIR / f"{programmer_type}.rate" | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Si el archivo existe, leer el valor | 
		
	
		
			
				|  |  |  |  |         if rate_file.exists(): | 
		
	
		
			
				|  |  |  |  |             try: | 
		
	
		
			
				|  |  |  |  |                 with open(rate_file, 'r', encoding='utf-8') as f: | 
		
	
		
			
				|  |  |  |  |                     rate = float(f.read().strip()) | 
		
	
		
			
				|  |  |  |  |             except (FileNotFoundError, ValueError) as e: | 
		
	
		
			
				|  |  |  |  |                 logger.warning(f"Error al leer {rate_file}: {e}") | 
		
	
		
			
				|  |  |  |  |                 # Caso especial para bash | 
		
	
		
			
				|  |  |  |  |                 if programmer_type == 'bash' and devops_rate is not None: | 
		
	
		
			
				|  |  |  |  |                     rate = 0.4 * devops_rate | 
		
	
		
			
				|  |  |  |  |                 else: | 
		
	
		
			
				|  |  |  |  |                     rate = get_default_rate(programmer_type) | 
		
	
		
			
				|  |  |  |  |         else: | 
		
	
		
			
				|  |  |  |  |             # Si no existe, usar valor por defecto | 
		
	
		
			
				|  |  |  |  |             # Caso especial para bash | 
		
	
		
			
				|  |  |  |  |             if programmer_type == 'bash' and devops_rate is not None: | 
		
	
		
			
				|  |  |  |  |                 rate = 0.4 * devops_rate | 
		
	
		
			
				|  |  |  |  |             else: | 
		
	
		
			
				|  |  |  |  |                 rate = get_default_rate(programmer_type) | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Usando valor predeterminado para {programmer_type}: {rate:.2f}") | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         # Formatear la salida para alinear correctamente | 
		
	
		
			
				|  |  |  |  |         print(f"{programmer_type.ljust(10)}: {rate:.2f} USD/hora") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | if __name__ == "__main__": | 
		
	
		
			
				|  |  |  |  |     # Parámetros de línea de comandos para controlar el comportamiento | 
		
	
		
			
				|  |  |  |  |     import argparse | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     logger.info("Iniciando actualización de tarifas...") | 
		
	
		
			
				|  |  |  |  |     # Obtener la lista de tipos válidos para incluirlos en la ayuda | 
		
	
		
			
				|  |  |  |  |     valid_types = get_programmer_types() | 
		
	
		
			
				|  |  |  |  |     valid_types_str = ", ".join(valid_types) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     parser = argparse.ArgumentParser(description='Actualiza tarifas por hora de diferentes tipos de programadores.') | 
		
	
		
			
				|  |  |  |  |     parser.add_argument('-q', '--quiet', action='store_true', help='No mostrar resultados individuales') | 
		
	
		
			
				|  |  |  |  |     parser.add_argument('-v', '--verbose', action='store_true', help='Mostrar información detallada del proceso') | 
		
	
		
			
				|  |  |  |  |     parser.add_argument('-l', '--list', action='store_true', help='Listar todas las tarifas disponibles') | 
		
	
		
			
				|  |  |  |  |     parser.add_argument('-t', '--type', choices=valid_types,  | 
		
	
		
			
				|  |  |  |  |                       help=f'Actualizar la tarifa para un tipo específico de programador. Tipos válidos: {valid_types_str}') | 
		
	
		
			
				|  |  |  |  |     parser.add_argument('-i', '--init', action='store_true',  | 
		
	
		
			
				|  |  |  |  |                       help='Actualizar todas las tarifas, incluso si ya existen archivos') | 
		
	
		
			
				|  |  |  |  |     args = parser.parse_args() | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Configurar nivel de log según parámetros | 
		
	
		
			
				|  |  |  |  |     if args.verbose: | 
		
	
		
			
				|  |  |  |  |         console_handler.setLevel(logging.INFO) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Configurar si se muestran resultados - usar variable global directamente | 
		
	
		
			
				|  |  |  |  |     # No es necesario declarar global aquí ya que estamos en el ámbito global | 
		
	
		
			
				|  |  |  |  |     SHOW_RESULTS = not args.quiet | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Si se solicita listar las tarifas, solo mostramos la lista y terminamos | 
		
	
		
			
				|  |  |  |  |     if args.list: | 
		
	
		
			
				|  |  |  |  |         list_rate_files() | 
		
	
		
			
				|  |  |  |  |         sys.exit(0) | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Verificar e instalar pycountry si es necesario | 
		
	
		
			
				|  |  |  |  |     pycountry_available = check_install_pycountry() | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Actualizar la variable global | 
		
	
		
			
				|  |  |  |  |     globals()['pycountry_available'] = pycountry_available | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Mostrar estado de validación de países | 
		
	
		
			
				|  |  |  |  |     if pycountry_available: | 
		
	
		
			
				|  |  |  |  |         logger.info("Validación de códigos de país habilitada con pycountry.") | 
		
	
		
			
				|  |  |  |  |     else: | 
		
	
		
			
				|  |  |  |  |         logger.info("Usando lista interna para validación básica de códigos de país.") | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     # Actualizar los archivos de tarifas | 
		
	
		
			
				|  |  |  |  |     update_rate_files() | 
		
	
		
			
				|  |  |  |  |     try: | 
		
	
		
			
				|  |  |  |  |         # Si se solicita inicializar/actualizar todas las tarifas | 
		
	
		
			
				|  |  |  |  |         if args.init: | 
		
	
		
			
				|  |  |  |  |             logger.info("Iniciando actualización forzada de todas las tarifas...") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |     logger.info("Proceso de actualización de tarifas completado.") | 
		
	
		
			
				|  |  |  |  |             if not args.quiet: | 
		
	
		
			
				|  |  |  |  |                 print("Actualizando todas las tarifas (forzado)...") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             update_rate_files(force_update=True) | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             if not args.quiet: | 
		
	
		
			
				|  |  |  |  |                 print("Proceso de actualización forzada completado exitosamente.") | 
		
	
		
			
				|  |  |  |  |         # Si se especifica un tipo de programador, actualizar solo ese tipo | 
		
	
		
			
				|  |  |  |  |         elif args.type: | 
		
	
		
			
				|  |  |  |  |             logger.info(f"Actualizando tarifa para el tipo: {args.type}") | 
		
	
		
			
				|  |  |  |  |             if not args.quiet: | 
		
	
		
			
				|  |  |  |  |                 print(f"Actualizando tarifa para {args.type}...") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             success = update_single_rate(args.type) | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             if success and not args.quiet: | 
		
	
		
			
				|  |  |  |  |                 print(f"Tarifa para {args.type} actualizada exitosamente.") | 
		
	
		
			
				|  |  |  |  |             elif not success: | 
		
	
		
			
				|  |  |  |  |                 sys.exit(1) | 
		
	
		
			
				|  |  |  |  |         else: | 
		
	
		
			
				|  |  |  |  |             # Actualizar archivos de tarifas faltantes | 
		
	
		
			
				|  |  |  |  |             logger.info("Iniciando actualización de tarifas faltantes...") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             if not args.quiet: | 
		
	
		
			
				|  |  |  |  |                 print("Actualizando tarifas faltantes...") | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             update_rate_files() | 
		
	
		
			
				|  |  |  |  |              | 
		
	
		
			
				|  |  |  |  |             if not args.quiet: | 
		
	
		
			
				|  |  |  |  |                 print("Proceso de actualización de tarifas completado exitosamente.") | 
		
	
		
			
				|  |  |  |  |                  | 
		
	
		
			
				|  |  |  |  |         logger.info("Proceso de actualización de tarifas completado.") | 
		
	
		
			
				|  |  |  |  |     except Exception as e: | 
		
	
		
			
				|  |  |  |  |         logger.error(f"Error durante la actualización de tarifas: {e}") | 
		
	
		
			
				|  |  |  |  |         print(f"ERROR: {e}") | 
		
	
		
			
				|  |  |  |  |         sys.exit(1) |