Compare commits
	
		
			No commits in common. "0efd6cf2b8cabdacf8948fca185b3de8581ce13c" and "233fda566146a3692c1bb1bbdbbe4039161a2ab1" have entirely different histories.
		
	
	
		
			0efd6cf2b8
			...
			233fda5661
		
	
		
					 11 changed files with 71 additions and 829 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -23,7 +23,6 @@ | |||
| 
 | ||||
| # Ignorar archivos específicos | ||||
| CLAUDE.md | ||||
| bin/config/rates/ | ||||
| 
 | ||||
| # Permitir archivos en carpetas específicas | ||||
| # !carpeta1/*.txt | ||||
|  |  | |||
							
								
								
									
										1
									
								
								bin/config/bash_la.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/bash_la.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| 20.00 | ||||
							
								
								
									
										1
									
								
								bin/config/bash_ww.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/bash_ww.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| 30.00 | ||||
							
								
								
									
										1
									
								
								bin/config/fullstack_la.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/fullstack_la.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| 100.00 | ||||
							
								
								
									
										1
									
								
								bin/config/fullstack_ww.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/fullstack_ww.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| 200.00 | ||||
							
								
								
									
										1
									
								
								bin/config/kdevs.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/kdevs.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| 3 | ||||
							
								
								
									
										1
									
								
								bin/config/python_la.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/python_la.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| 30.00 | ||||
							
								
								
									
										1
									
								
								bin/config/python_ww.rate
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								bin/config/python_ww.rate
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| 100.00 | ||||
|  | @ -1 +0,0 @@ | |||
| sonar | ||||
|  | @ -1,2 +0,0 @@ | |||
| What is the average hourly rate in USD for a [developer type] developer, expressed as a numerical value with two decimal places? | ||||
| Replace [developer type] with the desired specialization, such as "Python" or "JavaScript". | ||||
|  | @ -1,5 +1,4 @@ | |||
| #!/usr/bin/env python3 | ||||
| # -*- coding: utf-8 -*- | ||||
| """ | ||||
| Rate Update Script | ||||
| 
 | ||||
|  | @ -19,22 +18,15 @@ import requests | |||
| import sys | ||||
| import subprocess | ||||
| import importlib.util | ||||
| import time | ||||
| from pathlib import Path | ||||
| 
 | ||||
| # Configuración de logging | ||||
| # 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=[]) | ||||
| logging.basicConfig( | ||||
|     level=logging.INFO, | ||||
|     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | ||||
|     handlers=[logging.StreamHandler()] | ||||
| ) | ||||
| 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(): | ||||
|  | @ -85,96 +77,30 @@ 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 | ||||
| 
 | ||||
| # Archivos de configuración para la API de Perplexity | ||||
| # Archivo de configuración para el modelo de IA | ||||
| 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', encoding='utf-8') as f: | ||||
|             model = f.read().strip() | ||||
|             # Si hay un valor específico configurado, usarlo | ||||
|             if model and model.strip(): | ||||
|                 return model | ||||
|         with open(MODEL_CONFIG_FILE, 'r') as f: | ||||
|             return f.read().strip() | ||||
|     except FileNotFoundError: | ||||
|         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 | ||||
|         logger.warning(f"Archivo de configuración {MODEL_CONFIG_FILE} no encontrado. Usando modelo predeterminado 'o1'.") | ||||
|         return "o1" | ||||
| 
 | ||||
| def get_perplexity_api_key(): | ||||
|     """Obtener la clave API de Perplexity desde una variable de entorno o archivo.""" | ||||
|     # Intentar obtener la clave de la variable de entorno | ||||
|     """Obtener la clave API de Perplexity desde una 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: | ||||
|         # 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") | ||||
|         logger.error("No se encontró la clave API de Perplexity. Establezca la variable de entorno PERPLEXITY_API_KEY.") | ||||
|         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="sonar"): | ||||
| def query_perplexity(prompt, model="o1"): | ||||
|     """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 = { | ||||
|  | @ -182,62 +108,38 @@ def query_perplexity(prompt, model="sonar"): | |||
|         "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. Do not include any text, currency symbols, or other characters." | ||||
|                 "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." | ||||
|             }, | ||||
|             { | ||||
|                 "role": "user", | ||||
|                 "content": prompt | ||||
|             } | ||||
|         ], | ||||
|         "max_tokens": 100, | ||||
|         "temperature": temperature  # Usar temperatura configurada para respuestas | ||||
|         "max_tokens": 100 | ||||
|     } | ||||
|      | ||||
|     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) | ||||
|          | ||||
|         # 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 | ||||
|         response.raise_for_status() | ||||
|         result = response.json() | ||||
|          | ||||
|         # Extraer solo el valor numérico de la respuesta | ||||
|         content = result.get('choices', [{}])[0].get('message', {}).get('content', '') | ||||
|         logger.info(f"Contenido de la respuesta: {content}") | ||||
|          | ||||
|         # Extraer solo el valor numérico, limpiando cualquier formato | ||||
|         content_clean = content.replace(',', '') | ||||
|         match = re.search(r'(\d+\.\d+|\d+)', content_clean) | ||||
|          | ||||
|         # Intentar encontrar un número con 2 decimales en la respuesta | ||||
|         match = re.search(r'\$?(\d+\.\d{2})', content) | ||||
|         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 | ||||
|             return float(match.group(1)) | ||||
|          | ||||
|         # Si no encontramos un número con el patrón regular, intenta limpiar todo | ||||
|         # Si no encuentra un formato exacto, intentar convertir toda la respuesta a float | ||||
|         try: | ||||
|             # Eliminar cualquier símbolo de moneda y espacios | ||||
|             cleaned_content = re.sub(r'[^\d.]', '', content) | ||||
|             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 | ||||
|             return round(float(cleaned_content), 2) | ||||
|         except ValueError: | ||||
|             logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}") | ||||
|             return None | ||||
|  | @ -245,9 +147,6 @@ def query_perplexity(prompt, model="sonar"): | |||
|     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 | ||||
|  | @ -350,10 +249,9 @@ def generate_prompt(programmer_type, region_code): | |||
|         'it': 'Italia' | ||||
|     } | ||||
|      | ||||
|     # 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']: | ||||
|     # Archivos especiales como kdevs.rate se manejan de forma diferente | ||||
|     if programmer_type == 'kdevs': | ||||
|         # Simplemente mantener el valor actual para kdevs.rate | ||||
|         return None | ||||
|      | ||||
|     programmer_map = { | ||||
|  | @ -377,728 +275,69 @@ 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}') | ||||
|      | ||||
|     # 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.""" | ||||
|     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." | ||||
|      | ||||
|     return prompt | ||||
| 
 | ||||
