refactor: Major upgrade to v5.1b6 - AWS SSM support & Distributed Architecture
Core & Protocols: - Native AWS SSM support added (aws ssm start-session). - Improved Pexpect logic for ssm, kubectl, and docker. - Cleaned connection success messages (omitting ports for non-IP protocols). gRPC Layer: - Migrated gRPC modules to 'connpy/grpc_layer/'. - Implemented dynamic node naming (e.g. ssm-i-xxxx@aws) for accurate server-side logging. - Added automatic sys.path resolution for gRPC generated modules. - Enhanced InteractNode response with initial connection status. Printer & Concurrency: - Implemented ThreadLocalStream for isolated thread-safe output. - Self-healing Console objects to prevent 'closed file' errors in test/async environments. - Capture clean plugin output in remote executions. AI & Services: - Improved tool registration and debug visualization. - Restored native dictionary returns for AI tools to fix Web UI rendering. - Increased backup retention to 100 copies in SyncService. - Silenced noisy auto-sync CLI messages. Quality & Docs: - Total tests: 267 (all passing). - New test suites for gRPC layer and printer concurrency. - Updated .gitignore to exclude internal planning docs. - Full technical documentation regenerated with pdoc.
This commit is contained in:
+10
@@ -150,3 +150,13 @@ testremote/
|
|||||||
*.db
|
*.db
|
||||||
*.patch
|
*.patch
|
||||||
scratch.py
|
scratch.py
|
||||||
|
|
||||||
|
# Internal planning and implementation docs
|
||||||
|
PLAN_CAPA_SERVICIOS.md
|
||||||
|
implementation_plan.md
|
||||||
|
remote-plugin-implementation-plan.md
|
||||||
|
NETWORK_COMMAND_CENTER_PLAN.md
|
||||||
|
ssm_implemmetaiton_plan.md
|
||||||
|
async_interact_plan.md
|
||||||
|
repo_consolidado_limpio.md
|
||||||
|
connpy_roadmap.md
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
# Plan de Arquitectura: Creación de Capa de Servicios en Connpy
|
|
||||||
|
|
||||||
Este documento detalla el plan paso a paso para refactorizar `connpy` y extraer la lógica de negocio actual (acoplada en `connapp.py` y `api.py`) hacia una **Capa de Servicios (Service Layer)** limpia y reutilizable.
|
|
||||||
|
|
||||||
## 🎯 Objetivos
|
|
||||||
1. **Desacoplar la CLI (`connapp.py`)**: La CLI solo debe encargarse de procesar argumentos (`argparse`), solicitar datos al usuario (`inquirer`, `rich.prompt`) y renderizar la salida en pantalla (`rich`).
|
|
||||||
2. **Desacoplar la API (`api.py`)**: La API actual (Flask) y la futura API gRPC solo deben encargarse de exponer endpoints y delegar la ejecución a la capa subyacente.
|
|
||||||
3. **Centralizar la Lógica de Negocio**: Todas las operaciones sobre nodos, perfiles, configuración, ejecución de comandos, IA, plugins e importación/exportación vivirán en la nueva capa de servicios. Esto asegura que ejecutar una acción desde la CLI local, CLI remota, o API produzca **exactamente el mismo comportamiento**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ 1. Estructura de la Capa de Servicios
|
|
||||||
|
|
||||||
Crearemos un nuevo paquete `connpy/services/` que agrupe las distintas responsabilidades del dominio. Basado en todos los comandos de `connapp.py`, la estructura será:
|
|
||||||
|
|
||||||
```text
|
|
||||||
connpy/
|
|
||||||
└── services/
|
|
||||||
├── __init__.py
|
|
||||||
├── node_service.py # CRUD de nodos, carpetas, bulk, mover, copiar y listar
|
|
||||||
├── profile_service.py # CRUD de perfiles
|
|
||||||
├── execution_service.py # Ejecución de comandos en paralelo (ad-hoc, scripts, yaml, test)
|
|
||||||
├── import_export_service.py# Importación y exportación de configuración a YAML
|
|
||||||
├── ai_service.py # Interacciones con el Agente (Claude/LLMs) y su configuración
|
|
||||||
├── plugin_service.py # Habilitar, deshabilitar y listar plugins
|
|
||||||
├── config_service.py # Manejo de la configuración global de la app (case, fzf, idletime)
|
|
||||||
├── system_service.py # Control de ciclo de vida (iniciar/detener API local)
|
|
||||||
└── exceptions.py # Excepciones de negocio (ej. NodeNotFoundError)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ 2. Diseño de los Servicios (Casos de Uso Completos)
|
|
||||||
|
|
||||||
A continuación, la lista detallada de servicios mapeando cada funcionalidad de la aplicación actual:
|
|
||||||
|
|
||||||
### 1. `NodeService`
|
|
||||||
Maneja toda la interacción con `configfile` relacionada con la topología de red (nodos y carpetas).
|
|
||||||
- `list_nodes(filter: str/list) -> list`: Devuelve lista de nodos (comando `list`).
|
|
||||||
- `list_folders(filter: str/list) -> list`: Devuelve lista de carpetas.
|
|
||||||
- `get_node_details(unique: str) -> dict`: Devuelve configuración de un nodo (`node show`).
|
|
||||||
- `add_node(unique: str, data: dict) -> None`: Agrega un nuevo nodo (`node -a`).
|
|
||||||
- `update_node(unique: str, data: dict) -> None`: Modifica un nodo (`node -e`).
|
|
||||||
- `delete_node(unique: str) -> None`: Elimina un nodo (`node -r`).
|
|
||||||
- `move_node(src: str, dst: str) -> None`: Renombra o mueve nodos a otras carpetas (`move`).
|
|
||||||
- `copy_node(src: str, dst: str) -> None`: Duplica un nodo existente (`copy`).
|
|
||||||
- `bulk_add_nodes(folder: str, nodes_data: list) -> dict`: Lógica para procesar la creación masiva de nodos (`bulk`).
|
|
||||||
|
|
||||||
### 2. `ProfileService`
|
|
||||||
- `list_profiles() -> list`: Muestra los perfiles disponibles (`list`).
|
|
||||||
- `get_profile(name: str) -> dict`: Muestra un perfil (`profile show`).
|
|
||||||
- `add_profile(name: str, data: dict) -> None`: Agrega un perfil (`profile -a`).
|
|
||||||
- `update_profile(name: str, data: dict) -> None`: Modifica un perfil (`profile mod`).
|
|
||||||
- `delete_profile(name: str) -> None`: Elimina un perfil (`profile -r`).
|
|
||||||
|
|
||||||
### 3. `ExecutionService`
|
|
||||||
Encapsula la clase `core.nodes` para conexiones y envíos de comandos, abstrayéndola de `sys.stdout` o funciones `print`.
|
|
||||||
- `run_commands(nodes_list: list, commands: list) -> dict`: Llama a nodos en paralelo y devuelve un diccionario con los resultados (`run`).
|
|
||||||
- `test_commands(nodes_list: list, commands: list, expected: str) -> dict`: Valida el output esperado.
|
|
||||||
- `run_cli_script(nodes_list: list, script_path: str) -> dict`: Lee y ejecuta un script plano en los nodos.
|
|
||||||
- `run_yaml_playbook(playbook_path: str) -> dict`: Ejecuta la lógica compleja definida en un archivo YAML.
|
|
||||||
|
|
||||||
### 4. `ImportExportService`
|
|
||||||
- `export_to_yaml(folder_name: str, output_path: str) -> None`: Exporta la configuración completa de una carpeta de forma segura (`export`).
|
|
||||||
- `import_from_yaml(yaml_path: str, destination_folder: str) -> dict`: Parsea e importa nodos desde un archivo YAML asegurando que no haya colisiones críticas (`import`).
|
|
||||||
|
|
||||||
### 5. `PluginService`
|
|
||||||
- `list_plugins() -> list`: Devuelve el estado de todos los plugins detectados (activos/inactivos) (`plugin`).
|
|
||||||
- `enable_plugin(name: str) -> None`: Activa un plugin en la configuración.
|
|
||||||
- `disable_plugin(name: str) -> None`: Desactiva un plugin en la configuración.
|
|
||||||
|
|
||||||
### 6. `ConfigService`
|
|
||||||
- `update_setting(key: str, value: any) -> None`: Actualiza de forma genérica o específica (fzf, case, idletime, configfolder) en el `configfile` (`config`).
|
|
||||||
- `get_settings() -> dict`: Devuelve las configuraciones globales actuales.
|
|
||||||
|
|
||||||
### 7. `AIService`
|
|
||||||
Encapsula `connpy.ai.ai`.
|
|
||||||
- `ask(input_text: str, dryrun: bool, chat_history: list) -> dict/str`: Envia consulta al Agente (`ai`).
|
|
||||||
- `confirm(input_text: str) -> bool`: Mecanismo de seguridad.
|
|
||||||
- `configure_provider(provider: str, model: str, api_key: str) -> None`: Guarda configuración de OpenAI/Anthropic/Google en config (`config openai/anthropic/google`).
|
|
||||||
|
|
||||||
### 8. `SystemService`
|
|
||||||
- `start_api(host: str, port: int) -> None`: Levanta el daemon o proceso de la API (`api start`).
|
|
||||||
- `stop_api() -> None`: Baja el proceso local (`api stop`).
|
|
||||||
- `status_api() -> dict`: Devuelve el estado del proceso local.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔌 3. Sobre los Plugins (Core Plugins)
|
|
||||||
Los plugins de core (como `sync.py`) añaden sus propios `subparsers` directamente a la CLI (ej. `sync start`, `sync backup`, `sync restore`).
|
|
||||||
- **Arquitectura para Plugins**: Para mantener la capa de servicios limpia, los plugins deben instanciar su propio Service si requieren lógica compleja (ej. `GoogleSyncService` definido dentro de `core_plugins/sync.py`), o bien llamar a los servicios core que definimos arriba. El motor de plugins de la aplicación no se toca, pero el comportamiento dentro de los plugins debería alinearse a usar llamadas de la Capa de Servicios si tocan datos de nodos.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 4. Fases de Implementación Actualizadas
|
|
||||||
|
|
||||||
### Fase 1: Creación del Esqueleto y Modelos de Datos
|
|
||||||
1. Crear el directorio `connpy/services/` y los archivos listados.
|
|
||||||
2. Definir `exceptions.py` con errores como `NodeNotFoundError`, `ProfileNotFoundError`, `DuplicateEntityError`.
|
|
||||||
3. Crear el `connpy/services/__init__.py` que expondrá estos servicios para que puedan ser fácilmente importados (`from connpy.services import NodeService, ExecutionService`).
|
|
||||||
|
|
||||||
### Fase 2: Migración de CRUD y Configuración
|
|
||||||
1. Refactorizar la CLI y la API para instanciar y usar: `NodeService`, `ProfileService`, `ConfigService` y `PluginService`.
|
|
||||||
2. Todo el código de validación de variables (`_questions_nodes`, `_type_node`) permanecerá en `connapp.py` ya que pertenece a la "Presentación/CLI", pero los diccionarios limpios se pasarán al Servicio para su guardado final.
|
|
||||||
|
|
||||||
### Fase 3: Migración de Import/Export e IA
|
|
||||||
1. Extraer la lógica de YAML a `ImportExportService`.
|
|
||||||
2. Mover la configuración de las llaves API a `AIService`.
|
|
||||||
|
|
||||||
### Fase 4: Migración de Ejecución (El cambio más complejo)
|
|
||||||
1. Desacoplar `core.nodes` para que sea capaz de retornar estado consolidado (diccionarios con la salida de los comandos por nodo) en vez de imprimir asíncronamente en pantalla con `printer`.
|
|
||||||
2. Integrar `ExecutionService` en los comandos `run`, `node (connect)`, test, etc.
|
|
||||||
3. La CLI se subscribirá a los resultados que devuelve el `ExecutionService` para formatearlos con `rich`.
|
|
||||||
|
|
||||||
### Fase 5: Preparación para Cliente Servidor (gRPC/REST remoto)
|
|
||||||
1. Con los servicios totalmente aislados, si la CLI opera en "modo remoto", inyectará un Cliente Remoto que implementa las mismas interfaces (mismos métodos del `NodeService`) pero que serializa peticiones hacia la API en lugar de acceder directamente al archivo de configuración cifrado local.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Checklist para el éxito
|
|
||||||
- [ ] Ningún `print()`, `console.print()`, `Prompt.ask()` debe existir dentro del paquete `services/`.
|
|
||||||
- [ ] Todas las excepciones lanzadas por `services/` deben ser manejadas visualmente por la capa que los consuma (`connapp.py` las pinta, `api.py` devuelve 400/500 JSON).
|
|
||||||
- [ ] Asegurarse de que el comportamiento local (CLI sin red) no perciba pérdida de rendimiento.
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
[](https://github.com/fluzzi/connpy/blob/main/LICENSE)
|
[](https://github.com/fluzzi/connpy/blob/main/LICENSE)
|
||||||
[](https://pypi.org/pypi/connpy/)
|
[](https://pypi.org/pypi/connpy/)
|
||||||
|
|
||||||
Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker.
|
Connpy is a SSH, SFTP, Telnet, kubectl, Docker pod, and AWS SSM connection manager and automation module for Linux, Mac, and Docker.
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -29,7 +29,7 @@ docker compose -f path/to/folder/docker-compose.yml run -it connpy-app
|
|||||||
Connpy is committed to protecting your privacy. Our privacy policy explains how we handle user data:
|
Connpy is committed to protecting your privacy. Our privacy policy explains how we handle user data:
|
||||||
|
|
||||||
- **Data Access**: Connpy accesses data necessary for managing remote host connections, including server addresses, usernames, and passwords. This data is stored locally on your machine and is not transmitted or shared with any third parties.
|
- **Data Access**: Connpy accesses data necessary for managing remote host connections, including server addresses, usernames, and passwords. This data is stored locally on your machine and is not transmitted or shared with any third parties.
|
||||||
- **Data Usage**: User data is used solely for the purpose of managing and automating SSH and Telnet connections.
|
- **Data Usage**: User data is used solely for the purpose of managing and automating SSH, Telnet, and SSM connections.
|
||||||
- **Data Storage**: All connection details are stored locally and securely on your device. We do not store or process this data on our servers.
|
- **Data Storage**: All connection details are stored locally and securely on your device. We do not store or process this data on our servers.
|
||||||
- **Data Sharing**: We do not share any user data with third parties.
|
- **Data Sharing**: We do not share any user data with third parties.
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ For more detailed information, please read our [Privacy Policy](https://connpy.g
|
|||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec.
|
- Manage connections using SSH, SFTP, Telnet, kubectl, Docker exec, and AWS SSM.
|
||||||
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
|
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
|
||||||
- You can generate profiles and reference them from nodes using @profilename so you don't
|
- You can generate profiles and reference them from nodes using @profilename so you don't
|
||||||
need to edit multiple nodes when changing passwords or other information.
|
need to edit multiple nodes when changing passwords or other information.
|
||||||
|
|||||||
+2
-2
@@ -2,10 +2,10 @@
|
|||||||
'''
|
'''
|
||||||
## Connection manager
|
## Connection manager
|
||||||
|
|
||||||
Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker.
|
Connpy is a SSH, SFTP, Telnet, kubectl, Docker pod, and AWS SSM connection manager and automation module for Linux, Mac, and Docker.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec.
|
- Manage connections using SSH, SFTP, Telnet, kubectl, Docker exec, and AWS SSM.
|
||||||
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
|
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
|
||||||
- You can generate profiles and reference them from nodes using @profilename so you don't
|
- You can generate profiles and reference them from nodes using @profilename so you don't
|
||||||
need to edit multiple nodes when changing passwords or other information.
|
need to edit multiple nodes when changing passwords or other information.
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
__version__ = "5.1b5"
|
__version__ = "5.1b6"
|
||||||
|
|||||||
+113
-27
@@ -31,6 +31,8 @@ from . import printer
|
|||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
from rich.console import Group
|
||||||
|
from rich.rule import Rule
|
||||||
|
|
||||||
console = printer.console
|
console = printer.console
|
||||||
|
|
||||||
@@ -209,14 +211,20 @@ class ai:
|
|||||||
status_formatter (callable): Function(args_dict) -> status string.
|
status_formatter (callable): Function(args_dict) -> status string.
|
||||||
"""
|
"""
|
||||||
name = tool_definition["function"]["name"]
|
name = tool_definition["function"]["name"]
|
||||||
|
|
||||||
|
# Check if already registered to prevent duplicates
|
||||||
if target in ("engineer", "both"):
|
if target in ("engineer", "both"):
|
||||||
|
if not any(t["function"]["name"] == name for t in self.external_engineer_tools):
|
||||||
self.external_engineer_tools.append(tool_definition)
|
self.external_engineer_tools.append(tool_definition)
|
||||||
if target in ("architect", "both"):
|
if target in ("architect", "both"):
|
||||||
|
if not any(t["function"]["name"] == name for t in self.external_architect_tools):
|
||||||
self.external_architect_tools.append(tool_definition)
|
self.external_architect_tools.append(tool_definition)
|
||||||
|
|
||||||
self.external_tool_handlers[name] = handler
|
self.external_tool_handlers[name] = handler
|
||||||
if engineer_prompt:
|
|
||||||
|
if engineer_prompt and engineer_prompt not in self.engineer_prompt_extensions:
|
||||||
self.engineer_prompt_extensions.append(engineer_prompt)
|
self.engineer_prompt_extensions.append(engineer_prompt)
|
||||||
if architect_prompt:
|
if architect_prompt and architect_prompt not in self.architect_prompt_extensions:
|
||||||
self.architect_prompt_extensions.append(architect_prompt)
|
self.architect_prompt_extensions.append(architect_prompt)
|
||||||
if status_formatter:
|
if status_formatter:
|
||||||
self.tool_status_formatters[name] = status_formatter
|
self.tool_status_formatters[name] = status_formatter
|
||||||
@@ -448,12 +456,46 @@ class ai:
|
|||||||
|
|
||||||
def _truncate(self, text, limit=None):
|
def _truncate(self, text, limit=None):
|
||||||
"""Truncate text to specified limit, keeping head (60%) and tail (40%)."""
|
"""Truncate text to specified limit, keeping head (60%) and tail (40%)."""
|
||||||
|
if not isinstance(text, str): return str(text)
|
||||||
final_limit = limit or self.max_truncate
|
final_limit = limit or self.max_truncate
|
||||||
if len(text) <= final_limit: return text
|
if len(text) <= final_limit: return text
|
||||||
head_limit = int(final_limit * 0.6)
|
head_limit = int(final_limit * 0.6)
|
||||||
tail_limit = int(final_limit * 0.4)
|
tail_limit = int(final_limit * 0.4)
|
||||||
return (text[:head_limit] + f"\n\n[... OUTPUT TRUNCATED ...]\n\n" + text[-tail_limit:])
|
return (text[:head_limit] + f"\n\n[... OUTPUT TRUNCATED ...]\n\n" + text[-tail_limit:])
|
||||||
|
|
||||||
|
def _print_debug_observation(self, fn, obs):
|
||||||
|
"""Prints a tool observation in a readable way during debug mode."""
|
||||||
|
# Try to parse as JSON if it's a string
|
||||||
|
if isinstance(obs, str):
|
||||||
|
try:
|
||||||
|
obs_data = json.loads(obs)
|
||||||
|
except Exception:
|
||||||
|
obs_data = obs
|
||||||
|
else:
|
||||||
|
obs_data = obs
|
||||||
|
|
||||||
|
if isinstance(obs_data, dict):
|
||||||
|
elements = []
|
||||||
|
for k, v in obs_data.items():
|
||||||
|
elements.append(Text(f"• {k}:", style="key"))
|
||||||
|
# Use Text for values to ensure newlines are rendered
|
||||||
|
val = str(v)
|
||||||
|
# If it's a multiline string from a delegation task, keep it clean
|
||||||
|
elements.append(Text(val))
|
||||||
|
|
||||||
|
if not elements:
|
||||||
|
content = Text("Empty data set")
|
||||||
|
else:
|
||||||
|
# Add a small spacer instead of a Rule for cleaner look
|
||||||
|
content = Group(*elements)
|
||||||
|
elif isinstance(obs_data, list):
|
||||||
|
content = Text("\n".join(f"• {item}" for item in obs_data))
|
||||||
|
else:
|
||||||
|
content = Text(str(obs_data))
|
||||||
|
|
||||||
|
title = f"[bold]{fn}[/bold]"
|
||||||
|
self.console.print(Panel(content, title=title, border_style="ai_status"))
|
||||||
|
|
||||||
def manage_memory_tool(self, content, action="append"):
|
def manage_memory_tool(self, content, action="append"):
|
||||||
"""Save or update long-term memory. Only use when user explicitly requests it."""
|
"""Save or update long-term memory. Only use when user explicitly requests it."""
|
||||||
if not content or not content.strip():
|
if not content or not content.strip():
|
||||||
@@ -491,8 +533,8 @@ class ai:
|
|||||||
ts = data.get("tags")
|
ts = data.get("tags")
|
||||||
if isinstance(ts, dict): os_tag = ts.get("os", "unknown")
|
if isinstance(ts, dict): os_tag = ts.get("os", "unknown")
|
||||||
res[name] = {"os": os_tag}
|
res[name] = {"os": os_tag}
|
||||||
return json.dumps(res)
|
return res
|
||||||
return json.dumps({"count": len(matched_names), "nodes": matched_names, "note": "Use 'get_node_info' for details."})
|
return {"count": len(matched_names), "nodes": matched_names, "note": "Use 'get_node_info' for details."}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error listing nodes: {str(e)}"
|
return f"Error listing nodes: {str(e)}"
|
||||||
|
|
||||||
@@ -566,7 +608,7 @@ class ai:
|
|||||||
if not matched_names: return "No nodes found matching filter."
|
if not matched_names: return "No nodes found matching filter."
|
||||||
thisnodes_dict = self.config.getitems(matched_names, extract=True)
|
thisnodes_dict = self.config.getitems(matched_names, extract=True)
|
||||||
result = nodes(thisnodes_dict, config=self.config).run(commands)
|
result = nodes(thisnodes_dict, config=self.config).run(commands)
|
||||||
return self._truncate(json.dumps(result))
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error executing commands: {str(e)}"
|
return f"Error executing commands: {str(e)}"
|
||||||
|
|
||||||
@@ -575,7 +617,7 @@ class ai:
|
|||||||
try:
|
try:
|
||||||
d = self.config.getitem(node_name, extract=True)
|
d = self.config.getitem(node_name, extract=True)
|
||||||
if 'password' in d: d['password'] = '***'
|
if 'password' in d: d['password'] = '***'
|
||||||
return json.dumps(d)
|
return d
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error getting node info: {str(e)}"
|
return f"Error getting node info: {str(e)}"
|
||||||
|
|
||||||
@@ -619,7 +661,7 @@ class ai:
|
|||||||
self.console.print(f"[warning] You can press Ctrl+C to interrupt and get a summary.[/warning]")
|
self.console.print(f"[warning] You can press Ctrl+C to interrupt and get a summary.[/warning]")
|
||||||
soft_limit_warned = True
|
soft_limit_warned = True
|
||||||
|
|
||||||
if status: status.update(f"[ai_status]Engineer: Analyzing mission... (step {iteration})")
|
if status and not chat_history: status.update(f"[ai_status]Engineer: Analyzing mission... (step {iteration})")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
safe_messages = self._sanitize_messages(messages)
|
safe_messages = self._sanitize_messages(messages)
|
||||||
@@ -642,8 +684,8 @@ class ai:
|
|||||||
for tc in resp_msg.tool_calls:
|
for tc in resp_msg.tool_calls:
|
||||||
fn, args = tc.function.name, json.loads(tc.function.arguments)
|
fn, args = tc.function.name, json.loads(tc.function.arguments)
|
||||||
|
|
||||||
# Notificación en tiempo real de la tarea técnica
|
# Notificación en tiempo real de la tarea técnica (Only if not in Architect loop)
|
||||||
if status:
|
if status and not chat_history:
|
||||||
if fn == "list_nodes": status.update(f"[ai_status]Engineer: [SEARCH] {args.get('filter_pattern','.*')}")
|
if fn == "list_nodes": status.update(f"[ai_status]Engineer: [SEARCH] {args.get('filter_pattern','.*')}")
|
||||||
elif fn == "run_commands":
|
elif fn == "run_commands":
|
||||||
cmds = args.get('commands', [])
|
cmds = args.get('commands', [])
|
||||||
@@ -652,7 +694,8 @@ class ai:
|
|||||||
elif fn == "get_node_info": status.update(f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}")
|
elif fn == "get_node_info": status.update(f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}")
|
||||||
elif fn in self.tool_status_formatters: status.update(self.tool_status_formatters[fn](args))
|
elif fn in self.tool_status_formatters: status.update(self.tool_status_formatters[fn](args))
|
||||||
|
|
||||||
if debug: self.console.print(Panel(Text(json.dumps(args, indent=2)), title=f"[bold engineer]Engineer Tool: {fn}[/bold engineer]", border_style="engineer"))
|
if debug:
|
||||||
|
self._print_debug_observation(f"Decision: {fn}", args)
|
||||||
|
|
||||||
if fn == "list_nodes": obs = self.list_nodes_tool(**args)
|
if fn == "list_nodes": obs = self.list_nodes_tool(**args)
|
||||||
elif fn == "run_commands": obs = self.run_commands_tool(**args, status=status)
|
elif fn == "run_commands": obs = self.run_commands_tool(**args, status=status)
|
||||||
@@ -660,8 +703,12 @@ class ai:
|
|||||||
elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args)
|
elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args)
|
||||||
else: obs = f"Error: Unknown tool '{fn}'."
|
else: obs = f"Error: Unknown tool '{fn}'."
|
||||||
|
|
||||||
if debug: self.console.print(Panel(Text(str(obs)), title=f"[bold pass]Engineer Observation: {fn}[/bold pass]", border_style="success"))
|
if debug:
|
||||||
messages.append({"tool_call_id": tc.id, "role": "tool", "name": fn, "content": obs})
|
self._print_debug_observation(f"Observation: {fn}", obs)
|
||||||
|
|
||||||
|
# Ensure observation is a string and truncated for the LLM
|
||||||
|
obs_str = obs if isinstance(obs, str) else json.dumps(obs)
|
||||||
|
messages.append({"tool_call_id": tc.id, "role": "tool", "name": fn, "content": self._truncate(obs_str)})
|
||||||
|
|
||||||
if iteration >= self.hard_limit_iterations:
|
if iteration >= self.hard_limit_iterations:
|
||||||
self.console.print(f"[error]⛔ Engineer reached hard limit ({self.hard_limit_iterations} steps). Forcing stop.[/error]")
|
self.console.print(f"[error]⛔ Engineer reached hard limit ({self.hard_limit_iterations} steps). Forcing stop.[/error]")
|
||||||
@@ -675,30 +722,46 @@ class ai:
|
|||||||
|
|
||||||
def _get_engineer_tools(self):
|
def _get_engineer_tools(self):
|
||||||
"""Define tools available to the Engineer."""
|
"""Define tools available to the Engineer."""
|
||||||
tools = [
|
base_tools = [
|
||||||
{"type": "function", "function": {"name": "list_nodes", "description": "Lists available nodes in the inventory.", "parameters": {"type": "object", "properties": {"filter_pattern": {"type": "string", "description": "Regex to filter nodes (e.g. '.*', 'border.*')."}}}}},
|
{"type": "function", "function": {"name": "list_nodes", "description": "Lists available nodes in the inventory.", "parameters": {"type": "object", "properties": {"filter_pattern": {"type": "string", "description": "Regex to filter nodes (e.g. '.*', 'border.*')."}}}}},
|
||||||
{"type": "function", "function": {"name": "run_commands", "description": "Runs one or more commands on matched nodes. MANDATORY: You MUST call 'list_nodes' first to verify the target list.", "parameters": {"type": "object", "properties": {"nodes_filter": {"type": "string", "description": "Exact node name or verified filter pattern."}, "commands": {"type": "array", "items": {"type": "string"}, "description": "List of commands (e.g. ['show ip route', 'show int desc'])."}}, "required": ["nodes_filter", "commands"]}}},
|
{"type": "function", "function": {"name": "run_commands", "description": "Runs one or more commands on matched nodes. MANDATORY: You MUST call 'list_nodes' first to verify the target list.", "parameters": {"type": "object", "properties": {"nodes_filter": {"type": "string", "description": "Exact node name or verified filter pattern."}, "commands": {"type": "array", "items": {"type": "string"}, "description": "List of commands (e.g. ['show ip route', 'show int desc'])."}}, "required": ["nodes_filter", "commands"]}}},
|
||||||
{"type": "function", "function": {"name": "get_node_info", "description": "Gets full metadata for a specific node.", "parameters": {"type": "object", "properties": {"node_name": {"type": "string"}}, "required": ["node_name"]}}}
|
{"type": "function", "function": {"name": "get_node_info", "description": "Gets full metadata for a specific node.", "parameters": {"type": "object", "properties": {"node_name": {"type": "string"}}, "required": ["node_name"]}}}
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.architect_key:
|
if self.architect_key:
|
||||||
tools.extend([
|
base_tools.extend([
|
||||||
{"type": "function", "function": {"name": "consult_architect", "description": "Ask the Strategic Reasoning Engine for advice on complex design, architecture, or troubleshooting decisions. You remain in control and will present the response to the user. Use this for: configuration planning, design validation, complex troubleshooting.", "parameters": {"type": "object", "properties": {"question": {"type": "string", "description": "Strategic question or decision needed."}, "technical_summary": {"type": "string", "description": "Technical findings and context gathered so far."}}, "required": ["question", "technical_summary"]}}},
|
{"type": "function", "function": {"name": "consult_architect", "description": "Ask the Strategic Reasoning Engine for advice on complex design, architecture, or troubleshooting decisions. You remain in control and will present the response to the user. Use this for: configuration planning, design validation, complex troubleshooting.", "parameters": {"type": "object", "properties": {"question": {"type": "string", "description": "Strategic question or decision needed."}, "technical_summary": {"type": "string", "description": "Technical findings and context gathered so far."}}, "required": ["question", "technical_summary"]}}},
|
||||||
{"type": "function", "function": {"name": "escalate_to_architect", "description": "Transfer full control to the Strategic Reasoning Engine. Use ONLY when the user explicitly requests the Architect or when the problem requires strategic oversight beyond consultation. After escalation, the Architect takes over the conversation.", "parameters": {"type": "object", "properties": {"reason": {"type": "string", "description": "Why you're escalating (e.g. 'User requested Architect', 'Complex multi-site design needed')."}, "context": {"type": "string", "description": "Full context and findings to hand over."}}, "required": ["reason", "context"]}}}
|
{"type": "function", "function": {"name": "escalate_to_architect", "description": "Transfer full control to the Strategic Reasoning Engine. Use ONLY when the user explicitly requests the Architect or when the problem requires strategic oversight beyond consultation. After escalation, the Architect takes over the conversation.", "parameters": {"type": "object", "properties": {"reason": {"type": "string", "description": "Why you're escalating (e.g. 'User requested Architect', 'Complex multi-site design needed')."}, "context": {"type": "string", "description": "Full context and findings to hand over."}}, "required": ["reason", "context"]}}}
|
||||||
])
|
])
|
||||||
|
|
||||||
tools.extend(self.external_engineer_tools)
|
# Deduplicate by name to prevent Gemini BadRequestError
|
||||||
return tools
|
all_tools = base_tools + self.external_engineer_tools
|
||||||
|
seen_names = set()
|
||||||
|
unique_tools = []
|
||||||
|
for t in all_tools:
|
||||||
|
name = t["function"]["name"]
|
||||||
|
if name not in seen_names:
|
||||||
|
unique_tools.append(t)
|
||||||
|
seen_names.add(name)
|
||||||
|
return unique_tools
|
||||||
|
|
||||||
def _get_architect_tools(self):
|
def _get_architect_tools(self):
|
||||||
"""Define tools available to the Strategic Reasoning Engine."""
|
"""Define tools available to the Strategic Reasoning Engine."""
|
||||||
tools = [
|
base_tools = [
|
||||||
{"type": "function", "function": {"name": "delegate_to_engineer", "description": "Delegates a technical mission to the Engineer.", "parameters": {"type": "object", "properties": {"task": {"type": "string", "description": "Detailed technical mission or goal."}}, "required": ["task"]}}},
|
{"type": "function", "function": {"name": "delegate_to_engineer", "description": "Delegates a technical mission to the Engineer.", "parameters": {"type": "object", "properties": {"task": {"type": "string", "description": "Detailed technical mission or goal."}}, "required": ["task"]}}},
|
||||||
{"type": "function", "function": {"name": "return_to_engineer", "description": "Return control to the Engineer. Use this when your strategic analysis is complete and the Engineer should handle the rest of the conversation.", "parameters": {"type": "object", "properties": {"summary": {"type": "string", "description": "Brief summary of your analysis to hand over to the Engineer."}}, "required": ["summary"]}}},
|
{"type": "function", "function": {"name": "return_to_engineer", "description": "Return control to the Engineer. Use this when your strategic analysis is complete and the Engineer should handle the rest of the conversation.", "parameters": {"type": "object", "properties": {"summary": {"type": "string", "description": "Brief summary of your analysis to hand over to the Engineer."}}, "required": ["summary"]}}},
|
||||||
{"type": "function", "function": {"name": "manage_memory_tool", "description": "Saves information to long-term memory. MANDATORY: Only use this if the user explicitly asks to remember or save something.", "parameters": {"type": "object", "properties": {"content": {"type": "string"}, "action": {"type": "string", "enum": ["append", "replace"]}}, "required": ["content"]}}}
|
{"type": "function", "function": {"name": "manage_memory_tool", "description": "Saves information to long-term memory. MANDATORY: Only use this if the user explicitly asks to remember or save something.", "parameters": {"type": "object", "properties": {"content": {"type": "string"}, "action": {"type": "string", "enum": ["append", "replace"]}}, "required": ["content"]}}}
|
||||||
]
|
]
|
||||||
tools.extend(self.external_architect_tools)
|
|
||||||
return tools
|
all_tools = base_tools + self.external_architect_tools
|
||||||
|
seen_names = set()
|
||||||
|
unique_tools = []
|
||||||
|
for t in all_tools:
|
||||||
|
name = t["function"]["name"]
|
||||||
|
if name not in seen_names:
|
||||||
|
unique_tools.append(t)
|
||||||
|
seen_names.add(name)
|
||||||
|
return unique_tools
|
||||||
|
|
||||||
def _get_sessions(self):
|
def _get_sessions(self):
|
||||||
"""Returns a list of session metadata sorted by date."""
|
"""Returns a list of session metadata sorted by date."""
|
||||||
@@ -902,12 +965,16 @@ class ai:
|
|||||||
soft_limit_warned = True
|
soft_limit_warned = True
|
||||||
|
|
||||||
label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]"
|
label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]"
|
||||||
if status: status.update(f"{label} is thinking... (step {iteration})")
|
if status:
|
||||||
|
# Notify responder identity ONLY for web/remote clients (StatusBridge has is_web)
|
||||||
|
if getattr(status, "is_web", False):
|
||||||
|
status.update(f"__RESPONDER__:{current_brain}")
|
||||||
|
status.update(f"{label} is thinking... (step {iteration})")
|
||||||
|
|
||||||
streamed_response = False
|
streamed_response = False
|
||||||
try:
|
try:
|
||||||
safe_messages = self._sanitize_messages(messages)
|
safe_messages = self._sanitize_messages(messages)
|
||||||
if stream and not debug:
|
if stream and (not debug or chunk_callback):
|
||||||
response, streamed_response = self._stream_completion(
|
response, streamed_response = self._stream_completion(
|
||||||
model=model, messages=safe_messages, tools=tools, api_key=key,
|
model=model, messages=safe_messages, tools=tools, api_key=key,
|
||||||
status=status, label=label, debug=debug, num_retries=3,
|
status=status, label=label, debug=debug, num_retries=3,
|
||||||
@@ -947,7 +1014,10 @@ class ai:
|
|||||||
messages.append(msg_dict)
|
messages.append(msg_dict)
|
||||||
|
|
||||||
if debug and resp_msg.content:
|
if debug and resp_msg.content:
|
||||||
self.console.print(Panel(Markdown(resp_msg.content), title=f"{label} Reasoning", border_style="architect" if current_brain == "architect" else "engineer"))
|
# In CLI debug mode, only print intermediate reasoning if there are tool calls.
|
||||||
|
# If there are no tool calls, this content is the final answer and will be printed by the caller.
|
||||||
|
if resp_msg.tool_calls:
|
||||||
|
self.console.print(Panel(Markdown(resp_msg.content), title=f"[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]", border_style="architect" if current_brain == "architect" else "engineer"))
|
||||||
|
|
||||||
if not resp_msg.tool_calls: break
|
if not resp_msg.tool_calls: break
|
||||||
|
|
||||||
@@ -967,7 +1037,8 @@ class ai:
|
|||||||
if fn == "delegate_to_engineer": status.update(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
|
if fn == "delegate_to_engineer": status.update(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
|
||||||
elif fn == "manage_memory_tool": status.update(f"[architect]Architect: [UPDATING MEMORY]")
|
elif fn == "manage_memory_tool": status.update(f"[architect]Architect: [UPDATING MEMORY]")
|
||||||
|
|
||||||
if debug: self.console.print(Panel(Text(json.dumps(args, indent=2)), title=f"{label} Decision: {fn}", border_style="debug"))
|
if debug:
|
||||||
|
self._print_debug_observation(f"Decision: {fn}", args)
|
||||||
|
|
||||||
if fn == "delegate_to_engineer":
|
if fn == "delegate_to_engineer":
|
||||||
obs, eng_usage = self._engineer_loop(args["task"], status=status, debug=debug, chat_history=messages[:-1])
|
obs, eng_usage = self._engineer_loop(args["task"], status=status, debug=debug, chat_history=messages[:-1])
|
||||||
@@ -1026,8 +1097,12 @@ class ai:
|
|||||||
elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args)
|
elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args)
|
||||||
else: obs = f"Error: {fn} unknown."
|
else: obs = f"Error: {fn} unknown."
|
||||||
|
|
||||||
messages.append({"tool_call_id": tc.id, "role": "tool", "name": fn, "content": obs})
|
if debug and fn not in ["delegate_to_engineer", "consult_architect", "escalate_to_architect", "return_to_engineer"]:
|
||||||
|
self._print_debug_observation(f"Observation: {fn}", obs)
|
||||||
|
|
||||||
|
# Ensure observation is a string and truncated for the LLM
|
||||||
|
obs_str = obs if isinstance(obs, str) else json.dumps(obs)
|
||||||
|
messages.append({"tool_call_id": tc.id, "role": "tool", "name": fn, "content": self._truncate(obs_str)})
|
||||||
# Inject pending user message AFTER all tool responses are added
|
# Inject pending user message AFTER all tool responses are added
|
||||||
if pending_user_message:
|
if pending_user_message:
|
||||||
messages.append({"role": "user", "content": pending_user_message})
|
messages.append({"role": "user", "content": pending_user_message})
|
||||||
@@ -1053,14 +1128,25 @@ class ai:
|
|||||||
if last_msg.get("tool_calls"):
|
if last_msg.get("tool_calls"):
|
||||||
for tc in last_msg["tool_calls"]:
|
for tc in last_msg["tool_calls"]:
|
||||||
messages.append({"tool_call_id": tc.get("id"), "role": "tool", "name": tc.get("function", {}).get("name"), "content": "Operation cancelled by user."})
|
messages.append({"tool_call_id": tc.get("id"), "role": "tool", "name": tc.get("function", {}).get("name"), "content": "Operation cancelled by user."})
|
||||||
messages.append({"role": "user", "content": "USER INTERRUPTED. Briefly summarize what you were doing and stop."})
|
|
||||||
|
# Use a fresh list for the summary call to avoid history corruption
|
||||||
|
summary_messages = list(messages)
|
||||||
|
summary_messages.append({"role": "user", "content": "USER INTERRUPTED. Briefly summarize what you were doing and stop."})
|
||||||
try:
|
try:
|
||||||
safe_messages = self._sanitize_messages(messages)
|
safe_messages = self._sanitize_messages(summary_messages)
|
||||||
# Use tools=None to force a text summary during interruption
|
# Use tools=None to force a text summary during interruption
|
||||||
response = completion(model=model, messages=safe_messages, tools=None, api_key=key)
|
response = completion(model=model, messages=safe_messages, tools=None, api_key=key)
|
||||||
resp_msg = response.choices[0].message
|
resp_msg = response.choices[0].message
|
||||||
messages.append(resp_msg.model_dump(exclude_none=True))
|
messages.append(resp_msg.model_dump(exclude_none=True))
|
||||||
except Exception: pass
|
|
||||||
|
# IMPORTANT: Manually trigger callback for the summary so Web UI sees it
|
||||||
|
if chunk_callback and resp_msg.content:
|
||||||
|
chunk_callback(resp_msg.content)
|
||||||
|
except Exception:
|
||||||
|
error_msg = "Operation interrupted by user. Summary unavailable."
|
||||||
|
messages.append({"role": "assistant", "content": error_msg})
|
||||||
|
if chunk_callback:
|
||||||
|
chunk_callback(error_msg)
|
||||||
finally:
|
finally:
|
||||||
# Auto-save session
|
# Auto-save session
|
||||||
self.save_session(messages, model=model)
|
self.save_session(messages, model=model)
|
||||||
|
|||||||
+2
-2
@@ -48,7 +48,7 @@ def stop_api():
|
|||||||
return port
|
return port
|
||||||
|
|
||||||
def debug_api(port=8048, config=None):
|
def debug_api(port=8048, config=None):
|
||||||
from .grpc.server import serve
|
from .grpc_layer.server import serve
|
||||||
conf = config or configfile()
|
conf = config or configfile()
|
||||||
server = serve(conf, port=port, debug=True)
|
server = serve(conf, port=port, debug=True)
|
||||||
printer.info(f"gRPC Server running in debug mode on port {port}...")
|
printer.info(f"gRPC Server running in debug mode on port {port}...")
|
||||||
@@ -63,7 +63,7 @@ def start_server(port=8048, config=None):
|
|||||||
if base_dir not in sys.path:
|
if base_dir not in sys.path:
|
||||||
sys.path.insert(0, base_dir)
|
sys.path.insert(0, base_dir)
|
||||||
|
|
||||||
from connpy.grpc.server import serve
|
from connpy.grpc_layer.server import serve
|
||||||
conf = config or configfile()
|
conf = config or configfile()
|
||||||
server = serve(conf, port=port, debug=False)
|
server = serve(conf, port=port, debug=False)
|
||||||
_wait_for_termination()
|
_wait_for_termination()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ Here are some important instructions and tips for configuring your new node:
|
|||||||
- telnet
|
- telnet
|
||||||
- kubectl (`kubectl exec`)
|
- kubectl (`kubectl exec`)
|
||||||
- docker (`docker exec`)
|
- docker (`docker exec`)
|
||||||
|
- ssm (`aws ssm start-session`)
|
||||||
|
|
||||||
3. **Optional Values**:
|
3. **Optional Values**:
|
||||||
- You can leave any value empty except for the hostname/IP.
|
- You can leave any value empty except for the hostname/IP.
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ class NodeHandler:
|
|||||||
printer.error(f"Node '{args.data}' already exists.")
|
printer.error(f"Node '{args.data}' already exists.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
uniques = self.app.services.nodes.explode_unique(args.data)
|
uniques = self.app.services.nodes.explode_unique(args.data)
|
||||||
|
|
||||||
|
# Fast fail if parent folder does not exist
|
||||||
|
self.app.services.nodes.validate_parent_folder(args.data)
|
||||||
|
|
||||||
printer.console.print(Markdown(get_instructions()))
|
printer.console.print(Markdown(get_instructions()))
|
||||||
|
|
||||||
new_node_data = self.forms.questions_nodes(args.data, uniques)
|
new_node_data = self.forms.questions_nodes(args.data, uniques)
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ class Validators:
|
|||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^$)"):
|
def profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$)"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker or leave empty")
|
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker, ssm or leave empty")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^$|^@.+$)"):
|
def protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$|^@.+$)"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker leave empty or @profile")
|
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker, ssm, leave empty or @profile")
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
||||||
|
|||||||
@@ -445,7 +445,18 @@ class connapp:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if args.subcommand in getattr(self.plugins, "remote_plugins", {}):
|
if args.subcommand in getattr(self.plugins, "remote_plugins", {}):
|
||||||
|
import json as _json
|
||||||
for chunk in self.services.plugins.invoke_plugin(args.subcommand, args):
|
for chunk in self.services.plugins.invoke_plugin(args.subcommand, args):
|
||||||
|
if "__interact__" in chunk:
|
||||||
|
try:
|
||||||
|
data = _json.loads(chunk.strip())
|
||||||
|
params = data.get("__interact__")
|
||||||
|
if params:
|
||||||
|
self.services.nodes.connect_dynamic(params, debug=getattr(args, 'debug', False))
|
||||||
|
break
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
print(chunk, end="", flush=True)
|
||||||
|
else:
|
||||||
print(chunk, end="", flush=True)
|
print(chunk, end="", flush=True)
|
||||||
elif args.subcommand in self.plugins.plugins:
|
elif args.subcommand in self.plugins.plugins:
|
||||||
self.plugins.plugins[args.subcommand].Entrypoint(args, self.plugins.plugin_parsers[args.subcommand].parser, self)
|
self.plugins.plugins[args.subcommand].Entrypoint(args, self.plugins.plugin_parsers[args.subcommand].parser, self)
|
||||||
|
|||||||
+30
-8
@@ -264,7 +264,8 @@ class node:
|
|||||||
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
||||||
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
||||||
if logger:
|
if logger:
|
||||||
logger("success", "Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}")
|
||||||
|
|
||||||
if 'logfile' in dir(self):
|
if 'logfile' in dir(self):
|
||||||
# Initialize self.mylog
|
# Initialize self.mylog
|
||||||
@@ -343,7 +344,8 @@ class node:
|
|||||||
now = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')
|
now = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')
|
||||||
if connect == True:
|
if connect == True:
|
||||||
if logger:
|
if logger:
|
||||||
logger("success", "Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}")
|
||||||
|
|
||||||
# Attempt to set the terminal size
|
# Attempt to set the terminal size
|
||||||
try:
|
try:
|
||||||
@@ -444,7 +446,8 @@ class node:
|
|||||||
connect = self._connect(timeout = timeout, logger = logger)
|
connect = self._connect(timeout = timeout, logger = logger)
|
||||||
if connect == True:
|
if connect == True:
|
||||||
if logger:
|
if logger:
|
||||||
logger("success", "Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}")
|
||||||
|
|
||||||
# Attempt to set the terminal size
|
# Attempt to set the terminal size
|
||||||
try:
|
try:
|
||||||
@@ -549,6 +552,19 @@ class node:
|
|||||||
cmd += f" {docker_command}"
|
cmd += f" {docker_command}"
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
@MethodHook
|
||||||
|
def _generate_ssm_cmd(self):
|
||||||
|
region = self.tags.get("region", "") if isinstance(self.tags, dict) else ""
|
||||||
|
profile = self.tags.get("profile", "") if isinstance(self.tags, dict) else ""
|
||||||
|
cmd = f"aws ssm start-session --target {self.host}"
|
||||||
|
if region:
|
||||||
|
cmd += f" --region {region}"
|
||||||
|
if profile:
|
||||||
|
cmd += f" --profile {profile}"
|
||||||
|
if self.options:
|
||||||
|
cmd += f" {self.options}"
|
||||||
|
return cmd
|
||||||
|
|
||||||
@MethodHook
|
@MethodHook
|
||||||
def _get_cmd(self):
|
def _get_cmd(self):
|
||||||
if self.protocol in ["ssh", "sftp"]:
|
if self.protocol in ["ssh", "sftp"]:
|
||||||
@@ -559,6 +575,8 @@ class node:
|
|||||||
return self._generate_kube_cmd()
|
return self._generate_kube_cmd()
|
||||||
elif self.protocol == "docker":
|
elif self.protocol == "docker":
|
||||||
return self._generate_docker_cmd()
|
return self._generate_docker_cmd()
|
||||||
|
elif self.protocol == "ssm":
|
||||||
|
return self._generate_ssm_cmd()
|
||||||
else:
|
else:
|
||||||
printer.error(f"Invalid protocol: {self.protocol}")
|
printer.error(f"Invalid protocol: {self.protocol}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -579,7 +597,8 @@ class node:
|
|||||||
"sftp": ['yes/no', 'refused', 'supported', 'Invalid|[u|U]sage: sftp', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
|
"sftp": ['yes/no', 'refused', 'supported', 'Invalid|[u|U]sage: sftp', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
|
||||||
"telnet": ['[u|U]sername:', 'refused', 'supported', 'invalid|unrecognized option', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
|
"telnet": ['[u|U]sername:', 'refused', 'supported', 'invalid|unrecognized option', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
|
||||||
"kubectl": ['[u|U]sername:', '[r|R]efused', '[E|e]rror', 'DEPRECATED', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF, "expired|invalid"],
|
"kubectl": ['[u|U]sername:', '[r|R]efused', '[E|e]rror', 'DEPRECATED', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF, "expired|invalid"],
|
||||||
"docker": ['[u|U]sername:', 'Cannot', '[E|e]rror', 'failed', 'not a docker command', 'unknown', 'unable to resolve', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF]
|
"docker": ['[u|U]sername:', 'Cannot', '[E|e]rror', 'failed', 'not a docker command', 'unknown', 'unable to resolve', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF],
|
||||||
|
"ssm": ['[u|U]sername:', 'Cannot', '[E|e]rror', 'failed', 'SessionManagerPlugin', 'unknown', 'unable to resolve', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF]
|
||||||
}
|
}
|
||||||
|
|
||||||
error_indices = {
|
error_indices = {
|
||||||
@@ -587,7 +606,8 @@ class node:
|
|||||||
"sftp": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
|
"sftp": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
|
||||||
"telnet": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
|
"telnet": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
|
||||||
"kubectl": [1, 2, 3, 4, 8], # Define error indices for kube
|
"kubectl": [1, 2, 3, 4, 8], # Define error indices for kube
|
||||||
"docker": [1, 2, 3, 4, 5, 6, 7] # Define error indices for docker
|
"docker": [1, 2, 3, 4, 5, 6, 7], # Define error indices for docker
|
||||||
|
"ssm": [1, 2, 3, 4, 5, 6, 7]
|
||||||
}
|
}
|
||||||
|
|
||||||
eof_indices = {
|
eof_indices = {
|
||||||
@@ -595,7 +615,8 @@ class node:
|
|||||||
"sftp": [8, 9, 10, 11],
|
"sftp": [8, 9, 10, 11],
|
||||||
"telnet": [8, 9, 10, 11],
|
"telnet": [8, 9, 10, 11],
|
||||||
"kubectl": [5, 6, 7], # Define eof indices for kube
|
"kubectl": [5, 6, 7], # Define eof indices for kube
|
||||||
"docker": [8, 9, 10] # Define eof indices for docker
|
"docker": [8, 9, 10], # Define eof indices for docker
|
||||||
|
"ssm": [8, 9, 10]
|
||||||
}
|
}
|
||||||
|
|
||||||
initial_indices = {
|
initial_indices = {
|
||||||
@@ -603,7 +624,8 @@ class node:
|
|||||||
"sftp": [0],
|
"sftp": [0],
|
||||||
"telnet": [0],
|
"telnet": [0],
|
||||||
"kubectl": [0], # Define special indices for kube
|
"kubectl": [0], # Define special indices for kube
|
||||||
"docker": [0] # Define special indices for docker
|
"docker": [0], # Define special indices for docker
|
||||||
|
"ssm": [0]
|
||||||
}
|
}
|
||||||
|
|
||||||
attempts = 1
|
attempts = 1
|
||||||
@@ -627,7 +649,7 @@ class node:
|
|||||||
if results in initial_indices[self.protocol]:
|
if results in initial_indices[self.protocol]:
|
||||||
if self.protocol in ["ssh", "sftp"]:
|
if self.protocol in ["ssh", "sftp"]:
|
||||||
child.sendline('yes')
|
child.sendline('yes')
|
||||||
elif self.protocol in ["telnet", "kubectl", "docker"]:
|
elif self.protocol in ["telnet", "kubectl", "docker", "ssm"]:
|
||||||
if self.user:
|
if self.user:
|
||||||
child.sendline(self.user)
|
child.sendline(self.user)
|
||||||
else:
|
else:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# gRPC generated files use absolute imports that assume their directory is in sys.path.
|
||||||
|
# We add this directory to sys.path to allow imports like 'import connpy_pb2' to succeed.
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if current_dir not in sys.path:
|
||||||
|
sys.path.insert(0, current_dir)
|
||||||
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
|||||||
import grpc
|
import grpc
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from . import connpy_pb2 as connpy__pb2
|
import connpy_pb2 as connpy__pb2
|
||||||
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
|
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
|
||||||
|
|
||||||
GRPC_GENERATED_VERSION = '1.80.0'
|
GRPC_GENERATED_VERSION = '1.80.0'
|
||||||
@@ -85,6 +85,11 @@ class NodeServiceStub(object):
|
|||||||
request_serializer=connpy__pb2.BulkRequest.SerializeToString,
|
request_serializer=connpy__pb2.BulkRequest.SerializeToString,
|
||||||
response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
|
response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
|
||||||
_registered_method=True)
|
_registered_method=True)
|
||||||
|
self.validate_parent_folder = channel.unary_unary(
|
||||||
|
'/connpy.NodeService/validate_parent_folder',
|
||||||
|
request_serializer=connpy__pb2.IdRequest.SerializeToString,
|
||||||
|
response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
|
||||||
|
_registered_method=True)
|
||||||
self.set_reserved_names = channel.unary_unary(
|
self.set_reserved_names = channel.unary_unary(
|
||||||
'/connpy.NodeService/set_reserved_names',
|
'/connpy.NodeService/set_reserved_names',
|
||||||
request_serializer=connpy__pb2.ListRequest.SerializeToString,
|
request_serializer=connpy__pb2.ListRequest.SerializeToString,
|
||||||
@@ -170,6 +175,12 @@ class NodeServiceServicer(object):
|
|||||||
context.set_details('Method not implemented!')
|
context.set_details('Method not implemented!')
|
||||||
raise NotImplementedError('Method not implemented!')
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def validate_parent_folder(self, request, context):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
def set_reserved_names(self, request, context):
|
def set_reserved_names(self, request, context):
|
||||||
"""Missing associated documentation comment in .proto file."""
|
"""Missing associated documentation comment in .proto file."""
|
||||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
@@ -247,6 +258,11 @@ def add_NodeServiceServicer_to_server(servicer, server):
|
|||||||
request_deserializer=connpy__pb2.BulkRequest.FromString,
|
request_deserializer=connpy__pb2.BulkRequest.FromString,
|
||||||
response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
|
response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
|
||||||
),
|
),
|
||||||
|
'validate_parent_folder': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.validate_parent_folder,
|
||||||
|
request_deserializer=connpy__pb2.IdRequest.FromString,
|
||||||
|
response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
|
||||||
|
),
|
||||||
'set_reserved_names': grpc.unary_unary_rpc_method_handler(
|
'set_reserved_names': grpc.unary_unary_rpc_method_handler(
|
||||||
servicer.set_reserved_names,
|
servicer.set_reserved_names,
|
||||||
request_deserializer=connpy__pb2.ListRequest.FromString,
|
request_deserializer=connpy__pb2.ListRequest.FromString,
|
||||||
@@ -548,6 +564,33 @@ class NodeService(object):
|
|||||||
metadata,
|
metadata,
|
||||||
_registered_method=True)
|
_registered_method=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_parent_folder(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/connpy.NodeService/validate_parent_folder',
|
||||||
|
connpy__pb2.IdRequest.SerializeToString,
|
||||||
|
google_dot_protobuf_dot_empty__pb2.Empty.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_reserved_names(request,
|
def set_reserved_names(request,
|
||||||
target,
|
target,
|
||||||
@@ -12,6 +12,7 @@ from . import connpy_pb2, connpy_pb2_grpc, remote_plugin_pb2, remote_plugin_pb2_
|
|||||||
import json
|
import json
|
||||||
from .utils import to_value, from_value, to_struct, from_struct
|
from .utils import to_value, from_value, to_struct, from_struct
|
||||||
from ..services.exceptions import ConnpyError
|
from ..services.exceptions import ConnpyError
|
||||||
|
from .. import printer
|
||||||
|
|
||||||
# Import local services
|
# Import local services
|
||||||
from ..services.node_service import NodeService
|
from ..services.node_service import NodeService
|
||||||
@@ -24,6 +25,22 @@ from ..services.execution_service import ExecutionService
|
|||||||
from ..services.import_export_service import ImportExportService
|
from ..services.import_export_service import ImportExportService
|
||||||
|
|
||||||
def handle_errors(func):
|
def handle_errors(func):
|
||||||
|
import inspect
|
||||||
|
if inspect.isgeneratorfunction(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
for item in func(*args, **kwargs):
|
||||||
|
yield item
|
||||||
|
except ConnpyError as e:
|
||||||
|
context = kwargs.get("context") or args[-1]
|
||||||
|
context.abort(grpc.StatusCode.INTERNAL, str(e))
|
||||||
|
except Exception as e:
|
||||||
|
context = kwargs.get("context") or args[-1]
|
||||||
|
context.abort(grpc.StatusCode.UNKNOWN, str(e))
|
||||||
|
finally:
|
||||||
|
printer.clear_thread_state()
|
||||||
|
return wrapper
|
||||||
|
else:
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
@@ -33,6 +50,8 @@ def handle_errors(func):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
context = kwargs.get("context") or args[-1]
|
context = kwargs.get("context") or args[-1]
|
||||||
context.abort(grpc.StatusCode.UNKNOWN, str(e))
|
context.abort(grpc.StatusCode.UNKNOWN, str(e))
|
||||||
|
finally:
|
||||||
|
printer.clear_thread_state()
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
||||||
@@ -56,18 +75,72 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
|||||||
unique_id = first_req.id
|
unique_id = first_req.id
|
||||||
sftp = first_req.sftp
|
sftp = first_req.sftp
|
||||||
debug = first_req.debug
|
debug = first_req.debug
|
||||||
|
printer.console.print(f"[debug][DEBUG][/debug] gRPC interact_node request for: [bold cyan]{unique_id}[/bold cyan]")
|
||||||
|
|
||||||
|
if first_req.connection_params_json:
|
||||||
|
import json
|
||||||
|
params = json.loads(first_req.connection_params_json)
|
||||||
|
base_node_id = params.get("base_node")
|
||||||
|
# Valid attributes that a node object accepts
|
||||||
|
valid_attrs = ['host', 'options', 'logs', 'password', 'port', 'protocol', 'user', 'jumphost']
|
||||||
|
|
||||||
|
fallback_id = f"{unique_id}@remote"
|
||||||
|
if unique_id == "dynamic" and params.get("host"):
|
||||||
|
fallback_id = f"dynamic-{params.get('host')}@remote"
|
||||||
|
|
||||||
|
if base_node_id:
|
||||||
|
# Look up the base node in config and use its full data
|
||||||
|
nodes = self.service.config._getallnodes(base_node_id)
|
||||||
|
if nodes:
|
||||||
|
device = self.service.config.getitem(nodes[0])
|
||||||
|
# Override device properties with any passed in params
|
||||||
|
for attr in valid_attrs:
|
||||||
|
if attr in params:
|
||||||
|
device[attr] = params[attr]
|
||||||
|
|
||||||
|
if "tags" in params:
|
||||||
|
device_tags = device.get("tags", {})
|
||||||
|
if not isinstance(device_tags, dict):
|
||||||
|
device_tags = {}
|
||||||
|
device_tags.update(params["tags"])
|
||||||
|
device["tags"] = device_tags
|
||||||
|
|
||||||
|
node_name = params.get("name", base_node_id)
|
||||||
|
n = node(node_name, **device, config=self.service.config)
|
||||||
|
else:
|
||||||
|
# base_node not found, fall back to dynamic
|
||||||
|
node_name = params.get("name", fallback_id)
|
||||||
|
n = node(node_name, host=params.get("host", ""), config=self.service.config)
|
||||||
|
for attr in valid_attrs:
|
||||||
|
if attr in params:
|
||||||
|
setattr(n, attr, params[attr])
|
||||||
|
if "tags" in params:
|
||||||
|
n.tags = params["tags"]
|
||||||
|
else:
|
||||||
|
node_name = params.get("name", fallback_id)
|
||||||
|
n = node(node_name, host=params.get("host", ""), config=self.service.config)
|
||||||
|
for attr in valid_attrs:
|
||||||
|
if attr in params:
|
||||||
|
setattr(n, attr, params[attr])
|
||||||
|
if "tags" in params:
|
||||||
|
n.tags = params["tags"]
|
||||||
|
else:
|
||||||
node_data = self.service.config.getitem(unique_id, extract=False)
|
node_data = self.service.config.getitem(unique_id, extract=False)
|
||||||
|
if not node_data:
|
||||||
|
context.abort(grpc.StatusCode.NOT_FOUND, f"Node {unique_id} not found")
|
||||||
profile_service = ProfileService(self.service.config)
|
profile_service = ProfileService(self.service.config)
|
||||||
resolved_data = profile_service.resolve_node_data(node_data)
|
resolved_data = profile_service.resolve_node_data(node_data)
|
||||||
|
|
||||||
n = node(unique_id, **resolved_data, config=self.service.config)
|
n = node(unique_id, **resolved_data, config=self.service.config)
|
||||||
if sftp:
|
if sftp:
|
||||||
n.protocol = "sftp"
|
n.protocol = "sftp"
|
||||||
|
|
||||||
connect = n._connect(debug=debug)
|
connect = n._connect(debug=debug)
|
||||||
if connect != True:
|
if connect != True:
|
||||||
context.abort(grpc.StatusCode.INTERNAL, "Failed to connect to node")
|
yield connpy_pb2.InteractResponse(success=False, error_message=str(connect))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Signal successful connection to the client
|
||||||
|
yield connpy_pb2.InteractResponse(success=True)
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
@@ -145,6 +218,11 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
|
|||||||
def explode_unique(self, request, context):
|
def explode_unique(self, request, context):
|
||||||
return connpy_pb2.ValueResponse(data=to_value(self.service.explode_unique(request.id)))
|
return connpy_pb2.ValueResponse(data=to_value(self.service.explode_unique(request.id)))
|
||||||
|
|
||||||
|
@handle_errors
|
||||||
|
def validate_parent_folder(self, request, context):
|
||||||
|
self.service.validate_parent_folder(request.id)
|
||||||
|
return Empty()
|
||||||
|
|
||||||
@handle_errors
|
@handle_errors
|
||||||
def generate_cache(self, request, context):
|
def generate_cache(self, request, context):
|
||||||
self.service.generate_cache()
|
self.service.generate_cache()
|
||||||
@@ -446,16 +524,18 @@ class ImportExportServicer(connpy_pb2_grpc.ImportExportServiceServicer):
|
|||||||
return Empty()
|
return Empty()
|
||||||
|
|
||||||
class StatusBridge:
|
class StatusBridge:
|
||||||
def __init__(self, q, request_queue=None):
|
def __init__(self, q, request_queue=None, is_web=False):
|
||||||
self.q = q
|
self.q = q
|
||||||
self.request_queue = request_queue
|
self.request_queue = request_queue
|
||||||
self.on_interrupt = self._force_interrupt
|
self.on_interrupt = self._force_interrupt
|
||||||
self.thread = None
|
self.thread = None
|
||||||
|
self.is_web = is_web
|
||||||
|
|
||||||
def _force_interrupt(self):
|
def _force_interrupt(self):
|
||||||
"""Forcefully raise KeyboardInterrupt in the target thread."""
|
"""Forcefully raise KeyboardInterrupt in the target thread."""
|
||||||
if self.thread and self.thread.ident:
|
if self.thread and self.thread.ident:
|
||||||
# Standard Python trick to raise an exception in a specific thread
|
# Standard Python trick to raise an exception in a specific thread
|
||||||
|
import ctypes
|
||||||
ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
||||||
ctypes.c_long(self.thread.ident),
|
ctypes.c_long(self.thread.ident),
|
||||||
ctypes.py_object(KeyboardInterrupt)
|
ctypes.py_object(KeyboardInterrupt)
|
||||||
@@ -477,13 +557,32 @@ class StatusBridge:
|
|||||||
|
|
||||||
def _print_to_queue(self, msg_type, *args, **kwargs):
|
def _print_to_queue(self, msg_type, *args, **kwargs):
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from ..printer import connpy_theme
|
from ..printer import connpy_theme
|
||||||
|
|
||||||
|
processed_args = list(args)
|
||||||
|
if self.is_web:
|
||||||
|
# Remove Panels to avoid box characters on web, but preserve Title
|
||||||
|
processed_args = []
|
||||||
|
for arg in args:
|
||||||
|
if isinstance(arg, Panel):
|
||||||
|
# If it has a title, prepend it to the content to allow detection
|
||||||
|
content = arg.renderable
|
||||||
|
if arg.title:
|
||||||
|
processed_args.append(f"{arg.title}\n")
|
||||||
|
processed_args.append(content)
|
||||||
|
else:
|
||||||
|
processed_args.append(arg)
|
||||||
|
|
||||||
buf = StringIO()
|
buf = StringIO()
|
||||||
# Use a high-quality console for rendering with the app's theme
|
# force_terminal=False removes ANSI escape codes for Web
|
||||||
c = Console(file=buf, force_terminal=True, width=100, theme=connpy_theme)
|
c = Console(file=buf, force_terminal=not self.is_web, width=100, theme=connpy_theme)
|
||||||
c.print(*args, **kwargs)
|
c.print(*processed_args, **kwargs)
|
||||||
self.q.put((msg_type, buf.getvalue()))
|
|
||||||
|
text_content = buf.getvalue().strip()
|
||||||
|
if text_content:
|
||||||
|
self.q.put((msg_type, text_content))
|
||||||
|
|
||||||
def confirm(self, prompt, default="n"):
|
def confirm(self, prompt, default="n"):
|
||||||
"""Bridge confirmation to the gRPC client."""
|
"""Bridge confirmation to the gRPC client."""
|
||||||
@@ -521,93 +620,107 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
|
|||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
# In bidirectional mode, the first request contains the query
|
|
||||||
try:
|
|
||||||
first_request = next(request_iterator)
|
|
||||||
except StopIteration:
|
|
||||||
return
|
|
||||||
|
|
||||||
history = from_value(first_request.chat_history)
|
|
||||||
|
|
||||||
overrides = {}
|
|
||||||
if first_request.engineer_model: overrides["engineer_model"] = first_request.engineer_model
|
|
||||||
if first_request.engineer_api_key: overrides["engineer_api_key"] = first_request.engineer_api_key
|
|
||||||
if first_request.architect_model: overrides["architect_model"] = first_request.architect_model
|
|
||||||
if first_request.architect_api_key: overrides["architect_api_key"] = first_request.architect_api_key
|
|
||||||
|
|
||||||
chunk_queue = queue.Queue()
|
chunk_queue = queue.Queue()
|
||||||
request_queue = queue.Queue()
|
request_queue = queue.Queue()
|
||||||
bridge = StatusBridge(chunk_queue, request_queue=request_queue)
|
bridge = None
|
||||||
|
history = []
|
||||||
|
is_web = False
|
||||||
|
|
||||||
# Start a thread to pull subsequent requests from the client (confirmations)
|
# Dedicated event to signal AI thread to stop
|
||||||
def pull_requests():
|
ai_thread = None
|
||||||
try:
|
agent_instance = None
|
||||||
for req in request_iterator:
|
|
||||||
if req.interrupt and bridge.on_interrupt:
|
|
||||||
bridge.on_interrupt()
|
|
||||||
request_queue.put(req)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
request_queue.put(None)
|
|
||||||
|
|
||||||
threading.Thread(target=pull_requests, daemon=True).start()
|
|
||||||
|
|
||||||
def callback(chunk):
|
def callback(chunk):
|
||||||
chunk_queue.put(("text", chunk))
|
chunk_queue.put(("text", chunk))
|
||||||
|
|
||||||
result_container = {}
|
def run_ai_task(input_text, session_id, debug, overrides, trust):
|
||||||
|
nonlocal history, bridge, agent_instance
|
||||||
def run_ai():
|
|
||||||
try:
|
try:
|
||||||
|
# Run the AI interaction (this blocks this specific thread)
|
||||||
res = self.service.ask(
|
res = self.service.ask(
|
||||||
first_request.input_text,
|
input_text,
|
||||||
dryrun=first_request.dryrun,
|
|
||||||
chat_history=history if history else None,
|
chat_history=history if history else None,
|
||||||
session_id=first_request.session_id if first_request.session_id else None,
|
session_id=session_id,
|
||||||
debug=first_request.debug,
|
debug=debug,
|
||||||
status=bridge,
|
status=bridge,
|
||||||
console=bridge,
|
console=bridge,
|
||||||
confirm_handler=bridge.confirm,
|
confirm_handler=bridge.confirm,
|
||||||
chunk_callback=callback,
|
chunk_callback=callback,
|
||||||
trust=first_request.trust,
|
trust=trust,
|
||||||
**overrides
|
**overrides
|
||||||
)
|
)
|
||||||
result_container["res"] = res
|
|
||||||
|
# Update history for next message
|
||||||
|
if "chat_history" in res:
|
||||||
|
history = res["chat_history"]
|
||||||
|
|
||||||
|
# Send final chunk marker
|
||||||
|
chunk_queue.put(("final_mark", res))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
chunk_queue.put(("status", f"[bold fail]Error: {str(e)}[/bold fail]"))
|
import traceback
|
||||||
result_container["error"] = e
|
print(f"AI Task Error: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
chunk_queue.put(("status", f"Error: {str(e)}"))
|
||||||
|
|
||||||
|
def request_listener():
|
||||||
|
nonlocal bridge, is_web, ai_thread, agent_instance
|
||||||
|
try:
|
||||||
|
for req in request_iterator:
|
||||||
|
if req.interrupt:
|
||||||
|
if bridge and bridge.on_interrupt:
|
||||||
|
bridge.on_interrupt()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if req.confirmation_answer:
|
||||||
|
request_queue.put(req)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if req.input_text:
|
||||||
|
is_web = "web" in (req.session_id or "").lower() or (req.session_id or "").lower().startswith("ws-")
|
||||||
|
if not bridge:
|
||||||
|
bridge = StatusBridge(chunk_queue, request_queue=request_queue, is_web=is_web)
|
||||||
|
|
||||||
|
overrides = {}
|
||||||
|
if req.engineer_model: overrides["engineer_model"] = req.engineer_model
|
||||||
|
if req.engineer_api_key: overrides["engineer_api_key"] = req.engineer_api_key
|
||||||
|
|
||||||
|
# Start AI in its own thread so we can keep listening for interrupts
|
||||||
|
ai_thread = threading.Thread(
|
||||||
|
target=run_ai_task,
|
||||||
|
args=(req.input_text, req.session_id, req.debug, overrides, req.trust),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
ai_thread.start()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Request Listener Error: {e}")
|
||||||
finally:
|
finally:
|
||||||
chunk_queue.put(None) # Sentinel
|
# When client closes stream, send sentinel
|
||||||
|
chunk_queue.put((None, None))
|
||||||
|
|
||||||
t = threading.Thread(target=run_ai, daemon=True)
|
# Start listening for client requests/signals
|
||||||
bridge.thread = t
|
threading.Thread(target=request_listener, daemon=True).start()
|
||||||
t.start()
|
|
||||||
|
|
||||||
|
# Main response loop (yields to gRPC)
|
||||||
while True:
|
while True:
|
||||||
item = chunk_queue.get()
|
item = chunk_queue.get()
|
||||||
if item is None:
|
if item == (None, None):
|
||||||
break
|
break
|
||||||
|
|
||||||
msg_type, val = item
|
msg_type, val = item
|
||||||
if msg_type == "text":
|
if msg_type == "text":
|
||||||
yield connpy_pb2.AIResponse(text_chunk=val, is_final=False)
|
yield connpy_pb2.AIResponse(text_chunk=val, is_final=False)
|
||||||
elif msg_type == "status":
|
elif msg_type == "status":
|
||||||
yield connpy_pb2.AIResponse(status_update=val, is_final=False)
|
if is_web and "is thinking" in val.lower(): continue
|
||||||
|
clean_val = val.replace("[ai_status]", "").replace("[/ai_status]", "")
|
||||||
|
yield connpy_pb2.AIResponse(status_update=clean_val, is_final=False)
|
||||||
elif msg_type == "debug":
|
elif msg_type == "debug":
|
||||||
yield connpy_pb2.AIResponse(debug_message=val, is_final=False)
|
yield connpy_pb2.AIResponse(debug_message=val, is_final=False)
|
||||||
elif msg_type == "important":
|
elif msg_type == "important":
|
||||||
yield connpy_pb2.AIResponse(important_message=val, is_final=False)
|
yield connpy_pb2.AIResponse(important_message=val, is_final=False)
|
||||||
elif msg_type == "confirm":
|
elif msg_type == "confirm":
|
||||||
yield connpy_pb2.AIResponse(status_update=val, requires_confirmation=True, is_final=False)
|
yield connpy_pb2.AIResponse(status_update=val, requires_confirmation=True, is_final=False)
|
||||||
|
elif msg_type == "final_mark":
|
||||||
if "error" in result_container:
|
yield connpy_pb2.AIResponse(is_final=True, full_result=to_struct(val))
|
||||||
raise result_container["error"]
|
|
||||||
|
|
||||||
yield connpy_pb2.AIResponse(
|
|
||||||
is_final=True,
|
|
||||||
full_result=to_struct(result_container.get("res", {}))
|
|
||||||
)
|
|
||||||
|
|
||||||
@handle_errors
|
@handle_errors
|
||||||
def confirm(self, request, context):
|
def confirm(self, request, context):
|
||||||
@@ -663,8 +776,8 @@ class SystemServicer(connpy_pb2_grpc.SystemServiceServicer):
|
|||||||
class LoggingInterceptor(grpc.ServerInterceptor):
|
class LoggingInterceptor(grpc.ServerInterceptor):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from ..printer import connpy_theme
|
from ..printer import connpy_theme, get_original_stdout
|
||||||
self.console = Console(theme=connpy_theme)
|
self.console = Console(theme=connpy_theme, file=get_original_stdout())
|
||||||
|
|
||||||
def intercept_service(self, continuation, handler_call_details):
|
def intercept_service(self, continuation, handler_call_details):
|
||||||
import time
|
import time
|
||||||
@@ -73,11 +73,112 @@ class NodeStub:
|
|||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Fetch node details for the connection message
|
||||||
|
try:
|
||||||
|
node_details = self.get_node_details(unique_id)
|
||||||
|
host = node_details.get("host", "unknown")
|
||||||
|
port = str(node_details.get("port", ""))
|
||||||
|
protocol = "sftp" if sftp else node_details.get("protocol", "ssh")
|
||||||
|
port_str = f":{port}" if port and protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
conn_msg = f"Connected to {unique_id} at {host}{port_str} via: {protocol}"
|
||||||
|
except Exception:
|
||||||
|
conn_msg = f"Connected to {unique_id}"
|
||||||
|
|
||||||
old_tty = termios.tcgetattr(sys.stdin)
|
old_tty = termios.tcgetattr(sys.stdin)
|
||||||
try:
|
try:
|
||||||
tty.setraw(sys.stdin.fileno())
|
tty.setraw(sys.stdin.fileno())
|
||||||
response_iterator = self.stub.interact_node(request_generator())
|
response_iterator = self.stub.interact_node(request_generator())
|
||||||
|
|
||||||
|
# First response is connection status
|
||||||
|
try:
|
||||||
|
first_res = next(response_iterator)
|
||||||
|
if first_res.success:
|
||||||
|
# Connection established on server, show success message
|
||||||
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
||||||
|
printer.success(conn_msg)
|
||||||
|
tty.setraw(sys.stdin.fileno())
|
||||||
|
else:
|
||||||
|
# Connection failed on server
|
||||||
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
||||||
|
printer.error(f"Connection failed: {first_res.error_message}")
|
||||||
|
return
|
||||||
|
except StopIteration:
|
||||||
|
return
|
||||||
|
|
||||||
|
for res in response_iterator:
|
||||||
|
if res.stdout_data:
|
||||||
|
os.write(sys.stdout.fileno(), res.stdout_data)
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
||||||
|
|
||||||
|
@handle_errors
|
||||||
|
def connect_dynamic(self, connection_params, debug=False):
|
||||||
|
import sys
|
||||||
|
import select
|
||||||
|
import tty
|
||||||
|
import termios
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
params_json = json.dumps(connection_params)
|
||||||
|
|
||||||
|
def request_generator():
|
||||||
|
cols, rows = 80, 24
|
||||||
|
try:
|
||||||
|
size = os.get_terminal_size()
|
||||||
|
cols, rows = size.columns, size.lines
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
yield connpy_pb2.InteractRequest(
|
||||||
|
id="dynamic", debug=debug, cols=cols, rows=rows,
|
||||||
|
connection_params_json=params_json
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
r, _, _ = select.select([sys.stdin.fileno()], [], [])
|
||||||
|
if r:
|
||||||
|
try:
|
||||||
|
data = os.read(sys.stdin.fileno(), 1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
yield connpy_pb2.InteractRequest(stdin_data=data)
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Prepare connection message
|
||||||
|
try:
|
||||||
|
node_name = connection_params.get("name", "dynamic@remote")
|
||||||
|
host = connection_params.get("host", "dynamic")
|
||||||
|
port = str(connection_params.get("port", ""))
|
||||||
|
protocol = connection_params.get("protocol", "ssh")
|
||||||
|
port_str = f":{port}" if port and protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
conn_msg = f"Connected to {node_name} at {host}{port_str} via: {protocol}"
|
||||||
|
except Exception:
|
||||||
|
node_name = connection_params.get("name", "dynamic@remote") if isinstance(connection_params, dict) else "dynamic@remote"
|
||||||
|
conn_msg = f"Connected to {node_name}"
|
||||||
|
|
||||||
|
old_tty = termios.tcgetattr(sys.stdin)
|
||||||
|
try:
|
||||||
|
tty.setraw(sys.stdin.fileno())
|
||||||
|
response_iterator = self.stub.interact_node(request_generator())
|
||||||
|
|
||||||
|
# First response is connection status
|
||||||
|
try:
|
||||||
|
first_res = next(response_iterator)
|
||||||
|
if first_res.success:
|
||||||
|
# Connection established on server, show success message
|
||||||
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
||||||
|
printer.success(conn_msg)
|
||||||
|
tty.setraw(sys.stdin.fileno())
|
||||||
|
else:
|
||||||
|
# Connection failed on server
|
||||||
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
||||||
|
printer.error(f"Connection failed: {first_res.error_message}")
|
||||||
|
return
|
||||||
|
except StopIteration:
|
||||||
|
return
|
||||||
|
|
||||||
for res in response_iterator:
|
for res in response_iterator:
|
||||||
if res.stdout_data:
|
if res.stdout_data:
|
||||||
os.write(sys.stdout.fileno(), res.stdout_data)
|
os.write(sys.stdout.fileno(), res.stdout_data)
|
||||||
@@ -104,6 +205,10 @@ class NodeStub:
|
|||||||
def explode_unique(self, unique_id):
|
def explode_unique(self, unique_id):
|
||||||
return from_value(self.stub.explode_unique(connpy_pb2.IdRequest(id=unique_id)).data)
|
return from_value(self.stub.explode_unique(connpy_pb2.IdRequest(id=unique_id)).data)
|
||||||
|
|
||||||
|
@handle_errors
|
||||||
|
def validate_parent_folder(self, unique_id):
|
||||||
|
self.stub.validate_parent_folder(connpy_pb2.IdRequest(id=unique_id))
|
||||||
|
|
||||||
@handle_errors
|
@handle_errors
|
||||||
def generate_cache(self, nodes=None, folders=None, profiles=None):
|
def generate_cache(self, nodes=None, folders=None, profiles=None):
|
||||||
# 1. Update remote cache on server
|
# 1. Update remote cache on server
|
||||||
@@ -226,6 +331,30 @@ class ProfileStub:
|
|||||||
if self.node_stub:
|
if self.node_stub:
|
||||||
self.node_stub._trigger_local_cache_sync()
|
self.node_stub._trigger_local_cache_sync()
|
||||||
|
|
||||||
|
class ConfigStub:
|
||||||
|
def __init__(self, channel, remote_host):
|
||||||
|
self.stub = connpy_pb2_grpc.ConfigServiceStub(channel)
|
||||||
|
self.remote_host = remote_host
|
||||||
|
|
||||||
|
@handle_errors
|
||||||
|
def get_settings(self):
|
||||||
|
return from_struct(self.stub.get_settings(Empty()).data)
|
||||||
|
|
||||||
|
@handle_errors
|
||||||
|
def update_setting(self, key, value):
|
||||||
|
self.stub.update_setting(connpy_pb2.UpdateRequest(key=key, value=to_value(value)))
|
||||||
|
|
||||||
|
@handle_errors
|
||||||
|
def get_default_dir(self):
|
||||||
|
return self.stub.get_default_dir(Empty()).value
|
||||||
|
|
||||||
|
@handle_errors
|
||||||
|
def set_config_folder(self, folder):
|
||||||
|
self.stub.set_config_folder(connpy_pb2.StringRequest(value=folder))
|
||||||
|
|
||||||
|
@handle_errors
|
||||||
|
def encrypt_password(self, password):
|
||||||
|
return self.stub.encrypt_password(connpy_pb2.StringRequest(value=password)).value
|
||||||
|
|
||||||
class PluginStub:
|
class PluginStub:
|
||||||
def __init__(self, channel, remote_host):
|
def __init__(self, channel, remote_host):
|
||||||
+146
-30
@@ -1,7 +1,71 @@
|
|||||||
# Lazy-loaded printer module to speed up CLI startup
|
import sys
|
||||||
_console = None
|
import threading
|
||||||
_err_console = None
|
import io
|
||||||
_theme = None
|
|
||||||
|
_local = threading.local()
|
||||||
|
|
||||||
|
class ThreadLocalStream:
|
||||||
|
def __init__(self, original):
|
||||||
|
self._original = original
|
||||||
|
|
||||||
|
def _get_stream(self):
|
||||||
|
s = getattr(_local, 'stream', None)
|
||||||
|
return s if s is not None else self._original
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
stream = self._get_stream()
|
||||||
|
if stream:
|
||||||
|
stream.write(data)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
stream = self._get_stream()
|
||||||
|
if stream:
|
||||||
|
stream.flush()
|
||||||
|
|
||||||
|
def isatty(self):
|
||||||
|
stream = self._get_stream()
|
||||||
|
return stream.isatty() if stream else False
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
# Avoid recursion during initialization or if _original is not yet set
|
||||||
|
if name in ('_original', '_get_stream'):
|
||||||
|
raise AttributeError(name)
|
||||||
|
stream = self._get_stream()
|
||||||
|
if stream:
|
||||||
|
return getattr(stream, name)
|
||||||
|
raise AttributeError(f"'NoneType' object has no attribute '{name}'")
|
||||||
|
|
||||||
|
# Patch stdout/stderr only once at module level
|
||||||
|
if not isinstance(sys.stdout, ThreadLocalStream):
|
||||||
|
sys.stdout = ThreadLocalStream(sys.stdout)
|
||||||
|
if not isinstance(sys.stderr, ThreadLocalStream):
|
||||||
|
sys.stderr = ThreadLocalStream(sys.stderr)
|
||||||
|
|
||||||
|
def _get_local():
|
||||||
|
if not hasattr(_local, 'console'):
|
||||||
|
_local.console = None
|
||||||
|
if not hasattr(_local, 'err_console'):
|
||||||
|
_local.err_console = None
|
||||||
|
if not hasattr(_local, 'theme'):
|
||||||
|
_local.theme = None
|
||||||
|
return _local
|
||||||
|
|
||||||
|
def set_thread_stream(stream):
|
||||||
|
if stream is None:
|
||||||
|
if hasattr(_local, 'stream'):
|
||||||
|
del _local.stream
|
||||||
|
else:
|
||||||
|
_local.stream = stream
|
||||||
|
|
||||||
|
def get_original_stdout():
|
||||||
|
if isinstance(sys.stdout, ThreadLocalStream):
|
||||||
|
return sys.stdout._original
|
||||||
|
return sys.stdout
|
||||||
|
|
||||||
|
def get_original_stderr():
|
||||||
|
if isinstance(sys.stderr, ThreadLocalStream):
|
||||||
|
return sys.stderr._original
|
||||||
|
return sys.stderr
|
||||||
|
|
||||||
# Centralized design system
|
# Centralized design system
|
||||||
STYLES = {
|
STYLES = {
|
||||||
@@ -23,24 +87,76 @@ STYLES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _get_console():
|
def _get_console():
|
||||||
global _console, _theme
|
local = _get_local()
|
||||||
if _console is None:
|
|
||||||
|
# Self-healing patch: if sys.stdout was replaced (e.g. by pytest), re-wrap it.
|
||||||
|
if not isinstance(sys.stdout, ThreadLocalStream):
|
||||||
|
sys.stdout = ThreadLocalStream(sys.stdout)
|
||||||
|
|
||||||
|
current_out = sys.stdout
|
||||||
|
|
||||||
|
# Detect if we need to recreate the console (stream changed or closed)
|
||||||
|
needs_recreate = (local.console is None or
|
||||||
|
getattr(local, '_last_stdout', None) is not current_out)
|
||||||
|
|
||||||
|
# Extra check for closed files in test environments
|
||||||
|
if not needs_recreate and local.console is not None:
|
||||||
|
try:
|
||||||
|
if hasattr(local.console.file, 'closed') and local.console.file.closed:
|
||||||
|
needs_recreate = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if needs_recreate:
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.theme import Theme
|
from rich.theme import Theme
|
||||||
if _theme is None:
|
if local.theme is None:
|
||||||
_theme = Theme(STYLES)
|
local.theme = Theme(STYLES)
|
||||||
_console = Console(theme=_theme)
|
local.console = Console(theme=local.theme, file=current_out)
|
||||||
return _console
|
local._last_stdout = current_out
|
||||||
|
|
||||||
|
return local.console
|
||||||
|
|
||||||
def _get_err_console():
|
def _get_err_console():
|
||||||
global _err_console, _theme
|
local = _get_local()
|
||||||
if _err_console is None:
|
|
||||||
|
# Self-healing patch for stderr
|
||||||
|
if not isinstance(sys.stderr, ThreadLocalStream):
|
||||||
|
sys.stderr = ThreadLocalStream(sys.stderr)
|
||||||
|
|
||||||
|
current_err = sys.stderr
|
||||||
|
|
||||||
|
needs_recreate = (local.err_console is None or
|
||||||
|
getattr(local, '_last_stderr', None) is not current_err)
|
||||||
|
|
||||||
|
if not needs_recreate and local.err_console is not None:
|
||||||
|
try:
|
||||||
|
if hasattr(local.err_console.file, 'closed') and local.err_console.file.closed:
|
||||||
|
needs_recreate = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if needs_recreate:
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.theme import Theme
|
from rich.theme import Theme
|
||||||
if _theme is None:
|
if local.theme is None:
|
||||||
_theme = Theme(STYLES)
|
local.theme = Theme(STYLES)
|
||||||
_err_console = Console(stderr=True, theme=_theme)
|
local.err_console = Console(stderr=True, theme=local.theme, file=current_err)
|
||||||
return _err_console
|
local._last_stderr = current_err
|
||||||
|
|
||||||
|
return local.err_console
|
||||||
|
|
||||||
|
def set_thread_console(console):
|
||||||
|
_get_local().console = console
|
||||||
|
|
||||||
|
def set_thread_err_console(console):
|
||||||
|
_get_local().err_console = console
|
||||||
|
|
||||||
|
def clear_thread_state():
|
||||||
|
"""Removes all thread-local printer state. Useful for gRPC thread reuse."""
|
||||||
|
for attr in ["stream", "console", "err_console", "theme", "_last_stdout", "_last_stderr"]:
|
||||||
|
if hasattr(_local, attr):
|
||||||
|
delattr(_local, attr)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def console():
|
def console():
|
||||||
@@ -52,18 +168,18 @@ def err_console():
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def connpy_theme():
|
def connpy_theme():
|
||||||
global _theme
|
local = _get_local()
|
||||||
if _theme is None:
|
if local.theme is None:
|
||||||
from rich.theme import Theme
|
from rich.theme import Theme
|
||||||
_theme = Theme(STYLES)
|
local.theme = Theme(STYLES)
|
||||||
return _theme
|
return local.theme
|
||||||
|
|
||||||
def apply_theme(user_styles=None):
|
def apply_theme(user_styles=None):
|
||||||
"""
|
"""
|
||||||
Updates the global console themes with user-defined styles.
|
Updates the global console themes with user-defined styles.
|
||||||
If a style is missing in user_styles, it falls back to the default in STYLES.
|
If a style is missing in user_styles, it falls back to the default in STYLES.
|
||||||
"""
|
"""
|
||||||
global _theme, _console, _err_console
|
local = _get_local()
|
||||||
from rich.theme import Theme
|
from rich.theme import Theme
|
||||||
|
|
||||||
# Start with a copy of defaults
|
# Start with a copy of defaults
|
||||||
@@ -74,11 +190,11 @@ def apply_theme(user_styles=None):
|
|||||||
if key in active_styles:
|
if key in active_styles:
|
||||||
active_styles[key] = value
|
active_styles[key] = value
|
||||||
|
|
||||||
_theme = Theme(active_styles)
|
local.theme = Theme(active_styles)
|
||||||
if _console:
|
if local.console:
|
||||||
_console.push_theme(_theme)
|
local.console.push_theme(local.theme)
|
||||||
if _err_console:
|
if local.err_console:
|
||||||
_err_console.push_theme(_theme)
|
local.err_console.push_theme(local.theme)
|
||||||
return active_styles
|
return active_styles
|
||||||
|
|
||||||
|
|
||||||
@@ -273,10 +389,10 @@ err_console = _ErrConsoleProxy()
|
|||||||
# theme also needs to be lazy
|
# theme also needs to be lazy
|
||||||
class _ThemeProxy:
|
class _ThemeProxy:
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
global _theme
|
local = _get_local()
|
||||||
if _theme is None:
|
if local.theme is None:
|
||||||
from rich.theme import Theme
|
from rich.theme import Theme
|
||||||
_theme = Theme(STYLES)
|
local.theme = Theme(STYLES)
|
||||||
return getattr(_theme, name)
|
return getattr(local.theme, name)
|
||||||
|
|
||||||
connpy_theme = _ThemeProxy()
|
connpy_theme = _ThemeProxy()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ service NodeService {
|
|||||||
rpc delete_node (DeleteRequest) returns (google.protobuf.Empty) {}
|
rpc delete_node (DeleteRequest) returns (google.protobuf.Empty) {}
|
||||||
rpc move_node (MoveRequest) returns (google.protobuf.Empty) {}
|
rpc move_node (MoveRequest) returns (google.protobuf.Empty) {}
|
||||||
rpc bulk_add (BulkRequest) returns (google.protobuf.Empty) {}
|
rpc bulk_add (BulkRequest) returns (google.protobuf.Empty) {}
|
||||||
|
rpc validate_parent_folder (IdRequest) returns (google.protobuf.Empty) {}
|
||||||
rpc set_reserved_names (ListRequest) returns (google.protobuf.Empty) {}
|
rpc set_reserved_names (ListRequest) returns (google.protobuf.Empty) {}
|
||||||
rpc interact_node (stream InteractRequest) returns (stream InteractResponse) {}
|
rpc interact_node (stream InteractRequest) returns (stream InteractResponse) {}
|
||||||
rpc full_replace (FullReplaceRequest) returns (google.protobuf.Empty) {}
|
rpc full_replace (FullReplaceRequest) returns (google.protobuf.Empty) {}
|
||||||
@@ -87,10 +88,13 @@ message InteractRequest {
|
|||||||
bytes stdin_data = 4;
|
bytes stdin_data = 4;
|
||||||
int32 cols = 5;
|
int32 cols = 5;
|
||||||
int32 rows = 6;
|
int32 rows = 6;
|
||||||
|
string connection_params_json = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message InteractResponse {
|
message InteractResponse {
|
||||||
bytes stdout_data = 1;
|
bytes stdout_data = 1;
|
||||||
|
bool success = 2;
|
||||||
|
string error_message = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message FilterRequest {
|
message FilterRequest {
|
||||||
|
|||||||
@@ -73,10 +73,13 @@ class NodeService(BaseService):
|
|||||||
|
|
||||||
def get_node_details(self, unique_id):
|
def get_node_details(self, unique_id):
|
||||||
"""Return full configuration dictionary for a specific node."""
|
"""Return full configuration dictionary for a specific node."""
|
||||||
|
try:
|
||||||
details = self.config.getitem(unique_id)
|
details = self.config.getitem(unique_id)
|
||||||
if not details:
|
if not details:
|
||||||
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
||||||
return details
|
return details
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
||||||
|
|
||||||
def explode_unique(self, unique_id):
|
def explode_unique(self, unique_id):
|
||||||
"""Explode a unique ID into a dictionary of its parts."""
|
"""Explode a unique ID into a dictionary of its parts."""
|
||||||
@@ -86,6 +89,14 @@ class NodeService(BaseService):
|
|||||||
"""Generate and update the internal nodes cache."""
|
"""Generate and update the internal nodes cache."""
|
||||||
self.config._generate_nodes_cache(nodes=nodes, folders=folders, profiles=profiles)
|
self.config._generate_nodes_cache(nodes=nodes, folders=folders, profiles=profiles)
|
||||||
|
|
||||||
|
def validate_parent_folder(self, unique_id):
|
||||||
|
"""Check if parent folder exists for a given node unique ID."""
|
||||||
|
node_folder = unique_id.partition("@")[2]
|
||||||
|
if node_folder:
|
||||||
|
parent_folder = f"@{node_folder}"
|
||||||
|
if parent_folder not in self.config._getallfolders():
|
||||||
|
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
||||||
|
|
||||||
|
|
||||||
def add_node(self, unique_id, data, is_folder=False):
|
def add_node(self, unique_id, data, is_folder=False):
|
||||||
"""Logic for adding a new node or folder to configuration."""
|
"""Logic for adding a new node or folder to configuration."""
|
||||||
@@ -104,9 +115,7 @@ class NodeService(BaseService):
|
|||||||
|
|
||||||
# Check if parent folder exists when creating a subfolder
|
# Check if parent folder exists when creating a subfolder
|
||||||
if "subfolder" in uniques:
|
if "subfolder" in uniques:
|
||||||
parent_folder = f"@{uniques['folder']}"
|
self.validate_parent_folder(unique_id)
|
||||||
if parent_folder not in all_folders:
|
|
||||||
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
|
||||||
|
|
||||||
self.config._folder_add(**uniques)
|
self.config._folder_add(**uniques)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
@@ -115,11 +124,7 @@ class NodeService(BaseService):
|
|||||||
raise NodeAlreadyExistsError(f"Node '{unique_id}' already exists.")
|
raise NodeAlreadyExistsError(f"Node '{unique_id}' already exists.")
|
||||||
|
|
||||||
# Check if parent folder exists when creating a node in a folder
|
# Check if parent folder exists when creating a node in a folder
|
||||||
node_folder = unique_id.partition("@")[2]
|
self.validate_parent_folder(unique_id)
|
||||||
if node_folder:
|
|
||||||
parent_folder = f"@{node_folder}"
|
|
||||||
if parent_folder not in all_folders:
|
|
||||||
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
|
||||||
|
|
||||||
# Ensure 'id' is in data for config._connections_add
|
# Ensure 'id' is in data for config._connections_add
|
||||||
if "id" not in data:
|
if "id" not in data:
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ class PluginService(BaseService):
|
|||||||
from ..services.exceptions import InvalidConfigurationError
|
from ..services.exceptions import InvalidConfigurationError
|
||||||
from connpy.plugins import Plugins
|
from connpy.plugins import Plugins
|
||||||
class MockApp:
|
class MockApp:
|
||||||
|
is_mock = True
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
from ..core import node, nodes
|
from ..core import node, nodes
|
||||||
from ..ai import ai
|
from ..ai import ai
|
||||||
@@ -191,14 +192,20 @@ class PluginService(BaseService):
|
|||||||
self.ai = ai
|
self.ai = ai
|
||||||
|
|
||||||
self.services = ServiceProvider(config, mode="local")
|
self.services = ServiceProvider(config, mode="local")
|
||||||
|
|
||||||
|
# Get settings for CLI behavior
|
||||||
|
settings = self.services.config_svc.get_settings()
|
||||||
|
self.case = settings.get("case", False)
|
||||||
|
self.fzf = settings.get("fzf", False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.nodes_list = self.services.nodes.list_nodes()
|
self.nodes_list = self.services.nodes.list_nodes()
|
||||||
self.folders = self.services.nodes.list_folders()
|
self.folders = self.services.nodes.list_folders()
|
||||||
self.profiles = self.services.profiles.list_profiles()
|
self.profiles = self.services.profiles.list_profiles()
|
||||||
except Exception:
|
except Exception:
|
||||||
self.nodes_list = {}
|
self.nodes_list = []
|
||||||
self.folders = {}
|
self.folders = []
|
||||||
self.profiles = {}
|
self.profiles = []
|
||||||
|
|
||||||
args = Namespace(**args_dict)
|
args = Namespace(**args_dict)
|
||||||
|
|
||||||
@@ -225,26 +232,26 @@ class PluginService(BaseService):
|
|||||||
from .. import printer
|
from .. import printer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
old_console = printer.console
|
old_console = printer._get_console()
|
||||||
old_err_console = printer.err_console
|
old_err_console = printer._get_err_console()
|
||||||
|
|
||||||
printer.console = Console(file=buf, theme=printer.connpy_theme, force_terminal=True)
|
printer.set_thread_console(Console(file=buf, theme=printer.connpy_theme, force_terminal=True))
|
||||||
printer.err_console = Console(file=buf, theme=printer.connpy_theme, force_terminal=True)
|
printer.set_thread_err_console(Console(file=buf, theme=printer.connpy_theme, force_terminal=True))
|
||||||
|
printer.set_thread_stream(buf)
|
||||||
old_stdout = sys.stdout
|
|
||||||
sys.stdout = buf
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(module, "Entrypoint"):
|
if hasattr(module, "Entrypoint"):
|
||||||
module.Entrypoint(args, parser, app)
|
module.Entrypoint(args, parser, app)
|
||||||
except Exception as e:
|
except BaseException as e:
|
||||||
|
if not isinstance(e, SystemExit):
|
||||||
import traceback
|
import traceback
|
||||||
printer.err_console.print(traceback.format_exc())
|
printer.err_console.print(traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
sys.stdout = old_stdout
|
printer.set_thread_console(old_console)
|
||||||
printer.console = old_console
|
printer.set_thread_err_console(old_err_console)
|
||||||
printer.err_console = old_err_console
|
printer.set_thread_stream(None)
|
||||||
|
|
||||||
for line in buf.getvalue().splitlines(keepends=True):
|
for line in buf.getvalue().splitlines(keepends=True):
|
||||||
yield line
|
yield line
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class ServiceProvider:
|
|||||||
raise InvalidConfigurationError("Remote host must be specified in remote mode")
|
raise InvalidConfigurationError("Remote host must be specified in remote mode")
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
from ..grpc.stubs import NodeStub, ProfileStub, PluginStub, AIStub, ExecutionStub, ImportExportStub, SystemStub
|
from ..grpc_layer.stubs import NodeStub, ProfileStub, PluginStub, AIStub, ExecutionStub, ImportExportStub, SystemStub
|
||||||
|
|
||||||
channel = grpc.insecure_channel(self.remote_host)
|
channel = grpc.insecure_channel(self.remote_host)
|
||||||
|
|
||||||
|
|||||||
@@ -157,9 +157,9 @@ class SyncService(BaseService):
|
|||||||
if os.path.exists(self.config.key):
|
if os.path.exists(self.config.key):
|
||||||
zipf.write(self.config.key, ".osk")
|
zipf.write(self.config.key, ".osk")
|
||||||
|
|
||||||
# Manage retention (max 10 backups)
|
# Manage retention (max 100 backups)
|
||||||
backups = self.list_backups()
|
backups = self.list_backups()
|
||||||
if len(backups) >= 10:
|
if len(backups) >= 100:
|
||||||
oldest = min(backups, key=lambda x: x['timestamp'] or '0')
|
oldest = min(backups, key=lambda x: x['timestamp'] or '0')
|
||||||
self.delete_backup(oldest['id'])
|
self.delete_backup(oldest['id'])
|
||||||
|
|
||||||
@@ -360,7 +360,7 @@ class SyncService(BaseService):
|
|||||||
|
|
||||||
if not sync_enabled: return
|
if not sync_enabled: return
|
||||||
|
|
||||||
printer.info("Triggering auto-sync...")
|
|
||||||
if self.check_login_status() != True:
|
if self.check_login_status() != True:
|
||||||
printer.warning("Auto-sync: Not logged in to Google Drive.")
|
printer.warning("Auto-sync: Not logged in to Google Drive.")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -269,16 +269,16 @@ class TestToolMethods:
|
|||||||
|
|
||||||
def test_list_nodes_tool_found(self, myai):
|
def test_list_nodes_tool_found(self, myai):
|
||||||
result = myai.list_nodes_tool("router.*")
|
result = myai.list_nodes_tool("router.*")
|
||||||
parsed = json.loads(result)
|
parsed = json.loads(result) if isinstance(result, str) else result
|
||||||
assert "router1" in str(parsed)
|
assert "router1" in str(parsed)
|
||||||
|
|
||||||
def test_list_nodes_tool_not_found(self, myai):
|
def test_list_nodes_tool_not_found(self, myai):
|
||||||
result = myai.list_nodes_tool("nonexistent_pattern_xyz")
|
result = myai.list_nodes_tool("nonexistent_pattern_xyz")
|
||||||
assert "No nodes found" in result
|
assert "No nodes found" in str(result)
|
||||||
|
|
||||||
def test_get_node_info_masks_password(self, myai):
|
def test_get_node_info_masks_password(self, myai):
|
||||||
result = myai.get_node_info_tool("router1")
|
result = myai.get_node_info_tool("router1")
|
||||||
parsed = json.loads(result)
|
parsed = json.loads(result) if isinstance(result, str) else result
|
||||||
assert parsed["password"] == "***"
|
assert parsed["password"] == "***"
|
||||||
|
|
||||||
def test_is_safe_command_show(self, myai):
|
def test_is_safe_command_show(self, myai):
|
||||||
|
|||||||
@@ -99,6 +99,23 @@ class TestCommandGeneration:
|
|||||||
assert "telnet 10.0.0.1" in cmd
|
assert "telnet 10.0.0.1" in cmd
|
||||||
assert "23" in cmd
|
assert "23" in cmd
|
||||||
|
|
||||||
|
def test_ssm_cmd_basic(self):
|
||||||
|
n = self._make_node(protocol="ssm", host="i-12345")
|
||||||
|
cmd = n._get_cmd()
|
||||||
|
assert "aws ssm start-session" in cmd
|
||||||
|
assert "--target i-12345" in cmd
|
||||||
|
|
||||||
|
def test_ssm_cmd_tags(self):
|
||||||
|
n = self._make_node(protocol="ssm", host="i-12345", tags={"region": "us-west-2", "profile": "prod"})
|
||||||
|
cmd = n._get_cmd()
|
||||||
|
assert "--region us-west-2" in cmd
|
||||||
|
assert "--profile prod" in cmd
|
||||||
|
|
||||||
|
def test_ssm_cmd_options(self):
|
||||||
|
n = self._make_node(protocol="ssm", host="i-12345", options="--document-name AWS-StartInteractiveCommand")
|
||||||
|
cmd = n._get_cmd()
|
||||||
|
assert "--document-name AWS-StartInteractiveCommand" in cmd
|
||||||
|
|
||||||
def test_kubectl_cmd(self):
|
def test_kubectl_cmd(self):
|
||||||
n = self._make_node(protocol="kubectl", host="my-pod", tags={"kube_command": "/bin/sh"})
|
n = self._make_node(protocol="kubectl", host="my-pod", tags={"kube_command": "/bin/sh"})
|
||||||
cmd = n._get_cmd()
|
cmd = n._get_cmd()
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import pytest
|
||||||
|
import grpc
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from concurrent import futures
|
||||||
|
from connpy.grpc_layer import server, connpy_pb2, connpy_pb2_grpc, stubs
|
||||||
|
from connpy.services.exceptions import ConnpyError
|
||||||
|
|
||||||
|
class MockContext:
|
||||||
|
def abort(self, code, details):
|
||||||
|
raise Exception(f"gRPC Abort: {code} - {details}")
|
||||||
|
|
||||||
|
# --- UNIT TESTS (with mocks) ---
|
||||||
|
|
||||||
|
class TestNodeServicerNaming:
|
||||||
|
@pytest.fixture
|
||||||
|
def servicer(self, populated_config):
|
||||||
|
return server.NodeServicer(populated_config)
|
||||||
|
|
||||||
|
@patch("connpy.core.node")
|
||||||
|
def test_interact_node_uses_passed_name(self, mock_node, servicer):
|
||||||
|
# Setup request with custom name
|
||||||
|
params = {"name": "custom-node-name@test", "host": "1.2.3.4", "protocol": "ssh"}
|
||||||
|
request = connpy_pb2.InteractRequest(
|
||||||
|
id="dynamic",
|
||||||
|
connection_params_json=json.dumps(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock node to allow _connect
|
||||||
|
mock_node_instance = MagicMock()
|
||||||
|
mock_node_instance._connect.return_value = True
|
||||||
|
mock_node.return_value = mock_node_instance
|
||||||
|
|
||||||
|
# We only need the first iteration of the generator to check naming
|
||||||
|
gen = servicer.interact_node(iter([request]), MockContext())
|
||||||
|
next(gen) # Skip the success response
|
||||||
|
|
||||||
|
# Verify that node() was called with the custom name
|
||||||
|
mock_node.assert_called()
|
||||||
|
found = False
|
||||||
|
for call in mock_node.call_args_list:
|
||||||
|
if call.args[0] == "custom-node-name@test":
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
assert found
|
||||||
|
|
||||||
|
@patch("connpy.core.node")
|
||||||
|
def test_interact_node_fallback_naming(self, mock_node, servicer):
|
||||||
|
# Setup request without custom name but with host
|
||||||
|
params = {"host": "my-instance", "protocol": "ssm"}
|
||||||
|
request = connpy_pb2.InteractRequest(
|
||||||
|
id="dynamic",
|
||||||
|
connection_params_json=json.dumps(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_node_instance = MagicMock()
|
||||||
|
mock_node_instance._connect.return_value = True
|
||||||
|
mock_node.return_value = mock_node_instance
|
||||||
|
|
||||||
|
gen = servicer.interact_node(iter([request]), MockContext())
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# Verify fallback name: dynamic-{host}@remote
|
||||||
|
found = False
|
||||||
|
for call in mock_node.call_args_list:
|
||||||
|
if call.args[0] == "dynamic-my-instance@remote":
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
assert found
|
||||||
|
|
||||||
|
class TestStubsMessageFormatting:
|
||||||
|
@patch("termios.tcsetattr")
|
||||||
|
@patch("termios.tcgetattr")
|
||||||
|
@patch("tty.setraw")
|
||||||
|
@patch("os.read")
|
||||||
|
@patch("select.select")
|
||||||
|
def test_connect_dynamic_msg_formatting_ssm(self, mock_select, mock_read, mock_setraw, mock_getattr, mock_setattr):
|
||||||
|
from connpy.grpc_layer.stubs import NodeStub
|
||||||
|
|
||||||
|
mock_getattr.return_value = [0, 0, 0, 0, 0, 0, [0] * 32]
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
stub = NodeStub(mock_channel, "localhost:8048")
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.success = True
|
||||||
|
stub.stub.interact_node.return_value = iter([mock_resp])
|
||||||
|
|
||||||
|
with patch("connpy.printer.success") as mock_success:
|
||||||
|
with patch("sys.stdin.fileno", return_value=0):
|
||||||
|
mock_select.return_value = ([], [], [])
|
||||||
|
params = {"protocol": "ssm", "host": "i-12345", "name": "my-ssm-node@aws"}
|
||||||
|
|
||||||
|
with patch("select.select", side_effect=KeyboardInterrupt):
|
||||||
|
try:
|
||||||
|
stub.connect_dynamic(params)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mock_success.assert_called()
|
||||||
|
msg = mock_success.call_args[0][0]
|
||||||
|
assert "Connected to my-ssm-node@aws" in msg
|
||||||
|
assert "at i-12345" in msg
|
||||||
|
assert ":22" not in msg
|
||||||
|
assert "via: ssm" in msg
|
||||||
|
|
||||||
|
|
||||||
|
# --- INTEGRATION TESTS (Real Server/Stub Communication) ---
|
||||||
|
|
||||||
|
class TestGRPCIntegration:
|
||||||
|
@pytest.fixture
|
||||||
|
def grpc_server(self, populated_config):
|
||||||
|
"""Starts a local gRPC server for integration testing."""
|
||||||
|
srv = grpc.server(futures.ThreadPoolExecutor(max_workers=5))
|
||||||
|
|
||||||
|
# Register services
|
||||||
|
connpy_pb2_grpc.add_NodeServiceServicer_to_server(server.NodeServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ProfileServiceServicer_to_server(server.ProfileServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ConfigServiceServicer_to_server(server.ConfigServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ExecutionServiceServicer_to_server(server.ExecutionServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ImportExportServiceServicer_to_server(server.ImportExportServicer(populated_config), srv)
|
||||||
|
|
||||||
|
port = srv.add_insecure_port('127.0.0.1:0')
|
||||||
|
srv.start()
|
||||||
|
yield f"127.0.0.1:{port}"
|
||||||
|
srv.stop(0)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def channel(self, grpc_server):
|
||||||
|
with grpc.insecure_channel(grpc_server) as channel:
|
||||||
|
yield channel
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def node_stub(self, channel):
|
||||||
|
return stubs.NodeStub(channel, "localhost")
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def profile_stub(self, channel):
|
||||||
|
return stubs.ProfileStub(channel, "localhost")
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_stub(self, channel):
|
||||||
|
return stubs.ConfigStub(channel, "localhost")
|
||||||
|
|
||||||
|
def test_list_nodes_integration(self, node_stub):
|
||||||
|
nodes = node_stub.list_nodes()
|
||||||
|
assert "router1" in nodes
|
||||||
|
assert "server1@office" in nodes
|
||||||
|
|
||||||
|
def test_get_node_details_integration(self, node_stub):
|
||||||
|
details = node_stub.get_node_details("router1")
|
||||||
|
assert details["host"] == "10.0.0.1"
|
||||||
|
|
||||||
|
def test_node_not_found_integration(self, node_stub):
|
||||||
|
with pytest.raises(ConnpyError) as exc:
|
||||||
|
node_stub.get_node_details("non-existent")
|
||||||
|
assert "Node 'non-existent' not found." in str(exc.value)
|
||||||
|
|
||||||
|
def test_list_profiles_integration(self, profile_stub):
|
||||||
|
profiles = profile_stub.list_profiles()
|
||||||
|
assert "office-user" in profiles
|
||||||
|
|
||||||
|
def test_get_settings_integration(self, config_stub):
|
||||||
|
settings = config_stub.get_settings()
|
||||||
|
assert "idletime" in settings
|
||||||
|
|
||||||
|
def test_update_setting_integration(self, config_stub):
|
||||||
|
config_stub.update_setting("idletime", 99)
|
||||||
|
settings = config_stub.get_settings()
|
||||||
|
assert settings["idletime"] == 99
|
||||||
|
|
||||||
|
def test_add_delete_node_integration(self, node_stub):
|
||||||
|
node_stub.add_node("integration-test-node", {"host": "9.9.9.9"})
|
||||||
|
assert "integration-test-node" in node_stub.list_nodes()
|
||||||
|
node_stub.delete_node("integration-test-node")
|
||||||
|
assert "integration-test-node" not in node_stub.list_nodes()
|
||||||
|
|
||||||
|
def test_import_yaml_integration(self, channel, node_stub):
|
||||||
|
import yaml
|
||||||
|
from connpy.grpc_layer import stubs
|
||||||
|
stub = stubs.ImportExportStub(channel, "localhost")
|
||||||
|
|
||||||
|
# ImportExportService expects a flat dict of nodes, not a full config structure
|
||||||
|
inventory = {
|
||||||
|
"imported-node": {"host": "8.8.8.8", "protocol": "ssh", "type": "connection"}
|
||||||
|
}
|
||||||
|
yaml_content = yaml.dump(inventory)
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||||
|
f.write(yaml_content)
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
stub.import_from_file(temp_path)
|
||||||
|
# Verify the node was imported and is visible via NodeStub
|
||||||
|
nodes = node_stub.list_nodes()
|
||||||
|
assert "imported-node" in nodes
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.remove(temp_path)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import threading
|
||||||
|
import io
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import pytest
|
||||||
|
from connpy import printer
|
||||||
|
|
||||||
|
def test_printer_thread_isolation():
|
||||||
|
"""Verify that printer output is isolated per thread when using set_thread_stream."""
|
||||||
|
num_threads = 5
|
||||||
|
iterations = 20
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
def worker(thread_id):
|
||||||
|
# Create a private buffer for this thread
|
||||||
|
buf = io.StringIO()
|
||||||
|
printer.set_thread_stream(buf)
|
||||||
|
|
||||||
|
# Ensure we have a clean console for this thread
|
||||||
|
# In a real gRPC request, this happens automatically as it's a new thread
|
||||||
|
printer.set_thread_console(None)
|
||||||
|
|
||||||
|
# Each thread prints its own ID
|
||||||
|
expected_msg = f"Thread-{thread_id}"
|
||||||
|
for _ in range(iterations):
|
||||||
|
printer.info(expected_msg)
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
results[thread_id] = buf.getvalue()
|
||||||
|
printer.set_thread_stream(None)
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
for i in range(num_threads):
|
||||||
|
t = threading.Thread(target=worker, args=(i,))
|
||||||
|
threads.append(t)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
for t in threads:
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
for thread_id, output in results.items():
|
||||||
|
expected_msg = f"Thread-{thread_id}"
|
||||||
|
assert expected_msg in output
|
||||||
|
|
||||||
|
# Ensure no leaks
|
||||||
|
for other_id in range(num_threads):
|
||||||
|
if other_id == thread_id: continue
|
||||||
|
assert f"Thread-{other_id}" not in output
|
||||||
|
|
||||||
|
def test_printer_manual_stream():
|
||||||
|
"""Verify that setting a thread stream correctly captures printer output in the current thread."""
|
||||||
|
buf = io.StringIO()
|
||||||
|
|
||||||
|
# We must clear the thread-local console to force it to pick up the new sys.stdout proxy
|
||||||
|
printer.set_thread_console(None)
|
||||||
|
printer.set_thread_stream(buf)
|
||||||
|
|
||||||
|
printer.info("Captured-Message")
|
||||||
|
|
||||||
|
output = buf.getvalue()
|
||||||
|
printer.set_thread_stream(None)
|
||||||
|
printer.set_thread_console(None)
|
||||||
|
|
||||||
|
assert "Captured-Message" in output
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.ai_handler API documentation</title>
|
<title>connpy.cli.ai_handler API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -371,7 +371,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.api_handler API documentation</title>
|
<title>connpy.cli.api_handler API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -193,7 +193,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.config_handler API documentation</title>
|
<title>connpy.cli.config_handler API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -482,7 +482,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.context_handler API documentation</title>
|
<title>connpy.cli.context_handler API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -249,7 +249,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.forms API documentation</title>
|
<title>connpy.cli.forms API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -517,7 +517,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.help_text API documentation</title>
|
<title>connpy.cli.help_text API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -94,6 +94,7 @@ Here are some important instructions and tips for configuring your new node:
|
|||||||
- telnet
|
- telnet
|
||||||
- kubectl (`kubectl exec`)
|
- kubectl (`kubectl exec`)
|
||||||
- docker (`docker exec`)
|
- docker (`docker exec`)
|
||||||
|
- ssm (`aws ssm start-session`)
|
||||||
|
|
||||||
3. **Optional Values**:
|
3. **Optional Values**:
|
||||||
- You can leave any value empty except for the hostname/IP.
|
- You can leave any value empty except for the hostname/IP.
|
||||||
@@ -303,7 +304,7 @@ tasks:
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.helpers API documentation</title>
|
<title>connpy.cli.helpers API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -207,7 +207,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.import_export_handler API documentation</title>
|
<title>connpy.cli.import_export_handler API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -272,7 +272,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli API documentation</title>
|
<title>connpy.cli API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -137,7 +137,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.node_handler API documentation</title>
|
<title>connpy.cli.node_handler API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -168,6 +168,10 @@ el.replaceWith(d);
|
|||||||
printer.error(f"Node '{args.data}' already exists.")
|
printer.error(f"Node '{args.data}' already exists.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
uniques = self.app.services.nodes.explode_unique(args.data)
|
uniques = self.app.services.nodes.explode_unique(args.data)
|
||||||
|
|
||||||
|
# Fast fail if parent folder does not exist
|
||||||
|
self.app.services.nodes.validate_parent_folder(args.data)
|
||||||
|
|
||||||
printer.console.print(Markdown(get_instructions()))
|
printer.console.print(Markdown(get_instructions()))
|
||||||
|
|
||||||
new_node_data = self.forms.questions_nodes(args.data, uniques)
|
new_node_data = self.forms.questions_nodes(args.data, uniques)
|
||||||
@@ -310,6 +314,10 @@ el.replaceWith(d);
|
|||||||
printer.error(f"Node '{args.data}' already exists.")
|
printer.error(f"Node '{args.data}' already exists.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
uniques = self.app.services.nodes.explode_unique(args.data)
|
uniques = self.app.services.nodes.explode_unique(args.data)
|
||||||
|
|
||||||
|
# Fast fail if parent folder does not exist
|
||||||
|
self.app.services.nodes.validate_parent_folder(args.data)
|
||||||
|
|
||||||
printer.console.print(Markdown(get_instructions()))
|
printer.console.print(Markdown(get_instructions()))
|
||||||
|
|
||||||
new_node_data = self.forms.questions_nodes(args.data, uniques)
|
new_node_data = self.forms.questions_nodes(args.data, uniques)
|
||||||
@@ -598,7 +606,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.plugin_handler API documentation</title>
|
<title>connpy.cli.plugin_handler API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -385,7 +385,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.profile_handler API documentation</title>
|
<title>connpy.cli.profile_handler API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -314,7 +314,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.run_handler API documentation</title>
|
<title>connpy.cli.run_handler API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -363,7 +363,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.sync_handler API documentation</title>
|
<title>connpy.cli.sync_handler API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -427,7 +427,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.cli.validators API documentation</title>
|
<title>connpy.cli.validators API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -67,14 +67,14 @@ el.replaceWith(d);
|
|||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^$)"):
|
def profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$)"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker or leave empty")
|
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker, ssm or leave empty")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^$|^@.+$)"):
|
def protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$|^@.+$)"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker leave empty or @profile")
|
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker, ssm, leave empty or @profile")
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
||||||
@@ -389,16 +389,16 @@ el.replaceWith(d);
|
|||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
</dd>
|
</dd>
|
||||||
<dt id="connpy.cli.validators.Validators.profile_protocol_validation"><code class="name flex">
|
<dt id="connpy.cli.validators.Validators.profile_protocol_validation"><code class="name flex">
|
||||||
<span>def <span class="ident">profile_protocol_validation</span></span>(<span>self, answers, current, regex='(^ssh$|^telnet$|^kubectl$|^docker$|^$)')</span>
|
<span>def <span class="ident">profile_protocol_validation</span></span>(<span>self, answers, current, regex='(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$)')</span>
|
||||||
</code></dt>
|
</code></dt>
|
||||||
<dd>
|
<dd>
|
||||||
<details class="source">
|
<details class="source">
|
||||||
<summary>
|
<summary>
|
||||||
<span>Expand source code</span>
|
<span>Expand source code</span>
|
||||||
</summary>
|
</summary>
|
||||||
<pre><code class="python">def profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^$)"):
|
<pre><code class="python">def profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$)"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker or leave empty")
|
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker, ssm or leave empty")
|
||||||
return True</code></pre>
|
return True</code></pre>
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
@@ -425,16 +425,16 @@ el.replaceWith(d);
|
|||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
</dd>
|
</dd>
|
||||||
<dt id="connpy.cli.validators.Validators.protocol_validation"><code class="name flex">
|
<dt id="connpy.cli.validators.Validators.protocol_validation"><code class="name flex">
|
||||||
<span>def <span class="ident">protocol_validation</span></span>(<span>self, answers, current, regex='(^ssh$|^telnet$|^kubectl$|^docker$|^$|^@.+$)')</span>
|
<span>def <span class="ident">protocol_validation</span></span>(<span>self,<br>answers,<br>current,<br>regex='(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$|^@.+$)')</span>
|
||||||
</code></dt>
|
</code></dt>
|
||||||
<dd>
|
<dd>
|
||||||
<details class="source">
|
<details class="source">
|
||||||
<summary>
|
<summary>
|
||||||
<span>Expand source code</span>
|
<span>Expand source code</span>
|
||||||
</summary>
|
</summary>
|
||||||
<pre><code class="python">def protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^$|^@.+$)"):
|
<pre><code class="python">def protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$|^@.+$)"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker leave empty or @profile")
|
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker, ssm, leave empty or @profile")
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
||||||
@@ -508,7 +508,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
|
<title>connpy.grpc_layer.connpy_pb2 API documentation</title>
|
||||||
|
<meta name="description" content="Generated protocol buffer code.">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/typography.min.css" integrity="sha512-Y1DYSb995BAfxobCkKepB1BqJJTPrOp3zPL74AWFugHHmmdcvO+C48WLrUOlhGMc0QG7AE3f7gmvvcrmX2fDoA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" crossorigin>
|
||||||
|
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:1.5em;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:2em 0 .50em 0}h3{font-size:1.4em;margin:1.6em 0 .7em 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .2s ease-in-out}a:visited{color:#503}a:hover{color:#b62}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900;font-weight:bold}pre code{font-size:.8em;line-height:1.4em;padding:1em;display:block}code{background:#f3f3f3;font-family:"DejaVu Sans Mono",monospace;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source > summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible;min-width:max-content}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em 1em;margin:1em 0}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
|
||||||
|
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul ul{padding-left:1em}.toc > ul > li{margin-top:.5em}}</style>
|
||||||
|
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
|
||||||
|
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin></script>
|
||||||
|
<script>window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
hljs.configure({languages: ['bash', 'css', 'diff', 'graphql', 'ini', 'javascript', 'json', 'plaintext', 'python', 'python-repl', 'rust', 'shell', 'sql', 'typescript', 'xml', 'yaml']});
|
||||||
|
hljs.highlightAll();
|
||||||
|
/* Collapse source docstrings */
|
||||||
|
setTimeout(() => {
|
||||||
|
[...document.querySelectorAll('.hljs.language-python > .hljs-string')]
|
||||||
|
.filter(el => el.innerHTML.length > 200 && ['"""', "'''"].includes(el.innerHTML.substring(0, 3)))
|
||||||
|
.forEach(el => {
|
||||||
|
let d = document.createElement('details');
|
||||||
|
d.classList.add('hljs-string');
|
||||||
|
d.innerHTML = '<summary>"""</summary>' + el.innerHTML.substring(3);
|
||||||
|
el.replaceWith(d);
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
})</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<article id="content">
|
||||||
|
<header>
|
||||||
|
<h1 class="title">Module <code>connpy.grpc_layer.connpy_pb2</code></h1>
|
||||||
|
</header>
|
||||||
|
<section id="section-intro">
|
||||||
|
<p>Generated protocol buffer code.</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<nav id="sidebar">
|
||||||
|
<div class="toc">
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
<ul id="index">
|
||||||
|
<li><h3>Super-module</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code><a title="connpy.grpc_layer" href="index.html">connpy.grpc_layer</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
<footer id="footer">
|
||||||
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
|
<title>connpy.grpc_layer API documentation</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/typography.min.css" integrity="sha512-Y1DYSb995BAfxobCkKepB1BqJJTPrOp3zPL74AWFugHHmmdcvO+C48WLrUOlhGMc0QG7AE3f7gmvvcrmX2fDoA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" crossorigin>
|
||||||
|
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:1.5em;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:2em 0 .50em 0}h3{font-size:1.4em;margin:1.6em 0 .7em 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .2s ease-in-out}a:visited{color:#503}a:hover{color:#b62}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900;font-weight:bold}pre code{font-size:.8em;line-height:1.4em;padding:1em;display:block}code{background:#f3f3f3;font-family:"DejaVu Sans Mono",monospace;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source > summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible;min-width:max-content}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em 1em;margin:1em 0}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
|
||||||
|
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul ul{padding-left:1em}.toc > ul > li{margin-top:.5em}}</style>
|
||||||
|
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
|
||||||
|
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin></script>
|
||||||
|
<script>window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
hljs.configure({languages: ['bash', 'css', 'diff', 'graphql', 'ini', 'javascript', 'json', 'plaintext', 'python', 'python-repl', 'rust', 'shell', 'sql', 'typescript', 'xml', 'yaml']});
|
||||||
|
hljs.highlightAll();
|
||||||
|
/* Collapse source docstrings */
|
||||||
|
setTimeout(() => {
|
||||||
|
[...document.querySelectorAll('.hljs.language-python > .hljs-string')]
|
||||||
|
.filter(el => el.innerHTML.length > 200 && ['"""', "'''"].includes(el.innerHTML.substring(0, 3)))
|
||||||
|
.forEach(el => {
|
||||||
|
let d = document.createElement('details');
|
||||||
|
d.classList.add('hljs-string');
|
||||||
|
d.innerHTML = '<summary>"""</summary>' + el.innerHTML.substring(3);
|
||||||
|
el.replaceWith(d);
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
})</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<article id="content">
|
||||||
|
<header>
|
||||||
|
<h1 class="title">Module <code>connpy.grpc_layer</code></h1>
|
||||||
|
</header>
|
||||||
|
<section id="section-intro">
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2 class="section-title" id="header-submodules">Sub-modules</h2>
|
||||||
|
<dl>
|
||||||
|
<dt><code class="name"><a title="connpy.grpc_layer.connpy_pb2" href="connpy_pb2.html">connpy.grpc_layer.connpy_pb2</a></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>Generated protocol buffer code.</p></div>
|
||||||
|
</dd>
|
||||||
|
<dt><code class="name"><a title="connpy.grpc_layer.connpy_pb2_grpc" href="connpy_pb2_grpc.html">connpy.grpc_layer.connpy_pb2_grpc</a></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>Client and server classes corresponding to protobuf-defined services.</p></div>
|
||||||
|
</dd>
|
||||||
|
<dt><code class="name"><a title="connpy.grpc_layer.remote_plugin_pb2" href="remote_plugin_pb2.html">connpy.grpc_layer.remote_plugin_pb2</a></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>Generated protocol buffer code.</p></div>
|
||||||
|
</dd>
|
||||||
|
<dt><code class="name"><a title="connpy.grpc_layer.remote_plugin_pb2_grpc" href="remote_plugin_pb2_grpc.html">connpy.grpc_layer.remote_plugin_pb2_grpc</a></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>Client and server classes corresponding to protobuf-defined services.</p></div>
|
||||||
|
</dd>
|
||||||
|
<dt><code class="name"><a title="connpy.grpc_layer.server" href="server.html">connpy.grpc_layer.server</a></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt><code class="name"><a title="connpy.grpc_layer.stubs" href="stubs.html">connpy.grpc_layer.stubs</a></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt><code class="name"><a title="connpy.grpc_layer.utils" href="utils.html">connpy.grpc_layer.utils</a></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<nav id="sidebar">
|
||||||
|
<div class="toc">
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
<ul id="index">
|
||||||
|
<li><h3>Super-module</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code><a title="connpy" href="../index.html">connpy</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><h3><a href="#header-submodules">Sub-modules</a></h3>
|
||||||
|
<ul>
|
||||||
|
<li><code><a title="connpy.grpc_layer.connpy_pb2" href="connpy_pb2.html">connpy.grpc_layer.connpy_pb2</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc" href="connpy_pb2_grpc.html">connpy.grpc_layer.connpy_pb2_grpc</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2" href="remote_plugin_pb2.html">connpy.grpc_layer.remote_plugin_pb2</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2_grpc" href="remote_plugin_pb2_grpc.html">connpy.grpc_layer.remote_plugin_pb2_grpc</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.server" href="server.html">connpy.grpc_layer.server</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.stubs" href="stubs.html">connpy.grpc_layer.stubs</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.utils" href="utils.html">connpy.grpc_layer.utils</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
<footer id="footer">
|
||||||
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
|
<title>connpy.grpc_layer.remote_plugin_pb2 API documentation</title>
|
||||||
|
<meta name="description" content="Generated protocol buffer code.">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/typography.min.css" integrity="sha512-Y1DYSb995BAfxobCkKepB1BqJJTPrOp3zPL74AWFugHHmmdcvO+C48WLrUOlhGMc0QG7AE3f7gmvvcrmX2fDoA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" crossorigin>
|
||||||
|
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:1.5em;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:2em 0 .50em 0}h3{font-size:1.4em;margin:1.6em 0 .7em 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .2s ease-in-out}a:visited{color:#503}a:hover{color:#b62}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900;font-weight:bold}pre code{font-size:.8em;line-height:1.4em;padding:1em;display:block}code{background:#f3f3f3;font-family:"DejaVu Sans Mono",monospace;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source > summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible;min-width:max-content}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em 1em;margin:1em 0}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
|
||||||
|
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul ul{padding-left:1em}.toc > ul > li{margin-top:.5em}}</style>
|
||||||
|
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
|
||||||
|
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin></script>
|
||||||
|
<script>window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
hljs.configure({languages: ['bash', 'css', 'diff', 'graphql', 'ini', 'javascript', 'json', 'plaintext', 'python', 'python-repl', 'rust', 'shell', 'sql', 'typescript', 'xml', 'yaml']});
|
||||||
|
hljs.highlightAll();
|
||||||
|
/* Collapse source docstrings */
|
||||||
|
setTimeout(() => {
|
||||||
|
[...document.querySelectorAll('.hljs.language-python > .hljs-string')]
|
||||||
|
.filter(el => el.innerHTML.length > 200 && ['"""', "'''"].includes(el.innerHTML.substring(0, 3)))
|
||||||
|
.forEach(el => {
|
||||||
|
let d = document.createElement('details');
|
||||||
|
d.classList.add('hljs-string');
|
||||||
|
d.innerHTML = '<summary>"""</summary>' + el.innerHTML.substring(3);
|
||||||
|
el.replaceWith(d);
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
})</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<article id="content">
|
||||||
|
<header>
|
||||||
|
<h1 class="title">Module <code>connpy.grpc_layer.remote_plugin_pb2</code></h1>
|
||||||
|
</header>
|
||||||
|
<section id="section-intro">
|
||||||
|
<p>Generated protocol buffer code.</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2 class="section-title" id="header-classes">Classes</h2>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2.IdRequest"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">IdRequest</span></span>
|
||||||
|
<span>(</span><span>*args, **kwargs)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>A ProtocolMessage</p></div>
|
||||||
|
<h3>Ancestors</h3>
|
||||||
|
<ul class="hlist">
|
||||||
|
<li>google._upb._message.Message</li>
|
||||||
|
<li>google.protobuf.message.Message</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Class variables</h3>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2.IdRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2.OutputChunk"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">OutputChunk</span></span>
|
||||||
|
<span>(</span><span>*args, **kwargs)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>A ProtocolMessage</p></div>
|
||||||
|
<h3>Ancestors</h3>
|
||||||
|
<ul class="hlist">
|
||||||
|
<li>google._upb._message.Message</li>
|
||||||
|
<li>google.protobuf.message.Message</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Class variables</h3>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2.OutputChunk.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">PluginInvokeRequest</span></span>
|
||||||
|
<span>(</span><span>*args, **kwargs)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>A ProtocolMessage</p></div>
|
||||||
|
<h3>Ancestors</h3>
|
||||||
|
<ul class="hlist">
|
||||||
|
<li>google._upb._message.Message</li>
|
||||||
|
<li>google.protobuf.message.Message</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Class variables</h3>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2.StringResponse"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">StringResponse</span></span>
|
||||||
|
<span>(</span><span>*args, **kwargs)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>A ProtocolMessage</p></div>
|
||||||
|
<h3>Ancestors</h3>
|
||||||
|
<ul class="hlist">
|
||||||
|
<li>google._upb._message.Message</li>
|
||||||
|
<li>google.protobuf.message.Message</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Class variables</h3>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2.StringResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<nav id="sidebar">
|
||||||
|
<div class="toc">
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
<ul id="index">
|
||||||
|
<li><h3>Super-module</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code><a title="connpy.grpc_layer" href="index.html">connpy.grpc_layer</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><h3><a href="#header-classes">Classes</a></h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.grpc_layer.remote_plugin_pb2.IdRequest" href="#connpy.grpc_layer.remote_plugin_pb2.IdRequest">IdRequest</a></code></h4>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2.IdRequest.DESCRIPTOR" href="#connpy.grpc_layer.remote_plugin_pb2.IdRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.grpc_layer.remote_plugin_pb2.OutputChunk" href="#connpy.grpc_layer.remote_plugin_pb2.OutputChunk">OutputChunk</a></code></h4>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2.OutputChunk.DESCRIPTOR" href="#connpy.grpc_layer.remote_plugin_pb2.OutputChunk.DESCRIPTOR">DESCRIPTOR</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest" href="#connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest">PluginInvokeRequest</a></code></h4>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest.DESCRIPTOR" href="#connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.grpc_layer.remote_plugin_pb2.StringResponse" href="#connpy.grpc_layer.remote_plugin_pb2.StringResponse">StringResponse</a></code></h4>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2.StringResponse.DESCRIPTOR" href="#connpy.grpc_layer.remote_plugin_pb2.StringResponse.DESCRIPTOR">DESCRIPTOR</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
<footer id="footer">
|
||||||
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
|
<title>connpy.grpc_layer.remote_plugin_pb2_grpc API documentation</title>
|
||||||
|
<meta name="description" content="Client and server classes corresponding to protobuf-defined services.">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/typography.min.css" integrity="sha512-Y1DYSb995BAfxobCkKepB1BqJJTPrOp3zPL74AWFugHHmmdcvO+C48WLrUOlhGMc0QG7AE3f7gmvvcrmX2fDoA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" crossorigin>
|
||||||
|
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:1.5em;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:2em 0 .50em 0}h3{font-size:1.4em;margin:1.6em 0 .7em 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .2s ease-in-out}a:visited{color:#503}a:hover{color:#b62}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900;font-weight:bold}pre code{font-size:.8em;line-height:1.4em;padding:1em;display:block}code{background:#f3f3f3;font-family:"DejaVu Sans Mono",monospace;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source > summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible;min-width:max-content}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em 1em;margin:1em 0}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
|
||||||
|
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul ul{padding-left:1em}.toc > ul > li{margin-top:.5em}}</style>
|
||||||
|
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
|
||||||
|
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin></script>
|
||||||
|
<script>window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
hljs.configure({languages: ['bash', 'css', 'diff', 'graphql', 'ini', 'javascript', 'json', 'plaintext', 'python', 'python-repl', 'rust', 'shell', 'sql', 'typescript', 'xml', 'yaml']});
|
||||||
|
hljs.highlightAll();
|
||||||
|
/* Collapse source docstrings */
|
||||||
|
setTimeout(() => {
|
||||||
|
[...document.querySelectorAll('.hljs.language-python > .hljs-string')]
|
||||||
|
.filter(el => el.innerHTML.length > 200 && ['"""', "'''"].includes(el.innerHTML.substring(0, 3)))
|
||||||
|
.forEach(el => {
|
||||||
|
let d = document.createElement('details');
|
||||||
|
d.classList.add('hljs-string');
|
||||||
|
d.innerHTML = '<summary>"""</summary>' + el.innerHTML.substring(3);
|
||||||
|
el.replaceWith(d);
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
})</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<article id="content">
|
||||||
|
<header>
|
||||||
|
<h1 class="title">Module <code>connpy.grpc_layer.remote_plugin_pb2_grpc</code></h1>
|
||||||
|
</header>
|
||||||
|
<section id="section-intro">
|
||||||
|
<p>Client and server classes corresponding to protobuf-defined services.</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2 class="section-title" id="header-functions">Functions</h2>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2_grpc.add_RemotePluginServiceServicer_to_server"><code class="name flex">
|
||||||
|
<span>def <span class="ident">add_RemotePluginServiceServicer_to_server</span></span>(<span>servicer, server)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def add_RemotePluginServiceServicer_to_server(servicer, server):
|
||||||
|
rpc_method_handlers = {
|
||||||
|
'get_plugin_source': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.get_plugin_source,
|
||||||
|
request_deserializer=remote__plugin__pb2.IdRequest.FromString,
|
||||||
|
response_serializer=remote__plugin__pb2.StringResponse.SerializeToString,
|
||||||
|
),
|
||||||
|
'invoke_plugin': grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.invoke_plugin,
|
||||||
|
request_deserializer=remote__plugin__pb2.PluginInvokeRequest.FromString,
|
||||||
|
response_serializer=remote__plugin__pb2.OutputChunk.SerializeToString,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
|
'connpy_remote.RemotePluginService', rpc_method_handlers)
|
||||||
|
server.add_generic_rpc_handlers((generic_handler,))
|
||||||
|
server.add_registered_method_handlers('connpy_remote.RemotePluginService', rpc_method_handlers)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2 class="section-title" id="header-classes">Classes</h2>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginService"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">RemotePluginService</span></span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">class RemotePluginService(object):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_plugin_source(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/connpy_remote.RemotePluginService/get_plugin_source',
|
||||||
|
remote__plugin__pb2.IdRequest.SerializeToString,
|
||||||
|
remote__plugin__pb2.StringResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def invoke_plugin(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_stream(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/connpy_remote.RemotePluginService/invoke_plugin',
|
||||||
|
remote__plugin__pb2.PluginInvokeRequest.SerializeToString,
|
||||||
|
remote__plugin__pb2.OutputChunk.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||||
|
<h3>Static methods</h3>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginService.get_plugin_source"><code class="name flex">
|
||||||
|
<span>def <span class="ident">get_plugin_source</span></span>(<span>request,<br>target,<br>options=(),<br>channel_credentials=None,<br>call_credentials=None,<br>insecure=False,<br>compression=None,<br>wait_for_ready=None,<br>timeout=None,<br>metadata=None)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@staticmethod
|
||||||
|
def get_plugin_source(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/connpy_remote.RemotePluginService/get_plugin_source',
|
||||||
|
remote__plugin__pb2.IdRequest.SerializeToString,
|
||||||
|
remote__plugin__pb2.StringResponse.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginService.invoke_plugin"><code class="name flex">
|
||||||
|
<span>def <span class="ident">invoke_plugin</span></span>(<span>request,<br>target,<br>options=(),<br>channel_credentials=None,<br>call_credentials=None,<br>insecure=False,<br>compression=None,<br>wait_for_ready=None,<br>timeout=None,<br>metadata=None)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@staticmethod
|
||||||
|
def invoke_plugin(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_stream(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/connpy_remote.RemotePluginService/invoke_plugin',
|
||||||
|
remote__plugin__pb2.PluginInvokeRequest.SerializeToString,
|
||||||
|
remote__plugin__pb2.OutputChunk.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceServicer"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">RemotePluginServiceServicer</span></span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">class RemotePluginServiceServicer(object):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
|
||||||
|
def get_plugin_source(self, request, context):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def invoke_plugin(self, request, context):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||||
|
<h3>Subclasses</h3>
|
||||||
|
<ul class="hlist">
|
||||||
|
<li><a title="connpy.grpc_layer.server.PluginServicer" href="server.html#connpy.grpc_layer.server.PluginServicer">PluginServicer</a></li>
|
||||||
|
</ul>
|
||||||
|
<h3>Methods</h3>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceServicer.get_plugin_source"><code class="name flex">
|
||||||
|
<span>def <span class="ident">get_plugin_source</span></span>(<span>self, request, context)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def get_plugin_source(self, request, context):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceServicer.invoke_plugin"><code class="name flex">
|
||||||
|
<span>def <span class="ident">invoke_plugin</span></span>(<span>self, request, context)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def invoke_plugin(self, request, context):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceStub"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">RemotePluginServiceStub</span></span>
|
||||||
|
<span>(</span><span>channel)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">class RemotePluginServiceStub(object):
|
||||||
|
"""Missing associated documentation comment in .proto file."""
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: A grpc.Channel.
|
||||||
|
"""
|
||||||
|
self.get_plugin_source = channel.unary_unary(
|
||||||
|
'/connpy_remote.RemotePluginService/get_plugin_source',
|
||||||
|
request_serializer=remote__plugin__pb2.IdRequest.SerializeToString,
|
||||||
|
response_deserializer=remote__plugin__pb2.StringResponse.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
self.invoke_plugin = channel.unary_stream(
|
||||||
|
'/connpy_remote.RemotePluginService/invoke_plugin',
|
||||||
|
request_serializer=remote__plugin__pb2.PluginInvokeRequest.SerializeToString,
|
||||||
|
response_deserializer=remote__plugin__pb2.OutputChunk.FromString,
|
||||||
|
_registered_method=True)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Missing associated documentation comment in .proto file.</p>
|
||||||
|
<p>Constructor.</p>
|
||||||
|
<h2 id="args">Args</h2>
|
||||||
|
<dl>
|
||||||
|
<dt><strong><code>channel</code></strong></dt>
|
||||||
|
<dd>A grpc.Channel.</dd>
|
||||||
|
</dl></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<nav id="sidebar">
|
||||||
|
<div class="toc">
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
<ul id="index">
|
||||||
|
<li><h3>Super-module</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code><a title="connpy.grpc_layer" href="index.html">connpy.grpc_layer</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><h3><a href="#header-functions">Functions</a></h3>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2_grpc.add_RemotePluginServiceServicer_to_server" href="#connpy.grpc_layer.remote_plugin_pb2_grpc.add_RemotePluginServiceServicer_to_server">add_RemotePluginServiceServicer_to_server</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><h3><a href="#header-classes">Classes</a></h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginService" href="#connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginService">RemotePluginService</a></code></h4>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginService.get_plugin_source" href="#connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginService.get_plugin_source">get_plugin_source</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginService.invoke_plugin" href="#connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginService.invoke_plugin">invoke_plugin</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceServicer" href="#connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceServicer">RemotePluginServiceServicer</a></code></h4>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceServicer.get_plugin_source" href="#connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceServicer.get_plugin_source">get_plugin_source</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceServicer.invoke_plugin" href="#connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceServicer.invoke_plugin">invoke_plugin</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceStub" href="#connpy.grpc_layer.remote_plugin_pb2_grpc.RemotePluginServiceStub">RemotePluginServiceStub</a></code></h4>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
<footer id="footer">
|
||||||
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
|
<title>connpy.grpc_layer.utils API documentation</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/typography.min.css" integrity="sha512-Y1DYSb995BAfxobCkKepB1BqJJTPrOp3zPL74AWFugHHmmdcvO+C48WLrUOlhGMc0QG7AE3f7gmvvcrmX2fDoA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" crossorigin>
|
||||||
|
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:1.5em;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:2em 0 .50em 0}h3{font-size:1.4em;margin:1.6em 0 .7em 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .2s ease-in-out}a:visited{color:#503}a:hover{color:#b62}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900;font-weight:bold}pre code{font-size:.8em;line-height:1.4em;padding:1em;display:block}code{background:#f3f3f3;font-family:"DejaVu Sans Mono",monospace;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source > summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible;min-width:max-content}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em 1em;margin:1em 0}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
|
||||||
|
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul ul{padding-left:1em}.toc > ul > li{margin-top:.5em}}</style>
|
||||||
|
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
|
||||||
|
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin></script>
|
||||||
|
<script>window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
hljs.configure({languages: ['bash', 'css', 'diff', 'graphql', 'ini', 'javascript', 'json', 'plaintext', 'python', 'python-repl', 'rust', 'shell', 'sql', 'typescript', 'xml', 'yaml']});
|
||||||
|
hljs.highlightAll();
|
||||||
|
/* Collapse source docstrings */
|
||||||
|
setTimeout(() => {
|
||||||
|
[...document.querySelectorAll('.hljs.language-python > .hljs-string')]
|
||||||
|
.filter(el => el.innerHTML.length > 200 && ['"""', "'''"].includes(el.innerHTML.substring(0, 3)))
|
||||||
|
.forEach(el => {
|
||||||
|
let d = document.createElement('details');
|
||||||
|
d.classList.add('hljs-string');
|
||||||
|
d.innerHTML = '<summary>"""</summary>' + el.innerHTML.substring(3);
|
||||||
|
el.replaceWith(d);
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
})</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<article id="content">
|
||||||
|
<header>
|
||||||
|
<h1 class="title">Module <code>connpy.grpc_layer.utils</code></h1>
|
||||||
|
</header>
|
||||||
|
<section id="section-intro">
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2 class="section-title" id="header-functions">Functions</h2>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.grpc_layer.utils.from_struct"><code class="name flex">
|
||||||
|
<span>def <span class="ident">from_struct</span></span>(<span>struct)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def from_struct(struct):
|
||||||
|
if not struct:
|
||||||
|
return {}
|
||||||
|
return json_format.MessageToDict(struct, preserving_proto_field_name=True)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.grpc_layer.utils.from_value"><code class="name flex">
|
||||||
|
<span>def <span class="ident">from_value</span></span>(<span>val)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def from_value(val):
|
||||||
|
if not val.HasField("kind"):
|
||||||
|
return None
|
||||||
|
return json.loads(json_format.MessageToJson(val))</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.grpc_layer.utils.to_struct"><code class="name flex">
|
||||||
|
<span>def <span class="ident">to_struct</span></span>(<span>obj)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def to_struct(obj):
|
||||||
|
if not obj:
|
||||||
|
return Struct()
|
||||||
|
s = Struct()
|
||||||
|
json_format.ParseDict(obj, s)
|
||||||
|
return s</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.grpc_layer.utils.to_value"><code class="name flex">
|
||||||
|
<span>def <span class="ident">to_value</span></span>(<span>obj)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def to_value(obj):
|
||||||
|
if obj is None:
|
||||||
|
v = Value()
|
||||||
|
v.null_value = 0
|
||||||
|
return v
|
||||||
|
json_str = json.dumps(obj)
|
||||||
|
v = Value()
|
||||||
|
json_format.Parse(json_str, v)
|
||||||
|
return v</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<nav id="sidebar">
|
||||||
|
<div class="toc">
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
<ul id="index">
|
||||||
|
<li><h3>Super-module</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code><a title="connpy.grpc_layer" href="index.html">connpy.grpc_layer</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><h3><a href="#header-functions">Functions</a></h3>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.grpc_layer.utils.from_struct" href="#connpy.grpc_layer.utils.from_struct">from_struct</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.utils.from_value" href="#connpy.grpc_layer.utils.from_value">from_value</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.utils.to_struct" href="#connpy.grpc_layer.utils.to_struct">to_struct</a></code></li>
|
||||||
|
<li><code><a title="connpy.grpc_layer.utils.to_value" href="#connpy.grpc_layer.utils.to_value">to_value</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
<footer id="footer">
|
||||||
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+197
-59
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy API documentation</title>
|
<title>connpy API documentation</title>
|
||||||
<meta name="description" content="Connection manager …">
|
<meta name="description" content="Connection manager …">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -37,9 +37,9 @@ el.replaceWith(d);
|
|||||||
</header>
|
</header>
|
||||||
<section id="section-intro">
|
<section id="section-intro">
|
||||||
<h2 id="connection-manager">Connection manager</h2>
|
<h2 id="connection-manager">Connection manager</h2>
|
||||||
<p>Connpy is a SSH, SFTP, Telnet, kubectl, and Docker pod connection manager and automation module for Linux, Mac, and Docker.</p>
|
<p>Connpy is a SSH, SFTP, Telnet, kubectl, Docker pod, and AWS SSM connection manager and automation module for Linux, Mac, and Docker.</p>
|
||||||
<h3 id="features">Features</h3>
|
<h3 id="features">Features</h3>
|
||||||
<pre><code>- Manage connections using SSH, SFTP, Telnet, kubectl, and Docker exec.
|
<pre><code>- Manage connections using SSH, SFTP, Telnet, kubectl, Docker exec, and AWS SSM.
|
||||||
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
|
- Set contexts to manage specific nodes from specific contexts (work/home/clients/etc).
|
||||||
- You can generate profiles and reference them from nodes using @profilename so you don't
|
- You can generate profiles and reference them from nodes using @profilename so you don't
|
||||||
need to edit multiple nodes when changing passwords or other information.
|
need to edit multiple nodes when changing passwords or other information.
|
||||||
@@ -516,7 +516,7 @@ class Preload:
|
|||||||
<dd>
|
<dd>
|
||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
</dd>
|
</dd>
|
||||||
<dt><code class="name"><a title="connpy.grpc" href="grpc/index.html">connpy.grpc</a></code></dt>
|
<dt><code class="name"><a title="connpy.grpc_layer" href="grpc_layer/index.html">connpy.grpc_layer</a></code></dt>
|
||||||
<dd>
|
<dd>
|
||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -1116,14 +1116,20 @@ class ai:
|
|||||||
status_formatter (callable): Function(args_dict) -> status string.
|
status_formatter (callable): Function(args_dict) -> status string.
|
||||||
"""
|
"""
|
||||||
name = tool_definition["function"]["name"]
|
name = tool_definition["function"]["name"]
|
||||||
|
|
||||||
|
# Check if already registered to prevent duplicates
|
||||||
if target in ("engineer", "both"):
|
if target in ("engineer", "both"):
|
||||||
|
if not any(t["function"]["name"] == name for t in self.external_engineer_tools):
|
||||||
self.external_engineer_tools.append(tool_definition)
|
self.external_engineer_tools.append(tool_definition)
|
||||||
if target in ("architect", "both"):
|
if target in ("architect", "both"):
|
||||||
|
if not any(t["function"]["name"] == name for t in self.external_architect_tools):
|
||||||
self.external_architect_tools.append(tool_definition)
|
self.external_architect_tools.append(tool_definition)
|
||||||
|
|
||||||
self.external_tool_handlers[name] = handler
|
self.external_tool_handlers[name] = handler
|
||||||
if engineer_prompt:
|
|
||||||
|
if engineer_prompt and engineer_prompt not in self.engineer_prompt_extensions:
|
||||||
self.engineer_prompt_extensions.append(engineer_prompt)
|
self.engineer_prompt_extensions.append(engineer_prompt)
|
||||||
if architect_prompt:
|
if architect_prompt and architect_prompt not in self.architect_prompt_extensions:
|
||||||
self.architect_prompt_extensions.append(architect_prompt)
|
self.architect_prompt_extensions.append(architect_prompt)
|
||||||
if status_formatter:
|
if status_formatter:
|
||||||
self.tool_status_formatters[name] = status_formatter
|
self.tool_status_formatters[name] = status_formatter
|
||||||
@@ -1355,12 +1361,46 @@ class ai:
|
|||||||
|
|
||||||
def _truncate(self, text, limit=None):
|
def _truncate(self, text, limit=None):
|
||||||
"""Truncate text to specified limit, keeping head (60%) and tail (40%)."""
|
"""Truncate text to specified limit, keeping head (60%) and tail (40%)."""
|
||||||
|
if not isinstance(text, str): return str(text)
|
||||||
final_limit = limit or self.max_truncate
|
final_limit = limit or self.max_truncate
|
||||||
if len(text) <= final_limit: return text
|
if len(text) <= final_limit: return text
|
||||||
head_limit = int(final_limit * 0.6)
|
head_limit = int(final_limit * 0.6)
|
||||||
tail_limit = int(final_limit * 0.4)
|
tail_limit = int(final_limit * 0.4)
|
||||||
return (text[:head_limit] + f"\n\n[... OUTPUT TRUNCATED ...]\n\n" + text[-tail_limit:])
|
return (text[:head_limit] + f"\n\n[... OUTPUT TRUNCATED ...]\n\n" + text[-tail_limit:])
|
||||||
|
|
||||||
|
def _print_debug_observation(self, fn, obs):
|
||||||
|
"""Prints a tool observation in a readable way during debug mode."""
|
||||||
|
# Try to parse as JSON if it's a string
|
||||||
|
if isinstance(obs, str):
|
||||||
|
try:
|
||||||
|
obs_data = json.loads(obs)
|
||||||
|
except Exception:
|
||||||
|
obs_data = obs
|
||||||
|
else:
|
||||||
|
obs_data = obs
|
||||||
|
|
||||||
|
if isinstance(obs_data, dict):
|
||||||
|
elements = []
|
||||||
|
for k, v in obs_data.items():
|
||||||
|
elements.append(Text(f"• {k}:", style="key"))
|
||||||
|
# Use Text for values to ensure newlines are rendered
|
||||||
|
val = str(v)
|
||||||
|
# If it's a multiline string from a delegation task, keep it clean
|
||||||
|
elements.append(Text(val))
|
||||||
|
|
||||||
|
if not elements:
|
||||||
|
content = Text("Empty data set")
|
||||||
|
else:
|
||||||
|
# Add a small spacer instead of a Rule for cleaner look
|
||||||
|
content = Group(*elements)
|
||||||
|
elif isinstance(obs_data, list):
|
||||||
|
content = Text("\n".join(f"• {item}" for item in obs_data))
|
||||||
|
else:
|
||||||
|
content = Text(str(obs_data))
|
||||||
|
|
||||||
|
title = f"[bold]{fn}[/bold]"
|
||||||
|
self.console.print(Panel(content, title=title, border_style="ai_status"))
|
||||||
|
|
||||||
def manage_memory_tool(self, content, action="append"):
|
def manage_memory_tool(self, content, action="append"):
|
||||||
"""Save or update long-term memory. Only use when user explicitly requests it."""
|
"""Save or update long-term memory. Only use when user explicitly requests it."""
|
||||||
if not content or not content.strip():
|
if not content or not content.strip():
|
||||||
@@ -1398,8 +1438,8 @@ class ai:
|
|||||||
ts = data.get("tags")
|
ts = data.get("tags")
|
||||||
if isinstance(ts, dict): os_tag = ts.get("os", "unknown")
|
if isinstance(ts, dict): os_tag = ts.get("os", "unknown")
|
||||||
res[name] = {"os": os_tag}
|
res[name] = {"os": os_tag}
|
||||||
return json.dumps(res)
|
return res
|
||||||
return json.dumps({"count": len(matched_names), "nodes": matched_names, "note": "Use 'get_node_info' for details."})
|
return {"count": len(matched_names), "nodes": matched_names, "note": "Use 'get_node_info' for details."}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error listing nodes: {str(e)}"
|
return f"Error listing nodes: {str(e)}"
|
||||||
|
|
||||||
@@ -1473,7 +1513,7 @@ class ai:
|
|||||||
if not matched_names: return "No nodes found matching filter."
|
if not matched_names: return "No nodes found matching filter."
|
||||||
thisnodes_dict = self.config.getitems(matched_names, extract=True)
|
thisnodes_dict = self.config.getitems(matched_names, extract=True)
|
||||||
result = nodes(thisnodes_dict, config=self.config).run(commands)
|
result = nodes(thisnodes_dict, config=self.config).run(commands)
|
||||||
return self._truncate(json.dumps(result))
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error executing commands: {str(e)}"
|
return f"Error executing commands: {str(e)}"
|
||||||
|
|
||||||
@@ -1482,7 +1522,7 @@ class ai:
|
|||||||
try:
|
try:
|
||||||
d = self.config.getitem(node_name, extract=True)
|
d = self.config.getitem(node_name, extract=True)
|
||||||
if 'password' in d: d['password'] = '***'
|
if 'password' in d: d['password'] = '***'
|
||||||
return json.dumps(d)
|
return d
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error getting node info: {str(e)}"
|
return f"Error getting node info: {str(e)}"
|
||||||
|
|
||||||
@@ -1526,7 +1566,7 @@ class ai:
|
|||||||
self.console.print(f"[warning] You can press Ctrl+C to interrupt and get a summary.[/warning]")
|
self.console.print(f"[warning] You can press Ctrl+C to interrupt and get a summary.[/warning]")
|
||||||
soft_limit_warned = True
|
soft_limit_warned = True
|
||||||
|
|
||||||
if status: status.update(f"[ai_status]Engineer: Analyzing mission... (step {iteration})")
|
if status and not chat_history: status.update(f"[ai_status]Engineer: Analyzing mission... (step {iteration})")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
safe_messages = self._sanitize_messages(messages)
|
safe_messages = self._sanitize_messages(messages)
|
||||||
@@ -1549,8 +1589,8 @@ class ai:
|
|||||||
for tc in resp_msg.tool_calls:
|
for tc in resp_msg.tool_calls:
|
||||||
fn, args = tc.function.name, json.loads(tc.function.arguments)
|
fn, args = tc.function.name, json.loads(tc.function.arguments)
|
||||||
|
|
||||||
# Notificación en tiempo real de la tarea técnica
|
# Notificación en tiempo real de la tarea técnica (Only if not in Architect loop)
|
||||||
if status:
|
if status and not chat_history:
|
||||||
if fn == "list_nodes": status.update(f"[ai_status]Engineer: [SEARCH] {args.get('filter_pattern','.*')}")
|
if fn == "list_nodes": status.update(f"[ai_status]Engineer: [SEARCH] {args.get('filter_pattern','.*')}")
|
||||||
elif fn == "run_commands":
|
elif fn == "run_commands":
|
||||||
cmds = args.get('commands', [])
|
cmds = args.get('commands', [])
|
||||||
@@ -1559,7 +1599,8 @@ class ai:
|
|||||||
elif fn == "get_node_info": status.update(f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}")
|
elif fn == "get_node_info": status.update(f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}")
|
||||||
elif fn in self.tool_status_formatters: status.update(self.tool_status_formatters[fn](args))
|
elif fn in self.tool_status_formatters: status.update(self.tool_status_formatters[fn](args))
|
||||||
|
|
||||||
if debug: self.console.print(Panel(Text(json.dumps(args, indent=2)), title=f"[bold engineer]Engineer Tool: {fn}[/bold engineer]", border_style="engineer"))
|
if debug:
|
||||||
|
self._print_debug_observation(f"Decision: {fn}", args)
|
||||||
|
|
||||||
if fn == "list_nodes": obs = self.list_nodes_tool(**args)
|
if fn == "list_nodes": obs = self.list_nodes_tool(**args)
|
||||||
elif fn == "run_commands": obs = self.run_commands_tool(**args, status=status)
|
elif fn == "run_commands": obs = self.run_commands_tool(**args, status=status)
|
||||||
@@ -1567,8 +1608,12 @@ class ai:
|
|||||||
elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args)
|
elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args)
|
||||||
else: obs = f"Error: Unknown tool '{fn}'."
|
else: obs = f"Error: Unknown tool '{fn}'."
|
||||||
|
|
||||||
if debug: self.console.print(Panel(Text(str(obs)), title=f"[bold pass]Engineer Observation: {fn}[/bold pass]", border_style="success"))
|
if debug:
|
||||||
messages.append({"tool_call_id": tc.id, "role": "tool", "name": fn, "content": obs})
|
self._print_debug_observation(f"Observation: {fn}", obs)
|
||||||
|
|
||||||
|
# Ensure observation is a string and truncated for the LLM
|
||||||
|
obs_str = obs if isinstance(obs, str) else json.dumps(obs)
|
||||||
|
messages.append({"tool_call_id": tc.id, "role": "tool", "name": fn, "content": self._truncate(obs_str)})
|
||||||
|
|
||||||
if iteration >= self.hard_limit_iterations:
|
if iteration >= self.hard_limit_iterations:
|
||||||
self.console.print(f"[error]⛔ Engineer reached hard limit ({self.hard_limit_iterations} steps). Forcing stop.[/error]")
|
self.console.print(f"[error]⛔ Engineer reached hard limit ({self.hard_limit_iterations} steps). Forcing stop.[/error]")
|
||||||
@@ -1582,30 +1627,46 @@ class ai:
|
|||||||
|
|
||||||
def _get_engineer_tools(self):
|
def _get_engineer_tools(self):
|
||||||
"""Define tools available to the Engineer."""
|
"""Define tools available to the Engineer."""
|
||||||
tools = [
|
base_tools = [
|
||||||
{"type": "function", "function": {"name": "list_nodes", "description": "Lists available nodes in the inventory.", "parameters": {"type": "object", "properties": {"filter_pattern": {"type": "string", "description": "Regex to filter nodes (e.g. '.*', 'border.*')."}}}}},
|
{"type": "function", "function": {"name": "list_nodes", "description": "Lists available nodes in the inventory.", "parameters": {"type": "object", "properties": {"filter_pattern": {"type": "string", "description": "Regex to filter nodes (e.g. '.*', 'border.*')."}}}}},
|
||||||
{"type": "function", "function": {"name": "run_commands", "description": "Runs one or more commands on matched nodes. MANDATORY: You MUST call 'list_nodes' first to verify the target list.", "parameters": {"type": "object", "properties": {"nodes_filter": {"type": "string", "description": "Exact node name or verified filter pattern."}, "commands": {"type": "array", "items": {"type": "string"}, "description": "List of commands (e.g. ['show ip route', 'show int desc'])."}}, "required": ["nodes_filter", "commands"]}}},
|
{"type": "function", "function": {"name": "run_commands", "description": "Runs one or more commands on matched nodes. MANDATORY: You MUST call 'list_nodes' first to verify the target list.", "parameters": {"type": "object", "properties": {"nodes_filter": {"type": "string", "description": "Exact node name or verified filter pattern."}, "commands": {"type": "array", "items": {"type": "string"}, "description": "List of commands (e.g. ['show ip route', 'show int desc'])."}}, "required": ["nodes_filter", "commands"]}}},
|
||||||
{"type": "function", "function": {"name": "get_node_info", "description": "Gets full metadata for a specific node.", "parameters": {"type": "object", "properties": {"node_name": {"type": "string"}}, "required": ["node_name"]}}}
|
{"type": "function", "function": {"name": "get_node_info", "description": "Gets full metadata for a specific node.", "parameters": {"type": "object", "properties": {"node_name": {"type": "string"}}, "required": ["node_name"]}}}
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.architect_key:
|
if self.architect_key:
|
||||||
tools.extend([
|
base_tools.extend([
|
||||||
{"type": "function", "function": {"name": "consult_architect", "description": "Ask the Strategic Reasoning Engine for advice on complex design, architecture, or troubleshooting decisions. You remain in control and will present the response to the user. Use this for: configuration planning, design validation, complex troubleshooting.", "parameters": {"type": "object", "properties": {"question": {"type": "string", "description": "Strategic question or decision needed."}, "technical_summary": {"type": "string", "description": "Technical findings and context gathered so far."}}, "required": ["question", "technical_summary"]}}},
|
{"type": "function", "function": {"name": "consult_architect", "description": "Ask the Strategic Reasoning Engine for advice on complex design, architecture, or troubleshooting decisions. You remain in control and will present the response to the user. Use this for: configuration planning, design validation, complex troubleshooting.", "parameters": {"type": "object", "properties": {"question": {"type": "string", "description": "Strategic question or decision needed."}, "technical_summary": {"type": "string", "description": "Technical findings and context gathered so far."}}, "required": ["question", "technical_summary"]}}},
|
||||||
{"type": "function", "function": {"name": "escalate_to_architect", "description": "Transfer full control to the Strategic Reasoning Engine. Use ONLY when the user explicitly requests the Architect or when the problem requires strategic oversight beyond consultation. After escalation, the Architect takes over the conversation.", "parameters": {"type": "object", "properties": {"reason": {"type": "string", "description": "Why you're escalating (e.g. 'User requested Architect', 'Complex multi-site design needed')."}, "context": {"type": "string", "description": "Full context and findings to hand over."}}, "required": ["reason", "context"]}}}
|
{"type": "function", "function": {"name": "escalate_to_architect", "description": "Transfer full control to the Strategic Reasoning Engine. Use ONLY when the user explicitly requests the Architect or when the problem requires strategic oversight beyond consultation. After escalation, the Architect takes over the conversation.", "parameters": {"type": "object", "properties": {"reason": {"type": "string", "description": "Why you're escalating (e.g. 'User requested Architect', 'Complex multi-site design needed')."}, "context": {"type": "string", "description": "Full context and findings to hand over."}}, "required": ["reason", "context"]}}}
|
||||||
])
|
])
|
||||||
|
|
||||||
tools.extend(self.external_engineer_tools)
|
# Deduplicate by name to prevent Gemini BadRequestError
|
||||||
return tools
|
all_tools = base_tools + self.external_engineer_tools
|
||||||
|
seen_names = set()
|
||||||
|
unique_tools = []
|
||||||
|
for t in all_tools:
|
||||||
|
name = t["function"]["name"]
|
||||||
|
if name not in seen_names:
|
||||||
|
unique_tools.append(t)
|
||||||
|
seen_names.add(name)
|
||||||
|
return unique_tools
|
||||||
|
|
||||||
def _get_architect_tools(self):
|
def _get_architect_tools(self):
|
||||||
"""Define tools available to the Strategic Reasoning Engine."""
|
"""Define tools available to the Strategic Reasoning Engine."""
|
||||||
tools = [
|
base_tools = [
|
||||||
{"type": "function", "function": {"name": "delegate_to_engineer", "description": "Delegates a technical mission to the Engineer.", "parameters": {"type": "object", "properties": {"task": {"type": "string", "description": "Detailed technical mission or goal."}}, "required": ["task"]}}},
|
{"type": "function", "function": {"name": "delegate_to_engineer", "description": "Delegates a technical mission to the Engineer.", "parameters": {"type": "object", "properties": {"task": {"type": "string", "description": "Detailed technical mission or goal."}}, "required": ["task"]}}},
|
||||||
{"type": "function", "function": {"name": "return_to_engineer", "description": "Return control to the Engineer. Use this when your strategic analysis is complete and the Engineer should handle the rest of the conversation.", "parameters": {"type": "object", "properties": {"summary": {"type": "string", "description": "Brief summary of your analysis to hand over to the Engineer."}}, "required": ["summary"]}}},
|
{"type": "function", "function": {"name": "return_to_engineer", "description": "Return control to the Engineer. Use this when your strategic analysis is complete and the Engineer should handle the rest of the conversation.", "parameters": {"type": "object", "properties": {"summary": {"type": "string", "description": "Brief summary of your analysis to hand over to the Engineer."}}, "required": ["summary"]}}},
|
||||||
{"type": "function", "function": {"name": "manage_memory_tool", "description": "Saves information to long-term memory. MANDATORY: Only use this if the user explicitly asks to remember or save something.", "parameters": {"type": "object", "properties": {"content": {"type": "string"}, "action": {"type": "string", "enum": ["append", "replace"]}}, "required": ["content"]}}}
|
{"type": "function", "function": {"name": "manage_memory_tool", "description": "Saves information to long-term memory. MANDATORY: Only use this if the user explicitly asks to remember or save something.", "parameters": {"type": "object", "properties": {"content": {"type": "string"}, "action": {"type": "string", "enum": ["append", "replace"]}}, "required": ["content"]}}}
|
||||||
]
|
]
|
||||||
tools.extend(self.external_architect_tools)
|
|
||||||
return tools
|
all_tools = base_tools + self.external_architect_tools
|
||||||
|
seen_names = set()
|
||||||
|
unique_tools = []
|
||||||
|
for t in all_tools:
|
||||||
|
name = t["function"]["name"]
|
||||||
|
if name not in seen_names:
|
||||||
|
unique_tools.append(t)
|
||||||
|
seen_names.add(name)
|
||||||
|
return unique_tools
|
||||||
|
|
||||||
def _get_sessions(self):
|
def _get_sessions(self):
|
||||||
"""Returns a list of session metadata sorted by date."""
|
"""Returns a list of session metadata sorted by date."""
|
||||||
@@ -1809,12 +1870,16 @@ class ai:
|
|||||||
soft_limit_warned = True
|
soft_limit_warned = True
|
||||||
|
|
||||||
label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]"
|
label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]"
|
||||||
if status: status.update(f"{label} is thinking... (step {iteration})")
|
if status:
|
||||||
|
# Notify responder identity ONLY for web/remote clients (StatusBridge has is_web)
|
||||||
|
if getattr(status, "is_web", False):
|
||||||
|
status.update(f"__RESPONDER__:{current_brain}")
|
||||||
|
status.update(f"{label} is thinking... (step {iteration})")
|
||||||
|
|
||||||
streamed_response = False
|
streamed_response = False
|
||||||
try:
|
try:
|
||||||
safe_messages = self._sanitize_messages(messages)
|
safe_messages = self._sanitize_messages(messages)
|
||||||
if stream and not debug:
|
if stream and (not debug or chunk_callback):
|
||||||
response, streamed_response = self._stream_completion(
|
response, streamed_response = self._stream_completion(
|
||||||
model=model, messages=safe_messages, tools=tools, api_key=key,
|
model=model, messages=safe_messages, tools=tools, api_key=key,
|
||||||
status=status, label=label, debug=debug, num_retries=3,
|
status=status, label=label, debug=debug, num_retries=3,
|
||||||
@@ -1854,7 +1919,10 @@ class ai:
|
|||||||
messages.append(msg_dict)
|
messages.append(msg_dict)
|
||||||
|
|
||||||
if debug and resp_msg.content:
|
if debug and resp_msg.content:
|
||||||
self.console.print(Panel(Markdown(resp_msg.content), title=f"{label} Reasoning", border_style="architect" if current_brain == "architect" else "engineer"))
|
# In CLI debug mode, only print intermediate reasoning if there are tool calls.
|
||||||
|
# If there are no tool calls, this content is the final answer and will be printed by the caller.
|
||||||
|
if resp_msg.tool_calls:
|
||||||
|
self.console.print(Panel(Markdown(resp_msg.content), title=f"[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]", border_style="architect" if current_brain == "architect" else "engineer"))
|
||||||
|
|
||||||
if not resp_msg.tool_calls: break
|
if not resp_msg.tool_calls: break
|
||||||
|
|
||||||
@@ -1874,7 +1942,8 @@ class ai:
|
|||||||
if fn == "delegate_to_engineer": status.update(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
|
if fn == "delegate_to_engineer": status.update(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
|
||||||
elif fn == "manage_memory_tool": status.update(f"[architect]Architect: [UPDATING MEMORY]")
|
elif fn == "manage_memory_tool": status.update(f"[architect]Architect: [UPDATING MEMORY]")
|
||||||
|
|
||||||
if debug: self.console.print(Panel(Text(json.dumps(args, indent=2)), title=f"{label} Decision: {fn}", border_style="debug"))
|
if debug:
|
||||||
|
self._print_debug_observation(f"Decision: {fn}", args)
|
||||||
|
|
||||||
if fn == "delegate_to_engineer":
|
if fn == "delegate_to_engineer":
|
||||||
obs, eng_usage = self._engineer_loop(args["task"], status=status, debug=debug, chat_history=messages[:-1])
|
obs, eng_usage = self._engineer_loop(args["task"], status=status, debug=debug, chat_history=messages[:-1])
|
||||||
@@ -1933,8 +2002,12 @@ class ai:
|
|||||||
elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args)
|
elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args)
|
||||||
else: obs = f"Error: {fn} unknown."
|
else: obs = f"Error: {fn} unknown."
|
||||||
|
|
||||||
messages.append({"tool_call_id": tc.id, "role": "tool", "name": fn, "content": obs})
|
if debug and fn not in ["delegate_to_engineer", "consult_architect", "escalate_to_architect", "return_to_engineer"]:
|
||||||
|
self._print_debug_observation(f"Observation: {fn}", obs)
|
||||||
|
|
||||||
|
# Ensure observation is a string and truncated for the LLM
|
||||||
|
obs_str = obs if isinstance(obs, str) else json.dumps(obs)
|
||||||
|
messages.append({"tool_call_id": tc.id, "role": "tool", "name": fn, "content": self._truncate(obs_str)})
|
||||||
# Inject pending user message AFTER all tool responses are added
|
# Inject pending user message AFTER all tool responses are added
|
||||||
if pending_user_message:
|
if pending_user_message:
|
||||||
messages.append({"role": "user", "content": pending_user_message})
|
messages.append({"role": "user", "content": pending_user_message})
|
||||||
@@ -1960,14 +2033,25 @@ class ai:
|
|||||||
if last_msg.get("tool_calls"):
|
if last_msg.get("tool_calls"):
|
||||||
for tc in last_msg["tool_calls"]:
|
for tc in last_msg["tool_calls"]:
|
||||||
messages.append({"tool_call_id": tc.get("id"), "role": "tool", "name": tc.get("function", {}).get("name"), "content": "Operation cancelled by user."})
|
messages.append({"tool_call_id": tc.get("id"), "role": "tool", "name": tc.get("function", {}).get("name"), "content": "Operation cancelled by user."})
|
||||||
messages.append({"role": "user", "content": "USER INTERRUPTED. Briefly summarize what you were doing and stop."})
|
|
||||||
|
# Use a fresh list for the summary call to avoid history corruption
|
||||||
|
summary_messages = list(messages)
|
||||||
|
summary_messages.append({"role": "user", "content": "USER INTERRUPTED. Briefly summarize what you were doing and stop."})
|
||||||
try:
|
try:
|
||||||
safe_messages = self._sanitize_messages(messages)
|
safe_messages = self._sanitize_messages(summary_messages)
|
||||||
# Use tools=None to force a text summary during interruption
|
# Use tools=None to force a text summary during interruption
|
||||||
response = completion(model=model, messages=safe_messages, tools=None, api_key=key)
|
response = completion(model=model, messages=safe_messages, tools=None, api_key=key)
|
||||||
resp_msg = response.choices[0].message
|
resp_msg = response.choices[0].message
|
||||||
messages.append(resp_msg.model_dump(exclude_none=True))
|
messages.append(resp_msg.model_dump(exclude_none=True))
|
||||||
except Exception: pass
|
|
||||||
|
# IMPORTANT: Manually trigger callback for the summary so Web UI sees it
|
||||||
|
if chunk_callback and resp_msg.content:
|
||||||
|
chunk_callback(resp_msg.content)
|
||||||
|
except Exception:
|
||||||
|
error_msg = "Operation interrupted by user. Summary unavailable."
|
||||||
|
messages.append({"role": "assistant", "content": error_msg})
|
||||||
|
if chunk_callback:
|
||||||
|
chunk_callback(error_msg)
|
||||||
finally:
|
finally:
|
||||||
# Auto-save session
|
# Auto-save session
|
||||||
self.save_session(messages, model=model)
|
self.save_session(messages, model=model)
|
||||||
@@ -1989,7 +2073,7 @@ class ai:
|
|||||||
<dl>
|
<dl>
|
||||||
<dt id="connpy.ai.SAFE_COMMANDS"><code class="name">var <span class="ident">SAFE_COMMANDS</span></code></dt>
|
<dt id="connpy.ai.SAFE_COMMANDS"><code class="name">var <span class="ident">SAFE_COMMANDS</span></code></dt>
|
||||||
<dd>
|
<dd>
|
||||||
<div class="desc"></div>
|
<div class="desc"><p>The type of the None singleton.</p></div>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<h3>Instance variables</h3>
|
<h3>Instance variables</h3>
|
||||||
@@ -2132,12 +2216,16 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
|||||||
soft_limit_warned = True
|
soft_limit_warned = True
|
||||||
|
|
||||||
label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]"
|
label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]"
|
||||||
if status: status.update(f"{label} is thinking... (step {iteration})")
|
if status:
|
||||||
|
# Notify responder identity ONLY for web/remote clients (StatusBridge has is_web)
|
||||||
|
if getattr(status, "is_web", False):
|
||||||
|
status.update(f"__RESPONDER__:{current_brain}")
|
||||||
|
status.update(f"{label} is thinking... (step {iteration})")
|
||||||
|
|
||||||
streamed_response = False
|
streamed_response = False
|
||||||
try:
|
try:
|
||||||
safe_messages = self._sanitize_messages(messages)
|
safe_messages = self._sanitize_messages(messages)
|
||||||
if stream and not debug:
|
if stream and (not debug or chunk_callback):
|
||||||
response, streamed_response = self._stream_completion(
|
response, streamed_response = self._stream_completion(
|
||||||
model=model, messages=safe_messages, tools=tools, api_key=key,
|
model=model, messages=safe_messages, tools=tools, api_key=key,
|
||||||
status=status, label=label, debug=debug, num_retries=3,
|
status=status, label=label, debug=debug, num_retries=3,
|
||||||
@@ -2177,7 +2265,10 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
|||||||
messages.append(msg_dict)
|
messages.append(msg_dict)
|
||||||
|
|
||||||
if debug and resp_msg.content:
|
if debug and resp_msg.content:
|
||||||
self.console.print(Panel(Markdown(resp_msg.content), title=f"{label} Reasoning", border_style="architect" if current_brain == "architect" else "engineer"))
|
# In CLI debug mode, only print intermediate reasoning if there are tool calls.
|
||||||
|
# If there are no tool calls, this content is the final answer and will be printed by the caller.
|
||||||
|
if resp_msg.tool_calls:
|
||||||
|
self.console.print(Panel(Markdown(resp_msg.content), title=f"[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]", border_style="architect" if current_brain == "architect" else "engineer"))
|
||||||
|
|
||||||
if not resp_msg.tool_calls: break
|
if not resp_msg.tool_calls: break
|
||||||
|
|
||||||
@@ -2197,7 +2288,8 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
|||||||
if fn == "delegate_to_engineer": status.update(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
|
if fn == "delegate_to_engineer": status.update(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
|
||||||
elif fn == "manage_memory_tool": status.update(f"[architect]Architect: [UPDATING MEMORY]")
|
elif fn == "manage_memory_tool": status.update(f"[architect]Architect: [UPDATING MEMORY]")
|
||||||
|
|
||||||
if debug: self.console.print(Panel(Text(json.dumps(args, indent=2)), title=f"{label} Decision: {fn}", border_style="debug"))
|
if debug:
|
||||||
|
self._print_debug_observation(f"Decision: {fn}", args)
|
||||||
|
|
||||||
if fn == "delegate_to_engineer":
|
if fn == "delegate_to_engineer":
|
||||||
obs, eng_usage = self._engineer_loop(args["task"], status=status, debug=debug, chat_history=messages[:-1])
|
obs, eng_usage = self._engineer_loop(args["task"], status=status, debug=debug, chat_history=messages[:-1])
|
||||||
@@ -2256,8 +2348,12 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
|||||||
elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args)
|
elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args)
|
||||||
else: obs = f"Error: {fn} unknown."
|
else: obs = f"Error: {fn} unknown."
|
||||||
|
|
||||||
messages.append({"tool_call_id": tc.id, "role": "tool", "name": fn, "content": obs})
|
if debug and fn not in ["delegate_to_engineer", "consult_architect", "escalate_to_architect", "return_to_engineer"]:
|
||||||
|
self._print_debug_observation(f"Observation: {fn}", obs)
|
||||||
|
|
||||||
|
# Ensure observation is a string and truncated for the LLM
|
||||||
|
obs_str = obs if isinstance(obs, str) else json.dumps(obs)
|
||||||
|
messages.append({"tool_call_id": tc.id, "role": "tool", "name": fn, "content": self._truncate(obs_str)})
|
||||||
# Inject pending user message AFTER all tool responses are added
|
# Inject pending user message AFTER all tool responses are added
|
||||||
if pending_user_message:
|
if pending_user_message:
|
||||||
messages.append({"role": "user", "content": pending_user_message})
|
messages.append({"role": "user", "content": pending_user_message})
|
||||||
@@ -2283,14 +2379,25 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
|||||||
if last_msg.get("tool_calls"):
|
if last_msg.get("tool_calls"):
|
||||||
for tc in last_msg["tool_calls"]:
|
for tc in last_msg["tool_calls"]:
|
||||||
messages.append({"tool_call_id": tc.get("id"), "role": "tool", "name": tc.get("function", {}).get("name"), "content": "Operation cancelled by user."})
|
messages.append({"tool_call_id": tc.get("id"), "role": "tool", "name": tc.get("function", {}).get("name"), "content": "Operation cancelled by user."})
|
||||||
messages.append({"role": "user", "content": "USER INTERRUPTED. Briefly summarize what you were doing and stop."})
|
|
||||||
|
# Use a fresh list for the summary call to avoid history corruption
|
||||||
|
summary_messages = list(messages)
|
||||||
|
summary_messages.append({"role": "user", "content": "USER INTERRUPTED. Briefly summarize what you were doing and stop."})
|
||||||
try:
|
try:
|
||||||
safe_messages = self._sanitize_messages(messages)
|
safe_messages = self._sanitize_messages(summary_messages)
|
||||||
# Use tools=None to force a text summary during interruption
|
# Use tools=None to force a text summary during interruption
|
||||||
response = completion(model=model, messages=safe_messages, tools=None, api_key=key)
|
response = completion(model=model, messages=safe_messages, tools=None, api_key=key)
|
||||||
resp_msg = response.choices[0].message
|
resp_msg = response.choices[0].message
|
||||||
messages.append(resp_msg.model_dump(exclude_none=True))
|
messages.append(resp_msg.model_dump(exclude_none=True))
|
||||||
except Exception: pass
|
|
||||||
|
# IMPORTANT: Manually trigger callback for the summary so Web UI sees it
|
||||||
|
if chunk_callback and resp_msg.content:
|
||||||
|
chunk_callback(resp_msg.content)
|
||||||
|
except Exception:
|
||||||
|
error_msg = "Operation interrupted by user. Summary unavailable."
|
||||||
|
messages.append({"role": "assistant", "content": error_msg})
|
||||||
|
if chunk_callback:
|
||||||
|
chunk_callback(error_msg)
|
||||||
finally:
|
finally:
|
||||||
# Auto-save session
|
# Auto-save session
|
||||||
self.save_session(messages, model=model)
|
self.save_session(messages, model=model)
|
||||||
@@ -2366,7 +2473,7 @@ def confirm(self, user_input): return True</code></pre>
|
|||||||
try:
|
try:
|
||||||
d = self.config.getitem(node_name, extract=True)
|
d = self.config.getitem(node_name, extract=True)
|
||||||
if 'password' in d: d['password'] = '***'
|
if 'password' in d: d['password'] = '***'
|
||||||
return json.dumps(d)
|
return d
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error getting node info: {str(e)}"</code></pre>
|
return f"Error getting node info: {str(e)}"</code></pre>
|
||||||
</details>
|
</details>
|
||||||
@@ -2394,8 +2501,8 @@ def confirm(self, user_input): return True</code></pre>
|
|||||||
ts = data.get("tags")
|
ts = data.get("tags")
|
||||||
if isinstance(ts, dict): os_tag = ts.get("os", "unknown")
|
if isinstance(ts, dict): os_tag = ts.get("os", "unknown")
|
||||||
res[name] = {"os": os_tag}
|
res[name] = {"os": os_tag}
|
||||||
return json.dumps(res)
|
return res
|
||||||
return json.dumps({"count": len(matched_names), "nodes": matched_names, "note": "Use 'get_node_info' for details."})
|
return {"count": len(matched_names), "nodes": matched_names, "note": "Use 'get_node_info' for details."}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error listing nodes: {str(e)}"</code></pre>
|
return f"Error listing nodes: {str(e)}"</code></pre>
|
||||||
</details>
|
</details>
|
||||||
@@ -2498,14 +2605,20 @@ def confirm(self, user_input): return True</code></pre>
|
|||||||
status_formatter (callable): Function(args_dict) -> status string.
|
status_formatter (callable): Function(args_dict) -> status string.
|
||||||
"""
|
"""
|
||||||
name = tool_definition["function"]["name"]
|
name = tool_definition["function"]["name"]
|
||||||
|
|
||||||
|
# Check if already registered to prevent duplicates
|
||||||
if target in ("engineer", "both"):
|
if target in ("engineer", "both"):
|
||||||
|
if not any(t["function"]["name"] == name for t in self.external_engineer_tools):
|
||||||
self.external_engineer_tools.append(tool_definition)
|
self.external_engineer_tools.append(tool_definition)
|
||||||
if target in ("architect", "both"):
|
if target in ("architect", "both"):
|
||||||
|
if not any(t["function"]["name"] == name for t in self.external_architect_tools):
|
||||||
self.external_architect_tools.append(tool_definition)
|
self.external_architect_tools.append(tool_definition)
|
||||||
|
|
||||||
self.external_tool_handlers[name] = handler
|
self.external_tool_handlers[name] = handler
|
||||||
if engineer_prompt:
|
|
||||||
|
if engineer_prompt and engineer_prompt not in self.engineer_prompt_extensions:
|
||||||
self.engineer_prompt_extensions.append(engineer_prompt)
|
self.engineer_prompt_extensions.append(engineer_prompt)
|
||||||
if architect_prompt:
|
if architect_prompt and architect_prompt not in self.architect_prompt_extensions:
|
||||||
self.architect_prompt_extensions.append(architect_prompt)
|
self.architect_prompt_extensions.append(architect_prompt)
|
||||||
if status_formatter:
|
if status_formatter:
|
||||||
self.tool_status_formatters[name] = status_formatter</code></pre>
|
self.tool_status_formatters[name] = status_formatter</code></pre>
|
||||||
@@ -2601,7 +2714,7 @@ def confirm(self, user_input): return True</code></pre>
|
|||||||
if not matched_names: return "No nodes found matching filter."
|
if not matched_names: return "No nodes found matching filter."
|
||||||
thisnodes_dict = self.config.getitems(matched_names, extract=True)
|
thisnodes_dict = self.config.getitems(matched_names, extract=True)
|
||||||
result = nodes(thisnodes_dict, config=self.config).run(commands)
|
result = nodes(thisnodes_dict, config=self.config).run(commands)
|
||||||
return self._truncate(json.dumps(result))
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error executing commands: {str(e)}"</code></pre>
|
return f"Error executing commands: {str(e)}"</code></pre>
|
||||||
</details>
|
</details>
|
||||||
@@ -3761,7 +3874,8 @@ class node:
|
|||||||
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
||||||
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
||||||
if logger:
|
if logger:
|
||||||
logger("success", "Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}")
|
||||||
|
|
||||||
if 'logfile' in dir(self):
|
if 'logfile' in dir(self):
|
||||||
# Initialize self.mylog
|
# Initialize self.mylog
|
||||||
@@ -3840,7 +3954,8 @@ class node:
|
|||||||
now = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')
|
now = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')
|
||||||
if connect == True:
|
if connect == True:
|
||||||
if logger:
|
if logger:
|
||||||
logger("success", "Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}")
|
||||||
|
|
||||||
# Attempt to set the terminal size
|
# Attempt to set the terminal size
|
||||||
try:
|
try:
|
||||||
@@ -3941,7 +4056,8 @@ class node:
|
|||||||
connect = self._connect(timeout = timeout, logger = logger)
|
connect = self._connect(timeout = timeout, logger = logger)
|
||||||
if connect == True:
|
if connect == True:
|
||||||
if logger:
|
if logger:
|
||||||
logger("success", "Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}")
|
||||||
|
|
||||||
# Attempt to set the terminal size
|
# Attempt to set the terminal size
|
||||||
try:
|
try:
|
||||||
@@ -4046,6 +4162,19 @@ class node:
|
|||||||
cmd += f" {docker_command}"
|
cmd += f" {docker_command}"
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
@MethodHook
|
||||||
|
def _generate_ssm_cmd(self):
|
||||||
|
region = self.tags.get("region", "") if isinstance(self.tags, dict) else ""
|
||||||
|
profile = self.tags.get("profile", "") if isinstance(self.tags, dict) else ""
|
||||||
|
cmd = f"aws ssm start-session --target {self.host}"
|
||||||
|
if region:
|
||||||
|
cmd += f" --region {region}"
|
||||||
|
if profile:
|
||||||
|
cmd += f" --profile {profile}"
|
||||||
|
if self.options:
|
||||||
|
cmd += f" {self.options}"
|
||||||
|
return cmd
|
||||||
|
|
||||||
@MethodHook
|
@MethodHook
|
||||||
def _get_cmd(self):
|
def _get_cmd(self):
|
||||||
if self.protocol in ["ssh", "sftp"]:
|
if self.protocol in ["ssh", "sftp"]:
|
||||||
@@ -4056,6 +4185,8 @@ class node:
|
|||||||
return self._generate_kube_cmd()
|
return self._generate_kube_cmd()
|
||||||
elif self.protocol == "docker":
|
elif self.protocol == "docker":
|
||||||
return self._generate_docker_cmd()
|
return self._generate_docker_cmd()
|
||||||
|
elif self.protocol == "ssm":
|
||||||
|
return self._generate_ssm_cmd()
|
||||||
else:
|
else:
|
||||||
printer.error(f"Invalid protocol: {self.protocol}")
|
printer.error(f"Invalid protocol: {self.protocol}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -4076,7 +4207,8 @@ class node:
|
|||||||
"sftp": ['yes/no', 'refused', 'supported', 'Invalid|[u|U]sage: sftp', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
|
"sftp": ['yes/no', 'refused', 'supported', 'Invalid|[u|U]sage: sftp', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
|
||||||
"telnet": ['[u|U]sername:', 'refused', 'supported', 'invalid|unrecognized option', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
|
"telnet": ['[u|U]sername:', 'refused', 'supported', 'invalid|unrecognized option', 'ssh-keygen.*\"', 'timeout|timed.out', 'unavailable', 'closed', password_prompt, prompt, 'suspend', pexpect.EOF, pexpect.TIMEOUT, "No route to host", "resolve hostname", "no matching", "[b|B]ad (owner|permissions)"],
|
||||||
"kubectl": ['[u|U]sername:', '[r|R]efused', '[E|e]rror', 'DEPRECATED', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF, "expired|invalid"],
|
"kubectl": ['[u|U]sername:', '[r|R]efused', '[E|e]rror', 'DEPRECATED', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF, "expired|invalid"],
|
||||||
"docker": ['[u|U]sername:', 'Cannot', '[E|e]rror', 'failed', 'not a docker command', 'unknown', 'unable to resolve', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF]
|
"docker": ['[u|U]sername:', 'Cannot', '[E|e]rror', 'failed', 'not a docker command', 'unknown', 'unable to resolve', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF],
|
||||||
|
"ssm": ['[u|U]sername:', 'Cannot', '[E|e]rror', 'failed', 'SessionManagerPlugin', 'unknown', 'unable to resolve', pexpect.TIMEOUT, password_prompt, prompt, pexpect.EOF]
|
||||||
}
|
}
|
||||||
|
|
||||||
error_indices = {
|
error_indices = {
|
||||||
@@ -4084,7 +4216,8 @@ class node:
|
|||||||
"sftp": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
|
"sftp": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
|
||||||
"telnet": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
|
"telnet": [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16],
|
||||||
"kubectl": [1, 2, 3, 4, 8], # Define error indices for kube
|
"kubectl": [1, 2, 3, 4, 8], # Define error indices for kube
|
||||||
"docker": [1, 2, 3, 4, 5, 6, 7] # Define error indices for docker
|
"docker": [1, 2, 3, 4, 5, 6, 7], # Define error indices for docker
|
||||||
|
"ssm": [1, 2, 3, 4, 5, 6, 7]
|
||||||
}
|
}
|
||||||
|
|
||||||
eof_indices = {
|
eof_indices = {
|
||||||
@@ -4092,7 +4225,8 @@ class node:
|
|||||||
"sftp": [8, 9, 10, 11],
|
"sftp": [8, 9, 10, 11],
|
||||||
"telnet": [8, 9, 10, 11],
|
"telnet": [8, 9, 10, 11],
|
||||||
"kubectl": [5, 6, 7], # Define eof indices for kube
|
"kubectl": [5, 6, 7], # Define eof indices for kube
|
||||||
"docker": [8, 9, 10] # Define eof indices for docker
|
"docker": [8, 9, 10], # Define eof indices for docker
|
||||||
|
"ssm": [8, 9, 10]
|
||||||
}
|
}
|
||||||
|
|
||||||
initial_indices = {
|
initial_indices = {
|
||||||
@@ -4100,7 +4234,8 @@ class node:
|
|||||||
"sftp": [0],
|
"sftp": [0],
|
||||||
"telnet": [0],
|
"telnet": [0],
|
||||||
"kubectl": [0], # Define special indices for kube
|
"kubectl": [0], # Define special indices for kube
|
||||||
"docker": [0] # Define special indices for docker
|
"docker": [0], # Define special indices for docker
|
||||||
|
"ssm": [0]
|
||||||
}
|
}
|
||||||
|
|
||||||
attempts = 1
|
attempts = 1
|
||||||
@@ -4124,7 +4259,7 @@ class node:
|
|||||||
if results in initial_indices[self.protocol]:
|
if results in initial_indices[self.protocol]:
|
||||||
if self.protocol in ["ssh", "sftp"]:
|
if self.protocol in ["ssh", "sftp"]:
|
||||||
child.sendline('yes')
|
child.sendline('yes')
|
||||||
elif self.protocol in ["telnet", "kubectl", "docker"]:
|
elif self.protocol in ["telnet", "kubectl", "docker", "ssm"]:
|
||||||
if self.user:
|
if self.user:
|
||||||
child.sendline(self.user)
|
child.sendline(self.user)
|
||||||
else:
|
else:
|
||||||
@@ -4244,7 +4379,8 @@ def interact(self, debug = False, logger = None):
|
|||||||
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
|
||||||
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
|
||||||
if logger:
|
if logger:
|
||||||
logger("success", "Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}")
|
||||||
|
|
||||||
if 'logfile' in dir(self):
|
if 'logfile' in dir(self):
|
||||||
# Initialize self.mylog
|
# Initialize self.mylog
|
||||||
@@ -4337,7 +4473,8 @@ def run(self, commands, vars = None,*, folder = '', prompt = r'>$
|
|||||||
now = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')
|
now = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')
|
||||||
if connect == True:
|
if connect == True:
|
||||||
if logger:
|
if logger:
|
||||||
logger("success", "Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}")
|
||||||
|
|
||||||
# Attempt to set the terminal size
|
# Attempt to set the terminal size
|
||||||
try:
|
try:
|
||||||
@@ -4479,7 +4616,8 @@ def test(self, commands, expected, vars = None,*, prompt = r'>$|#$|\$$|&g
|
|||||||
connect = self._connect(timeout = timeout, logger = logger)
|
connect = self._connect(timeout = timeout, logger = logger)
|
||||||
if connect == True:
|
if connect == True:
|
||||||
if logger:
|
if logger:
|
||||||
logger("success", "Connected to " + self.unique + " at " + self.host + (":" if self.port != '' else '') + self.port + " via: " + self.protocol)
|
port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else ""
|
||||||
|
logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}")
|
||||||
|
|
||||||
# Attempt to set the terminal size
|
# Attempt to set the terminal size
|
||||||
try:
|
try:
|
||||||
@@ -5268,7 +5406,7 @@ def test(self, commands, expected, vars = None,*, prompt = None, parallel = 10,
|
|||||||
<li><h3><a href="#header-submodules">Sub-modules</a></h3>
|
<li><h3><a href="#header-submodules">Sub-modules</a></h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code><a title="connpy.cli" href="cli/index.html">connpy.cli</a></code></li>
|
<li><code><a title="connpy.cli" href="cli/index.html">connpy.cli</a></code></li>
|
||||||
<li><code><a title="connpy.grpc" href="grpc/index.html">connpy.grpc</a></code></li>
|
<li><code><a title="connpy.grpc_layer" href="grpc_layer/index.html">connpy.grpc_layer</a></code></li>
|
||||||
<li><code><a title="connpy.proto" href="proto/index.html">connpy.proto</a></code></li>
|
<li><code><a title="connpy.proto" href="proto/index.html">connpy.proto</a></code></li>
|
||||||
<li><code><a title="connpy.services" href="services/index.html">connpy.services</a></code></li>
|
<li><code><a title="connpy.services" href="services/index.html">connpy.services</a></code></li>
|
||||||
<li><code><a title="connpy.tests" href="tests/index.html">connpy.tests</a></code></li>
|
<li><code><a title="connpy.tests" href="tests/index.html">connpy.tests</a></code></li>
|
||||||
@@ -5331,7 +5469,7 @@ def test(self, commands, expected, vars = None,*, prompt = None, parallel = 10,
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.proto API documentation</title>
|
<title>connpy.proto API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -60,7 +60,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.ai_service API documentation</title>
|
<title>connpy.services.ai_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -265,7 +265,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.base API documentation</title>
|
<title>connpy.services.base API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -152,7 +152,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.config_service API documentation</title>
|
<title>connpy.services.config_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -311,7 +311,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.context_service API documentation</title>
|
<title>connpy.services.context_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -370,7 +370,7 @@ def current_context(self) -> str:
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.exceptions API documentation</title>
|
<title>connpy.services.exceptions API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -268,7 +268,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.execution_service API documentation</title>
|
<title>connpy.services.execution_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -395,7 +395,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.import_export_service API documentation</title>
|
<title>connpy.services.import_export_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -279,7 +279,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services API documentation</title>
|
<title>connpy.services API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -1234,10 +1234,13 @@ el.replaceWith(d);
|
|||||||
|
|
||||||
def get_node_details(self, unique_id):
|
def get_node_details(self, unique_id):
|
||||||
"""Return full configuration dictionary for a specific node."""
|
"""Return full configuration dictionary for a specific node."""
|
||||||
|
try:
|
||||||
details = self.config.getitem(unique_id)
|
details = self.config.getitem(unique_id)
|
||||||
if not details:
|
if not details:
|
||||||
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
||||||
return details
|
return details
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
||||||
|
|
||||||
def explode_unique(self, unique_id):
|
def explode_unique(self, unique_id):
|
||||||
"""Explode a unique ID into a dictionary of its parts."""
|
"""Explode a unique ID into a dictionary of its parts."""
|
||||||
@@ -1247,6 +1250,14 @@ el.replaceWith(d);
|
|||||||
"""Generate and update the internal nodes cache."""
|
"""Generate and update the internal nodes cache."""
|
||||||
self.config._generate_nodes_cache(nodes=nodes, folders=folders, profiles=profiles)
|
self.config._generate_nodes_cache(nodes=nodes, folders=folders, profiles=profiles)
|
||||||
|
|
||||||
|
def validate_parent_folder(self, unique_id):
|
||||||
|
"""Check if parent folder exists for a given node unique ID."""
|
||||||
|
node_folder = unique_id.partition("@")[2]
|
||||||
|
if node_folder:
|
||||||
|
parent_folder = f"@{node_folder}"
|
||||||
|
if parent_folder not in self.config._getallfolders():
|
||||||
|
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
||||||
|
|
||||||
|
|
||||||
def add_node(self, unique_id, data, is_folder=False):
|
def add_node(self, unique_id, data, is_folder=False):
|
||||||
"""Logic for adding a new node or folder to configuration."""
|
"""Logic for adding a new node or folder to configuration."""
|
||||||
@@ -1265,9 +1276,7 @@ el.replaceWith(d);
|
|||||||
|
|
||||||
# Check if parent folder exists when creating a subfolder
|
# Check if parent folder exists when creating a subfolder
|
||||||
if "subfolder" in uniques:
|
if "subfolder" in uniques:
|
||||||
parent_folder = f"@{uniques['folder']}"
|
self.validate_parent_folder(unique_id)
|
||||||
if parent_folder not in all_folders:
|
|
||||||
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
|
||||||
|
|
||||||
self.config._folder_add(**uniques)
|
self.config._folder_add(**uniques)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
@@ -1276,11 +1285,7 @@ el.replaceWith(d);
|
|||||||
raise NodeAlreadyExistsError(f"Node '{unique_id}' already exists.")
|
raise NodeAlreadyExistsError(f"Node '{unique_id}' already exists.")
|
||||||
|
|
||||||
# Check if parent folder exists when creating a node in a folder
|
# Check if parent folder exists when creating a node in a folder
|
||||||
node_folder = unique_id.partition("@")[2]
|
self.validate_parent_folder(unique_id)
|
||||||
if node_folder:
|
|
||||||
parent_folder = f"@{node_folder}"
|
|
||||||
if parent_folder not in all_folders:
|
|
||||||
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
|
||||||
|
|
||||||
# Ensure 'id' is in data for config._connections_add
|
# Ensure 'id' is in data for config._connections_add
|
||||||
if "id" not in data:
|
if "id" not in data:
|
||||||
@@ -1453,9 +1458,7 @@ el.replaceWith(d);
|
|||||||
|
|
||||||
# Check if parent folder exists when creating a subfolder
|
# Check if parent folder exists when creating a subfolder
|
||||||
if "subfolder" in uniques:
|
if "subfolder" in uniques:
|
||||||
parent_folder = f"@{uniques['folder']}"
|
self.validate_parent_folder(unique_id)
|
||||||
if parent_folder not in all_folders:
|
|
||||||
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
|
||||||
|
|
||||||
self.config._folder_add(**uniques)
|
self.config._folder_add(**uniques)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
@@ -1464,11 +1467,7 @@ el.replaceWith(d);
|
|||||||
raise NodeAlreadyExistsError(f"Node '{unique_id}' already exists.")
|
raise NodeAlreadyExistsError(f"Node '{unique_id}' already exists.")
|
||||||
|
|
||||||
# Check if parent folder exists when creating a node in a folder
|
# Check if parent folder exists when creating a node in a folder
|
||||||
node_folder = unique_id.partition("@")[2]
|
self.validate_parent_folder(unique_id)
|
||||||
if node_folder:
|
|
||||||
parent_folder = f"@{node_folder}"
|
|
||||||
if parent_folder not in all_folders:
|
|
||||||
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
|
||||||
|
|
||||||
# Ensure 'id' is in data for config._connections_add
|
# Ensure 'id' is in data for config._connections_add
|
||||||
if "id" not in data:
|
if "id" not in data:
|
||||||
@@ -1651,10 +1650,13 @@ el.replaceWith(d);
|
|||||||
</summary>
|
</summary>
|
||||||
<pre><code class="python">def get_node_details(self, unique_id):
|
<pre><code class="python">def get_node_details(self, unique_id):
|
||||||
"""Return full configuration dictionary for a specific node."""
|
"""Return full configuration dictionary for a specific node."""
|
||||||
|
try:
|
||||||
details = self.config.getitem(unique_id)
|
details = self.config.getitem(unique_id)
|
||||||
if not details:
|
if not details:
|
||||||
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
||||||
return details</code></pre>
|
return details
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
raise NodeNotFoundError(f"Node '{unique_id}' not found.")</code></pre>
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"><p>Return full configuration dictionary for a specific node.</p></div>
|
<div class="desc"><p>Return full configuration dictionary for a specific node.</p></div>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -1801,6 +1803,24 @@ el.replaceWith(d);
|
|||||||
</details>
|
</details>
|
||||||
<div class="desc"><p>Explicitly update an existing node.</p></div>
|
<div class="desc"><p>Explicitly update an existing node.</p></div>
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt id="connpy.services.NodeService.validate_parent_folder"><code class="name flex">
|
||||||
|
<span>def <span class="ident">validate_parent_folder</span></span>(<span>self, unique_id)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def validate_parent_folder(self, unique_id):
|
||||||
|
"""Check if parent folder exists for a given node unique ID."""
|
||||||
|
node_folder = unique_id.partition("@")[2]
|
||||||
|
if node_folder:
|
||||||
|
parent_folder = f"@{node_folder}"
|
||||||
|
if parent_folder not in self.config._getallfolders():
|
||||||
|
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Check if parent folder exists for a given node unique ID.</p></div>
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<h3>Inherited members</h3>
|
<h3>Inherited members</h3>
|
||||||
<ul class="hlist">
|
<ul class="hlist">
|
||||||
@@ -1996,6 +2016,7 @@ el.replaceWith(d);
|
|||||||
from ..services.exceptions import InvalidConfigurationError
|
from ..services.exceptions import InvalidConfigurationError
|
||||||
from connpy.plugins import Plugins
|
from connpy.plugins import Plugins
|
||||||
class MockApp:
|
class MockApp:
|
||||||
|
is_mock = True
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
from ..core import node, nodes
|
from ..core import node, nodes
|
||||||
from ..ai import ai
|
from ..ai import ai
|
||||||
@@ -2007,14 +2028,20 @@ el.replaceWith(d);
|
|||||||
self.ai = ai
|
self.ai = ai
|
||||||
|
|
||||||
self.services = ServiceProvider(config, mode="local")
|
self.services = ServiceProvider(config, mode="local")
|
||||||
|
|
||||||
|
# Get settings for CLI behavior
|
||||||
|
settings = self.services.config_svc.get_settings()
|
||||||
|
self.case = settings.get("case", False)
|
||||||
|
self.fzf = settings.get("fzf", False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.nodes_list = self.services.nodes.list_nodes()
|
self.nodes_list = self.services.nodes.list_nodes()
|
||||||
self.folders = self.services.nodes.list_folders()
|
self.folders = self.services.nodes.list_folders()
|
||||||
self.profiles = self.services.profiles.list_profiles()
|
self.profiles = self.services.profiles.list_profiles()
|
||||||
except Exception:
|
except Exception:
|
||||||
self.nodes_list = {}
|
self.nodes_list = []
|
||||||
self.folders = {}
|
self.folders = []
|
||||||
self.profiles = {}
|
self.profiles = []
|
||||||
|
|
||||||
args = Namespace(**args_dict)
|
args = Namespace(**args_dict)
|
||||||
|
|
||||||
@@ -2041,26 +2068,26 @@ el.replaceWith(d);
|
|||||||
from .. import printer
|
from .. import printer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
old_console = printer.console
|
old_console = printer._get_console()
|
||||||
old_err_console = printer.err_console
|
old_err_console = printer._get_err_console()
|
||||||
|
|
||||||
printer.console = Console(file=buf, theme=printer.connpy_theme, force_terminal=True)
|
printer.set_thread_console(Console(file=buf, theme=printer.connpy_theme, force_terminal=True))
|
||||||
printer.err_console = Console(file=buf, theme=printer.connpy_theme, force_terminal=True)
|
printer.set_thread_err_console(Console(file=buf, theme=printer.connpy_theme, force_terminal=True))
|
||||||
|
printer.set_thread_stream(buf)
|
||||||
old_stdout = sys.stdout
|
|
||||||
sys.stdout = buf
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(module, "Entrypoint"):
|
if hasattr(module, "Entrypoint"):
|
||||||
module.Entrypoint(args, parser, app)
|
module.Entrypoint(args, parser, app)
|
||||||
except Exception as e:
|
except BaseException as e:
|
||||||
|
if not isinstance(e, SystemExit):
|
||||||
import traceback
|
import traceback
|
||||||
printer.err_console.print(traceback.format_exc())
|
printer.err_console.print(traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
sys.stdout = old_stdout
|
printer.set_thread_console(old_console)
|
||||||
printer.console = old_console
|
printer.set_thread_err_console(old_err_console)
|
||||||
printer.err_console = old_err_console
|
printer.set_thread_stream(None)
|
||||||
|
|
||||||
for line in buf.getvalue().splitlines(keepends=True):
|
for line in buf.getvalue().splitlines(keepends=True):
|
||||||
yield line</code></pre>
|
yield line</code></pre>
|
||||||
@@ -2265,6 +2292,7 @@ el.replaceWith(d);
|
|||||||
from ..services.exceptions import InvalidConfigurationError
|
from ..services.exceptions import InvalidConfigurationError
|
||||||
from connpy.plugins import Plugins
|
from connpy.plugins import Plugins
|
||||||
class MockApp:
|
class MockApp:
|
||||||
|
is_mock = True
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
from ..core import node, nodes
|
from ..core import node, nodes
|
||||||
from ..ai import ai
|
from ..ai import ai
|
||||||
@@ -2276,14 +2304,20 @@ el.replaceWith(d);
|
|||||||
self.ai = ai
|
self.ai = ai
|
||||||
|
|
||||||
self.services = ServiceProvider(config, mode="local")
|
self.services = ServiceProvider(config, mode="local")
|
||||||
|
|
||||||
|
# Get settings for CLI behavior
|
||||||
|
settings = self.services.config_svc.get_settings()
|
||||||
|
self.case = settings.get("case", False)
|
||||||
|
self.fzf = settings.get("fzf", False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.nodes_list = self.services.nodes.list_nodes()
|
self.nodes_list = self.services.nodes.list_nodes()
|
||||||
self.folders = self.services.nodes.list_folders()
|
self.folders = self.services.nodes.list_folders()
|
||||||
self.profiles = self.services.profiles.list_profiles()
|
self.profiles = self.services.profiles.list_profiles()
|
||||||
except Exception:
|
except Exception:
|
||||||
self.nodes_list = {}
|
self.nodes_list = []
|
||||||
self.folders = {}
|
self.folders = []
|
||||||
self.profiles = {}
|
self.profiles = []
|
||||||
|
|
||||||
args = Namespace(**args_dict)
|
args = Namespace(**args_dict)
|
||||||
|
|
||||||
@@ -2310,26 +2344,26 @@ el.replaceWith(d);
|
|||||||
from .. import printer
|
from .. import printer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
old_console = printer.console
|
old_console = printer._get_console()
|
||||||
old_err_console = printer.err_console
|
old_err_console = printer._get_err_console()
|
||||||
|
|
||||||
printer.console = Console(file=buf, theme=printer.connpy_theme, force_terminal=True)
|
printer.set_thread_console(Console(file=buf, theme=printer.connpy_theme, force_terminal=True))
|
||||||
printer.err_console = Console(file=buf, theme=printer.connpy_theme, force_terminal=True)
|
printer.set_thread_err_console(Console(file=buf, theme=printer.connpy_theme, force_terminal=True))
|
||||||
|
printer.set_thread_stream(buf)
|
||||||
old_stdout = sys.stdout
|
|
||||||
sys.stdout = buf
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(module, "Entrypoint"):
|
if hasattr(module, "Entrypoint"):
|
||||||
module.Entrypoint(args, parser, app)
|
module.Entrypoint(args, parser, app)
|
||||||
except Exception as e:
|
except BaseException as e:
|
||||||
|
if not isinstance(e, SystemExit):
|
||||||
import traceback
|
import traceback
|
||||||
printer.err_console.print(traceback.format_exc())
|
printer.err_console.print(traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
sys.stdout = old_stdout
|
printer.set_thread_console(old_console)
|
||||||
printer.console = old_console
|
printer.set_thread_err_console(old_err_console)
|
||||||
printer.err_console = old_err_console
|
printer.set_thread_stream(None)
|
||||||
|
|
||||||
for line in buf.getvalue().splitlines(keepends=True):
|
for line in buf.getvalue().splitlines(keepends=True):
|
||||||
yield line</code></pre>
|
yield line</code></pre>
|
||||||
@@ -3118,7 +3152,7 @@ el.replaceWith(d);
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<h4><code><a title="connpy.services.NodeService" href="#connpy.services.NodeService">NodeService</a></code></h4>
|
<h4><code><a title="connpy.services.NodeService" href="#connpy.services.NodeService">NodeService</a></code></h4>
|
||||||
<ul class="two-column">
|
<ul class="">
|
||||||
<li><code><a title="connpy.services.NodeService.add_node" href="#connpy.services.NodeService.add_node">add_node</a></code></li>
|
<li><code><a title="connpy.services.NodeService.add_node" href="#connpy.services.NodeService.add_node">add_node</a></code></li>
|
||||||
<li><code><a title="connpy.services.NodeService.bulk_add" href="#connpy.services.NodeService.bulk_add">bulk_add</a></code></li>
|
<li><code><a title="connpy.services.NodeService.bulk_add" href="#connpy.services.NodeService.bulk_add">bulk_add</a></code></li>
|
||||||
<li><code><a title="connpy.services.NodeService.connect_node" href="#connpy.services.NodeService.connect_node">connect_node</a></code></li>
|
<li><code><a title="connpy.services.NodeService.connect_node" href="#connpy.services.NodeService.connect_node">connect_node</a></code></li>
|
||||||
@@ -3132,6 +3166,7 @@ el.replaceWith(d);
|
|||||||
<li><code><a title="connpy.services.NodeService.list_nodes" href="#connpy.services.NodeService.list_nodes">list_nodes</a></code></li>
|
<li><code><a title="connpy.services.NodeService.list_nodes" href="#connpy.services.NodeService.list_nodes">list_nodes</a></code></li>
|
||||||
<li><code><a title="connpy.services.NodeService.move_node" href="#connpy.services.NodeService.move_node">move_node</a></code></li>
|
<li><code><a title="connpy.services.NodeService.move_node" href="#connpy.services.NodeService.move_node">move_node</a></code></li>
|
||||||
<li><code><a title="connpy.services.NodeService.update_node" href="#connpy.services.NodeService.update_node">update_node</a></code></li>
|
<li><code><a title="connpy.services.NodeService.update_node" href="#connpy.services.NodeService.update_node">update_node</a></code></li>
|
||||||
|
<li><code><a title="connpy.services.NodeService.validate_parent_folder" href="#connpy.services.NodeService.validate_parent_folder">validate_parent_folder</a></code></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -3180,7 +3215,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.node_service API documentation</title>
|
<title>connpy.services.node_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -123,10 +123,13 @@ el.replaceWith(d);
|
|||||||
|
|
||||||
def get_node_details(self, unique_id):
|
def get_node_details(self, unique_id):
|
||||||
"""Return full configuration dictionary for a specific node."""
|
"""Return full configuration dictionary for a specific node."""
|
||||||
|
try:
|
||||||
details = self.config.getitem(unique_id)
|
details = self.config.getitem(unique_id)
|
||||||
if not details:
|
if not details:
|
||||||
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
||||||
return details
|
return details
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
||||||
|
|
||||||
def explode_unique(self, unique_id):
|
def explode_unique(self, unique_id):
|
||||||
"""Explode a unique ID into a dictionary of its parts."""
|
"""Explode a unique ID into a dictionary of its parts."""
|
||||||
@@ -136,6 +139,14 @@ el.replaceWith(d);
|
|||||||
"""Generate and update the internal nodes cache."""
|
"""Generate and update the internal nodes cache."""
|
||||||
self.config._generate_nodes_cache(nodes=nodes, folders=folders, profiles=profiles)
|
self.config._generate_nodes_cache(nodes=nodes, folders=folders, profiles=profiles)
|
||||||
|
|
||||||
|
def validate_parent_folder(self, unique_id):
|
||||||
|
"""Check if parent folder exists for a given node unique ID."""
|
||||||
|
node_folder = unique_id.partition("@")[2]
|
||||||
|
if node_folder:
|
||||||
|
parent_folder = f"@{node_folder}"
|
||||||
|
if parent_folder not in self.config._getallfolders():
|
||||||
|
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
||||||
|
|
||||||
|
|
||||||
def add_node(self, unique_id, data, is_folder=False):
|
def add_node(self, unique_id, data, is_folder=False):
|
||||||
"""Logic for adding a new node or folder to configuration."""
|
"""Logic for adding a new node or folder to configuration."""
|
||||||
@@ -154,9 +165,7 @@ el.replaceWith(d);
|
|||||||
|
|
||||||
# Check if parent folder exists when creating a subfolder
|
# Check if parent folder exists when creating a subfolder
|
||||||
if "subfolder" in uniques:
|
if "subfolder" in uniques:
|
||||||
parent_folder = f"@{uniques['folder']}"
|
self.validate_parent_folder(unique_id)
|
||||||
if parent_folder not in all_folders:
|
|
||||||
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
|
||||||
|
|
||||||
self.config._folder_add(**uniques)
|
self.config._folder_add(**uniques)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
@@ -165,11 +174,7 @@ el.replaceWith(d);
|
|||||||
raise NodeAlreadyExistsError(f"Node '{unique_id}' already exists.")
|
raise NodeAlreadyExistsError(f"Node '{unique_id}' already exists.")
|
||||||
|
|
||||||
# Check if parent folder exists when creating a node in a folder
|
# Check if parent folder exists when creating a node in a folder
|
||||||
node_folder = unique_id.partition("@")[2]
|
self.validate_parent_folder(unique_id)
|
||||||
if node_folder:
|
|
||||||
parent_folder = f"@{node_folder}"
|
|
||||||
if parent_folder not in all_folders:
|
|
||||||
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
|
||||||
|
|
||||||
# Ensure 'id' is in data for config._connections_add
|
# Ensure 'id' is in data for config._connections_add
|
||||||
if "id" not in data:
|
if "id" not in data:
|
||||||
@@ -342,9 +347,7 @@ el.replaceWith(d);
|
|||||||
|
|
||||||
# Check if parent folder exists when creating a subfolder
|
# Check if parent folder exists when creating a subfolder
|
||||||
if "subfolder" in uniques:
|
if "subfolder" in uniques:
|
||||||
parent_folder = f"@{uniques['folder']}"
|
self.validate_parent_folder(unique_id)
|
||||||
if parent_folder not in all_folders:
|
|
||||||
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
|
||||||
|
|
||||||
self.config._folder_add(**uniques)
|
self.config._folder_add(**uniques)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
@@ -353,11 +356,7 @@ el.replaceWith(d);
|
|||||||
raise NodeAlreadyExistsError(f"Node '{unique_id}' already exists.")
|
raise NodeAlreadyExistsError(f"Node '{unique_id}' already exists.")
|
||||||
|
|
||||||
# Check if parent folder exists when creating a node in a folder
|
# Check if parent folder exists when creating a node in a folder
|
||||||
node_folder = unique_id.partition("@")[2]
|
self.validate_parent_folder(unique_id)
|
||||||
if node_folder:
|
|
||||||
parent_folder = f"@{node_folder}"
|
|
||||||
if parent_folder not in all_folders:
|
|
||||||
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")
|
|
||||||
|
|
||||||
# Ensure 'id' is in data for config._connections_add
|
# Ensure 'id' is in data for config._connections_add
|
||||||
if "id" not in data:
|
if "id" not in data:
|
||||||
@@ -540,10 +539,13 @@ el.replaceWith(d);
|
|||||||
</summary>
|
</summary>
|
||||||
<pre><code class="python">def get_node_details(self, unique_id):
|
<pre><code class="python">def get_node_details(self, unique_id):
|
||||||
"""Return full configuration dictionary for a specific node."""
|
"""Return full configuration dictionary for a specific node."""
|
||||||
|
try:
|
||||||
details = self.config.getitem(unique_id)
|
details = self.config.getitem(unique_id)
|
||||||
if not details:
|
if not details:
|
||||||
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
raise NodeNotFoundError(f"Node '{unique_id}' not found.")
|
||||||
return details</code></pre>
|
return details
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
raise NodeNotFoundError(f"Node '{unique_id}' not found.")</code></pre>
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"><p>Return full configuration dictionary for a specific node.</p></div>
|
<div class="desc"><p>Return full configuration dictionary for a specific node.</p></div>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -690,6 +692,24 @@ el.replaceWith(d);
|
|||||||
</details>
|
</details>
|
||||||
<div class="desc"><p>Explicitly update an existing node.</p></div>
|
<div class="desc"><p>Explicitly update an existing node.</p></div>
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt id="connpy.services.node_service.NodeService.validate_parent_folder"><code class="name flex">
|
||||||
|
<span>def <span class="ident">validate_parent_folder</span></span>(<span>self, unique_id)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def validate_parent_folder(self, unique_id):
|
||||||
|
"""Check if parent folder exists for a given node unique ID."""
|
||||||
|
node_folder = unique_id.partition("@")[2]
|
||||||
|
if node_folder:
|
||||||
|
parent_folder = f"@{node_folder}"
|
||||||
|
if parent_folder not in self.config._getallfolders():
|
||||||
|
raise NodeNotFoundError(f"Folder '{parent_folder}' not found.")</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Check if parent folder exists for a given node unique ID.</p></div>
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<h3>Inherited members</h3>
|
<h3>Inherited members</h3>
|
||||||
<ul class="hlist">
|
<ul class="hlist">
|
||||||
@@ -717,7 +737,7 @@ el.replaceWith(d);
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<h4><code><a title="connpy.services.node_service.NodeService" href="#connpy.services.node_service.NodeService">NodeService</a></code></h4>
|
<h4><code><a title="connpy.services.node_service.NodeService" href="#connpy.services.node_service.NodeService">NodeService</a></code></h4>
|
||||||
<ul class="two-column">
|
<ul class="">
|
||||||
<li><code><a title="connpy.services.node_service.NodeService.add_node" href="#connpy.services.node_service.NodeService.add_node">add_node</a></code></li>
|
<li><code><a title="connpy.services.node_service.NodeService.add_node" href="#connpy.services.node_service.NodeService.add_node">add_node</a></code></li>
|
||||||
<li><code><a title="connpy.services.node_service.NodeService.bulk_add" href="#connpy.services.node_service.NodeService.bulk_add">bulk_add</a></code></li>
|
<li><code><a title="connpy.services.node_service.NodeService.bulk_add" href="#connpy.services.node_service.NodeService.bulk_add">bulk_add</a></code></li>
|
||||||
<li><code><a title="connpy.services.node_service.NodeService.connect_node" href="#connpy.services.node_service.NodeService.connect_node">connect_node</a></code></li>
|
<li><code><a title="connpy.services.node_service.NodeService.connect_node" href="#connpy.services.node_service.NodeService.connect_node">connect_node</a></code></li>
|
||||||
@@ -731,6 +751,7 @@ el.replaceWith(d);
|
|||||||
<li><code><a title="connpy.services.node_service.NodeService.list_nodes" href="#connpy.services.node_service.NodeService.list_nodes">list_nodes</a></code></li>
|
<li><code><a title="connpy.services.node_service.NodeService.list_nodes" href="#connpy.services.node_service.NodeService.list_nodes">list_nodes</a></code></li>
|
||||||
<li><code><a title="connpy.services.node_service.NodeService.move_node" href="#connpy.services.node_service.NodeService.move_node">move_node</a></code></li>
|
<li><code><a title="connpy.services.node_service.NodeService.move_node" href="#connpy.services.node_service.NodeService.move_node">move_node</a></code></li>
|
||||||
<li><code><a title="connpy.services.node_service.NodeService.update_node" href="#connpy.services.node_service.NodeService.update_node">update_node</a></code></li>
|
<li><code><a title="connpy.services.node_service.NodeService.update_node" href="#connpy.services.node_service.NodeService.update_node">update_node</a></code></li>
|
||||||
|
<li><code><a title="connpy.services.node_service.NodeService.validate_parent_folder" href="#connpy.services.node_service.NodeService.validate_parent_folder">validate_parent_folder</a></code></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -739,7 +760,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.plugin_service API documentation</title>
|
<title>connpy.services.plugin_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -231,6 +231,7 @@ el.replaceWith(d);
|
|||||||
from ..services.exceptions import InvalidConfigurationError
|
from ..services.exceptions import InvalidConfigurationError
|
||||||
from connpy.plugins import Plugins
|
from connpy.plugins import Plugins
|
||||||
class MockApp:
|
class MockApp:
|
||||||
|
is_mock = True
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
from ..core import node, nodes
|
from ..core import node, nodes
|
||||||
from ..ai import ai
|
from ..ai import ai
|
||||||
@@ -242,14 +243,20 @@ el.replaceWith(d);
|
|||||||
self.ai = ai
|
self.ai = ai
|
||||||
|
|
||||||
self.services = ServiceProvider(config, mode="local")
|
self.services = ServiceProvider(config, mode="local")
|
||||||
|
|
||||||
|
# Get settings for CLI behavior
|
||||||
|
settings = self.services.config_svc.get_settings()
|
||||||
|
self.case = settings.get("case", False)
|
||||||
|
self.fzf = settings.get("fzf", False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.nodes_list = self.services.nodes.list_nodes()
|
self.nodes_list = self.services.nodes.list_nodes()
|
||||||
self.folders = self.services.nodes.list_folders()
|
self.folders = self.services.nodes.list_folders()
|
||||||
self.profiles = self.services.profiles.list_profiles()
|
self.profiles = self.services.profiles.list_profiles()
|
||||||
except Exception:
|
except Exception:
|
||||||
self.nodes_list = {}
|
self.nodes_list = []
|
||||||
self.folders = {}
|
self.folders = []
|
||||||
self.profiles = {}
|
self.profiles = []
|
||||||
|
|
||||||
args = Namespace(**args_dict)
|
args = Namespace(**args_dict)
|
||||||
|
|
||||||
@@ -276,26 +283,26 @@ el.replaceWith(d);
|
|||||||
from .. import printer
|
from .. import printer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
old_console = printer.console
|
old_console = printer._get_console()
|
||||||
old_err_console = printer.err_console
|
old_err_console = printer._get_err_console()
|
||||||
|
|
||||||
printer.console = Console(file=buf, theme=printer.connpy_theme, force_terminal=True)
|
printer.set_thread_console(Console(file=buf, theme=printer.connpy_theme, force_terminal=True))
|
||||||
printer.err_console = Console(file=buf, theme=printer.connpy_theme, force_terminal=True)
|
printer.set_thread_err_console(Console(file=buf, theme=printer.connpy_theme, force_terminal=True))
|
||||||
|
printer.set_thread_stream(buf)
|
||||||
old_stdout = sys.stdout
|
|
||||||
sys.stdout = buf
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(module, "Entrypoint"):
|
if hasattr(module, "Entrypoint"):
|
||||||
module.Entrypoint(args, parser, app)
|
module.Entrypoint(args, parser, app)
|
||||||
except Exception as e:
|
except BaseException as e:
|
||||||
|
if not isinstance(e, SystemExit):
|
||||||
import traceback
|
import traceback
|
||||||
printer.err_console.print(traceback.format_exc())
|
printer.err_console.print(traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
sys.stdout = old_stdout
|
printer.set_thread_console(old_console)
|
||||||
printer.console = old_console
|
printer.set_thread_err_console(old_err_console)
|
||||||
printer.err_console = old_err_console
|
printer.set_thread_stream(None)
|
||||||
|
|
||||||
for line in buf.getvalue().splitlines(keepends=True):
|
for line in buf.getvalue().splitlines(keepends=True):
|
||||||
yield line</code></pre>
|
yield line</code></pre>
|
||||||
@@ -500,6 +507,7 @@ el.replaceWith(d);
|
|||||||
from ..services.exceptions import InvalidConfigurationError
|
from ..services.exceptions import InvalidConfigurationError
|
||||||
from connpy.plugins import Plugins
|
from connpy.plugins import Plugins
|
||||||
class MockApp:
|
class MockApp:
|
||||||
|
is_mock = True
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
from ..core import node, nodes
|
from ..core import node, nodes
|
||||||
from ..ai import ai
|
from ..ai import ai
|
||||||
@@ -511,14 +519,20 @@ el.replaceWith(d);
|
|||||||
self.ai = ai
|
self.ai = ai
|
||||||
|
|
||||||
self.services = ServiceProvider(config, mode="local")
|
self.services = ServiceProvider(config, mode="local")
|
||||||
|
|
||||||
|
# Get settings for CLI behavior
|
||||||
|
settings = self.services.config_svc.get_settings()
|
||||||
|
self.case = settings.get("case", False)
|
||||||
|
self.fzf = settings.get("fzf", False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.nodes_list = self.services.nodes.list_nodes()
|
self.nodes_list = self.services.nodes.list_nodes()
|
||||||
self.folders = self.services.nodes.list_folders()
|
self.folders = self.services.nodes.list_folders()
|
||||||
self.profiles = self.services.profiles.list_profiles()
|
self.profiles = self.services.profiles.list_profiles()
|
||||||
except Exception:
|
except Exception:
|
||||||
self.nodes_list = {}
|
self.nodes_list = []
|
||||||
self.folders = {}
|
self.folders = []
|
||||||
self.profiles = {}
|
self.profiles = []
|
||||||
|
|
||||||
args = Namespace(**args_dict)
|
args = Namespace(**args_dict)
|
||||||
|
|
||||||
@@ -545,26 +559,26 @@ el.replaceWith(d);
|
|||||||
from .. import printer
|
from .. import printer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
old_console = printer.console
|
old_console = printer._get_console()
|
||||||
old_err_console = printer.err_console
|
old_err_console = printer._get_err_console()
|
||||||
|
|
||||||
printer.console = Console(file=buf, theme=printer.connpy_theme, force_terminal=True)
|
printer.set_thread_console(Console(file=buf, theme=printer.connpy_theme, force_terminal=True))
|
||||||
printer.err_console = Console(file=buf, theme=printer.connpy_theme, force_terminal=True)
|
printer.set_thread_err_console(Console(file=buf, theme=printer.connpy_theme, force_terminal=True))
|
||||||
|
printer.set_thread_stream(buf)
|
||||||
old_stdout = sys.stdout
|
|
||||||
sys.stdout = buf
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(module, "Entrypoint"):
|
if hasattr(module, "Entrypoint"):
|
||||||
module.Entrypoint(args, parser, app)
|
module.Entrypoint(args, parser, app)
|
||||||
except Exception as e:
|
except BaseException as e:
|
||||||
|
if not isinstance(e, SystemExit):
|
||||||
import traceback
|
import traceback
|
||||||
printer.err_console.print(traceback.format_exc())
|
printer.err_console.print(traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
sys.stdout = old_stdout
|
printer.set_thread_console(old_console)
|
||||||
printer.console = old_console
|
printer.set_thread_err_console(old_err_console)
|
||||||
printer.err_console = old_err_console
|
printer.set_thread_stream(None)
|
||||||
|
|
||||||
for line in buf.getvalue().splitlines(keepends=True):
|
for line in buf.getvalue().splitlines(keepends=True):
|
||||||
yield line</code></pre>
|
yield line</code></pre>
|
||||||
@@ -657,7 +671,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.profile_service API documentation</title>
|
<title>connpy.services.profile_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -429,7 +429,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.provider API documentation</title>
|
<title>connpy.services.provider API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -123,7 +123,7 @@ el.replaceWith(d);
|
|||||||
raise InvalidConfigurationError("Remote host must be specified in remote mode")
|
raise InvalidConfigurationError("Remote host must be specified in remote mode")
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
from ..grpc.stubs import NodeStub, ProfileStub, PluginStub, AIStub, ExecutionStub, ImportExportStub, SystemStub
|
from ..grpc_layer.stubs import NodeStub, ProfileStub, PluginStub, AIStub, ExecutionStub, ImportExportStub, SystemStub
|
||||||
|
|
||||||
channel = grpc.insecure_channel(self.remote_host)
|
channel = grpc.insecure_channel(self.remote_host)
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.sync_service API documentation</title>
|
<title>connpy.services.sync_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -195,9 +195,9 @@ el.replaceWith(d);
|
|||||||
if os.path.exists(self.config.key):
|
if os.path.exists(self.config.key):
|
||||||
zipf.write(self.config.key, ".osk")
|
zipf.write(self.config.key, ".osk")
|
||||||
|
|
||||||
# Manage retention (max 10 backups)
|
# Manage retention (max 100 backups)
|
||||||
backups = self.list_backups()
|
backups = self.list_backups()
|
||||||
if len(backups) >= 10:
|
if len(backups) >= 100:
|
||||||
oldest = min(backups, key=lambda x: x['timestamp'] or '0')
|
oldest = min(backups, key=lambda x: x['timestamp'] or '0')
|
||||||
self.delete_backup(oldest['id'])
|
self.delete_backup(oldest['id'])
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ el.replaceWith(d);
|
|||||||
|
|
||||||
if not sync_enabled: return
|
if not sync_enabled: return
|
||||||
|
|
||||||
printer.info("Triggering auto-sync...")
|
|
||||||
if self.check_login_status() != True:
|
if self.check_login_status() != True:
|
||||||
printer.warning("Auto-sync: Not logged in to Google Drive.")
|
printer.warning("Auto-sync: Not logged in to Google Drive.")
|
||||||
return
|
return
|
||||||
@@ -549,9 +549,9 @@ el.replaceWith(d);
|
|||||||
if os.path.exists(self.config.key):
|
if os.path.exists(self.config.key):
|
||||||
zipf.write(self.config.key, ".osk")
|
zipf.write(self.config.key, ".osk")
|
||||||
|
|
||||||
# Manage retention (max 10 backups)
|
# Manage retention (max 100 backups)
|
||||||
backups = self.list_backups()
|
backups = self.list_backups()
|
||||||
if len(backups) >= 10:
|
if len(backups) >= 100:
|
||||||
oldest = min(backups, key=lambda x: x['timestamp'] or '0')
|
oldest = min(backups, key=lambda x: x['timestamp'] or '0')
|
||||||
self.delete_backup(oldest['id'])
|
self.delete_backup(oldest['id'])
|
||||||
|
|
||||||
@@ -819,7 +819,7 @@ el.replaceWith(d);
|
|||||||
|
|
||||||
if not sync_enabled: return
|
if not sync_enabled: return
|
||||||
|
|
||||||
printer.info("Triggering auto-sync...")
|
|
||||||
if self.check_login_status() != True:
|
if self.check_login_status() != True:
|
||||||
printer.warning("Auto-sync: Not logged in to Google Drive.")
|
printer.warning("Auto-sync: Not logged in to Google Drive.")
|
||||||
return
|
return
|
||||||
@@ -964,7 +964,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.services.system_service API documentation</title>
|
<title>connpy.services.system_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -325,7 +325,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.conftest API documentation</title>
|
<title>connpy.tests.conftest API documentation</title>
|
||||||
<meta name="description" content="Shared fixtures for connpy tests …">
|
<meta name="description" content="Shared fixtures for connpy tests …">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -258,7 +258,7 @@ def tmp_config_dir(tmp_path):
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests API documentation</title>
|
<title>connpy.tests API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -72,6 +72,10 @@ el.replaceWith(d);
|
|||||||
<dd>
|
<dd>
|
||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt><code class="name"><a title="connpy.tests.test_grpc_layer" href="test_grpc_layer.html">connpy.tests.test_grpc_layer</a></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
<dt><code class="name"><a title="connpy.tests.test_hooks" href="test_hooks.html">connpy.tests.test_hooks</a></code></dt>
|
<dt><code class="name"><a title="connpy.tests.test_hooks" href="test_hooks.html">connpy.tests.test_hooks</a></code></dt>
|
||||||
<dd>
|
<dd>
|
||||||
<div class="desc"><p>Tests for connpy.hooks module — MethodHook and ClassHook.</p></div>
|
<div class="desc"><p>Tests for connpy.hooks module — MethodHook and ClassHook.</p></div>
|
||||||
@@ -88,6 +92,10 @@ el.replaceWith(d);
|
|||||||
<dd>
|
<dd>
|
||||||
<div class="desc"><p>Tests for connpy.printer module.</p></div>
|
<div class="desc"><p>Tests for connpy.printer module.</p></div>
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt><code class="name"><a title="connpy.tests.test_printer_concurrency" href="test_printer_concurrency.html">connpy.tests.test_printer_concurrency</a></code></dt>
|
||||||
|
<dd>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
<dt><code class="name"><a title="connpy.tests.test_profile_service" href="test_profile_service.html">connpy.tests.test_profile_service</a></code></dt>
|
<dt><code class="name"><a title="connpy.tests.test_profile_service" href="test_profile_service.html">connpy.tests.test_profile_service</a></code></dt>
|
||||||
<dd>
|
<dd>
|
||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
@@ -129,10 +137,12 @@ el.replaceWith(d);
|
|||||||
<li><code><a title="connpy.tests.test_connapp" href="test_connapp.html">connpy.tests.test_connapp</a></code></li>
|
<li><code><a title="connpy.tests.test_connapp" href="test_connapp.html">connpy.tests.test_connapp</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_core" href="test_core.html">connpy.tests.test_core</a></code></li>
|
<li><code><a title="connpy.tests.test_core" href="test_core.html">connpy.tests.test_core</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_execution_service" href="test_execution_service.html">connpy.tests.test_execution_service</a></code></li>
|
<li><code><a title="connpy.tests.test_execution_service" href="test_execution_service.html">connpy.tests.test_execution_service</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer" href="test_grpc_layer.html">connpy.tests.test_grpc_layer</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_hooks" href="test_hooks.html">connpy.tests.test_hooks</a></code></li>
|
<li><code><a title="connpy.tests.test_hooks" href="test_hooks.html">connpy.tests.test_hooks</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_node_service" href="test_node_service.html">connpy.tests.test_node_service</a></code></li>
|
<li><code><a title="connpy.tests.test_node_service" href="test_node_service.html">connpy.tests.test_node_service</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_plugins" href="test_plugins.html">connpy.tests.test_plugins</a></code></li>
|
<li><code><a title="connpy.tests.test_plugins" href="test_plugins.html">connpy.tests.test_plugins</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_printer" href="test_printer.html">connpy.tests.test_printer</a></code></li>
|
<li><code><a title="connpy.tests.test_printer" href="test_printer.html">connpy.tests.test_printer</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_printer_concurrency" href="test_printer_concurrency.html">connpy.tests.test_printer_concurrency</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_profile_service" href="test_profile_service.html">connpy.tests.test_profile_service</a></code></li>
|
<li><code><a title="connpy.tests.test_profile_service" href="test_profile_service.html">connpy.tests.test_profile_service</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_provider" href="test_provider.html">connpy.tests.test_provider</a></code></li>
|
<li><code><a title="connpy.tests.test_provider" href="test_provider.html">connpy.tests.test_provider</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_sync" href="test_sync.html">connpy.tests.test_sync</a></code></li>
|
<li><code><a title="connpy.tests.test_sync" href="test_sync.html">connpy.tests.test_sync</a></code></li>
|
||||||
@@ -142,7 +152,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_ai API documentation</title>
|
<title>connpy.tests.test_ai API documentation</title>
|
||||||
<meta name="description" content="Tests for connpy.ai module.">
|
<meta name="description" content="Tests for connpy.ai module.">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -1339,16 +1339,16 @@ def myai(self, ai_config, mock_litellm):
|
|||||||
|
|
||||||
def test_list_nodes_tool_found(self, myai):
|
def test_list_nodes_tool_found(self, myai):
|
||||||
result = myai.list_nodes_tool("router.*")
|
result = myai.list_nodes_tool("router.*")
|
||||||
parsed = json.loads(result)
|
parsed = json.loads(result) if isinstance(result, str) else result
|
||||||
assert "router1" in str(parsed)
|
assert "router1" in str(parsed)
|
||||||
|
|
||||||
def test_list_nodes_tool_not_found(self, myai):
|
def test_list_nodes_tool_not_found(self, myai):
|
||||||
result = myai.list_nodes_tool("nonexistent_pattern_xyz")
|
result = myai.list_nodes_tool("nonexistent_pattern_xyz")
|
||||||
assert "No nodes found" in result
|
assert "No nodes found" in str(result)
|
||||||
|
|
||||||
def test_get_node_info_masks_password(self, myai):
|
def test_get_node_info_masks_password(self, myai):
|
||||||
result = myai.get_node_info_tool("router1")
|
result = myai.get_node_info_tool("router1")
|
||||||
parsed = json.loads(result)
|
parsed = json.loads(result) if isinstance(result, str) else result
|
||||||
assert parsed["password"] == "***"
|
assert parsed["password"] == "***"
|
||||||
|
|
||||||
def test_is_safe_command_show(self, myai):
|
def test_is_safe_command_show(self, myai):
|
||||||
@@ -1393,7 +1393,7 @@ def myai(self, ai_config, mock_litellm):
|
|||||||
</summary>
|
</summary>
|
||||||
<pre><code class="python">def test_get_node_info_masks_password(self, myai):
|
<pre><code class="python">def test_get_node_info_masks_password(self, myai):
|
||||||
result = myai.get_node_info_tool("router1")
|
result = myai.get_node_info_tool("router1")
|
||||||
parsed = json.loads(result)
|
parsed = json.loads(result) if isinstance(result, str) else result
|
||||||
assert parsed["password"] == "***"</code></pre>
|
assert parsed["password"] == "***"</code></pre>
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
@@ -1462,7 +1462,7 @@ def myai(self, ai_config, mock_litellm):
|
|||||||
</summary>
|
</summary>
|
||||||
<pre><code class="python">def test_list_nodes_tool_found(self, myai):
|
<pre><code class="python">def test_list_nodes_tool_found(self, myai):
|
||||||
result = myai.list_nodes_tool("router.*")
|
result = myai.list_nodes_tool("router.*")
|
||||||
parsed = json.loads(result)
|
parsed = json.loads(result) if isinstance(result, str) else result
|
||||||
assert "router1" in str(parsed)</code></pre>
|
assert "router1" in str(parsed)</code></pre>
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
@@ -1477,7 +1477,7 @@ def myai(self, ai_config, mock_litellm):
|
|||||||
</summary>
|
</summary>
|
||||||
<pre><code class="python">def test_list_nodes_tool_not_found(self, myai):
|
<pre><code class="python">def test_list_nodes_tool_not_found(self, myai):
|
||||||
result = myai.list_nodes_tool("nonexistent_pattern_xyz")
|
result = myai.list_nodes_tool("nonexistent_pattern_xyz")
|
||||||
assert "No nodes found" in result</code></pre>
|
assert "No nodes found" in str(result)</code></pre>
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
</dd>
|
</dd>
|
||||||
@@ -1731,7 +1731,7 @@ def myai(self, ai_config, mock_litellm):
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_capture API documentation</title>
|
<title>connpy.tests.test_capture API documentation</title>
|
||||||
<meta name="description" content="Tests for connpy.core_plugins.capture">
|
<meta name="description" content="Tests for connpy.core_plugins.capture">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -245,7 +245,7 @@ def mock_connapp():
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_completion API documentation</title>
|
<title>connpy.tests.test_completion API documentation</title>
|
||||||
<meta name="description" content="Tests for connpy.completion module.">
|
<meta name="description" content="Tests for connpy.completion module.">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -257,7 +257,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_configfile API documentation</title>
|
<title>connpy.tests.test_configfile API documentation</title>
|
||||||
<meta name="description" content="Tests for connpy.configfile module.">
|
<meta name="description" content="Tests for connpy.configfile module.">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -2005,7 +2005,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_connapp API documentation</title>
|
<title>connpy.tests.test_connapp API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -699,7 +699,7 @@ def test_run(mock_run_commands, app):
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_core API documentation</title>
|
<title>connpy.tests.test_core API documentation</title>
|
||||||
<meta name="description" content="Tests for connpy.core module — node and nodes classes.">
|
<meta name="description" content="Tests for connpy.core module — node and nodes classes.">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -93,6 +93,23 @@ el.replaceWith(d);
|
|||||||
assert "telnet 10.0.0.1" in cmd
|
assert "telnet 10.0.0.1" in cmd
|
||||||
assert "23" in cmd
|
assert "23" in cmd
|
||||||
|
|
||||||
|
def test_ssm_cmd_basic(self):
|
||||||
|
n = self._make_node(protocol="ssm", host="i-12345")
|
||||||
|
cmd = n._get_cmd()
|
||||||
|
assert "aws ssm start-session" in cmd
|
||||||
|
assert "--target i-12345" in cmd
|
||||||
|
|
||||||
|
def test_ssm_cmd_tags(self):
|
||||||
|
n = self._make_node(protocol="ssm", host="i-12345", tags={"region": "us-west-2", "profile": "prod"})
|
||||||
|
cmd = n._get_cmd()
|
||||||
|
assert "--region us-west-2" in cmd
|
||||||
|
assert "--profile prod" in cmd
|
||||||
|
|
||||||
|
def test_ssm_cmd_options(self):
|
||||||
|
n = self._make_node(protocol="ssm", host="i-12345", options="--document-name AWS-StartInteractiveCommand")
|
||||||
|
cmd = n._get_cmd()
|
||||||
|
assert "--document-name AWS-StartInteractiveCommand" in cmd
|
||||||
|
|
||||||
def test_kubectl_cmd(self):
|
def test_kubectl_cmd(self):
|
||||||
n = self._make_node(protocol="kubectl", host="my-pod", tags={"kube_command": "/bin/sh"})
|
n = self._make_node(protocol="kubectl", host="my-pod", tags={"kube_command": "/bin/sh"})
|
||||||
cmd = n._get_cmd()
|
cmd = n._get_cmd()
|
||||||
@@ -271,6 +288,53 @@ el.replaceWith(d);
|
|||||||
</details>
|
</details>
|
||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_core.TestCommandGeneration.test_ssm_cmd_basic"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_ssm_cmd_basic</span></span>(<span>self)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_ssm_cmd_basic(self):
|
||||||
|
n = self._make_node(protocol="ssm", host="i-12345")
|
||||||
|
cmd = n._get_cmd()
|
||||||
|
assert "aws ssm start-session" in cmd
|
||||||
|
assert "--target i-12345" in cmd</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_core.TestCommandGeneration.test_ssm_cmd_options"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_ssm_cmd_options</span></span>(<span>self)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_ssm_cmd_options(self):
|
||||||
|
n = self._make_node(protocol="ssm", host="i-12345", options="--document-name AWS-StartInteractiveCommand")
|
||||||
|
cmd = n._get_cmd()
|
||||||
|
assert "--document-name AWS-StartInteractiveCommand" in cmd</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_core.TestCommandGeneration.test_ssm_cmd_tags"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_ssm_cmd_tags</span></span>(<span>self)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_ssm_cmd_tags(self):
|
||||||
|
n = self._make_node(protocol="ssm", host="i-12345", tags={"region": "us-west-2", "profile": "prod"})
|
||||||
|
cmd = n._get_cmd()
|
||||||
|
assert "--region us-west-2" in cmd
|
||||||
|
assert "--profile prod" in cmd</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
<dt id="connpy.tests.test_core.TestCommandGeneration.test_telnet_cmd"><code class="name flex">
|
<dt id="connpy.tests.test_core.TestCommandGeneration.test_telnet_cmd"><code class="name flex">
|
||||||
<span>def <span class="ident">test_telnet_cmd</span></span>(<span>self)</span>
|
<span>def <span class="ident">test_telnet_cmd</span></span>(<span>self)</span>
|
||||||
</code></dt>
|
</code></dt>
|
||||||
@@ -1239,6 +1303,9 @@ el.replaceWith(d);
|
|||||||
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_no_user" href="#connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_no_user">test_ssh_cmd_no_user</a></code></li>
|
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_no_user" href="#connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_no_user">test_ssh_cmd_no_user</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_options" href="#connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_options">test_ssh_cmd_options</a></code></li>
|
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_options" href="#connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_options">test_ssh_cmd_options</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_port" href="#connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_port">test_ssh_cmd_port</a></code></li>
|
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_port" href="#connpy.tests.test_core.TestCommandGeneration.test_ssh_cmd_port">test_ssh_cmd_port</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_ssm_cmd_basic" href="#connpy.tests.test_core.TestCommandGeneration.test_ssm_cmd_basic">test_ssm_cmd_basic</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_ssm_cmd_options" href="#connpy.tests.test_core.TestCommandGeneration.test_ssm_cmd_options">test_ssm_cmd_options</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_ssm_cmd_tags" href="#connpy.tests.test_core.TestCommandGeneration.test_ssm_cmd_tags">test_ssm_cmd_tags</a></code></li>
|
||||||
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_telnet_cmd" href="#connpy.tests.test_core.TestCommandGeneration.test_telnet_cmd">test_telnet_cmd</a></code></li>
|
<li><code><a title="connpy.tests.test_core.TestCommandGeneration.test_telnet_cmd" href="#connpy.tests.test_core.TestCommandGeneration.test_telnet_cmd">test_telnet_cmd</a></code></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@@ -1302,7 +1369,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_execution_service API documentation</title>
|
<title>connpy.tests.test_execution_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -142,7 +142,7 @@ Regression: ExecutionService.test_commands currently ignores on_node_complete.</
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,715 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
|
<title>connpy.tests.test_grpc_layer API documentation</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/typography.min.css" integrity="sha512-Y1DYSb995BAfxobCkKepB1BqJJTPrOp3zPL74AWFugHHmmdcvO+C48WLrUOlhGMc0QG7AE3f7gmvvcrmX2fDoA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" crossorigin>
|
||||||
|
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:1.5em;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:2em 0 .50em 0}h3{font-size:1.4em;margin:1.6em 0 .7em 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .2s ease-in-out}a:visited{color:#503}a:hover{color:#b62}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900;font-weight:bold}pre code{font-size:.8em;line-height:1.4em;padding:1em;display:block}code{background:#f3f3f3;font-family:"DejaVu Sans Mono",monospace;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source > summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible;min-width:max-content}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em 1em;margin:1em 0}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
|
||||||
|
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul ul{padding-left:1em}.toc > ul > li{margin-top:.5em}}</style>
|
||||||
|
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
|
||||||
|
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin></script>
|
||||||
|
<script>window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
hljs.configure({languages: ['bash', 'css', 'diff', 'graphql', 'ini', 'javascript', 'json', 'plaintext', 'python', 'python-repl', 'rust', 'shell', 'sql', 'typescript', 'xml', 'yaml']});
|
||||||
|
hljs.highlightAll();
|
||||||
|
/* Collapse source docstrings */
|
||||||
|
setTimeout(() => {
|
||||||
|
[...document.querySelectorAll('.hljs.language-python > .hljs-string')]
|
||||||
|
.filter(el => el.innerHTML.length > 200 && ['"""', "'''"].includes(el.innerHTML.substring(0, 3)))
|
||||||
|
.forEach(el => {
|
||||||
|
let d = document.createElement('details');
|
||||||
|
d.classList.add('hljs-string');
|
||||||
|
d.innerHTML = '<summary>"""</summary>' + el.innerHTML.substring(3);
|
||||||
|
el.replaceWith(d);
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
})</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<article id="content">
|
||||||
|
<header>
|
||||||
|
<h1 class="title">Module <code>connpy.tests.test_grpc_layer</code></h1>
|
||||||
|
</header>
|
||||||
|
<section id="section-intro">
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2 class="section-title" id="header-classes">Classes</h2>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.MockContext"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">MockContext</span></span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">class MockContext:
|
||||||
|
def abort(self, code, details):
|
||||||
|
raise Exception(f"gRPC Abort: {code} - {details}")</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
<h3>Methods</h3>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.MockContext.abort"><code class="name flex">
|
||||||
|
<span>def <span class="ident">abort</span></span>(<span>self, code, details)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def abort(self, code, details):
|
||||||
|
raise Exception(f"gRPC Abort: {code} - {details}")</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">TestGRPCIntegration</span></span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">class TestGRPCIntegration:
|
||||||
|
@pytest.fixture
|
||||||
|
def grpc_server(self, populated_config):
|
||||||
|
"""Starts a local gRPC server for integration testing."""
|
||||||
|
srv = grpc.server(futures.ThreadPoolExecutor(max_workers=5))
|
||||||
|
|
||||||
|
# Register services
|
||||||
|
connpy_pb2_grpc.add_NodeServiceServicer_to_server(server.NodeServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ProfileServiceServicer_to_server(server.ProfileServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ConfigServiceServicer_to_server(server.ConfigServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ExecutionServiceServicer_to_server(server.ExecutionServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ImportExportServiceServicer_to_server(server.ImportExportServicer(populated_config), srv)
|
||||||
|
|
||||||
|
port = srv.add_insecure_port('127.0.0.1:0')
|
||||||
|
srv.start()
|
||||||
|
yield f"127.0.0.1:{port}"
|
||||||
|
srv.stop(0)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def channel(self, grpc_server):
|
||||||
|
with grpc.insecure_channel(grpc_server) as channel:
|
||||||
|
yield channel
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def node_stub(self, channel):
|
||||||
|
return stubs.NodeStub(channel, "localhost")
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def profile_stub(self, channel):
|
||||||
|
return stubs.ProfileStub(channel, "localhost")
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_stub(self, channel):
|
||||||
|
return stubs.ConfigStub(channel, "localhost")
|
||||||
|
|
||||||
|
def test_list_nodes_integration(self, node_stub):
|
||||||
|
nodes = node_stub.list_nodes()
|
||||||
|
assert "router1" in nodes
|
||||||
|
assert "server1@office" in nodes
|
||||||
|
|
||||||
|
def test_get_node_details_integration(self, node_stub):
|
||||||
|
details = node_stub.get_node_details("router1")
|
||||||
|
assert details["host"] == "10.0.0.1"
|
||||||
|
|
||||||
|
def test_node_not_found_integration(self, node_stub):
|
||||||
|
with pytest.raises(ConnpyError) as exc:
|
||||||
|
node_stub.get_node_details("non-existent")
|
||||||
|
assert "Node 'non-existent' not found." in str(exc.value)
|
||||||
|
|
||||||
|
def test_list_profiles_integration(self, profile_stub):
|
||||||
|
profiles = profile_stub.list_profiles()
|
||||||
|
assert "office-user" in profiles
|
||||||
|
|
||||||
|
def test_get_settings_integration(self, config_stub):
|
||||||
|
settings = config_stub.get_settings()
|
||||||
|
assert "idletime" in settings
|
||||||
|
|
||||||
|
def test_update_setting_integration(self, config_stub):
|
||||||
|
config_stub.update_setting("idletime", 99)
|
||||||
|
settings = config_stub.get_settings()
|
||||||
|
assert settings["idletime"] == 99
|
||||||
|
|
||||||
|
def test_add_delete_node_integration(self, node_stub):
|
||||||
|
node_stub.add_node("integration-test-node", {"host": "9.9.9.9"})
|
||||||
|
assert "integration-test-node" in node_stub.list_nodes()
|
||||||
|
node_stub.delete_node("integration-test-node")
|
||||||
|
assert "integration-test-node" not in node_stub.list_nodes()
|
||||||
|
|
||||||
|
def test_import_yaml_integration(self, channel, node_stub):
|
||||||
|
import yaml
|
||||||
|
from connpy.grpc_layer import stubs
|
||||||
|
stub = stubs.ImportExportStub(channel, "localhost")
|
||||||
|
|
||||||
|
# ImportExportService expects a flat dict of nodes, not a full config structure
|
||||||
|
inventory = {
|
||||||
|
"imported-node": {"host": "8.8.8.8", "protocol": "ssh", "type": "connection"}
|
||||||
|
}
|
||||||
|
yaml_content = yaml.dump(inventory)
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||||
|
f.write(yaml_content)
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
stub.import_from_file(temp_path)
|
||||||
|
# Verify the node was imported and is visible via NodeStub
|
||||||
|
nodes = node_stub.list_nodes()
|
||||||
|
assert "imported-node" in nodes
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.remove(temp_path)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
<h3>Methods</h3>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.channel"><code class="name flex">
|
||||||
|
<span>def <span class="ident">channel</span></span>(<span>self, grpc_server)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@pytest.fixture
|
||||||
|
def channel(self, grpc_server):
|
||||||
|
with grpc.insecure_channel(grpc_server) as channel:
|
||||||
|
yield channel</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.config_stub"><code class="name flex">
|
||||||
|
<span>def <span class="ident">config_stub</span></span>(<span>self, channel)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@pytest.fixture
|
||||||
|
def config_stub(self, channel):
|
||||||
|
return stubs.ConfigStub(channel, "localhost")</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.grpc_server"><code class="name flex">
|
||||||
|
<span>def <span class="ident">grpc_server</span></span>(<span>self, populated_config)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@pytest.fixture
|
||||||
|
def grpc_server(self, populated_config):
|
||||||
|
"""Starts a local gRPC server for integration testing."""
|
||||||
|
srv = grpc.server(futures.ThreadPoolExecutor(max_workers=5))
|
||||||
|
|
||||||
|
# Register services
|
||||||
|
connpy_pb2_grpc.add_NodeServiceServicer_to_server(server.NodeServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ProfileServiceServicer_to_server(server.ProfileServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ConfigServiceServicer_to_server(server.ConfigServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ExecutionServiceServicer_to_server(server.ExecutionServicer(populated_config), srv)
|
||||||
|
connpy_pb2_grpc.add_ImportExportServiceServicer_to_server(server.ImportExportServicer(populated_config), srv)
|
||||||
|
|
||||||
|
port = srv.add_insecure_port('127.0.0.1:0')
|
||||||
|
srv.start()
|
||||||
|
yield f"127.0.0.1:{port}"
|
||||||
|
srv.stop(0)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Starts a local gRPC server for integration testing.</p></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.node_stub"><code class="name flex">
|
||||||
|
<span>def <span class="ident">node_stub</span></span>(<span>self, channel)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@pytest.fixture
|
||||||
|
def node_stub(self, channel):
|
||||||
|
return stubs.NodeStub(channel, "localhost")</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.profile_stub"><code class="name flex">
|
||||||
|
<span>def <span class="ident">profile_stub</span></span>(<span>self, channel)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@pytest.fixture
|
||||||
|
def profile_stub(self, channel):
|
||||||
|
return stubs.ProfileStub(channel, "localhost")</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_add_delete_node_integration"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_add_delete_node_integration</span></span>(<span>self, node_stub)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_add_delete_node_integration(self, node_stub):
|
||||||
|
node_stub.add_node("integration-test-node", {"host": "9.9.9.9"})
|
||||||
|
assert "integration-test-node" in node_stub.list_nodes()
|
||||||
|
node_stub.delete_node("integration-test-node")
|
||||||
|
assert "integration-test-node" not in node_stub.list_nodes()</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_get_node_details_integration"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_get_node_details_integration</span></span>(<span>self, node_stub)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_get_node_details_integration(self, node_stub):
|
||||||
|
details = node_stub.get_node_details("router1")
|
||||||
|
assert details["host"] == "10.0.0.1"</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_get_settings_integration"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_get_settings_integration</span></span>(<span>self, config_stub)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_get_settings_integration(self, config_stub):
|
||||||
|
settings = config_stub.get_settings()
|
||||||
|
assert "idletime" in settings</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_import_yaml_integration"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_import_yaml_integration</span></span>(<span>self, channel, node_stub)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_import_yaml_integration(self, channel, node_stub):
|
||||||
|
import yaml
|
||||||
|
from connpy.grpc_layer import stubs
|
||||||
|
stub = stubs.ImportExportStub(channel, "localhost")
|
||||||
|
|
||||||
|
# ImportExportService expects a flat dict of nodes, not a full config structure
|
||||||
|
inventory = {
|
||||||
|
"imported-node": {"host": "8.8.8.8", "protocol": "ssh", "type": "connection"}
|
||||||
|
}
|
||||||
|
yaml_content = yaml.dump(inventory)
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||||
|
f.write(yaml_content)
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
stub.import_from_file(temp_path)
|
||||||
|
# Verify the node was imported and is visible via NodeStub
|
||||||
|
nodes = node_stub.list_nodes()
|
||||||
|
assert "imported-node" in nodes
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.remove(temp_path)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_list_nodes_integration"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_list_nodes_integration</span></span>(<span>self, node_stub)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_list_nodes_integration(self, node_stub):
|
||||||
|
nodes = node_stub.list_nodes()
|
||||||
|
assert "router1" in nodes
|
||||||
|
assert "server1@office" in nodes</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_list_profiles_integration"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_list_profiles_integration</span></span>(<span>self, profile_stub)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_list_profiles_integration(self, profile_stub):
|
||||||
|
profiles = profile_stub.list_profiles()
|
||||||
|
assert "office-user" in profiles</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_node_not_found_integration"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_node_not_found_integration</span></span>(<span>self, node_stub)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_node_not_found_integration(self, node_stub):
|
||||||
|
with pytest.raises(ConnpyError) as exc:
|
||||||
|
node_stub.get_node_details("non-existent")
|
||||||
|
assert "Node 'non-existent' not found." in str(exc.value)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_update_setting_integration"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_update_setting_integration</span></span>(<span>self, config_stub)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_update_setting_integration(self, config_stub):
|
||||||
|
config_stub.update_setting("idletime", 99)
|
||||||
|
settings = config_stub.get_settings()
|
||||||
|
assert settings["idletime"] == 99</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestNodeServicerNaming"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">TestNodeServicerNaming</span></span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">class TestNodeServicerNaming:
|
||||||
|
@pytest.fixture
|
||||||
|
def servicer(self, populated_config):
|
||||||
|
return server.NodeServicer(populated_config)
|
||||||
|
|
||||||
|
@patch("connpy.core.node")
|
||||||
|
def test_interact_node_uses_passed_name(self, mock_node, servicer):
|
||||||
|
# Setup request with custom name
|
||||||
|
params = {"name": "custom-node-name@test", "host": "1.2.3.4", "protocol": "ssh"}
|
||||||
|
request = connpy_pb2.InteractRequest(
|
||||||
|
id="dynamic",
|
||||||
|
connection_params_json=json.dumps(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock node to allow _connect
|
||||||
|
mock_node_instance = MagicMock()
|
||||||
|
mock_node_instance._connect.return_value = True
|
||||||
|
mock_node.return_value = mock_node_instance
|
||||||
|
|
||||||
|
# We only need the first iteration of the generator to check naming
|
||||||
|
gen = servicer.interact_node(iter([request]), MockContext())
|
||||||
|
next(gen) # Skip the success response
|
||||||
|
|
||||||
|
# Verify that node() was called with the custom name
|
||||||
|
mock_node.assert_called()
|
||||||
|
found = False
|
||||||
|
for call in mock_node.call_args_list:
|
||||||
|
if call.args[0] == "custom-node-name@test":
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
assert found
|
||||||
|
|
||||||
|
@patch("connpy.core.node")
|
||||||
|
def test_interact_node_fallback_naming(self, mock_node, servicer):
|
||||||
|
# Setup request without custom name but with host
|
||||||
|
params = {"host": "my-instance", "protocol": "ssm"}
|
||||||
|
request = connpy_pb2.InteractRequest(
|
||||||
|
id="dynamic",
|
||||||
|
connection_params_json=json.dumps(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_node_instance = MagicMock()
|
||||||
|
mock_node_instance._connect.return_value = True
|
||||||
|
mock_node.return_value = mock_node_instance
|
||||||
|
|
||||||
|
gen = servicer.interact_node(iter([request]), MockContext())
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# Verify fallback name: dynamic-{host}@remote
|
||||||
|
found = False
|
||||||
|
for call in mock_node.call_args_list:
|
||||||
|
if call.args[0] == "dynamic-my-instance@remote":
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
assert found</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
<h3>Methods</h3>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestNodeServicerNaming.servicer"><code class="name flex">
|
||||||
|
<span>def <span class="ident">servicer</span></span>(<span>self, populated_config)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@pytest.fixture
|
||||||
|
def servicer(self, populated_config):
|
||||||
|
return server.NodeServicer(populated_config)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestNodeServicerNaming.test_interact_node_fallback_naming"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_interact_node_fallback_naming</span></span>(<span>self, mock_node, servicer)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@patch("connpy.core.node")
|
||||||
|
def test_interact_node_fallback_naming(self, mock_node, servicer):
|
||||||
|
# Setup request without custom name but with host
|
||||||
|
params = {"host": "my-instance", "protocol": "ssm"}
|
||||||
|
request = connpy_pb2.InteractRequest(
|
||||||
|
id="dynamic",
|
||||||
|
connection_params_json=json.dumps(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_node_instance = MagicMock()
|
||||||
|
mock_node_instance._connect.return_value = True
|
||||||
|
mock_node.return_value = mock_node_instance
|
||||||
|
|
||||||
|
gen = servicer.interact_node(iter([request]), MockContext())
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# Verify fallback name: dynamic-{host}@remote
|
||||||
|
found = False
|
||||||
|
for call in mock_node.call_args_list:
|
||||||
|
if call.args[0] == "dynamic-my-instance@remote":
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
assert found</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestNodeServicerNaming.test_interact_node_uses_passed_name"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_interact_node_uses_passed_name</span></span>(<span>self, mock_node, servicer)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@patch("connpy.core.node")
|
||||||
|
def test_interact_node_uses_passed_name(self, mock_node, servicer):
|
||||||
|
# Setup request with custom name
|
||||||
|
params = {"name": "custom-node-name@test", "host": "1.2.3.4", "protocol": "ssh"}
|
||||||
|
request = connpy_pb2.InteractRequest(
|
||||||
|
id="dynamic",
|
||||||
|
connection_params_json=json.dumps(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock node to allow _connect
|
||||||
|
mock_node_instance = MagicMock()
|
||||||
|
mock_node_instance._connect.return_value = True
|
||||||
|
mock_node.return_value = mock_node_instance
|
||||||
|
|
||||||
|
# We only need the first iteration of the generator to check naming
|
||||||
|
gen = servicer.interact_node(iter([request]), MockContext())
|
||||||
|
next(gen) # Skip the success response
|
||||||
|
|
||||||
|
# Verify that node() was called with the custom name
|
||||||
|
mock_node.assert_called()
|
||||||
|
found = False
|
||||||
|
for call in mock_node.call_args_list:
|
||||||
|
if call.args[0] == "custom-node-name@test":
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
assert found</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestStubsMessageFormatting"><code class="flex name class">
|
||||||
|
<span>class <span class="ident">TestStubsMessageFormatting</span></span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">class TestStubsMessageFormatting:
|
||||||
|
@patch("termios.tcsetattr")
|
||||||
|
@patch("termios.tcgetattr")
|
||||||
|
@patch("tty.setraw")
|
||||||
|
@patch("os.read")
|
||||||
|
@patch("select.select")
|
||||||
|
def test_connect_dynamic_msg_formatting_ssm(self, mock_select, mock_read, mock_setraw, mock_getattr, mock_setattr):
|
||||||
|
from connpy.grpc_layer.stubs import NodeStub
|
||||||
|
|
||||||
|
mock_getattr.return_value = [0, 0, 0, 0, 0, 0, [0] * 32]
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
stub = NodeStub(mock_channel, "localhost:8048")
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.success = True
|
||||||
|
stub.stub.interact_node.return_value = iter([mock_resp])
|
||||||
|
|
||||||
|
with patch("connpy.printer.success") as mock_success:
|
||||||
|
with patch("sys.stdin.fileno", return_value=0):
|
||||||
|
mock_select.return_value = ([], [], [])
|
||||||
|
params = {"protocol": "ssm", "host": "i-12345", "name": "my-ssm-node@aws"}
|
||||||
|
|
||||||
|
with patch("select.select", side_effect=KeyboardInterrupt):
|
||||||
|
try:
|
||||||
|
stub.connect_dynamic(params)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mock_success.assert_called()
|
||||||
|
msg = mock_success.call_args[0][0]
|
||||||
|
assert "Connected to my-ssm-node@aws" in msg
|
||||||
|
assert "at i-12345" in msg
|
||||||
|
assert ":22" not in msg
|
||||||
|
assert "via: ssm" in msg</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
<h3>Methods</h3>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.tests.test_grpc_layer.TestStubsMessageFormatting.test_connect_dynamic_msg_formatting_ssm"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_connect_dynamic_msg_formatting_ssm</span></span>(<span>self, mock_select, mock_read, mock_setraw, mock_getattr, mock_setattr)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@patch("termios.tcsetattr")
|
||||||
|
@patch("termios.tcgetattr")
|
||||||
|
@patch("tty.setraw")
|
||||||
|
@patch("os.read")
|
||||||
|
@patch("select.select")
|
||||||
|
def test_connect_dynamic_msg_formatting_ssm(self, mock_select, mock_read, mock_setraw, mock_getattr, mock_setattr):
|
||||||
|
from connpy.grpc_layer.stubs import NodeStub
|
||||||
|
|
||||||
|
mock_getattr.return_value = [0, 0, 0, 0, 0, 0, [0] * 32]
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
stub = NodeStub(mock_channel, "localhost:8048")
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.success = True
|
||||||
|
stub.stub.interact_node.return_value = iter([mock_resp])
|
||||||
|
|
||||||
|
with patch("connpy.printer.success") as mock_success:
|
||||||
|
with patch("sys.stdin.fileno", return_value=0):
|
||||||
|
mock_select.return_value = ([], [], [])
|
||||||
|
params = {"protocol": "ssm", "host": "i-12345", "name": "my-ssm-node@aws"}
|
||||||
|
|
||||||
|
with patch("select.select", side_effect=KeyboardInterrupt):
|
||||||
|
try:
|
||||||
|
stub.connect_dynamic(params)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mock_success.assert_called()
|
||||||
|
msg = mock_success.call_args[0][0]
|
||||||
|
assert "Connected to my-ssm-node@aws" in msg
|
||||||
|
assert "at i-12345" in msg
|
||||||
|
assert ":22" not in msg
|
||||||
|
assert "via: ssm" in msg</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<nav id="sidebar">
|
||||||
|
<div class="toc">
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
<ul id="index">
|
||||||
|
<li><h3>Super-module</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code><a title="connpy.tests" href="index.html">connpy.tests</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><h3><a href="#header-classes">Classes</a></h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.tests.test_grpc_layer.MockContext" href="#connpy.tests.test_grpc_layer.MockContext">MockContext</a></code></h4>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.MockContext.abort" href="#connpy.tests.test_grpc_layer.MockContext.abort">abort</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration">TestGRPCIntegration</a></code></h4>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.channel" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.channel">channel</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.config_stub" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.config_stub">config_stub</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.grpc_server" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.grpc_server">grpc_server</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.node_stub" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.node_stub">node_stub</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.profile_stub" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.profile_stub">profile_stub</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_add_delete_node_integration" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.test_add_delete_node_integration">test_add_delete_node_integration</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_get_node_details_integration" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.test_get_node_details_integration">test_get_node_details_integration</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_get_settings_integration" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.test_get_settings_integration">test_get_settings_integration</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_import_yaml_integration" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.test_import_yaml_integration">test_import_yaml_integration</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_list_nodes_integration" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.test_list_nodes_integration">test_list_nodes_integration</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_list_profiles_integration" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.test_list_profiles_integration">test_list_profiles_integration</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_node_not_found_integration" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.test_node_not_found_integration">test_node_not_found_integration</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestGRPCIntegration.test_update_setting_integration" href="#connpy.tests.test_grpc_layer.TestGRPCIntegration.test_update_setting_integration">test_update_setting_integration</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.tests.test_grpc_layer.TestNodeServicerNaming" href="#connpy.tests.test_grpc_layer.TestNodeServicerNaming">TestNodeServicerNaming</a></code></h4>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestNodeServicerNaming.servicer" href="#connpy.tests.test_grpc_layer.TestNodeServicerNaming.servicer">servicer</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestNodeServicerNaming.test_interact_node_fallback_naming" href="#connpy.tests.test_grpc_layer.TestNodeServicerNaming.test_interact_node_fallback_naming">test_interact_node_fallback_naming</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestNodeServicerNaming.test_interact_node_uses_passed_name" href="#connpy.tests.test_grpc_layer.TestNodeServicerNaming.test_interact_node_uses_passed_name">test_interact_node_uses_passed_name</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4><code><a title="connpy.tests.test_grpc_layer.TestStubsMessageFormatting" href="#connpy.tests.test_grpc_layer.TestStubsMessageFormatting">TestStubsMessageFormatting</a></code></h4>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.tests.test_grpc_layer.TestStubsMessageFormatting.test_connect_dynamic_msg_formatting_ssm" href="#connpy.tests.test_grpc_layer.TestStubsMessageFormatting.test_connect_dynamic_msg_formatting_ssm">test_connect_dynamic_msg_formatting_ssm</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
<footer id="footer">
|
||||||
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_hooks API documentation</title>
|
<title>connpy.tests.test_hooks API documentation</title>
|
||||||
<meta name="description" content="Tests for connpy.hooks module — MethodHook and ClassHook.">
|
<meta name="description" content="Tests for connpy.hooks module — MethodHook and ClassHook.">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -673,7 +673,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_node_service API documentation</title>
|
<title>connpy.tests.test_node_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -178,7 +178,7 @@ Regression: connapp._mod calls add_node instead of update_node.</p></div>
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_plugins API documentation</title>
|
<title>connpy.tests.test_plugins API documentation</title>
|
||||||
<meta name="description" content="Tests for connpy.plugins module.">
|
<meta name="description" content="Tests for connpy.plugins module.">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -917,7 +917,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_printer API documentation</title>
|
<title>connpy.tests.test_printer API documentation</title>
|
||||||
<meta name="description" content="Tests for connpy.printer module.">
|
<meta name="description" content="Tests for connpy.printer module.">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -459,7 +459,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
|
<title>connpy.tests.test_printer_concurrency API documentation</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/typography.min.css" integrity="sha512-Y1DYSb995BAfxobCkKepB1BqJJTPrOp3zPL74AWFugHHmmdcvO+C48WLrUOlhGMc0QG7AE3f7gmvvcrmX2fDoA==" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" crossorigin>
|
||||||
|
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:1.5em;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:2em 0 .50em 0}h3{font-size:1.4em;margin:1.6em 0 .7em 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .2s ease-in-out}a:visited{color:#503}a:hover{color:#b62}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900;font-weight:bold}pre code{font-size:.8em;line-height:1.4em;padding:1em;display:block}code{background:#f3f3f3;font-family:"DejaVu Sans Mono",monospace;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source > summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible;min-width:max-content}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em 1em;margin:1em 0}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
|
||||||
|
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul ul{padding-left:1em}.toc > ul > li{margin-top:.5em}}</style>
|
||||||
|
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
|
||||||
|
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin></script>
|
||||||
|
<script>window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
hljs.configure({languages: ['bash', 'css', 'diff', 'graphql', 'ini', 'javascript', 'json', 'plaintext', 'python', 'python-repl', 'rust', 'shell', 'sql', 'typescript', 'xml', 'yaml']});
|
||||||
|
hljs.highlightAll();
|
||||||
|
/* Collapse source docstrings */
|
||||||
|
setTimeout(() => {
|
||||||
|
[...document.querySelectorAll('.hljs.language-python > .hljs-string')]
|
||||||
|
.filter(el => el.innerHTML.length > 200 && ['"""', "'''"].includes(el.innerHTML.substring(0, 3)))
|
||||||
|
.forEach(el => {
|
||||||
|
let d = document.createElement('details');
|
||||||
|
d.classList.add('hljs-string');
|
||||||
|
d.innerHTML = '<summary>"""</summary>' + el.innerHTML.substring(3);
|
||||||
|
el.replaceWith(d);
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
})</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<article id="content">
|
||||||
|
<header>
|
||||||
|
<h1 class="title">Module <code>connpy.tests.test_printer_concurrency</code></h1>
|
||||||
|
</header>
|
||||||
|
<section id="section-intro">
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2 class="section-title" id="header-functions">Functions</h2>
|
||||||
|
<dl>
|
||||||
|
<dt id="connpy.tests.test_printer_concurrency.test_printer_manual_stream"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_printer_manual_stream</span></span>(<span>)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_printer_manual_stream():
|
||||||
|
"""Verify that setting a thread stream correctly captures printer output in the current thread."""
|
||||||
|
buf = io.StringIO()
|
||||||
|
|
||||||
|
# We must clear the thread-local console to force it to pick up the new sys.stdout proxy
|
||||||
|
printer.set_thread_console(None)
|
||||||
|
printer.set_thread_stream(buf)
|
||||||
|
|
||||||
|
printer.info("Captured-Message")
|
||||||
|
|
||||||
|
output = buf.getvalue()
|
||||||
|
printer.set_thread_stream(None)
|
||||||
|
printer.set_thread_console(None)
|
||||||
|
|
||||||
|
assert "Captured-Message" in output</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Verify that setting a thread stream correctly captures printer output in the current thread.</p></div>
|
||||||
|
</dd>
|
||||||
|
<dt id="connpy.tests.test_printer_concurrency.test_printer_thread_isolation"><code class="name flex">
|
||||||
|
<span>def <span class="ident">test_printer_thread_isolation</span></span>(<span>)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">def test_printer_thread_isolation():
|
||||||
|
"""Verify that printer output is isolated per thread when using set_thread_stream."""
|
||||||
|
num_threads = 5
|
||||||
|
iterations = 20
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
def worker(thread_id):
|
||||||
|
# Create a private buffer for this thread
|
||||||
|
buf = io.StringIO()
|
||||||
|
printer.set_thread_stream(buf)
|
||||||
|
|
||||||
|
# Ensure we have a clean console for this thread
|
||||||
|
# In a real gRPC request, this happens automatically as it's a new thread
|
||||||
|
printer.set_thread_console(None)
|
||||||
|
|
||||||
|
# Each thread prints its own ID
|
||||||
|
expected_msg = f"Thread-{thread_id}"
|
||||||
|
for _ in range(iterations):
|
||||||
|
printer.info(expected_msg)
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
results[thread_id] = buf.getvalue()
|
||||||
|
printer.set_thread_stream(None)
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
for i in range(num_threads):
|
||||||
|
t = threading.Thread(target=worker, args=(i,))
|
||||||
|
threads.append(t)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
for t in threads:
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
for thread_id, output in results.items():
|
||||||
|
expected_msg = f"Thread-{thread_id}"
|
||||||
|
assert expected_msg in output
|
||||||
|
|
||||||
|
# Ensure no leaks
|
||||||
|
for other_id in range(num_threads):
|
||||||
|
if other_id == thread_id: continue
|
||||||
|
assert f"Thread-{other_id}" not in output</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Verify that printer output is isolated per thread when using set_thread_stream.</p></div>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<nav id="sidebar">
|
||||||
|
<div class="toc">
|
||||||
|
<ul></ul>
|
||||||
|
</div>
|
||||||
|
<ul id="index">
|
||||||
|
<li><h3>Super-module</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code><a title="connpy.tests" href="index.html">connpy.tests</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><h3><a href="#header-functions">Functions</a></h3>
|
||||||
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.tests.test_printer_concurrency.test_printer_manual_stream" href="#connpy.tests.test_printer_concurrency.test_printer_manual_stream">test_printer_manual_stream</a></code></li>
|
||||||
|
<li><code><a title="connpy.tests.test_printer_concurrency.test_printer_thread_isolation" href="#connpy.tests.test_printer_concurrency.test_printer_thread_isolation">test_printer_thread_isolation</a></code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
<footer id="footer">
|
||||||
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_profile_service API documentation</title>
|
<title>connpy.tests.test_profile_service API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -192,7 +192,7 @@ Regression: ProfileService currently doesn't resolve inheritance within profiles
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_provider API documentation</title>
|
<title>connpy.tests.test_provider API documentation</title>
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -139,7 +139,7 @@ el.replaceWith(d);
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="generator" content="pdoc3 0.11.5">
|
<meta name="generator" content="pdoc3 0.11.6">
|
||||||
<title>connpy.tests.test_sync API documentation</title>
|
<title>connpy.tests.test_sync API documentation</title>
|
||||||
<meta name="description" content="Tests for connpy.services.sync_service">
|
<meta name="description" content="Tests for connpy.services.sync_service">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
|
||||||
@@ -354,7 +354,7 @@ def test_perform_restore(self, mock_remove, mock_dirname, mock_exists, MockZipFi
|
|||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p>
|
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,744 +0,0 @@
|
|||||||
# 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 --list` → **reemplazar** 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()`
|
|
||||||
|
|
||||||
```python
|
|
||||||
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()`
|
|
||||||
|
|
||||||
```python
|
|
||||||
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
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 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:
|
|
||||||
|
|
||||||
```python
|
|
||||||
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`
|
|
||||||
|
|
||||||
```python
|
|
||||||
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`
|
|
||||||
|
|
||||||
```python
|
|
||||||
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`
|
|
||||||
|
|
||||||
```python
|
|
||||||
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`
|
|
||||||
|
|
||||||
```python
|
|
||||||
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`
|
|
||||||
|
|
||||||
```python
|
|
||||||
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`
|
|
||||||
|
|
||||||
```python
|
|
||||||
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`
|
|
||||||
|
|
||||||
```python
|
|
||||||
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`
|
|
||||||
|
|
||||||
```python
|
|
||||||
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.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Detalle de bugs corregidos (click para expandir)</summary>
|
|
||||||
|
|
||||||
### 🔴 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 |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
```python
|
|
||||||
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`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 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`:
|
|
||||||
```python
|
|
||||||
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:
|
|
||||||
```python
|
|
||||||
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
|
|
||||||
```bash
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
# Remote Plugin Support — Implementation Plan
|
|
||||||
|
|
||||||
## Objetivo
|
|
||||||
|
|
||||||
Cuando connpy opera en modo remoto, el usuario puede usar plugins instalados **solo en el server**. La ejecución es completamente transparente: el cliente construye el argparse localmente (usando el source descargado del server) y todo lo demás corre en el server vía gRPC streaming.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Arquitectura
|
|
||||||
|
|
||||||
```
|
|
||||||
Cliente Server
|
|
||||||
─────── ──────
|
|
||||||
connpy aws vpc vpc-123
|
|
||||||
│
|
|
||||||
├─ init() remoto
|
|
||||||
│ ├─ gRPC: list_plugins() → ["aws", "monitor"]
|
|
||||||
│ ├─ gRPC: get_plugin_source("aws") → aws.py source (texto)
|
|
||||||
│ ├─ Carga Parser en RAM → agrega subcomando al argparse
|
|
||||||
│ └─ Marca "aws" como remote_plugin
|
|
||||||
│
|
|
||||||
├─ argparse parsea args localmente (usa el Parser descargado)
|
|
||||||
│
|
|
||||||
└─ dispatch():
|
|
||||||
└─ "aws" es remote_plugin
|
|
||||||
└─ gRPC: invoke_plugin("aws", args_json) streaming
|
|
||||||
└─ Entrypoint(args, parser, connapp) ──→ output
|
|
||||||
←─── chunks de stdout/stderr ──┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Regla fundamental
|
|
||||||
- **`Parser`** → corre en el **cliente** (construye argparse)
|
|
||||||
- **`Entrypoint`** → corre en el **server** (toda la lógica del plugin)
|
|
||||||
- El plugin **no sabe** si está siendo ejecutado local o remotamente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cache de plugins remotos
|
|
||||||
|
|
||||||
### Estructura en disco
|
|
||||||
```
|
|
||||||
{configdir}/remote_plugins/
|
|
||||||
├── aws.py ← source descargado del server
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cuándo se actualiza
|
|
||||||
Cada vez que `connpy` arranca en modo remoto, descarga y **sobreescribe** la cache en la ruta especificada por el archivo `.folder` activo. Sin hash, sin TTL, sin lógica extra. Siempre fresco.
|
|
||||||
|
|
||||||
### Uso por completion.py
|
|
||||||
`completion.py` incluye `{configdir}/remote_plugins/` como directorio adicional al escanear plugins. Carga `_connpy_tree()` desde el `.py` cacheado y automáticamente le inyecta la función `get_cwd()` para que pueda completar rutas locales sin dependencias extras.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Gestión de plugin: `connpy plugin`
|
|
||||||
|
|
||||||
### `--list`: muestra local y remoto
|
|
||||||
|
|
||||||
```
|
|
||||||
connpy plugin --list
|
|
||||||
|
|
||||||
LOCAL:
|
|
||||||
aws [active]
|
|
||||||
tools [active]
|
|
||||||
|
|
||||||
REMOTE:
|
|
||||||
aws [shadowed] ← mismo nombre, local tiene prioridad
|
|
||||||
monitor [active]
|
|
||||||
deploy [active]
|
|
||||||
```
|
|
||||||
|
|
||||||
Estados posibles:
|
|
||||||
| Estado | Significado |
|
|
||||||
|---|---|
|
|
||||||
| `[active]` | Activo y usable |
|
|
||||||
| `[shadowed]` | Existe pero el otro lado tiene prioridad |
|
|
||||||
| `[disabled]` | Explícitamente desactivado |
|
|
||||||
|
|
||||||
### Prioridad cuando el mismo plugin existe en ambos lados
|
|
||||||
|
|
||||||
**Local gana por defecto.** Override guardado en:
|
|
||||||
```json
|
|
||||||
// ~/.config/conn/plugin_preferences.json
|
|
||||||
{
|
|
||||||
"aws": "remote"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Semántica de `--enable`
|
|
||||||
|
|
||||||
Activa el plugin pedido. Si el mismo plugin existe en el otro lado → ese queda shadowed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
connpy plugin --enable aws # activa local, remoto queda shadowed
|
|
||||||
connpy plugin --enable aws --remote # activa remoto, local queda shadowed
|
|
||||||
```
|
|
||||||
|
|
||||||
### Semántica de `--disable`
|
|
||||||
|
|
||||||
Desactiva el plugin indicado. **NO activa automáticamente el del otro lado.**
|
|
||||||
Permite tener ambos desactivados si el usuario lo desea.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
connpy plugin --disable aws # desactiva aws local (remoto no cambia)
|
|
||||||
connpy plugin --disable aws --remote # desactiva aws remoto (local no cambia)
|
|
||||||
```
|
|
||||||
|
|
||||||
### `--add`, `--del`, `--update`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
connpy plugin --add myplug myfile.py # instala local
|
|
||||||
connpy plugin --add myplug myfile.py --remote # sube al server vía gRPC
|
|
||||||
connpy plugin --del myplug # borra local
|
|
||||||
connpy plugin --del myplug --remote # borra del server
|
|
||||||
connpy plugin --update myplug myfile.py # actualiza local
|
|
||||||
connpy plugin --update myplug myfile.py --remote # actualiza en server
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Archivos a modificar
|
|
||||||
|
|
||||||
| Archivo | Cambio |
|
|
||||||
|---|---|
|
|
||||||
| `grpc/connpy_pb2_grpc.py` | Agregar `get_plugin_source` + `invoke_plugin` a PluginService |
|
|
||||||
| `grpc/connpy_pb2.py` | Agregar `PluginInvokeRequest` + `OutputChunk` messages |
|
|
||||||
| `grpc/server.py` | Implementar métodos en `PluginServicer` |
|
|
||||||
| `grpc/stubs.py` | Agregar métodos a `PluginStub` |
|
|
||||||
| `services/plugin_service.py` | Agregar `get_plugin_source()` + `invoke_plugin()` |
|
|
||||||
| `connpy/plugins.py` | Remote loading, preferences, `remote_plugins` dict |
|
|
||||||
| `connapp.py` | Remote plugin init + dispatch proxy |
|
|
||||||
| `cli/plugin_handler.py` | Flag `--remote`, `--list` unificado, enable/disable con prefs |
|
|
||||||
| `completion.py` | Incluir cache remoto en `_get_plugins()` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Nuevo gRPC en `PluginService`
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Mensajes nuevos
|
|
||||||
class PluginInvokeRequest:
|
|
||||||
name: str
|
|
||||||
args_json: str # argparse.Namespace serializado como JSON (solo tipos básicos)
|
|
||||||
|
|
||||||
class OutputChunk:
|
|
||||||
text: str
|
|
||||||
is_error: bool
|
|
||||||
|
|
||||||
# Métodos nuevos en PluginService
|
|
||||||
get_plugin_source(IdRequest) → StringResponse
|
|
||||||
invoke_plugin(PluginInvokeRequest) → stream OutputChunk
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Serialización de args
|
|
||||||
|
|
||||||
`argparse.Namespace` se serializa filtrando solo tipos básicos (str, int, float, bool, list, None):
|
|
||||||
|
|
||||||
```python
|
|
||||||
args_dict = {k: v for k, v in vars(args_namespace).items()
|
|
||||||
if isinstance(v, (str, int, float, bool, list, type(None)))}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Limitación conocida**: plugins remotos no pueden usar `argparse.FileType`. Deben recibir paths como strings y abrir el archivo en el server.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Flujo de enable/disable con conflictos
|
|
||||||
|
|
||||||
```
|
|
||||||
Estado inicial:
|
|
||||||
LOCAL: aws [active]
|
|
||||||
REMOTE: aws [shadowed]
|
|
||||||
|
|
||||||
connpy plugin --enable aws --remote
|
|
||||||
→ preferences: {"aws": "remote"}
|
|
||||||
LOCAL: aws [shadowed]
|
|
||||||
REMOTE: aws [active]
|
|
||||||
|
|
||||||
connpy plugin --disable aws --remote
|
|
||||||
→ gRPC disable en server, NO toca el local ni el pref
|
|
||||||
LOCAL: aws [shadowed] ← sigue con pref=remote pero remoto está disabled
|
|
||||||
REMOTE: aws [disabled]
|
|
||||||
|
|
||||||
connpy plugin --enable aws
|
|
||||||
→ Borra "aws" del preferences (vuelve a default = local)
|
|
||||||
LOCAL: aws [active]
|
|
||||||
REMOTE: aws [shadowed]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Captura de output en el server
|
|
||||||
|
|
||||||
El server redirige `sys.stdout` durante la ejecución del `Entrypoint` y hace yield de cada línea:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def invoke_plugin(self, name, args_dict):
|
|
||||||
import sys, io
|
|
||||||
from argparse import Namespace
|
|
||||||
args = Namespace(**args_dict)
|
|
||||||
old_stdout = sys.stdout
|
|
||||||
sys.stdout = buf = io.StringIO()
|
|
||||||
try:
|
|
||||||
plugin.Entrypoint(args, parser, connapp)
|
|
||||||
finally:
|
|
||||||
sys.stdout = old_stdout
|
|
||||||
for line in buf.getvalue().splitlines(keepends=True):
|
|
||||||
yield line
|
|
||||||
```
|
|
||||||
|
|
||||||
Si el plugin usa Rich o escribe directo al fd, se puede aislar en un subprocess.
|
|
||||||
Reference in New Issue
Block a user