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:
@@ -3,7 +3,7 @@
|
||||
</p>
|
||||
|
||||
|
||||
# Connpy (v6.1.0)
|
||||
# Connpy (v6.1.1)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
</p>
|
||||
|
||||
|
||||
# Connpy (v6.1.0)
|
||||
# Connpy (v6.1.1)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
[](https://pypi.org/pypi/connpy/)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "6.1.0"
|
||||
__version__ = "6.1.1"
|
||||
|
||||
+3
-1
@@ -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:
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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", [])
|
||||
|
||||
+72
-5
@@ -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&cacheSeconds=86400"></a>
|
||||
@@ -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.
|
||||
</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, '_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</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, '_publickey_obj', 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>
|
||||
|
||||
Reference in New Issue
Block a user