Files
connpy/implementation_plan.md
T

34 KiB

Plan: Auditoría Completa + Descomposición del Monolito connapp.py

Estado Actual — Auditoría de la Arquitectura

Servicios ( Bien planteados)

Servicio Archivo Responsabilidad Estado
BaseService base.py Config compartida, hooks, validación de nombres reservados OK
NodeService node_service.py CRUD nodos/carpetas, list, move, bulk, connect OK
ProfileService profile_service.py CRUD perfiles, resolución de @profile OK
ConfigService config_service.py Settings, encrypt, config folder OK
ExecutionService execution_service.py run/test commands en nodos OK
ImportExportService import_export_service.py YAML import/export OK
PluginService plugin_service.py add/del/enable/disable plugins OK
SystemService system_service.py API start/stop/restart/status OK
AIService ai_service.py ask, sessions, provider config ⚠️ Parcial*

* AIService existe pero connapp._func_ai bypasea completamente al servicio e instancia directamente ai(self.config). El servicio solo se usa para list_sessions y delete_session.

Excepciones ( Limpias)

La jerarquía ConnpyError > {NodeNotFoundError, ProfileNotFoundError, etc.} es correcta.


Problemas Detectados

🐛 Bug 1: Métodos duplicados en connapp.py

_case, _fzf, _idletime, _configfolder y _ai_config están definidos dos veces:

  • Primera vez: líneas ~606-627 (versiones viejas sin feedback, sin try/except)
  • Segunda vez: líneas ~735-768 (versiones nuevas con printer.success)

Python sobrescribe la primera con la segunda, así que la app funciona, pero el _func_others en línea 600 mapea a métodos que llaman a las versiones antiguas (las cuales nunca se ejecutan realmente). Esto es código muerto que genera confusión.

🐛 Bug 2: self.config accesos directos pendientes

Quedan 3 accesos directos a self.config que rompen SOA:

  • Línea 250: self.config.defaultdir → debería usar self.config_service o un accessor
  • Línea 622-623: self.config.config["ai"] → debería usar self.config_service.get_settings()
    (este es el primer _ai_config duplicado, se elimina con Bug 1)
  • Línea 885: self.myai = ai(self.config, **arguments) → directo a config, debería pasar por servicio

🐛 Bug 3: self.config = config duplicado (líneas 59-60)

La línea de asignación está repetida dos veces.

⚠️ Bug 4: print() raw en lugar de printer

8 usos de print() nativo en connapp.py:

  • Líneas 434, 547, 646, 834: YAML dumps de --show, list, plugin --listreemplazar con el nuevo printer.data() con syntax highlighting
  • Líneas 604, 619: shell completion/fzf wrapper output → correcto, es output que va a .bashrc, debe seguir con print() crudo
  • Líneas 904, 939: print("\r") spacer en AI → reemplazar con console.print()

⚠️ Bug 5: ImportError en AIService.delete_session

Línea 34 de ai_service.py referencia InvalidConfigurationError pero no lo importa.


Fase 0 — Sistema de Diseño Visual (Rich Output)

Antes de migrar código, debemos definir el lenguaje visual unificado de toda la CLI. Todos los handlers van a usar las mismas funciones de output. El objetivo es que cada tipo de output tenga una apariencia consistente, profesional y con colores.

0.1 Paleta de Colores

Uso Color Rich Ejemplo
Éxito / OK green [✓] Node added successfully
Error red [✗] Node not found
Warning / Info menor yellow [!] Plugin already enabled
Info / neutral cyan [i] Editing: ['router1']
Títulos / headers bold cyan Paneles, reglas
Data keys (YAML) blue Syntax highlighting de YAML
Data values (YAML) white/default Syntax highlighting de YAML
AI Engineer blue Panel border blue
AI Architect medium_purple Panel border purple
Test PASS bold green ✓ PASS
Test FAIL bold red ✗ FAIL
Dim/metadata dim Token counts, timestamps

