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.
This commit is contained in:
2026-08-13 12:55:03 -03:00
parent 0632a510ad
commit 32fe18d27a
15 changed files with 127 additions and 21 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
</p>
# Connpy (v6.1.0)
# Connpy (v6.1.1)
[![](https://img.shields.io/pypi/v/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
[![](https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
[![](https://img.shields.io/pypi/dm/connpy.svg?style=flat-square&cacheSeconds=86400)](https://pypi.org/pypi/connpy/)
+1 -1
View File
@@ -5,7 +5,7 @@
</p>
# Connpy (v6.1.0)
# Connpy (v6.1.1)
[![](https://img.shields.io/pypi/v/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
[![](https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
[![](https://img.shields.io/pypi/dm/connpy.svg?style=flat-square&cacheSeconds=86400)](https://pypi.org/pypi/connpy/)
+1 -1
View File
@@ -1 +1 @@
__version__ = "6.1.0"
__version__ = "6.1.1"
+3 -1
View File
@@ -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:
+2 -1
View File
@@ -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"))
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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", [])
+29 -5
View File
@@ -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()
+2 -2
View File
@@ -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)
-2
View File
@@ -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
+6
View File
@@ -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&#34;[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]&#34;, border_style=&#34;architect&#34; if current_brain == &#34;architect&#34; else &#34;engineer&#34;))
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=&#34;[architect]Architect Consultation[/architect]&#34;, border_style=&#34;architect&#34;))
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&#34;[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]&#34;, border_style=&#34;architect&#34; if current_brain == &#34;architect&#34; else &#34;engineer&#34;))
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=&#34;[architect]Architect Consultation[/architect]&#34;, border_style=&#34;architect&#34;))
if status:
try: status.start()
+4
View File
@@ -148,6 +148,7 @@ el.replaceWith(d);
title = &#34;[architect][bold]Network Architect[/bold][/architect]&#34; if responder == &#34;architect&#34; else &#34;[engineer][bold]Network Engineer[/bold][/engineer]&#34;
if not result.get(&#34;streamed&#34;):
from rich.markdown import Markdown
mdprint(Panel(Markdown(result[&#34;response&#34;]), title=title, border_style=border, expand=False))
if &#34;usage&#34; in result:
@@ -167,6 +168,7 @@ el.replaceWith(d);
printer.info(f&#34;Session &#39;{session_id}&#39; not found. Starting clean.&#34;)
if not history:
from rich.markdown import Markdown
mdprint(Rule(style=&#34;engineer&#34;))
mdprint(Markdown(&#34;**Networking Expert Agent**: Hi! I&#39;m your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType &#39;exit&#39; to quit.\n&#34;))
mdprint(Rule(style=&#34;engineer&#34;))
@@ -573,6 +575,7 @@ el.replaceWith(d);
printer.info(f&#34;Session &#39;{session_id}&#39; not found. Starting clean.&#34;)
if not history:
from rich.markdown import Markdown
mdprint(Rule(style=&#34;engineer&#34;))
mdprint(Markdown(&#34;**Networking Expert Agent**: Hi! I&#39;m your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType &#39;exit&#39; to quit.\n&#34;))
mdprint(Rule(style=&#34;engineer&#34;))
@@ -626,6 +629,7 @@ el.replaceWith(d);
title = &#34;[architect][bold]Network Architect[/bold][/architect]&#34; if responder == &#34;architect&#34; else &#34;[engineer][bold]Network Engineer[/bold][/engineer]&#34;
if not result.get(&#34;streamed&#34;):
from rich.markdown import Markdown
mdprint(Panel(Markdown(result[&#34;response&#34;]), title=title, border_style=border, expand=False))
if &#34;usage&#34; in result:
+2
View File
@@ -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)
+2
View File
@@ -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(&#34;guide&#34;):
from rich.markdown import Markdown
self.console.print(Panel(Markdown(result[&#34;guide&#34;]), title=f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;, border_style=persona_color))
commands = result.get(&#34;commands&#34;, [])
@@ -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(&#34;guide&#34;):
from rich.markdown import Markdown
self.console.print(Panel(Markdown(result[&#34;guide&#34;]), title=f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;, border_style=persona_color))
commands = result.get(&#34;commands&#34;, [])
+72 -5
View File
@@ -41,7 +41,7 @@ el.replaceWith(d);
<p align="center">
<img src="https://nginx.gederico.dynu.net/images/CONNPY-resized.png" alt="App Logo">
</p>
<h1 id="connpy-v610">Connpy (v6.1.0)</h1>
<h1 id="connpy-v611">Connpy (v6.1.1)</h1>
<p><a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/v/connpy.svg?style=flat-square"></a>
<a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square"></a>
<a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/dm/connpy.svg?style=flat-square&amp;cacheSeconds=86400"></a>
@@ -819,16 +819,39 @@ class configfile:
self.connections = config[&#34;connections&#34;]
self.profiles = config[&#34;profiles&#34;]
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, &#39;_privatekey_obj&#39;, 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, &#39;_publickey_obj&#39;, 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):
&#34;&#34;&#34;Get config setting with shared fallback for inheritable keys.&#34;&#34;&#34;
@@ -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,&#39;wb&#39;) as f:
f.write(key.export_key(&#39;PEM&#39;))
@@ -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.
</code></pre></div>
<h3>Instance variables</h3>
<dl>
<dt id="connpy.configfile.privatekey"><code class="name">prop <span class="ident">privatekey</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def privatekey(self):
if getattr(self, &#39;_privatekey_obj&#39;, 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</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.configfile.publickey"><code class="name">prop <span class="ident">publickey</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def publickey(self):
if getattr(self, &#39;_publickey_obj&#39;, None) is None:
self._publickey_obj = self.privatekey.publickey()
return self._publickey_obj</code></pre>
</details>
<div class="desc"></div>
</dd>
</dl>
<h3>Methods</h3>
<dl>
<dt id="connpy.configfile.encrypt"><code class="name flex">
@@ -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,
<nav id="sidebar">
<div class="toc">
<ul>
<li><a href="#connpy-v610">Connpy (v6.1.0)</a><ul>
<li><a href="#connpy-v611">Connpy (v6.1.1)</a><ul>
<li><a href="#1-ai-system">1. 🤖 AI System</a><ul>
<li><a href="#1a-terminal-copilot-ctrlspace">1a. Terminal Copilot (Ctrl+Space)</a></li>
<li><a href="#1b-ai-chat-conn-ai">1b. AI Chat (conn ai)</a></li>
@@ -4104,6 +4169,8 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
<li><code><a title="connpy.configfile.get_effective_setting" href="#connpy.configfile.get_effective_setting">get_effective_setting</a></code></li>
<li><code><a title="connpy.configfile.getitem" href="#connpy.configfile.getitem">getitem</a></code></li>
<li><code><a title="connpy.configfile.getitems" href="#connpy.configfile.getitems">getitems</a></code></li>
<li><code><a title="connpy.configfile.privatekey" href="#connpy.configfile.privatekey">privatekey</a></code></li>
<li><code><a title="connpy.configfile.publickey" href="#connpy.configfile.publickey">publickey</a></code></li>
</ul>
</li>
<li>