| 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 | ||||
|      | ||||
| def update_rate_files(): | ||||
|     """Actualiza los archivos de tarifas con datos de Perplexity.""" | ||||
|     # 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.") | ||||
|     # 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.") | ||||
|      | ||||
|     # Crear la carpeta rates si no existe | ||||
|     os.makedirs(RATES_DIR, exist_ok=True) | ||||
|     for rate_file in rate_files: | ||||
|         programmer_type, region_code = parse_rate_filename(rate_file) | ||||
|          | ||||
|     # 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.") | ||||
|         if programmer_type is None: | ||||
|             logger.warning(f"No se pudo analizar el nombre del archivo: {rate_file}, saltando.") | ||||
|             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}") | ||||
|         # 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 | ||||
|         else: | ||||
|             # Para otros tipos, procedemos como antes | ||||
|             prompt = generate_prompt_base(programmer_type) | ||||
|             prompt = generate_prompt(programmer_type, region_code) | ||||
|          | ||||
|             # Variable para almacenar la tarifa | ||||
|             rate = None | ||||
|         logger.info(f"Consultando tarifa para {programmer_type}" +  | ||||
|                    (f" en región {region_code}" if region_code else "")) | ||||
|          | ||||
|             # Si la API está disponible, intentar consultarla | ||||
|             if api_available: | ||||
|                 logger.info(f"Consultando tarifa para {programmer_type}") | ||||
|         # Consultar a Perplexity | ||||
|         rate = query_perplexity(prompt, model) | ||||
|          | ||||
|                 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}") | ||||
|         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}") | ||||
|         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") | ||||
|             logger.error(f"No se pudo obtener la tarifa para {programmer_type}" +  | ||||
|                         (f" en región {region_code}" if region_code else "")) | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     # Parámetros de línea de comandos para controlar el comportamiento | ||||
|     import argparse | ||||
|      | ||||
|     # 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) | ||||
|     logger.info("Iniciando actualización de tarifas...") | ||||
|      | ||||
|     # 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.") | ||||
|      | ||||
|     try: | ||||
|         # Si se solicita inicializar/actualizar todas las tarifas | ||||
|         if args.init: | ||||
|             logger.info("Iniciando actualización forzada de todas las tarifas...") | ||||
|     # Actualizar los archivos de tarifas | ||||
|     update_rate_files() | ||||
|      | ||||
|             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) | ||||
|     logger.info("Proceso de actualización de tarifas completado.") | ||||
		Loading…
	
		Reference in a new issue