0.2 Funciones del printer — Ampliación

El módulo printer.py actual tiene: info, success, error, warning, custom, table, start, debug. Hay que agregar funciones nuevas para cubrir todos los tipos de output:

Nuevas funciones a agregar en printer.py

Función Propósito Diseño
printer.data(title, content, language="yaml") Mostrar datos estructurados (nodo, perfil, lista) con syntax highlighting Panel con título bold cyan, body con Syntax(content, language)
printer.node_panel(unique, output, status) Panel de resultado de ejecución en un nodo Panel con borde green/red según status, título con /, body con output
printer.test_panel(unique, results_dict) Panel de resultado de test en un nodo Igual que node_panel pero con resultados pass/fail por check
printer.test_summary(results) Resumen consolidado de tests Múltiples test_panel
printer.header(text) Separador/título de sección Rule(text, style="bold cyan")
printer.kv(key, value) Key-value inline [bold]{key}[/bold]: {value}
printer.confirm_action(item, action) Mensaje pre-confirmación [i] {action}: {item} estilizado

Ejemplo concreto: printer.data()

def data(title, content, language="yaml"):
    """Display structured data with syntax highlighting inside a panel."""
    from rich.syntax import Syntax
    from rich.panel import Panel
    syntax = Syntax(content, language, theme="monokai", word_wrap=True)
    panel = Panel(syntax, title=f"[bold cyan]{title}[/bold cyan]", 
                  border_style="dim", expand=False)
    console.print(panel)

Antes (actual):

[router1] 
host: 10.0.0.1
protocol: ssh
port: '22'
user: admin

Después (nuevo):

╭─ router1 ────────────────────╮
│ host: 10.0.0.1               │
│ protocol: ssh                │
│ port: '22'                   │
│ user: admin                  │ 
╰──────────────────────────────╯

Con syntax highlighting YAML (keys en azul, values en blanco).

Ejemplo concreto: printer.node_panel()

def node_panel(unique, output, status):
    """Display node execution result in a styled panel."""
    from rich.panel import Panel
    from rich.text import Text
    
    if status == 0:
        status_str = "[bold green]✓ PASS[/bold green]"
        border = "green"
    else:
        status_str = f"[bold red]✗ FAIL({status})[/bold red]"
        border = "red"
    
    title = f"[bold]{unique}[/bold] — {status_str}"
    body = Text(output.strip() + "\n") if output and output.strip() else Text()
    console.print(Panel(body, title=title, border_style=border))

Ejemplo concreto: lista de nodos/perfiles

# En list handler, en vez de yaml dump + print:
items = node_service.list_nodes()
yaml_str = yaml.dump(items, sort_keys=False, default_flow_style=False)
printer.data("nodes", yaml_str)

# Para plugins:
plugins = plugin_service.list_plugins()
yaml_str = yaml.dump(plugins, sort_keys=False, default_flow_style=False)
printer.data("plugins", yaml_str)

0.3 Mapa de Outputs Actuales → Nuevos

Comando Output actual Output nuevo
node --show router1 printer.custom(name, "") + print(yaml) printer.data(name, yaml_str)
profile --show myprofile printer.custom(name, "") + print(yaml) printer.data(name, yaml_str)
list nodes printer.custom("nodes", "") + print(yaml) printer.data("nodes", yaml_str)
list folders printer.custom("folders", "") + print(yaml) printer.data("folders", yaml_str)
list profiles printer.custom("profiles", "") + print(yaml) printer.data("profiles", yaml_str)
plugin --list printer.custom("plugins", "") + print(yaml) printer.data("plugins", yaml_str)
run node1 "cmd" _print_node_panel() inline printer.node_panel(unique, output, status)
YAML run test _print_test_summary() inline printer.test_panel() + printer.test_summary()
config --* printer.success("Config saved") Sin cambio (ya es correcto)
ai "query" Rich Panel + print("\r") Rich Panel + console.print()
ai --list printer.table(...) Sin cambio (ya es correcto)
node -a, -e, -r printer.success/error Sin cambio (ya es correcto)

