From 32fe18d27a55c0c75e60de7d4b528ef130fd8777 Mon Sep 17 00:00:00 2001
From: Fede Luzzi
Date: Thu, 13 Aug 2026 12:55:03 -0300
Subject: [PATCH] perf(cli,core): lazy load Crypto and rich.markdown modules to
boost startup latency and bump to v6.1.1 - Implement lazy `@property` getters
for `configfile.privatekey` and `configfile.publickey` to defer RSA key
loading and pycryptodome imports until explicitly accessed. - Move top-level
`Crypto` imports inside local methods in `configfile.py`, `core.py`, and
`config_service.py`. - Remove top-level `from rich.markdown import Markdown`
across CLI handlers (`node_handler.py`, `ai_handler.py`, `terminal_ui.py`)
and `ai.py`, deferring parser load to active rendering calls. - Reduce CLI
startup load latency from ~172 ms to ~49 ms (>3.5x speedup / ~71% latency
reduction). - Regenerate HTML documentation in `docs/` via `pdoc`. - Bump
version to v6.1.1.
---
README.md | 2 +-
connpy/__init__.py | 2 +-
connpy/_version.py | 2 +-
connpy/ai.py | 4 +-
connpy/cli/ai_handler.py | 3 +-
connpy/cli/node_handler.py | 2 +-
connpy/cli/terminal_ui.py | 2 +-
connpy/configfile.py | 34 ++++++++++++--
connpy/core.py | 4 +-
connpy/services/config_service.py | 2 -
docs/connpy/ai.html | 6 +++
docs/connpy/cli/ai_handler.html | 4 ++
docs/connpy/cli/node_handler.html | 2 +
docs/connpy/cli/terminal_ui.html | 2 +
docs/connpy/index.html | 77 +++++++++++++++++++++++++++++--
15 files changed, 127 insertions(+), 21 deletions(-)
diff --git a/README.md b/README.md
index c2d6af5..7b1bc99 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
-# Connpy (v6.1.0)
+# Connpy (v6.1.1)
[](https://pypi.org/pypi/connpy/)
[](https://pypi.org/pypi/connpy/)
[](https://pypi.org/pypi/connpy/)
diff --git a/connpy/__init__.py b/connpy/__init__.py
index d520eee..61827e8 100644
--- a/connpy/__init__.py
+++ b/connpy/__init__.py
@@ -5,7 +5,7 @@
-# Connpy (v6.1.0)
+# Connpy (v6.1.1)
[](https://pypi.org/pypi/connpy/)
[](https://pypi.org/pypi/connpy/)
[](https://pypi.org/pypi/connpy/)
diff --git a/connpy/_version.py b/connpy/_version.py
index 7856d12..7f8a859 100644
--- a/connpy/_version.py
+++ b/connpy/_version.py
@@ -1 +1 @@
-__version__ = "6.1.0"
+__version__ = "6.1.1"
diff --git a/connpy/ai.py b/connpy/ai.py
index a0b3f4e..5ba7ae1 100755
--- a/connpy/ai.py
+++ b/connpy/ai.py
@@ -33,7 +33,6 @@ def stream_chunk_builder(*args, **kwargs):
return _stream_chunk_builder(*args, **kwargs)
from .hooks import ClassHook, MethodHook
from . import printer
-from rich.markdown import Markdown
from rich.panel import Panel
from rich.text import Text
from rich.console import Group
@@ -1183,6 +1182,7 @@ class ai:
if status:
try: status.stop()
except: pass
+ from rich.markdown import Markdown
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 status:
try: status.start()
@@ -1230,6 +1230,7 @@ class ai:
if status:
try: status.stop()
except: pass
+ from rich.markdown import Markdown
self.console.print(Panel(Markdown(obs), title="[architect]Architect Consultation[/architect]", border_style="architect"))
if status:
try: status.start()
@@ -1872,6 +1873,7 @@ class PlaybookBuilderAgent:
chunk_callback(resp_msg.content)
elif not resp_msg.tool_calls:
# In direct non-streaming output, print markdown
+ from rich.markdown import Markdown
self.console.print(Markdown(resp_msg.content))
if not resp_msg.tool_calls:
diff --git a/connpy/cli/ai_handler.py b/connpy/cli/ai_handler.py
index fabc377..6e7e149 100644
--- a/connpy/cli/ai_handler.py
+++ b/connpy/cli/ai_handler.py
@@ -1,6 +1,5 @@
import sys
from rich.panel import Panel
-from rich.markdown import Markdown
from rich.rule import Rule
from rich.prompt import Prompt
@@ -102,6 +101,7 @@ class AIHandler:
title = "[architect][bold]Network Architect[/bold][/architect]" if responder == "architect" else "[engineer][bold]Network Engineer[/bold][/engineer]"
if not result.get("streamed"):
+ from rich.markdown import Markdown
mdprint(Panel(Markdown(result["response"]), title=title, border_style=border, expand=False))
if "usage" in result:
@@ -121,6 +121,7 @@ class AIHandler:
printer.info(f"Session '{session_id}' not found. Starting clean.")
if not history:
+ from rich.markdown import Markdown
mdprint(Rule(style="engineer"))
mdprint(Markdown("**Networking Expert Agent**: Hi! I'm your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType 'exit' to quit.\n"))
mdprint(Rule(style="engineer"))
diff --git a/connpy/cli/node_handler.py b/connpy/cli/node_handler.py
index 4555bac..0b1c9c8 100644
--- a/connpy/cli/node_handler.py
+++ b/connpy/cli/node_handler.py
@@ -1,6 +1,5 @@
import sys
import yaml
-from rich.markdown import Markdown
from .. import printer
from ..services.exceptions import ConnpyError, InvalidConfigurationError
@@ -156,6 +155,7 @@ class NodeHandler:
# Fast fail if parent folder does not exist
self.app.services.nodes.validate_parent_folder(args.data)
+ from rich.markdown import Markdown
printer.console.print(Markdown(get_instructions()))
new_node_data = self.forms.questions_nodes(args.data, uniques)
diff --git a/connpy/cli/terminal_ui.py b/connpy/cli/terminal_ui.py
index 0515f00..57c12d2 100644
--- a/connpy/cli/terminal_ui.py
+++ b/connpy/cli/terminal_ui.py
@@ -11,7 +11,6 @@ from textwrap import dedent
from rich.console import Console
from rich.panel import Panel
-from rich.markdown import Markdown
from prompt_toolkit import PromptSession
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.filters import has_completions
@@ -459,6 +458,7 @@ class CopilotInterface:
# If no chunks were streamed but we have a guide, print it as a panel
if first_chunk and result and result.get("guide"):
+ from rich.markdown import Markdown
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color))
commands = result.get("commands", [])
diff --git a/connpy/configfile.py b/connpy/configfile.py
index 461cdf8..fa9e605 100755
--- a/connpy/configfile.py
+++ b/connpy/configfile.py
@@ -6,8 +6,6 @@ import re
import sys
import yaml
import shutil
-from Crypto.PublicKey import RSA
-from Crypto.Cipher import PKCS1_OAEP
from pathlib import Path
from copy import deepcopy
from .hooks import MethodHook, ClassHook
@@ -139,16 +137,39 @@ class configfile:
self.connections = config["connections"]
self.profiles = config["profiles"]
+ self._privatekey_obj = None
+ self._publickey_obj = None
if not os.path.exists(self.key):
self._createkey(self.key)
- with open(self.key) as f:
- self.privatekey = RSA.import_key(f.read())
- self.publickey = self.privatekey.publickey()
# Self-heal text caches if they are missing
if not os.path.exists(self.fzf_cachefile) or not os.path.exists(self.folders_cachefile) or not os.path.exists(self.profiles_cachefile):
self._generate_nodes_cache()
+ @property
+ def privatekey(self):
+ if getattr(self, '_privatekey_obj', None) is None:
+ from Crypto.PublicKey import RSA
+ if not os.path.exists(self.key):
+ self._createkey(self.key)
+ with open(self.key) as f:
+ self._privatekey_obj = RSA.import_key(f.read())
+ return self._privatekey_obj
+
+ @privatekey.setter
+ def privatekey(self, value):
+ self._privatekey_obj = value
+
+ @property
+ def publickey(self):
+ if getattr(self, '_publickey_obj', None) is None:
+ self._publickey_obj = self.privatekey.publickey()
+ return self._publickey_obj
+
+ @publickey.setter
+ def publickey(self, value):
+ self._publickey_obj = value
+
def get_effective_setting(self, key, default=None):
"""Get config setting with shared fallback for inheritable keys."""
@@ -297,6 +318,7 @@ class configfile:
def _createkey(self, keyfile):
#Create key file
+ from Crypto.PublicKey import RSA
key = RSA.generate(2048)
with open(keyfile,'wb') as f:
f.write(key.export_key('PEM'))
@@ -623,6 +645,8 @@ class configfile:
if keyfile is None:
keyfile = self.key
with open(keyfile) as f:
+ from Crypto.PublicKey import RSA
+ from Crypto.Cipher import PKCS1_OAEP
key = RSA.import_key(f.read())
f.close()
publickey = key.publickey()
diff --git a/connpy/core.py b/connpy/core.py
index 5f30e50..5175a56 100755
--- a/connpy/core.py
+++ b/connpy/core.py
@@ -4,8 +4,6 @@ import os
import re
import pexpect
import shlex
-from Crypto.PublicKey import RSA
-from Crypto.Cipher import PKCS1_OAEP
import ast
from time import sleep,time
import datetime
@@ -274,6 +272,8 @@ class node:
if keyfile is None:
keyfile = self.key
if keyfile is not None:
+ from Crypto.PublicKey import RSA
+ from Crypto.Cipher import PKCS1_OAEP
with open(keyfile) as f:
key = RSA.import_key(f.read())
decryptor = PKCS1_OAEP.new(key)
diff --git a/connpy/services/config_service.py b/connpy/services/config_service.py
index 5f900bd..7e1fa28 100644
--- a/connpy/services/config_service.py
+++ b/connpy/services/config_service.py
@@ -2,8 +2,6 @@ import os
import shutil
import base64
from typing import Any, Dict
-from Crypto.PublicKey import RSA
-from Crypto.Cipher import PKCS1_OAEP
from .base import BaseService
from .exceptions import ConnpyError, InvalidConfigurationError, NodeNotFoundError
diff --git a/docs/connpy/ai.html b/docs/connpy/ai.html
index 7785f77..5913b4a 100644
--- a/docs/connpy/ai.html
+++ b/docs/connpy/ai.html
@@ -255,6 +255,7 @@ el.replaceWith(d);
chunk_callback(resp_msg.content)
elif not resp_msg.tool_calls:
# In direct non-streaming output, print markdown
+ from rich.markdown import Markdown
self.console.print(Markdown(resp_msg.content))
if not resp_msg.tool_calls:
@@ -388,6 +389,7 @@ el.replaceWith(d);
chunk_callback(resp_msg.content)
elif not resp_msg.tool_calls:
# In direct non-streaming output, print markdown
+ from rich.markdown import Markdown
self.console.print(Markdown(resp_msg.content))
if not resp_msg.tool_calls:
@@ -1621,6 +1623,7 @@ class ai:
if status:
try: status.stop()
except: pass
+ from rich.markdown import Markdown
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 status:
try: status.start()
@@ -1668,6 +1671,7 @@ class ai:
if status:
try: status.stop()
except: pass
+ from rich.markdown import Markdown
self.console.print(Panel(Markdown(obs), title="[architect]Architect Consultation[/architect]", border_style="architect"))
if status:
try: status.start()
@@ -2579,6 +2583,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
if status:
try: status.stop()
except: pass
+ from rich.markdown import Markdown
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 status:
try: status.start()
@@ -2626,6 +2631,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
if status:
try: status.stop()
except: pass
+ from rich.markdown import Markdown
self.console.print(Panel(Markdown(obs), title="[architect]Architect Consultation[/architect]", border_style="architect"))
if status:
try: status.start()
diff --git a/docs/connpy/cli/ai_handler.html b/docs/connpy/cli/ai_handler.html
index f915c21..8827e33 100644
--- a/docs/connpy/cli/ai_handler.html
+++ b/docs/connpy/cli/ai_handler.html
@@ -148,6 +148,7 @@ el.replaceWith(d);
title = "[architect][bold]Network Architect[/bold][/architect]" if responder == "architect" else "[engineer][bold]Network Engineer[/bold][/engineer]"
if not result.get("streamed"):
+ from rich.markdown import Markdown
mdprint(Panel(Markdown(result["response"]), title=title, border_style=border, expand=False))
if "usage" in result:
@@ -167,6 +168,7 @@ el.replaceWith(d);
printer.info(f"Session '{session_id}' not found. Starting clean.")
if not history:
+ from rich.markdown import Markdown
mdprint(Rule(style="engineer"))
mdprint(Markdown("**Networking Expert Agent**: Hi! I'm your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType 'exit' to quit.\n"))
mdprint(Rule(style="engineer"))
@@ -573,6 +575,7 @@ el.replaceWith(d);
printer.info(f"Session '{session_id}' not found. Starting clean.")
if not history:
+ from rich.markdown import Markdown
mdprint(Rule(style="engineer"))
mdprint(Markdown("**Networking Expert Agent**: Hi! I'm your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType 'exit' to quit.\n"))
mdprint(Rule(style="engineer"))
@@ -626,6 +629,7 @@ el.replaceWith(d);
title = "[architect][bold]Network Architect[/bold][/architect]" if responder == "architect" else "[engineer][bold]Network Engineer[/bold][/engineer]"
if not result.get("streamed"):
+ from rich.markdown import Markdown
mdprint(Panel(Markdown(result["response"]), title=title, border_style=border, expand=False))
if "usage" in result:
diff --git a/docs/connpy/cli/node_handler.html b/docs/connpy/cli/node_handler.html
index 977bf19..1448004 100644
--- a/docs/connpy/cli/node_handler.html
+++ b/docs/connpy/cli/node_handler.html
@@ -204,6 +204,7 @@ el.replaceWith(d);
# Fast fail if parent folder does not exist
self.app.services.nodes.validate_parent_folder(args.data)
+ from rich.markdown import Markdown
printer.console.print(Markdown(get_instructions()))
new_node_data = self.forms.questions_nodes(args.data, uniques)
@@ -374,6 +375,7 @@ def forms(self):
# Fast fail if parent folder does not exist
self.app.services.nodes.validate_parent_folder(args.data)
+ from rich.markdown import Markdown
printer.console.print(Markdown(get_instructions()))
new_node_data = self.forms.questions_nodes(args.data, uniques)
diff --git a/docs/connpy/cli/terminal_ui.html b/docs/connpy/cli/terminal_ui.html
index c53528f..c13d0a7 100644
--- a/docs/connpy/cli/terminal_ui.html
+++ b/docs/connpy/cli/terminal_ui.html
@@ -492,6 +492,7 @@ el.replaceWith(d);
# If no chunks were streamed but we have a guide, print it as a panel
if first_chunk and result and result.get("guide"):
+ from rich.markdown import Markdown
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color))
commands = result.get("commands", [])
@@ -997,6 +998,7 @@ el.replaceWith(d);
# If no chunks were streamed but we have a guide, print it as a panel
if first_chunk and result and result.get("guide"):
+ from rich.markdown import Markdown
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color))
commands = result.get("commands", [])
diff --git a/docs/connpy/index.html b/docs/connpy/index.html
index f974912..8d5b805 100644
--- a/docs/connpy/index.html
+++ b/docs/connpy/index.html
@@ -41,7 +41,7 @@ el.replaceWith(d);
-Connpy (v6.1.0)
+Connpy (v6.1.1)
@@ -819,16 +819,39 @@ class configfile:
self.connections = config["connections"]
self.profiles = config["profiles"]
+ self._privatekey_obj = None
+ self._publickey_obj = None
if not os.path.exists(self.key):
self._createkey(self.key)
- with open(self.key) as f:
- self.privatekey = RSA.import_key(f.read())
- self.publickey = self.privatekey.publickey()
# Self-heal text caches if they are missing
if not os.path.exists(self.fzf_cachefile) or not os.path.exists(self.folders_cachefile) or not os.path.exists(self.profiles_cachefile):
self._generate_nodes_cache()
+ @property
+ def privatekey(self):
+ if getattr(self, '_privatekey_obj', None) is None:
+ from Crypto.PublicKey import RSA
+ if not os.path.exists(self.key):
+ self._createkey(self.key)
+ with open(self.key) as f:
+ self._privatekey_obj = RSA.import_key(f.read())
+ return self._privatekey_obj
+
+ @privatekey.setter
+ def privatekey(self, value):
+ self._privatekey_obj = value
+
+ @property
+ def publickey(self):
+ if getattr(self, '_publickey_obj', None) is None:
+ self._publickey_obj = self.privatekey.publickey()
+ return self._publickey_obj
+
+ @publickey.setter
+ def publickey(self, value):
+ self._publickey_obj = value
+
def get_effective_setting(self, key, default=None):
"""Get config setting with shared fallback for inheritable keys."""
@@ -977,6 +1000,7 @@ class configfile:
def _createkey(self, keyfile):
#Create key file
+ from Crypto.PublicKey import RSA
key = RSA.generate(2048)
with open(keyfile,'wb') as f:
f.write(key.export_key('PEM'))
@@ -1303,6 +1327,8 @@ class configfile:
if keyfile is None:
keyfile = self.key
with open(keyfile) as f:
+ from Crypto.PublicKey import RSA
+ from Crypto.Cipher import PKCS1_OAEP
key = RSA.import_key(f.read())
f.close()
publickey = key.publickey()
@@ -1331,6 +1357,41 @@ class configfile:
- publickey (obj): Object containing the public key to decrypt
passwords.
+
Instance variables
+
+prop privatekey
+-
+
+
+Expand source code
+
+@property
+def privatekey(self):
+ if getattr(self, '_privatekey_obj', None) is None:
+ from Crypto.PublicKey import RSA
+ if not os.path.exists(self.key):
+ self._createkey(self.key)
+ with open(self.key) as f:
+ self._privatekey_obj = RSA.import_key(f.read())
+ return self._privatekey_obj
+
+
+
+prop publickey
+-
+
+
+Expand source code
+
+@property
+def publickey(self):
+ if getattr(self, '_publickey_obj', None) is None:
+ self._publickey_obj = self.privatekey.publickey()
+ return self._publickey_obj
+
+
+
+
Methods
@@ -1363,6 +1424,8 @@ def encrypt(self, password, keyfile=None):
if keyfile is None:
keyfile = self.key
with open(keyfile) as f:
+ from Crypto.PublicKey import RSA
+ from Crypto.Cipher import PKCS1_OAEP
key = RSA.import_key(f.read())
f.close()
publickey = key.publickey()
@@ -1839,6 +1902,8 @@ class node:
if keyfile is None:
keyfile = self.key
if keyfile is not None:
+ from Crypto.PublicKey import RSA
+ from Crypto.Cipher import PKCS1_OAEP
with open(keyfile) as f:
key = RSA.import_key(f.read())
decryptor = PKCS1_OAEP.new(key)
@@ -4022,7 +4087,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,