#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Programa: rate_update.py Descripción: Actualiza las tarifas por hora de diferentes tipos de programadores Autor: Mauro Rosero P. Creación: 2025-03-12 Actualización: 2025-03-12 Versión: 1.2.0 Licencia: AGPL """ import os import glob import re import json import logging 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=[]) 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(): """Verifica si pycountry está instalado y lo instala si es necesario.""" if importlib.util.find_spec("pycountry") is None: logger.info("La biblioteca pycountry no está instalada.") # Verificar si pip está disponible try: # Primero comprobamos si pip está instalado pip_check = subprocess.run([sys.executable, "-m", "pip", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if pip_check.returncode == 0: logger.warning("La biblioteca pycountry no está instalada.") logger.warning("Se continuará sin validación de códigos de país.") logger.warning("Para habilitar la validación, instale pycountry manualmente:") logger.warning(" sudo pip3 install pycountry") logger.warning("O:") logger.warning(" sudo apt-get install python3-pycountry") return False else: logger.warning("pip no está instalado. No se puede instalar pycountry automáticamente.") logger.warning("Se continuará sin validación de códigos de país.") logger.warning("Para habilitar la validación, instale pycountry manualmente:") logger.warning(" sudo apt-get install python3-pip") logger.warning(" sudo pip3 install pycountry") logger.warning("O:") logger.warning(" sudo apt-get install python3-pycountry") return False except FileNotFoundError: logger.warning("No se pudo ejecutar pip. El sistema no puede encontrar el ejecutable de Python.") logger.warning("Se continuará sin validación de códigos de país.") logger.warning("Para habilitar la validación, verifique su instalación de Python y pycountry.") return False # Intentamos importar pycountry try: global pycountry import pycountry return True except ImportError: logger.warning("No se puede importar pycountry. Continuando sin validación de códigos de país.") return False # 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 = BASE_DIR / 'data' / 'rates' # Nueva ubicación para archivos de tarifas # 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', 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 '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 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: # 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("Se usarán valores predeterminados para las tarifas.") logger.error("Para obtener tarifas más precisas, configure la API key de Perplexity:") 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") if SHOW_RESULTS: print("\nADVERTENCIA: No se encontró la clave API de Perplexity.") print("Se usarán valores predeterminados para las tarifas.") print("Para obtener tarifas más precisas, configure la API key visitando:") print("https://docs.perplexity.ai/docs/getting-started") return None # 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"): """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 = { "Authorization": f"Bearer {api_key}", "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." }, { "role": "user", "content": prompt } ], "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) # 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() 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) 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: 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 except ValueError: logger.error(f"No se pudo extraer un valor numérico de la respuesta: {content}") return None 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 def is_valid_country_code(code): """ Verifica si un código de país de 2 letras es válido. Usa pycountry si está disponible, o una lista básica de códigos comunes. También acepta códigos especiales 'ww' y 'la'. """ # Códigos especiales para propósitos internos if code in ['ww', 'la']: # worldwide y Latin America return True # Lista básica de códigos de país comunes (ISO 3166-1 alpha-2) common_country_codes = [ 'af', 'al', 'dz', 'as', 'ad', 'ao', 'ai', 'aq', 'ag', 'ar', 'am', 'aw', 'au', 'at', 'az', 'bs', 'bh', 'bd', 'bb', 'by', 'be', 'bz', 'bj', 'bm', 'bt', 'bo', 'ba', 'bw', 'br', 'bn', 'bg', 'bf', 'bi', 'cv', 'kh', 'cm', 'ca', 'ky', 'cf', 'td', 'cl', 'cn', 'co', 'km', 'cg', 'cd', 'ck', 'cr', 'ci', 'hr', 'cu', 'cy', 'cz', 'dk', 'dj', 'dm', 'do', 'ec', 'eg', 'sv', 'gq', 'er', 'ee', 'et', 'fk', 'fo', 'fj', 'fi', 'fr', 'gf', 'pf', 'ga', 'gm', 'ge', 'de', 'gh', 'gi', 'gr', 'gl', 'gd', 'gp', 'gu', 'gt', 'gn', 'gw', 'gy', 'ht', 'va', 'hn', 'hk', 'hu', 'is', 'in', 'id', 'ir', 'iq', 'ie', 'il', 'it', 'jm', 'jp', 'jo', 'kz', 'ke', 'ki', 'kp', 'kr', 'kw', 'kg', 'la', 'lv', 'lb', 'ls', 'lr', 'ly', 'li', 'lt', 'lu', 'mo', 'mk', 'mg', 'mw', 'my', 'mv', 'ml', 'mt', 'mh', 'mq', 'mr', 'mu', 'yt', 'mx', 'fm', 'md', 'mc', 'mn', 'me', 'ms', 'ma', 'mz', 'mm', 'na', 'nr', 'np', 'nl', 'nc', 'nz', 'ni', 'ne', 'ng', 'nu', 'nf', 'mp', 'no', 'om', 'pk', 'pw', 'ps', 'pa', 'pg', 'py', 'pe', 'ph', 'pn', 'pl', 'pt', 'pr', 'qa', 're', 'ro', 'ru', 'rw', 'bl', 'sh', 'kn', 'lc', 'mf', 'pm', 'vc', 'ws', 'sm', 'st', 'sa', 'sn', 'rs', 'sc', 'sl', 'sg', 'sx', 'sk', 'si', 'sb', 'so', 'za', 'gs', 'ss', 'es', 'lk', 'sd', 'sr', 'sj', 'sz', 'se', 'ch', 'sy', 'tw', 'tj', 'tz', 'th', 'tl', 'tg', 'tk', 'to', 'tt', 'tn', 'tr', 'tm', 'tc', 'tv', 'ug', 'ua', 'ae', 'gb', 'us', 'uy', 'uz', 'vu', 've', 'vn', 'vg', 'vi', 'wf', 'eh', 'ye', 'zm', 'zw' ] # Si pycountry está disponible, lo usamos para validación global pycountry_available if pycountry_available: try: # Verifica si el código existe en pycountry return pycountry.countries.get(alpha_2=code.upper()) is not None except (AttributeError, Exception) as e: logger.warning(f"Error al validar el código de país '{code}' con pycountry: {e}") # Fallback a la lista básica # Si pycountry no está disponible o falló, usamos la lista básica return code.lower() in common_country_codes def parse_rate_filename(filename): """ Analiza el nombre del archivo de tarifa para extraer el tipo de programador y la región. Formato: [tipo_de_programador]_[código_país].rate Valida el código de país usando pycountry. """ base_name = os.path.basename(filename) match = re.match(r'([a-z]+)_([a-z]{2})\.rate$', base_name) if not match: # Para archivos como kdevs.rate que no siguen el patrón estándar if base_name.endswith('.rate'): programmer_type = base_name.replace('.rate', '') return programmer_type, None return None, None programmer_type, region_code = match.groups() # Validar el código de país if not is_valid_country_code(region_code): logger.warning(f"Código de país no válido '{region_code}' en el archivo {base_name}") # Aun así continuamos con el proceso return programmer_type, region_code def generate_prompt(programmer_type, region_code): """Genera un prompt para consultar a Perplexity sobre la tarifa horaria.""" region_map = { 'ww': 'mundial', 'la': 'Latinoamérica', 'pa': 'Panamá', 'co': 'Colombia', 'mx': 'México', 'ar': 'Argentina', 'br': 'Brasil', 'cl': 'Chile', 'pe': 'Perú', 'ec': 'Ecuador', 'us': 'Estados Unidos', 'ca': 'Canadá', 'uk': 'Reino Unido', 'de': 'Alemania', 'fr': 'Francia', 'es': 'España', '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']: return None programmer_map = { 'bash': 'programador de scripts Bash/Shell', 'python': 'desarrollador Python', 'fullstack': 'desarrollador Full Stack', 'frontend': 'desarrollador Frontend', 'backend': 'desarrollador Backend', 'devops': 'ingeniero DevOps', 'mobile': 'desarrollador de aplicaciones móviles', 'java': 'desarrollador Java', 'php': 'desarrollador PHP', 'ruby': 'desarrollador Ruby', 'dotnet': 'desarrollador .NET', 'data': 'científico de datos', 'ml': 'ingeniero de Machine Learning', 'cloud': 'arquitecto Cloud', 'odoo': 'desarrollador Odoo' } 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.""" 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 # Obtener modelo configurado model = get_ai_model() logger.info(f"Usando modelo de IA: {model}") # Verificar disponibilidad de la API api_available = True api_key = get_perplexity_api_key() if api_key is None: 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) # 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 api_key = get_perplexity_api_key() if api_key is None: 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 # 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: # 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 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: 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 # 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() # 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...") 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)