0.4 Implementación

  1. Editar connpy/printer.py para agregar las nuevas funciones (data, node_panel, test_panel, test_summary, header, kv)
  2. Los handlers usarán estas funciones en vez de crear Panels inline
  3. _print_node_panel, _print_node_test_panel, _print_test_summary de connapp.py se eliminan y se reemplazan por las funciones del printer

Important

La regla es: toda presentación visual pasa por printer. Los handlers nunca deben importar Rich directamente ni construir Panels. Solo llaman a printer.data(), printer.node_panel(), etc. Esto garantiza consistencia visual en toda la app.

0.5 Argparse con Rich (rich-argparse)

El output de --help de argparse es texto plano sin colores. Usando la librería rich-argparse se obtiene un help coloreado como drop-in replacement:

from rich_argparse import RichHelpFormatter

parser = argparse.ArgumentParser(
    prog="connpy",
    description="SSH and Telnet connection manager",
    formatter_class=RichHelpFormatter  # ← solo cambiar esto
)

Esto afecta solo al --help. Los errores de argparse ya los interceptamos con parser.error = self._custom_error que usa printer.error().

Output argparse Estado actual Con rich-argparse
--help Texto monótono plano Argumentos en cyan, usage en bold, secciones claras
Errores de parsing Ya usa printer.error() Sin cambio
--version Ya usa printer.info() Sin cambio

Fase 1 — Limpieza Pre-Migración

Antes de tocar la estructura de archivos, hay que limpiar el código existente.

1.1 Implementar las nuevas funciones de printer.py

  • Agregar data(), node_panel(), test_panel(), test_summary(), header(), kv() según lo definido en Fase 0
  • Agregar test en test_printer.py para las funciones nuevas

1.2 Agregar rich-argparse

  • Agregar rich-argparse a requirements.txt
  • En connapp.py, cambiar formatter_class=argparse.RawTextHelpFormatter por formatter_class=RichHelpFormatter en todos los parsers (~12 instancias)
  • Los subparsers que no tienen formatter explícito heredan del padre, así que con poner en defaultparser y en los que usan RawTextHelpFormatter alcanza

1.3 Eliminar métodos duplicados

  • Borrar las definiciones antiguas de _case, _fzf, _idletime, _configfolder, _ai_config (líneas 603-627)
  • Dejar solamente las definiciones de líneas 735-768 que tienen feedback con printer.success
  • Mover _fzf_wrapper y _completion (que son únicos) a la zona cercana a las versiones finales

1.4 Corregir asignación duplicada

  • Remover la línea 60 (self.config = config duplicada)

1.5 Corregir self.config.defaultdir

  • Agregar método get_default_dir() a ConfigService que retorne self.config.defaultdir
  • Reemplazar en connapp.py línea 250

1.6 Corregir import faltante en AIService

  • Agregar from .exceptions import InvalidConfigurationError en ai_service.py

1.7 Reemplazar print() raw por printer

  • Reemplazar los 4 YAML dumps (print(yaml_output)) por printer.data(title, yaml_str)
  • Reemplazar print("\r") por console.print() en las líneas 904 y 939
  • Mantener print() crudo solo para shell completion/fzf wrapper output (necesita output limpio sin estilos)

Fase 2 — Descomposición del Monolito

El objetivo es reducir connapp.py de 1803 líneas a un orquestador limpio de ~200-300 líneas que solo:

  1. Define los parsers de argparse
  2. Despacha a handlers del paquete connpy/cli/

Estructura propuesta del paquete connpy/cli/

connpy/cli/
├── __init__.py          # Exporta todos los handlers
├── node_handler.py      # _connect, _add, _del, _mod, _show
├── profile_handler.py   # _profile_add, _profile_del, _profile_mod, _profile_show
├── config_handler.py    # _case, _fzf, _idletime, _configfolder, _ai_config, _completion, _fzf_wrapper
├── run_handler.py       # _node_run, _yaml_run, _yaml_generate, _cli_run
├── ai_handler.py        # _func_ai (modo single + interactive)
├── api_handler.py       # _func_api
├── plugin_handler.py    # _func_plugin
├── import_export_handler.py  # _func_import, _func_export, _bulk
├── helpers.py           # _choose (selector fzf/inquirer)
├── validators.py        # Todas las *_validation functions (host, port, protocol, tags, jumphost, etc.)
├── forms.py             # _questions_nodes, _questions_edit, _questions_profiles, _questions_bulk
└── help_text.py         # _help, _print_instructions (completion scripts, YAML template, etc.)

2.1 Crear connpy/cli/__init__.py

  • Exportar todas las clases handler
  • Definir una clase base CLIHandler con:
    • self.app → referencia al connapp (para acceder a servicios)
    • self.services → acceso directo a los servicios
    • Acceso rápido a printer (todos los outputs pasan por ahí)

2.2 Crear connpy/cli/helpers.py

Extraer el único método utilitario de UI compartido:

Método Descripción
choose(list, name, action, fzf, case) Selector inquirer/fzf

Note

Los métodos _print_node_panel, _print_node_test_panel, _print_test_summary ya no van aquí. Ahora viven en printer.py como printer.node_panel(), printer.test_panel(), printer.test_summary().

2.3 Crear connpy/cli/validators.py

Extraer todas las funciones de validación de inquirer (~14 funciones):

Función Uso
host_validation Validar hostname
protocol_validation Validar protocolo de nodo
profile_protocol_validation Validar protocolo de perfil
port_validation Validar puerto de nodo
profile_port_validation Validar puerto de perfil
pass_validation Validar password @profile
tags_validation Validar tags dict
profile_tags_validation Validar tags de perfil
jumphost_validation Validar jumphost
profile_jumphost_validation Validar jumphost de perfil
default_validation Validación default @profile
bulk_node_validation Validar nodo en bulk
bulk_folder_validation Validar folder en bulk
bulk_host_validation Validar host en bulk

Estas funciones necesitan acceso a self.profiles, self.nodes_list, self.folders y self.case. Se les pasará un contexto o se guardará como atributo de la clase.

2.4 Crear connpy/cli/forms.py

Extraer los formularios interactivos de inquirer:

Función Líneas approx Descripción
questions_nodes() 1378-1450 Formulario completo de nodo
questions_edit() 1363-1376 Checkboxes de qué editar
questions_profiles() 1452-1512 Formulario completo de perfil
questions_bulk() 1514-1545 Formulario de bulk add

Estas funciones usan validators y servicios (para obtener defaults de nodos/perfiles).

2.5 Crear connpy/cli/help_text.py

Extraer todo el texto estático:

Función Descripción
get_help(type, parsers) Genera help text (usage, end, node)
get_instructions(type) Wizard instructions, completion scripts, fzf wrapper, YAML template

Este módulo es puro texto, sin dependencias de servicios.

2.6 Crear connpy/cli/node_handler.py

class NodeHandler:
    def __init__(self, app):
        self.app = app
    
    def connect(self, args)     # Actual _connect
    def add(self, args)         # Actual _add  
    def delete(self, args)      # Actual _del
    def modify(self, args)      # Actual _mod
    def show(self, args)        # Usa printer.data() para YAML
    def dispatch(self, args)    # Actual _func_node

2.7 Crear connpy/cli/profile_handler.py

class ProfileHandler:
    def __init__(self, app):
        self.app = app
    
    def add(self, args)         # Actual _profile_add
    def delete(self, args)      # Actual _profile_del
    def modify(self, args)      # Actual _profile_mod
    def show(self, args)        # Usa printer.data() para YAML
    def dispatch(self, args)    # Actual _func_profile

2.8 Crear connpy/cli/config_handler.py

class ConfigHandler:
    def __init__(self, app):
        self.app = app
    
    def set_case(self, args)
    def set_fzf(self, args)
    def set_idletime(self, args)
    def set_config_folder(self, args)
    def set_ai_config(self, args)
    def show_completion(self, args)
    def show_fzf_wrapper(self, args)
    def dispatch(self, args)

2.9 Crear connpy/cli/run_handler.py

class RunHandler:
    def __init__(self, app):
        self.app = app
    
    def node_run(self, args)        # Usa printer.header() + printer.node_panel()
    def yaml_run(self, args)        # Playbook YAML
    def yaml_generate(self, args)   # Generar template
    def cli_run(self, script)       # Usa printer.header() + printer.node_panel/test_panel
    def dispatch(self, args)

2.10 Crear connpy/cli/ai_handler.py

class AIHandler:
    def __init__(self, app):
        self.app = app
    
    def single_question(self, args, myai, session_id)  # Modo single shot
    def interactive_chat(self, args, myai, session_id)  # Modo interactivo
    def list_sessions(self, args)                       # Usa printer.table()
    def delete_session(self, args)
    def dispatch(self, args)

2.11 Crear connpy/cli/api_handler.py

class APIHandler:
    def __init__(self, app):
        self.app = app
    
    def dispatch(self, args)   # Start/stop/restart/debug

2.12 Crear connpy/cli/plugin_handler.py

class PluginHandler:
    def __init__(self, app):
        self.app = app
    
    def dispatch(self, args)   # add/update/del/enable/disable/list
                               # list usa printer.data() para YAML

2.13 Crear connpy/cli/import_export_handler.py

class ImportExportHandler:
    def __init__(self, app):
        self.app = app
    
    def import_file(self, args)
    def export_file(self, args)
    def bulk(self, args)
    def dispatch_import(self, args)
    def dispatch_export(self, args)

Fase 2.5 — Auditoría y Correcciones Post-Refactor

Revisión exhaustiva línea por línea de los 12 archivos del paquete connpy/cli/ comparados contra el connapp.py original. Todos los bugs críticos (B1-B7) y mejoras (M1-M5) han sido corregidos.

Detalle de bugs corregidos (click para expandir)

🔴 Bugs Críticos (Corregidos)

Bug Archivo Problema Fix
B1 run_handler.py node_run pasaba comandos como lista separada en vez de string unido commands = [" ".join(args.data[1:])]
B2 run_handler.py cli_run no pasaba folder ni prompt al execution service Agregados como parámetros opcionales a ExecutionService
B3 ai_handler.py Sesiones usan ai_service pero AI usa ai() directo Validado que ambos leen del mismo storage
B4 ai_handler.py Faltaba error msg cuando sesión no carga Agregado branch else con printer.error()
B5 ai_handler.py Faltaba mensaje de historial previo al resumir Agregado printer.info() con count de mensajes
B6 ai_handler.py KeyboardInterrupt mataba el chat entero Doble try/except: interno (continue) + externo (exit)
B7 api_handler.py Lógica if/elif rota + bypass de system_service Corregido a elif y llamadas directas a connpy.api

🟢 Mejoras de Calidad (Corregidas)

ID Archivo Acción
M1 cli/__init__.py Clase CLIHandler muerta eliminada
M2 cli/config_handler.py Handlers huérfanos eliminados
M3 cli/commands/ Directorio vacío eliminado
M4 cli/ai_handler.py Import no usado eliminado
M5 cli/profile_handler.py Import no usado eliminado

Fase 2.7 — Sistema de Temas Persistente

Se implementó un sistema de temas centralizado y persistente que permite personalizar todos los colores de la CLI.

Componentes implementados

Archivo Cambio
printer.py STYLES, DARK_THEME, LIGHT_THEME + función apply_theme() con merge y fallback
services/config_service.py apply_theme_from_file() — acepta dark, light, o path a YAML
cli/config_handler.py Handler set_theme con dispatch y aplicación inmediata
connapp.py Flag --theme THEME + _apply_app_theme() que sincroniza printer y RichHelpFormatter

Características

  • connpy config --theme dark / light / custom.yaml
  • Persistido en config.yaml bajo config.theme
  • Auto-cargado al inicio vía connapp._apply_app_theme()
  • Fallback: keys faltantes en el YAML usan los defaults de STYLES
  • Afecta: paneles, tablas, AI (Engineer/Architect), y menús --help

Eliminación de colores hardcodeados

Auditoría completa de ai.py, ai_handler.py, run_handler.py, connapp.py: cero colores literales fuera de printer.py. Todos usan aliases semánticos (engineer, architect, error, warning, unavailable, etc.).


Fase 3 — Dynamic Service Backend (ServiceProvider Pattern)

Objetivo

Hacer que el CLI sea agnóstico del backend. En vez de que los handlers accedan a servicios locales hardcodeados (self.app.node_service), pasan por un ServiceProvider que decide qué implementación usar. Por defecto → servicios locales. Con --remote → stubs gRPC (a implementar después). Zero refactoring del CLI cuando se agregue gRPC.

Arquitectura

┌─────────────────────────────────────────────────────┐
│                   CLI Handlers                       │
│  (NodeHandler, ProfileHandler, RunHandler, etc.)     │
│                                                      │
│  self.app.services.nodes.list_nodes()                │
│  self.app.services.config_svc.update_setting()       │
└──────────────────────┬──────────────────────────────┘
                       │
              ┌────────▼────────┐
              │ ServiceProvider  │  ← decides backend
              │                  │
              │ mode = "local"   │  (default)
              │ mode = "remote"  │  (--remote / config)
              └───────┬──┬──────┘
                      │  │
          ┌───────────┘  └───────────┐
          ▼                          ▼
  ┌───────────────┐        ┌─────────────────┐
  │ Local Services │        │  gRPC Stubs      │
  │ (current code) │        │  (Fase 4+, TBD)  │
  │                │        │                  │
  │ NodeService    │        │ NodeServiceStub  │
  │ ProfileService │        │ ProfileStub      │
  │ ConfigService  │        │ ConfigStub       │
  │ ...            │        │ ...              │
  └───────────────┘        └─────────────────┘

3.1 Crear connpy/services/provider.py [NEW]

Fachada ligera que expone atributos de servicio. El provider recibe un mode y un config e instancia el backend correcto.

class ServiceProvider:
    """Dynamic service backend. Transparently provides local or remote services."""
    
    def __init__(self, config, mode="local", remote_host=None):
        self.mode = mode
        self.config = config
        self.remote_host = remote_host
        
        if mode == "local":
            self._init_local()
        elif mode == "remote":
            self._init_remote()
        else:
            raise ValueError(f"Unknown service mode: {mode}")
    
    def _init_local(self):
        from .node_service import NodeService
        from .profile_service import ProfileService
        from .config_service import ConfigService
        from .plugin_service import PluginService
        from .ai_service import AIService
        from .system_service import SystemService
        from .execution_service import ExecutionService
        from .import_export_service import ImportExportService
        
        self.nodes = NodeService(self.config)
        self.profiles = ProfileService(self.config)
        self.config_svc = ConfigService(self.config)
        self.plugins = PluginService(self.config)
        self.ai = AIService(self.config)
        self.system = SystemService(self.config)
        self.execution = ExecutionService(self.config)
        self.import_export = ImportExportService(self.config)
    
    def _init_remote(self):
        # Fase 4+: gRPC stubs go here
        raise NotImplementedError(
            "Remote mode (gRPC) is not yet available. "
            "Use local mode or wait for the gRPC implementation."
        )

Note

Los nombres de atributos son cortos y limpios: services.nodes, services.profiles, services.config_svc (evita colisión con self.config), services.execution, etc.

3.2 Refactorizar connapp.__init__ [MODIFY]

Reemplazar las 8 instanciaciones individuales por un único ServiceProvider:

# ANTES (actual):
self.node_service = NodeService(self.config)
self.profile_service = ProfileService(self.config)
self.config_service = ConfigService(self.config)
self.plugin_service = PluginService(self.config)
self.ai_service = AIService(self.config)
self.system_service = SystemService(self.config)
self.execution_service = ExecutionService(self.config)
self.import_export_service = ImportExportService(self.config)

# DESPUÉS:
from .services.provider import ServiceProvider
mode = self.config.config.get("service_mode", "local")
remote_host = self.config.config.get("remote_host", None)
self.services = ServiceProvider(self.config, mode=mode, remote_host=remote_host)

3.3 Agregar flags --service-mode y --remote globales [MODIFY connapp.py]

Agregar a defaultparser:

defaultparser.add_argument("--service-mode", dest="service_mode", choices=["local", "remote"],
                           help="Set the backend service mode (local or remote)")
defaultparser.add_argument("--remote", dest="remote_host", metavar="HOST:PORT",
                           help="Connect to a remote connpy service via gRPC (requires --service-mode remote)")

Y en start(), después del parsing:

mode = args.service_mode or self.config.config.get("service_mode", "local")
remote_host = args.remote_host or self.config.config.get("remote_host", None)

self.services = ServiceProvider(self.config, mode=mode, remote_host=remote_host)

3.4 Migrar handlers al nuevo API [MODIFY cli/*.py]

Renombrar todas las referencias en los handlers:

Antes Después
self.app.node_service self.app.services.nodes
self.app.profile_service self.app.services.profiles
self.app.config_service self.app.services.config_svc
self.app.plugin_service self.app.services.plugins
self.app.ai_service self.app.services.ai
self.app.system_service self.app.services.system
self.app.execution_service self.app.services.execution
self.app.import_export_service self.app.services.import_export

Important

Estrategia de migración: Hacer un full rename en todos los handlers en un solo pase (clean break). Son find-replace directos y todos los handlers están en connpy/cli/.

3.5 Actualizar tests [MODIFY tests/]

  • Actualizar mocks para usar app.services.nodes en vez de app.node_service
  • Agregar test para ServiceProvider con modo local y verificar que remote lanza NotImplementedError

Fase 4 — Servidor gRPC y Stubs Remotos

Con el ServiceProvider en su lugar (Fase 3), la aplicación ahora es agnóstica de si sus servicios se ejecutan localmente o de forma remota. Esta fase consiste en implementar la comunicación gRPC real.

Arquitectura de gRPC

  • Protocol Buffers: Un único archivo .proto (connpy.proto) que define todos los mensajes y servicios.
  • Servidor (connpy api -s): Un servidor gRPC que instancia los servicios locales (como lo hacía ServiceProvider en modo local) y procesa las peticiones de los clientes.
  • Cliente (connpy --remote <host>): Stubs (proxies) que exponen la misma interfaz de los servicios locales y serializan las llamadas a través de la red hacia el servidor.

4.1 Definir proto files (connpy/proto/connpy.proto)

  • Crear los mensajes base: Node, Folder, Profile, Theme, Plugin, etc.
  • Definir servicios que mapeen la interfaz existente de los servicios Python:
    • NodeService (list_nodes, list_folders, move_node, bulk_add, etc.)
    • ProfileService (list_profiles, add_profile, get_profile, etc.)
    • ConfigService (get_settings, update_setting, set_config_folder, etc.)
    • PluginService (list_plugins, add_plugin, enable_plugin, etc.)
    • ExecutionService (run_commands)

4.2 Generar código Python

  • Agregar grpcio y grpcio-tools a requirements.txt.
  • Ejecutar el compilador protoc para generar connpy_pb2.py y connpy_pb2_grpc.py.

4.3 Implementar Servidor gRPC (connpy/grpc/server.py)

  • Crear las clases de servidor (ej. NodeServicer, ProfileServicer) que hereden de las generadas por protoc.
  • Cada Servicer debe recibir una instancia de Configfile en su constructor, inicializar el servicio local correspondiente (ej. NodeService(config)) y redirigir las llamadas RPC a este servicio local.
  • Reemplazo total de la API: Eliminar completamente el servidor Flask/Waitress actual en connpy/api.py. No se mantiene nada de la API REST anterior. Modificar connpy/cli/api_handler.py y connpy/api.py para que connpy api -s arranque exclusivamente este nuevo servidor gRPC en el puerto especificado.

4.4 Implementar Stubs en el Cliente (connpy/grpc/stubs.py)

  • Crear las clases proxy (NodeStub, ProfileStub, etc.) que cumplan con la misma firma que los servicios locales.
  • Cada Stub debe recibir un grpc.Channel, construir el Stub correspondiente generado por protoc (ej. connpy_pb2_grpc.NodeServiceStub) y llamar a los métodos gRPC serializando/deserializando los argumentos y respuestas.

4.5 Conectar Stubs a ServiceProvider

  • En connpy/services/provider.py, modificar _init_remote(self) para que en lugar de asignar RemoteStub(), construya un grpc.insecure_channel(self.remote_host).
  • Instanciar los stubs reales y asignarlos a las propiedades de la clase (self.nodes = NodeStub(channel), self.profiles = ProfileStub(channel), etc.).
  • Mantener la inicialización de ConfigService en modo mixto (lee de local para saber la configuración de red y temas visuales, pero podría enviar cambios al servidor si es necesario, o mantener las configuraciones de interfaz estrictamente locales).

4.6 Manejo de Errores

  • Envolver las excepciones grpc.RpcError en ConnpyError del cliente para que los handlers del CLI las impriman limpiamente y no lancen un stacktrace sucio.

Fase 5 — Verificación Final

5.1 Correr suite completa

pytest connpy/tests/

5.2 Tests de integración manual

  • connpy node -s router1 → Verifica printer.data() con syntax highlighting
  • connpy list nodes → Verifica printer.data() con panel
  • connpy plugin --list → Verifica printer.data() con panel
  • connpy config --allow-uppercase true → Verifica config handler
  • connpy run router1 "show version" → Verifica printer.node_panel()
  • connpy ai "hello" → Verifica AI handler
  • connpy --remote localhost:50051 list nodes → Verifica que lanza NotImplementedError (hasta Fase 4)

5.3 Verificar que wc -l connpy/connapp.py < 400


Resumen de Ejecución

Fase Estado Descripción Archivos involucrados
0 Sistema de diseño visual Rich connpy/printer.py
1 Limpieza pre-migración + adoptar printer nuevo connapp.py, ai_service.py, config_service.py, printer.py
2 Crear paquete cli/ con 12 módulos connpy/cli/*.py
2.5 Auditoría post-refactor: fix bugs B1-B7 + limpieza M1-M5 cli/*.py, services/execution_service.py
2.7 Sistema de temas persistente + eliminación de colores hardcodeados printer.py, ai.py, ai_handler.py, config_service.py, config_handler.py, connapp.py
3 Dynamic Service Backend (ServiceProvider) services/provider.py (nuevo), connapp.py, cli/*.py, tests/
4 Servidor gRPC y Stubs Remotos proto/, grpc/, services/provider.py, api_handler.py
4.5 📋 Auditoría Post-gRPC (Context/Sync/Completion) cli/helpers.py, connapp.py, tests/
5 📋 Verificación final Todos

Important

La Fase 3 completada estableció el ServiceProvider. Agregar gRPC en Fase 4 es implementar el servidor y los stubs en el cliente, logrando comunicación real sin tocar la lógica de los handlers del CLI.

Warning

El método _func_ai / AIHandler instancia ai(self.config) directamente, bypasseando AIService. Esto es intencional: el AI necesita estado largo de sesión (self.myai) y un refactor completo del AIService sería trabajo aparte. El ServiceProvider no afecta este flujo.