feat(cli,core): add local interactive shell (conn shell), Ctrl+Space passthrough and bump to v6.1.0

- Add `conn shell` CLI subcommand for spawning local interactive shells with AI Copilot support.

- Implement `protocol="local"` on core `node` class with PTY process spawning, dynamic PWD tracking (/proc/PID/cwd + OSC 7), and AI metadata mapping.

- Implement `_is_child_connpy_active()` to inspect PTY slave foreground process group (tcgetpgrp + /proc/PGID/cmdline).

- Automatically pass `Ctrl+Space` (b'\x00') down to child foreground `conn` / `connpy` processes when running nested sessions in local shell.

- Add `--shell-command`, `--shell-prompt`, and `--shell-os` configuration options to `conn config`.

- Update `README.md` and `connpy/__init__.py` documentation to include `conn shell` and PAT API Token management.

- Add comprehensive unit tests in `test_local_shell.py` and `test_completion.py`.

- Bump version to v6.1.0 and regenerate full HTML documentation in `docs/` via pdoc.
This commit is contained in:
2026-08-12 12:03:59 -03:00
parent 835283eba6
commit 0632a510ad
33 changed files with 7130 additions and 2784 deletions
+3190
View File
File diff suppressed because it is too large Load Diff
+52 -3
View File
@@ -77,7 +77,10 @@ el.replaceWith(d);
"trusted_commands": self.set_ai_config,
"service_mode": self.set_service_mode,
"remote_host": self.set_remote_host,
"sync_remote": self.set_sync_remote
"sync_remote": self.set_sync_remote,
"shell_command": self.set_shell_config,
"shell_prompt": self.set_shell_config,
"shell_os": self.set_shell_config
}
handler = actions.get(getattr(args, "command", None))
if handler:
@@ -232,7 +235,23 @@ el.replaceWith(d);
return parsed
raise ValueError()
except Exception:
raise InvalidConfigurationError(&#34;Auth parameter must be a valid JSON/YAML string, or a path to a JSON/YAML file.&#34;)</code></pre>
raise InvalidConfigurationError(&#34;Auth parameter must be a valid JSON/YAML string, or a path to a JSON/YAML file.&#34;)
def set_shell_config(self, args):
key = args.command.replace(&#34;shell_&#34;, &#34;&#34;)
val = args.data[0] if isinstance(args.data, list) else args.data
try:
settings = self.app.services.config_svc.get_settings()
shell_cfg = settings.get(&#34;shell&#34;, {}) if isinstance(settings.get(&#34;shell&#34;), dict) else {}
if str(val).lower() in [&#34;none&#34;, &#34;clear&#34;, &#34;&#34;]:
if key in shell_cfg:
del shell_cfg[key]
else:
shell_cfg[key] = val
self.app.services.config_svc.update_setting(&#34;shell&#34;, shell_cfg)
printer.success(&#34;Config saved&#34;)
except (ConnpyError, InvalidConfigurationError) as e:
printer.error(str(e))</code></pre>
</details>
<div class="desc"></div>
<h3>Methods</h3>
@@ -263,7 +282,10 @@ el.replaceWith(d);
&#34;trusted_commands&#34;: self.set_ai_config,
&#34;service_mode&#34;: self.set_service_mode,
&#34;remote_host&#34;: self.set_remote_host,
&#34;sync_remote&#34;: self.set_sync_remote
&#34;sync_remote&#34;: self.set_sync_remote,
&#34;shell_command&#34;: self.set_shell_config,
&#34;shell_prompt&#34;: self.set_shell_config,
&#34;shell_os&#34;: self.set_shell_config
}
handler = actions.get(getattr(args, &#34;command&#34;, None))
if handler:
@@ -434,6 +456,32 @@ el.replaceWith(d);
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.config_handler.ConfigHandler.set_shell_config"><code class="name flex">
<span>def <span class="ident">set_shell_config</span></span>(<span>self, args)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def set_shell_config(self, args):
key = args.command.replace(&#34;shell_&#34;, &#34;&#34;)
val = args.data[0] if isinstance(args.data, list) else args.data
try:
settings = self.app.services.config_svc.get_settings()
shell_cfg = settings.get(&#34;shell&#34;, {}) if isinstance(settings.get(&#34;shell&#34;), dict) else {}
if str(val).lower() in [&#34;none&#34;, &#34;clear&#34;, &#34;&#34;]:
if key in shell_cfg:
del shell_cfg[key]
else:
shell_cfg[key] = val
self.app.services.config_svc.update_setting(&#34;shell&#34;, shell_cfg)
printer.success(&#34;Config saved&#34;)
except (ConnpyError, InvalidConfigurationError) as e:
printer.error(str(e))</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.config_handler.ConfigHandler.set_sync_remote"><code class="name flex">
<span>def <span class="ident">set_sync_remote</span></span>(<span>self, args)</span>
</code></dt>
@@ -538,6 +586,7 @@ el.replaceWith(d);
<li><code><a title="connpy.cli.config_handler.ConfigHandler.set_idletime" href="#connpy.cli.config_handler.ConfigHandler.set_idletime">set_idletime</a></code></li>
<li><code><a title="connpy.cli.config_handler.ConfigHandler.set_remote_host" href="#connpy.cli.config_handler.ConfigHandler.set_remote_host">set_remote_host</a></code></li>
<li><code><a title="connpy.cli.config_handler.ConfigHandler.set_service_mode" href="#connpy.cli.config_handler.ConfigHandler.set_service_mode">set_service_mode</a></code></li>
<li><code><a title="connpy.cli.config_handler.ConfigHandler.set_shell_config" href="#connpy.cli.config_handler.ConfigHandler.set_shell_config">set_shell_config</a></code></li>
<li><code><a title="connpy.cli.config_handler.ConfigHandler.set_sync_remote" href="#connpy.cli.config_handler.ConfigHandler.set_sync_remote">set_sync_remote</a></code></li>
<li><code><a title="connpy.cli.config_handler.ConfigHandler.set_theme" href="#connpy.cli.config_handler.ConfigHandler.set_theme">set_theme</a></code></li>
<li><code><a title="connpy.cli.config_handler.ConfigHandler.show_completion" href="#connpy.cli.config_handler.ConfigHandler.show_completion">show_completion</a></code></li>
+10
View File
@@ -61,6 +61,7 @@ el.replaceWith(d);
self.validators = Validators(app)
def questions_edit(self):
import inquirer
questions = []
questions.append(inquirer.Confirm(&#34;host&#34;, message=&#34;Edit Hostname/IP?&#34;))
questions.append(inquirer.Confirm(&#34;protocol&#34;, message=&#34;Edit Protocol/app?&#34;))
@@ -74,6 +75,7 @@ el.replaceWith(d);
return inquirer.prompt(questions)
def questions_nodes(self, unique, uniques=None, edit=None):
import inquirer
try:
defaults = self.app.services.nodes.get_node_details(unique)
if &#34;tags&#34; not in defaults:
@@ -151,6 +153,7 @@ el.replaceWith(d);
return result
def questions_profiles(self, unique, edit=None):
import inquirer
try:
defaults = self.app.services.profiles.get_profile(unique, resolve=False)
if &#34;tags&#34; not in defaults:
@@ -216,6 +219,7 @@ el.replaceWith(d);
return result
def questions_bulk(self, nodes=&#34;&#34;, hosts=&#34;&#34;):
import inquirer
questions = []
questions.append(inquirer.Text(&#34;ids&#34;, message=&#34;add a comma separated list of nodes to add&#34;, default=nodes, validate=self.validators.bulk_node_validation))
questions.append(inquirer.Text(&#34;location&#34;, message=&#34;Add a @folder, @subfolder@folder or leave empty&#34;, validate=self.validators.bulk_folder_validation))
@@ -253,6 +257,7 @@ el.replaceWith(d);
def mcp_wizard(self, mcp_servers):
&#34;&#34;&#34;Interactive wizard to manage MCP servers.&#34;&#34;&#34;
import inquirer
from .helpers import theme
while True:
@@ -345,6 +350,7 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def mcp_wizard(self, mcp_servers):
&#34;&#34;&#34;Interactive wizard to manage MCP servers.&#34;&#34;&#34;
import inquirer
from .helpers import theme
while True:
@@ -435,6 +441,7 @@ el.replaceWith(d);
<span>Expand source code</span>
</summary>
<pre><code class="python">def questions_bulk(self, nodes=&#34;&#34;, hosts=&#34;&#34;):
import inquirer
questions = []
questions.append(inquirer.Text(&#34;ids&#34;, message=&#34;add a comma separated list of nodes to add&#34;, default=nodes, validate=self.validators.bulk_node_validation))
questions.append(inquirer.Text(&#34;location&#34;, message=&#34;Add a @folder, @subfolder@folder or leave empty&#34;, validate=self.validators.bulk_folder_validation))
@@ -481,6 +488,7 @@ el.replaceWith(d);
<span>Expand source code</span>
</summary>
<pre><code class="python">def questions_edit(self):
import inquirer
questions = []
questions.append(inquirer.Confirm(&#34;host&#34;, message=&#34;Edit Hostname/IP?&#34;))
questions.append(inquirer.Confirm(&#34;protocol&#34;, message=&#34;Edit Protocol/app?&#34;))
@@ -504,6 +512,7 @@ el.replaceWith(d);
<span>Expand source code</span>
</summary>
<pre><code class="python">def questions_nodes(self, unique, uniques=None, edit=None):
import inquirer
try:
defaults = self.app.services.nodes.get_node_details(unique)
if &#34;tags&#34; not in defaults:
@@ -591,6 +600,7 @@ el.replaceWith(d);
<span>Expand source code</span>
</summary>
<pre><code class="python">def questions_profiles(self, unique, edit=None):
import inquirer
try:
defaults = self.app.services.profiles.get_profile(unique, resolve=False)
if &#34;tags&#34; not in defaults:
+22 -36
View File
@@ -68,6 +68,7 @@ el.replaceWith(d);
else:
return answer[0]
else:
import inquirer
questions = [inquirer.List(name, message=&#34;Pick {} to {}:&#34;.format(name,action), choices=list_, carousel=True)]
answer = inquirer.prompt(questions, theme=theme)
if answer == None:
@@ -125,6 +126,26 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def get_theme():
&#34;&#34;&#34;Returns a fresh instance of the theme with current colors.&#34;&#34;&#34;
from inquirer.themes import Default, term
class ConnpyTheme(Default):
def __init__(self):
super().__init__()
try:
from ..printer import _global_active_styles
# Use user_prompt as primary accent, fallback to info/cyan
accent = _global_active_styles.get(&#34;user_prompt&#34;, _global_active_styles.get(&#34;info&#34;, &#34;cyan&#34;))
accent_color = hex_to_blessed(accent)
self.Question.mark_color = accent_color
self.List.selection_color = accent_color
self.List.selection_cursor = &#34;&gt;&#34;
except:
# Absolute fallback to standard cyan
self.Question.mark_color = term.cyan
self.List.selection_color = term.bold_cyan
self.List.selection_cursor = &#34;&gt;&#34;
return ConnpyTheme()</code></pre>
</details>
<div class="desc"><p>Returns a fresh instance of the theme with current colors.</p></div>
@@ -139,6 +160,7 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def hex_to_blessed(hex_str):
&#34;&#34;&#34;Convert hex color string to blessed/ansi format.&#34;&#34;&#34;
from inquirer.themes import term
if not hex_str or not isinstance(hex_str, str):
return term.normal
@@ -242,39 +264,6 @@ el.replaceWith(d);
<section>
<h2 class="section-title" id="header-classes">Classes</h2>
<dl>
<dt id="connpy.cli.helpers.ConnpyTheme"><code class="flex name class">
<span>class <span class="ident">ConnpyTheme</span></span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class ConnpyTheme(Default):
def __init__(self):
super().__init__()
try:
from ..printer import _global_active_styles
# Use user_prompt as primary accent, fallback to info/cyan
accent = _global_active_styles.get(&#34;user_prompt&#34;, _global_active_styles.get(&#34;info&#34;, &#34;cyan&#34;))
accent_color = hex_to_blessed(accent)
self.Question.mark_color = accent_color
self.List.selection_color = accent_color
self.List.selection_cursor = &#34;&gt;&#34;
except:
# Absolute fallback to standard cyan
self.Question.mark_color = term.cyan
self.List.selection_color = term.bold_cyan
self.List.selection_cursor = &#34;&gt;&#34;</code></pre>
</details>
<div class="desc"></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>inquirer.themes.Default</li>
<li>inquirer.themes.Theme</li>
</ul>
</dd>
<dt id="connpy.cli.helpers.ThemeProxy"><code class="flex name class">
<span>class <span class="ident">ThemeProxy</span></span>
</code></dt>
@@ -322,9 +311,6 @@ el.replaceWith(d);
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
<li>
<h4><code><a title="connpy.cli.helpers.ConnpyTheme" href="#connpy.cli.helpers.ConnpyTheme">ConnpyTheme</a></code></h4>
</li>
<li>
<h4><code><a title="connpy.cli.helpers.ThemeProxy" href="#connpy.cli.helpers.ThemeProxy">ThemeProxy</a></code></h4>
</li>
</ul>
+33 -1
View File
@@ -58,12 +58,24 @@ el.replaceWith(d);
<pre><code class="python">class ImportExportHandler:
def __init__(self, app):
self.app = app
self.forms = Forms(app)
self._forms = None
@property
def forms(self):
if self._forms is None:
from .forms import Forms
self._forms = Forms(self.app)
return self._forms
@forms.setter
def forms(self, value):
self._forms = value
def dispatch_import(self, args):
file_path = args.data[0]
try:
printer.warning(&#34;This could overwrite your current configuration!&#34;)
import inquirer
question = [inquirer.Confirm(&#34;import&#34;, message=f&#34;Are you sure you want to import {file_path}?&#34;)]
confirm = inquirer.prompt(question)
if confirm == None or not confirm[&#34;import&#34;]:
@@ -135,6 +147,24 @@ el.replaceWith(d);
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
<h3>Instance variables</h3>
<dl>
<dt id="connpy.cli.import_export_handler.ImportExportHandler.forms"><code class="name">prop <span class="ident">forms</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def forms(self):
if self._forms is None:
from .forms import Forms
self._forms = Forms(self.app)
return self._forms</code></pre>
</details>
<div class="desc"></div>
</dd>
</dl>
<h3>Methods</h3>
<dl>
<dt id="connpy.cli.import_export_handler.ImportExportHandler.bulk"><code class="name flex">
@@ -228,6 +258,7 @@ el.replaceWith(d);
file_path = args.data[0]
try:
printer.warning(&#34;This could overwrite your current configuration!&#34;)
import inquirer
question = [inquirer.Confirm(&#34;import&#34;, message=f&#34;Are you sure you want to import {file_path}?&#34;)]
confirm = inquirer.prompt(question)
if confirm == None or not confirm[&#34;import&#34;]:
@@ -264,6 +295,7 @@ el.replaceWith(d);
<li><code><a title="connpy.cli.import_export_handler.ImportExportHandler.bulk" href="#connpy.cli.import_export_handler.ImportExportHandler.bulk">bulk</a></code></li>
<li><code><a title="connpy.cli.import_export_handler.ImportExportHandler.dispatch_export" href="#connpy.cli.import_export_handler.ImportExportHandler.dispatch_export">dispatch_export</a></code></li>
<li><code><a title="connpy.cli.import_export_handler.ImportExportHandler.dispatch_import" href="#connpy.cli.import_export_handler.ImportExportHandler.dispatch_import">dispatch_import</a></code></li>
<li><code><a title="connpy.cli.import_export_handler.ImportExportHandler.forms" href="#connpy.cli.import_export_handler.ImportExportHandler.forms">forms</a></code></li>
</ul>
</li>
</ul>
+5
View File
@@ -92,6 +92,10 @@ el.replaceWith(d);
<dd>
<div class="desc"></div>
</dd>
<dt><code class="name"><a title="connpy.cli.shell_handler" href="shell_handler.html">connpy.cli.shell_handler</a></code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt><code class="name"><a title="connpy.cli.sso_handler" href="sso_handler.html">connpy.cli.sso_handler</a></code></dt>
<dd>
<div class="desc"></div>
@@ -146,6 +150,7 @@ el.replaceWith(d);
<li><code><a title="connpy.cli.plugin_handler" href="plugin_handler.html">connpy.cli.plugin_handler</a></code></li>
<li><code><a title="connpy.cli.profile_handler" href="profile_handler.html">connpy.cli.profile_handler</a></code></li>
<li><code><a title="connpy.cli.run_handler" href="run_handler.html">connpy.cli.run_handler</a></code></li>
<li><code><a title="connpy.cli.shell_handler" href="shell_handler.html">connpy.cli.shell_handler</a></code></li>
<li><code><a title="connpy.cli.sso_handler" href="sso_handler.html">connpy.cli.sso_handler</a></code></li>
<li><code><a title="connpy.cli.sync_handler" href="sync_handler.html">connpy.cli.sync_handler</a></code></li>
<li><code><a title="connpy.cli.terminal_ui" href="terminal_ui.html">connpy.cli.terminal_ui</a></code></li>
+211 -2
View File
@@ -70,6 +70,14 @@ el.replaceWith(d);
sys.exit(1)
def login(self, args):
# Handle token management actions first
if getattr(args, &#34;create_token&#34;, None):
return self.create_token(args)
if getattr(args, &#34;list_tokens&#34;, False):
return self.list_tokens(args)
if getattr(args, &#34;revoke_token&#34;, None):
return self.revoke_token(args)
if getattr(args, &#34;status&#34;, False):
return self.show_status()
@@ -191,11 +199,137 @@ el.replaceWith(d);
exp_dt = datetime.datetime.fromtimestamp(exp, datetime.timezone.utc)
printer.info(f&#34;Expires at: {exp_dt.strftime(&#39;%Y-%m-%d %H:%M:%S UTC&#39;)}&#34;)
except Exception as e:
printer.error(f&#34;Failed to check local session status: {e}&#34;)</code></pre>
printer.error(f&#34;Failed to check local session status: {e}&#34;)
def _get_auth_service(self):
&#34;&#34;&#34;Gets an authenticated auth service stub, reusing existing or creating one.&#34;&#34;&#34;
auth_service = getattr(self.app.services, &#34;auth&#34;, None)
if not auth_service:
import grpc
from ..grpc_layer.stubs import AuthStub
remote_host = self.app.services.remote_host or self.app.config.config.get(&#34;remote_host&#34;)
if not remote_host:
printer.error(&#34;Remote host is not configured. Run &#39;connpy config --remote HOST:PORT&#39; first.&#34;)
sys.exit(1)
try:
# Load existing session token for authentication
token_path = os.path.join(self.app.config.defaultdir, &#34;.token&#34;)
if not os.path.exists(token_path):
printer.error(&#34;No active session. Please log in first using &#39;connpy login&#39;.&#34;)
sys.exit(1)
with open(token_path, &#34;r&#34;) as f:
session_token = f.read().strip()
from ..grpc_layer.stubs import AuthClientInterceptor
interceptor = AuthClientInterceptor(lambda: session_token)
channel = grpc.intercept_channel(grpc.insecure_channel(remote_host), interceptor)
auth_service = AuthStub(channel, remote_host=remote_host)
except Exception as e:
printer.error(f&#34;Failed to connect to remote server: {e}&#34;)
sys.exit(1)
return auth_service
def create_token(self, args):
auth_service = self._get_auth_service()
name = args.create_token
expires_days = getattr(args, &#34;expires_days&#34;, 0) or 0
try:
result = auth_service.create_api_token(name, expires_in_days=expires_days)
printer.success(f&#34;API token &#39;{name}&#39; created successfully.&#34;)
printer.warning(&#34;⚠ Copy this token now. It will NOT be shown again:&#34;)
printer.data(&#34;Token&#34;, result[&#34;raw_token&#34;])
printer.info(f&#34;Token ID: {result[&#39;token_id&#39;]}&#34;)
if expires_days &gt; 0:
printer.info(f&#34;Expires in: {expires_days} days&#34;)
else:
printer.info(&#34;Expires: Never (permanent)&#34;)
except ConnpyError as e:
printer.error(f&#34;Failed to create token: {e}&#34;)
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to create token: {e}&#34;)
sys.exit(1)
def list_tokens(self, args):
auth_service = self._get_auth_service()
try:
tokens = auth_service.list_api_tokens()
if not tokens:
printer.info(&#34;No API tokens found.&#34;)
return
import yaml
# Clean up empty strings from protobuf defaults
cleaned = []
for t in tokens:
cleaned.append({
&#34;token_id&#34;: t[&#34;token_id&#34;],
&#34;name&#34;: t[&#34;name&#34;],
&#34;prefix&#34;: t[&#34;token_prefix&#34;],
&#34;created&#34;: t[&#34;created_at&#34;] or &#34;N/A&#34;,
&#34;last_used&#34;: t[&#34;last_used_at&#34;] or &#34;Never&#34;,
&#34;expires&#34;: t[&#34;expires_at&#34;] or &#34;Never&#34;,
})
yaml_str = yaml.dump(cleaned, sort_keys=False, default_flow_style=False)
printer.data(&#34;API Tokens&#34;, yaml_str)
except ConnpyError as e:
printer.error(f&#34;Failed to list tokens: {e}&#34;)
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to list tokens: {e}&#34;)
sys.exit(1)
def revoke_token(self, args):
auth_service = self._get_auth_service()
token_id = args.revoke_token
try:
auth_service.revoke_api_token(token_id)
printer.success(f&#34;Token &#39;{token_id}&#39; revoked successfully.&#34;)
except ConnpyError as e:
printer.error(f&#34;Failed to revoke token: {e}&#34;)
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to revoke token: {e}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
<h3>Methods</h3>
<dl>
<dt id="connpy.cli.login_handler.LoginHandler.create_token"><code class="name flex">
<span>def <span class="ident">create_token</span></span>(<span>self, args)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def create_token(self, args):
auth_service = self._get_auth_service()
name = args.create_token
expires_days = getattr(args, &#34;expires_days&#34;, 0) or 0
try:
result = auth_service.create_api_token(name, expires_in_days=expires_days)
printer.success(f&#34;API token &#39;{name}&#39; created successfully.&#34;)
printer.warning(&#34;⚠ Copy this token now. It will NOT be shown again:&#34;)
printer.data(&#34;Token&#34;, result[&#34;raw_token&#34;])
printer.info(f&#34;Token ID: {result[&#39;token_id&#39;]}&#34;)
if expires_days &gt; 0:
printer.info(f&#34;Expires in: {expires_days} days&#34;)
else:
printer.info(&#34;Expires: Never (permanent)&#34;)
except ConnpyError as e:
printer.error(f&#34;Failed to create token: {e}&#34;)
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to create token: {e}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.login_handler.LoginHandler.dispatch"><code class="name flex">
<span>def <span class="ident">dispatch</span></span>(<span>self, args)</span>
</code></dt>
@@ -216,6 +350,46 @@ el.replaceWith(d);
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.login_handler.LoginHandler.list_tokens"><code class="name flex">
<span>def <span class="ident">list_tokens</span></span>(<span>self, args)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def list_tokens(self, args):
auth_service = self._get_auth_service()
try:
tokens = auth_service.list_api_tokens()
if not tokens:
printer.info(&#34;No API tokens found.&#34;)
return
import yaml
# Clean up empty strings from protobuf defaults
cleaned = []
for t in tokens:
cleaned.append({
&#34;token_id&#34;: t[&#34;token_id&#34;],
&#34;name&#34;: t[&#34;name&#34;],
&#34;prefix&#34;: t[&#34;token_prefix&#34;],
&#34;created&#34;: t[&#34;created_at&#34;] or &#34;N/A&#34;,
&#34;last_used&#34;: t[&#34;last_used_at&#34;] or &#34;Never&#34;,
&#34;expires&#34;: t[&#34;expires_at&#34;] or &#34;Never&#34;,
})
yaml_str = yaml.dump(cleaned, sort_keys=False, default_flow_style=False)
printer.data(&#34;API Tokens&#34;, yaml_str)
except ConnpyError as e:
printer.error(f&#34;Failed to list tokens: {e}&#34;)
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to list tokens: {e}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.login_handler.LoginHandler.login"><code class="name flex">
<span>def <span class="ident">login</span></span>(<span>self, args)</span>
</code></dt>
@@ -225,6 +399,14 @@ el.replaceWith(d);
<span>Expand source code</span>
</summary>
<pre><code class="python">def login(self, args):
# Handle token management actions first
if getattr(args, &#34;create_token&#34;, None):
return self.create_token(args)
if getattr(args, &#34;list_tokens&#34;, False):
return self.list_tokens(args)
if getattr(args, &#34;revoke_token&#34;, None):
return self.revoke_token(args)
if getattr(args, &#34;status&#34;, False):
return self.show_status()
@@ -312,6 +494,30 @@ el.replaceWith(d);
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.login_handler.LoginHandler.revoke_token"><code class="name flex">
<span>def <span class="ident">revoke_token</span></span>(<span>self, args)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def revoke_token(self, args):
auth_service = self._get_auth_service()
token_id = args.revoke_token
try:
auth_service.revoke_api_token(token_id)
printer.success(f&#34;Token &#39;{token_id}&#39; revoked successfully.&#34;)
except ConnpyError as e:
printer.error(f&#34;Failed to revoke token: {e}&#34;)
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to revoke token: {e}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.login_handler.LoginHandler.show_status"><code class="name flex">
<span>def <span class="ident">show_status</span></span>(<span>self)</span>
</code></dt>
@@ -389,10 +595,13 @@ el.replaceWith(d);
<ul>
<li>
<h4><code><a title="connpy.cli.login_handler.LoginHandler" href="#connpy.cli.login_handler.LoginHandler">LoginHandler</a></code></h4>
<ul class="">
<ul class="two-column">
<li><code><a title="connpy.cli.login_handler.LoginHandler.create_token" href="#connpy.cli.login_handler.LoginHandler.create_token">create_token</a></code></li>
<li><code><a title="connpy.cli.login_handler.LoginHandler.dispatch" href="#connpy.cli.login_handler.LoginHandler.dispatch">dispatch</a></code></li>
<li><code><a title="connpy.cli.login_handler.LoginHandler.list_tokens" href="#connpy.cli.login_handler.LoginHandler.list_tokens">list_tokens</a></code></li>
<li><code><a title="connpy.cli.login_handler.LoginHandler.login" href="#connpy.cli.login_handler.LoginHandler.login">login</a></code></li>
<li><code><a title="connpy.cli.login_handler.LoginHandler.logout" href="#connpy.cli.login_handler.LoginHandler.logout">logout</a></code></li>
<li><code><a title="connpy.cli.login_handler.LoginHandler.revoke_token" href="#connpy.cli.login_handler.LoginHandler.revoke_token">revoke_token</a></code></li>
<li><code><a title="connpy.cli.login_handler.LoginHandler.show_status" href="#connpy.cli.login_handler.LoginHandler.show_status">show_status</a></code></li>
</ul>
</li>
+33 -1
View File
@@ -58,7 +58,18 @@ el.replaceWith(d);
<pre><code class="python">class NodeHandler:
def __init__(self, app):
self.app = app
self.forms = Forms(app)
self._forms = None
@property
def forms(self):
if self._forms is None:
from .forms import Forms
self._forms = Forms(self.app)
return self._forms
@forms.setter
def forms(self, value):
self._forms = value
def _filter_exact_match(self, matches, query):
if not query or len(matches) &lt;= 1:
@@ -146,6 +157,7 @@ el.replaceWith(d);
sys.exit(2)
printer.info(f&#34;Removing: {matches}&#34;)
import inquirer
question = [inquirer.Confirm(&#34;delete&#34;, message=&#34;Are you sure you want to continue?&#34;)]
confirm = inquirer.prompt(question)
if confirm == None or not confirm[&#34;delete&#34;]:
@@ -306,6 +318,24 @@ el.replaceWith(d);
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
<h3>Instance variables</h3>
<dl>
<dt id="connpy.cli.node_handler.NodeHandler.forms"><code class="name">prop <span class="ident">forms</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def forms(self):
if self._forms is None:
from .forms import Forms
self._forms = Forms(self.app)
return self._forms</code></pre>
</details>
<div class="desc"></div>
</dd>
</dl>
<h3>Methods</h3>
<dl>
<dt id="connpy.cli.node_handler.NodeHandler.add"><code class="name flex">
@@ -434,6 +464,7 @@ el.replaceWith(d);
sys.exit(2)
printer.info(f&#34;Removing: {matches}&#34;)
import inquirer
question = [inquirer.Confirm(&#34;delete&#34;, message=&#34;Are you sure you want to continue?&#34;)]
confirm = inquirer.prompt(question)
if confirm == None or not confirm[&#34;delete&#34;]:
@@ -630,6 +661,7 @@ el.replaceWith(d);
<li><code><a title="connpy.cli.node_handler.NodeHandler.connect" href="#connpy.cli.node_handler.NodeHandler.connect">connect</a></code></li>
<li><code><a title="connpy.cli.node_handler.NodeHandler.delete" href="#connpy.cli.node_handler.NodeHandler.delete">delete</a></code></li>
<li><code><a title="connpy.cli.node_handler.NodeHandler.dispatch" href="#connpy.cli.node_handler.NodeHandler.dispatch">dispatch</a></code></li>
<li><code><a title="connpy.cli.node_handler.NodeHandler.forms" href="#connpy.cli.node_handler.NodeHandler.forms">forms</a></code></li>
<li><code><a title="connpy.cli.node_handler.NodeHandler.modify" href="#connpy.cli.node_handler.NodeHandler.modify">modify</a></code></li>
<li><code><a title="connpy.cli.node_handler.NodeHandler.show" href="#connpy.cli.node_handler.NodeHandler.show">show</a></code></li>
<li><code><a title="connpy.cli.node_handler.NodeHandler.version" href="#connpy.cli.node_handler.NodeHandler.version">version</a></code></li>
+34 -2
View File
@@ -58,7 +58,18 @@ el.replaceWith(d);
<pre><code class="python">class ProfileHandler:
def __init__(self, app):
self.app = app
self.forms = Forms(app)
self._forms = None
@property
def forms(self):
if self._forms is None:
from .forms import Forms
self._forms = Forms(self.app)
return self._forms
@forms.setter
def forms(self, value):
self._forms = value
def dispatch(self, args):
if not self.app.case:
@@ -78,6 +89,7 @@ el.replaceWith(d);
printer.error(&#34;Can&#39;t delete default profile&#34;)
sys.exit(6)
import inquirer
question = [inquirer.Confirm(&#34;delete&#34;, message=f&#34;Are you sure you want to delete {name}?&#34;)]
confirm = inquirer.prompt(question)
if confirm == None or not confirm[&#34;delete&#34;]:
@@ -145,6 +157,24 @@ el.replaceWith(d);
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
<h3>Instance variables</h3>
<dl>
<dt id="connpy.cli.profile_handler.ProfileHandler.forms"><code class="name">prop <span class="ident">forms</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def forms(self):
if self._forms is None:
from .forms import Forms
self._forms = Forms(self.app)
return self._forms</code></pre>
</details>
<div class="desc"></div>
</dd>
</dl>
<h3>Methods</h3>
<dl>
<dt id="connpy.cli.profile_handler.ProfileHandler.add"><code class="name flex">
@@ -194,6 +224,7 @@ el.replaceWith(d);
printer.error(&#34;Can&#39;t delete default profile&#34;)
sys.exit(6)
import inquirer
question = [inquirer.Confirm(&#34;delete&#34;, message=f&#34;Are you sure you want to delete {name}?&#34;)]
confirm = inquirer.prompt(question)
if confirm == None or not confirm[&#34;delete&#34;]:
@@ -300,10 +331,11 @@ el.replaceWith(d);
<ul>
<li>
<h4><code><a title="connpy.cli.profile_handler.ProfileHandler" href="#connpy.cli.profile_handler.ProfileHandler">ProfileHandler</a></code></h4>
<ul class="">
<ul class="two-column">
<li><code><a title="connpy.cli.profile_handler.ProfileHandler.add" href="#connpy.cli.profile_handler.ProfileHandler.add">add</a></code></li>
<li><code><a title="connpy.cli.profile_handler.ProfileHandler.delete" href="#connpy.cli.profile_handler.ProfileHandler.delete">delete</a></code></li>
<li><code><a title="connpy.cli.profile_handler.ProfileHandler.dispatch" href="#connpy.cli.profile_handler.ProfileHandler.dispatch">dispatch</a></code></li>
<li><code><a title="connpy.cli.profile_handler.ProfileHandler.forms" href="#connpy.cli.profile_handler.ProfileHandler.forms">forms</a></code></li>
<li><code><a title="connpy.cli.profile_handler.ProfileHandler.modify" href="#connpy.cli.profile_handler.ProfileHandler.modify">modify</a></code></li>
<li><code><a title="connpy.cli.profile_handler.ProfileHandler.show" href="#connpy.cli.profile_handler.ProfileHandler.show">show</a></code></li>
</ul>
+187
View File
@@ -0,0 +1,187 @@
<!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.5">
<title>connpy.cli.shell_handler 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.cli.shell_handler</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.cli.shell_handler.ShellHandler"><code class="flex name class">
<span>class <span class="ident">ShellHandler</span></span>
<span>(</span><span>app)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class ShellHandler:
def __init__(self, app):
self.app = app
def dispatch(self, args):
shell_config = self.app.config.config.get(&#34;shell&#34;, {}) if hasattr(self.app.config, &#34;config&#34;) else {}
command = getattr(args, &#39;command_override&#39;, None) or shell_config.get(&#34;command&#34;) or os.environ.get(&#34;SHELL&#34;, &#34;/bin/bash&#34;)
try:
exe = shlex.split(command)[0]
except Exception:
exe = command
if not shutil.which(exe):
printer.error(f&#34;Shell command executable not found: {exe}&#34;)
sys.exit(1)
node_info = self._build_local_identity(shell_config)
tags = {
&#34;os&#34;: node_info[&#34;os&#34;],
&#34;prompt&#34;: node_info[&#34;prompt&#34;]
}
n = node(
unique=node_info[&#34;name&#34;],
host=command,
protocol=&#34;local&#34;,
config=self.app.config,
tags=tags
)
capture_file = getattr(args, &#39;capture_file&#39;, None)
if capture_file:
n.logs = capture_file
elif shell_config.get(&#34;logging&#34;):
n.logs = shell_config.get(&#34;log_path&#34;, os.path.expanduser(&#34;~/.config/conn/shell_logs/session.log&#34;))
n.interact(debug=getattr(args, &#39;debug&#39;, False))
def _build_local_identity(self, shell_config):
return {
&#34;name&#34;: &#34;local-shell&#34;,
&#34;host&#34;: socket.gethostname(),
&#34;os&#34;: shell_config.get(&#34;os&#34;, &#34;linux&#34;),
&#34;prompt&#34;: shell_config.get(&#34;prompt&#34;, r&#39;\$\s*$|#\s*$&#39;)
}</code></pre>
</details>
<div class="desc"></div>
<h3>Methods</h3>
<dl>
<dt id="connpy.cli.shell_handler.ShellHandler.dispatch"><code class="name flex">
<span>def <span class="ident">dispatch</span></span>(<span>self, args)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def dispatch(self, args):
shell_config = self.app.config.config.get(&#34;shell&#34;, {}) if hasattr(self.app.config, &#34;config&#34;) else {}
command = getattr(args, &#39;command_override&#39;, None) or shell_config.get(&#34;command&#34;) or os.environ.get(&#34;SHELL&#34;, &#34;/bin/bash&#34;)
try:
exe = shlex.split(command)[0]
except Exception:
exe = command
if not shutil.which(exe):
printer.error(f&#34;Shell command executable not found: {exe}&#34;)
sys.exit(1)
node_info = self._build_local_identity(shell_config)
tags = {
&#34;os&#34;: node_info[&#34;os&#34;],
&#34;prompt&#34;: node_info[&#34;prompt&#34;]
}
n = node(
unique=node_info[&#34;name&#34;],
host=command,
protocol=&#34;local&#34;,
config=self.app.config,
tags=tags
)
capture_file = getattr(args, &#39;capture_file&#39;, None)
if capture_file:
n.logs = capture_file
elif shell_config.get(&#34;logging&#34;):
n.logs = shell_config.get(&#34;log_path&#34;, os.path.expanduser(&#34;~/.config/conn/shell_logs/session.log&#34;))
n.interact(debug=getattr(args, &#39;debug&#39;, False))</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.cli" href="index.html">connpy.cli</a></code></li>
</ul>
</li>
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
<li>
<h4><code><a title="connpy.cli.shell_handler.ShellHandler" href="#connpy.cli.shell_handler.ShellHandler">ShellHandler</a></code></h4>
<ul class="">
<li><code><a title="connpy.cli.shell_handler.ShellHandler.dispatch" href="#connpy.cli.shell_handler.ShellHandler.dispatch">dispatch</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.5</a>.</p>
</footer>
</body>
</html>
+4
View File
@@ -92,6 +92,7 @@ el.replaceWith(d);
sys.exit(1)
def add_provider(self, args):
import inquirer
provider = args.provider
sso = self.app.config.config.get(&#34;sso&#34;, {})
providers = sso.setdefault(&#34;providers&#34;, {})
@@ -165,6 +166,7 @@ el.replaceWith(d);
sys.exit(1)
# Confirm delete
import inquirer
questions = [inquirer.Confirm(&#34;confirm&#34;, message=f&#34;Are you sure you want to delete SSO Provider &#39;{provider}&#39;?&#34;, default=False)]
answers = inquirer.prompt(questions)
if not answers or not answers[&#34;confirm&#34;]:
@@ -225,6 +227,7 @@ el.replaceWith(d);
<span>Expand source code</span>
</summary>
<pre><code class="python">def add_provider(self, args):
import inquirer
provider = args.provider
sso = self.app.config.config.get(&#34;sso&#34;, {})
providers = sso.setdefault(&#34;providers&#34;, {})
@@ -308,6 +311,7 @@ el.replaceWith(d);
sys.exit(1)
# Confirm delete
import inquirer
questions = [inquirer.Confirm(&#34;confirm&#34;, message=f&#34;Are you sure you want to delete SSO Provider &#39;{provider}&#39;?&#34;, default=False)]
answers = inquirer.prompt(questions)
if not answers or not answers[&#34;confirm&#34;]:
+110 -18
View File
@@ -57,25 +57,39 @@ el.replaceWith(d);
</summary>
<pre><code class="python">class CopilotInterface:
def __init__(self, config, history=None, pt_input=None, pt_output=None, rich_file=None, session_state=None):
from ..services.ai_service import AIService
self.config = config
self.history = history or InMemoryHistory()
self.pt_input = pt_input
self.pt_output = pt_output
self.rich_file = rich_file
self.ai_service = AIService(config)
self.session_state = session_state if session_state is not None else {
&#39;persona&#39;: &#39;engineer&#39;,
&#39;trust_mode&#39;: False,
&#39;memories&#39;: [],
&#39;os&#39;: None,
&#39;prompt&#39;: None
}
self.mode_range, self.mode_single, self.mode_lines = 0, 1, 2
self.session_state = session_state if session_state is not None else {}
self.session_state.setdefault(&#39;persona&#39;, &#39;engineer&#39;)
self.session_state.setdefault(&#39;trust_mode&#39;, False)
self.session_state.setdefault(&#39;memories&#39;, [])
self.session_state.setdefault(&#39;os&#39;, None)
self.session_state.setdefault(&#39;prompt&#39;, None)
self.session_state.setdefault(&#39;context_mode&#39;, self.mode_range)
self.session_state.setdefault(&#39;context_cmd&#39;, 1)
self.session_state.setdefault(&#39;context_lines&#39;, 50)
self.session_state.setdefault(&#39;last_total_cmds&#39;, None)
self.session_state.setdefault(&#39;last_total_lines&#39;, None)
if rich_file:
self.console = Console(theme=connpy_theme, force_terminal=True, file=rich_file)
else:
self.console = Console(theme=connpy_theme)
self.mode_range, self.mode_single, self.mode_lines = 0, 1, 2
def _sync_session_context(self, state: dict):
&#34;&#34;&#34;Persist current context mode, depth, total commands, and total lines into session_state.&#34;&#34;&#34;
self.session_state[&#39;context_mode&#39;] = state[&#39;context_mode&#39;]
self.session_state[&#39;context_cmd&#39;] = state[&#39;context_cmd&#39;]
self.session_state[&#39;context_lines&#39;] = state[&#39;context_lines&#39;]
self.session_state[&#39;last_total_cmds&#39;] = state[&#39;total_cmds&#39;]
self.session_state[&#39;last_total_lines&#39;] = state[&#39;total_lines&#39;]
def _get_theme_color(self, style_name: str, fallback: str = &#34;white&#34;) -&gt; str:
&#34;&#34;&#34;Extract Hex or ANSI color name from the active rich theme.&#34;&#34;&#34;
@@ -109,16 +123,52 @@ el.replaceWith(d);
last_line = buffer.split(&#39;\n&#39;)[-1].strip() if buffer.strip() else &#34;(prompt)&#34;
blocks = self.ai_service.build_context_blocks(raw_bytes, cmd_byte_positions, node_info, last_line=last_line)
total_cmds = len(blocks)
total_lines = len(buffer.split(&#39;\n&#39;))
saved_mode = self.session_state.get(&#39;context_mode&#39;, self.mode_range)
saved_cmd = self.session_state.get(&#39;context_cmd&#39;, 1)
saved_lines = self.session_state.get(&#39;context_lines&#39;, min(50, total_lines))
last_total_cmds = self.session_state.get(&#39;last_total_cmds&#39;, None)
last_total_lines = self.session_state.get(&#39;last_total_lines&#39;, None)
is_range = saved_mode in (self.mode_range, 0, &#39;RANGE&#39;, &#39;range&#39;)
is_lines = saved_mode in (self.mode_lines, 2, &#39;LINES&#39;, &#39;lines&#39;)
is_single = saved_mode in (self.mode_single, 1, &#39;SINGLE&#39;, &#39;single&#39;)
if is_range or is_single:
if last_total_cmds is not None and total_cmds &gt; last_total_cmds and saved_cmd &gt; 1:
new_cmds = total_cmds - last_total_cmds
initial_cmd = saved_cmd + new_cmds
else:
initial_cmd = saved_cmd
initial_lines = saved_lines
elif is_lines:
if last_total_lines is not None and total_lines &gt; last_total_lines and saved_lines &gt; 50:
new_lines = total_lines - last_total_lines
initial_lines = saved_lines + new_lines
else:
initial_lines = saved_lines
initial_cmd = saved_cmd
else:
initial_cmd = saved_cmd
initial_lines = saved_lines
state = {
&#39;context_cmd&#39;: 1,
&#39;total_cmds&#39;: len(blocks),
&#39;total_lines&#39;: len(buffer.split(&#39;\n&#39;)),
&#39;context_lines&#39;: min(50, len(buffer.split(&#39;\n&#39;))),
&#39;context_mode&#39;: self.mode_range,
&#39;context_cmd&#39;: min(max(1, initial_cmd), max(1, total_cmds)),
&#39;total_cmds&#39;: total_cmds,
&#39;total_lines&#39;: total_lines,
&#39;context_lines&#39;: min(max(1, initial_lines), max(1, total_lines)),
&#39;context_mode&#39;: saved_mode,
&#39;cancelled&#39;: False,
&#39;toolbar_msg&#39;: &#39;&#39;,
&#39;msg_expiry&#39;: 0
}
self.session_state[&#39;context_mode&#39;] = saved_mode
self.session_state[&#39;context_cmd&#39;] = max(1, initial_cmd)
self.session_state[&#39;context_lines&#39;] = max(1, initial_lines)
self.session_state[&#39;last_total_cmds&#39;] = total_cmds
self.session_state[&#39;last_total_lines&#39;] = total_lines
# 1. Visual Separation
self.console.print(&#34;&#34;) # Real line break
@@ -137,6 +187,7 @@ el.replaceWith(d);
state[&#39;context_lines&#39;] = min(state[&#39;context_lines&#39;] + 50, state[&#39;total_lines&#39;])
else:
state[&#39;context_cmd&#39;] = min(state[&#39;context_cmd&#39;] + 1, state[&#39;total_cmds&#39;])
self._sync_session_context(state)
event.app.invalidate()
@bindings.add(&#39;c-down&#39;)
def _(event):
@@ -144,6 +195,7 @@ el.replaceWith(d);
state[&#39;context_lines&#39;] = max(state[&#39;context_lines&#39;] - 50, min(50, state[&#39;total_lines&#39;]))
else:
state[&#39;context_cmd&#39;] = max(state[&#39;context_cmd&#39;] - 1, 1)
self._sync_session_context(state)
event.app.invalidate()
@bindings.add(&#39;tab&#39;)
def _(event):
@@ -153,6 +205,7 @@ el.replaceWith(d);
buf.complete_next()
else:
state[&#39;context_mode&#39;] = (state[&#39;context_mode&#39;] + 1) % 3
self._sync_session_context(state)
event.app.invalidate()
@bindings.add(&#39;escape&#39;, eager=True)
@bindings.add(&#39;c-c&#39;)
@@ -575,16 +628,52 @@ el.replaceWith(d);
last_line = buffer.split(&#39;\n&#39;)[-1].strip() if buffer.strip() else &#34;(prompt)&#34;
blocks = self.ai_service.build_context_blocks(raw_bytes, cmd_byte_positions, node_info, last_line=last_line)
total_cmds = len(blocks)
total_lines = len(buffer.split(&#39;\n&#39;))
saved_mode = self.session_state.get(&#39;context_mode&#39;, self.mode_range)
saved_cmd = self.session_state.get(&#39;context_cmd&#39;, 1)
saved_lines = self.session_state.get(&#39;context_lines&#39;, min(50, total_lines))
last_total_cmds = self.session_state.get(&#39;last_total_cmds&#39;, None)
last_total_lines = self.session_state.get(&#39;last_total_lines&#39;, None)
is_range = saved_mode in (self.mode_range, 0, &#39;RANGE&#39;, &#39;range&#39;)
is_lines = saved_mode in (self.mode_lines, 2, &#39;LINES&#39;, &#39;lines&#39;)
is_single = saved_mode in (self.mode_single, 1, &#39;SINGLE&#39;, &#39;single&#39;)
if is_range or is_single:
if last_total_cmds is not None and total_cmds &gt; last_total_cmds and saved_cmd &gt; 1:
new_cmds = total_cmds - last_total_cmds
initial_cmd = saved_cmd + new_cmds
else:
initial_cmd = saved_cmd
initial_lines = saved_lines
elif is_lines:
if last_total_lines is not None and total_lines &gt; last_total_lines and saved_lines &gt; 50:
new_lines = total_lines - last_total_lines
initial_lines = saved_lines + new_lines
else:
initial_lines = saved_lines
initial_cmd = saved_cmd
else:
initial_cmd = saved_cmd
initial_lines = saved_lines
state = {
&#39;context_cmd&#39;: 1,
&#39;total_cmds&#39;: len(blocks),
&#39;total_lines&#39;: len(buffer.split(&#39;\n&#39;)),
&#39;context_lines&#39;: min(50, len(buffer.split(&#39;\n&#39;))),
&#39;context_mode&#39;: self.mode_range,
&#39;context_cmd&#39;: min(max(1, initial_cmd), max(1, total_cmds)),
&#39;total_cmds&#39;: total_cmds,
&#39;total_lines&#39;: total_lines,
&#39;context_lines&#39;: min(max(1, initial_lines), max(1, total_lines)),
&#39;context_mode&#39;: saved_mode,
&#39;cancelled&#39;: False,
&#39;toolbar_msg&#39;: &#39;&#39;,
&#39;msg_expiry&#39;: 0
}
self.session_state[&#39;context_mode&#39;] = saved_mode
self.session_state[&#39;context_cmd&#39;] = max(1, initial_cmd)
self.session_state[&#39;context_lines&#39;] = max(1, initial_lines)
self.session_state[&#39;last_total_cmds&#39;] = total_cmds
self.session_state[&#39;last_total_lines&#39;] = total_lines
# 1. Visual Separation
self.console.print(&#34;&#34;) # Real line break
@@ -603,6 +692,7 @@ el.replaceWith(d);
state[&#39;context_lines&#39;] = min(state[&#39;context_lines&#39;] + 50, state[&#39;total_lines&#39;])
else:
state[&#39;context_cmd&#39;] = min(state[&#39;context_cmd&#39;] + 1, state[&#39;total_cmds&#39;])
self._sync_session_context(state)
event.app.invalidate()
@bindings.add(&#39;c-down&#39;)
def _(event):
@@ -610,6 +700,7 @@ el.replaceWith(d);
state[&#39;context_lines&#39;] = max(state[&#39;context_lines&#39;] - 50, min(50, state[&#39;total_lines&#39;]))
else:
state[&#39;context_cmd&#39;] = max(state[&#39;context_cmd&#39;] - 1, 1)
self._sync_session_context(state)
event.app.invalidate()
@bindings.add(&#39;tab&#39;)
def _(event):
@@ -619,6 +710,7 @@ el.replaceWith(d);
buf.complete_next()
else:
state[&#39;context_mode&#39;] = (state[&#39;context_mode&#39;] + 1) % 3
self._sync_session_context(state)
event.app.invalidate()
@bindings.add(&#39;escape&#39;, eager=True)
@bindings.add(&#39;c-c&#39;)
+48 -48
View File
@@ -61,61 +61,61 @@ el.replaceWith(d);
def host_validation(self, answers, current, regex = &#34;^.+$&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Host cannot be empty&#34;)
_raise_val_err(&#34;Host cannot be empty&#34;)
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
return True
def profile_protocol_validation(self, answers, current, regex = &#34;(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$)&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick between ssh, telnet, kubectl, docker, ssm or leave empty&#34;)
_raise_val_err(&#34;Pick between ssh, telnet, kubectl, docker, ssm or leave empty&#34;)
return True
def protocol_validation(self, answers, current, regex = &#34;(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$|^@.+$)&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick between ssh, telnet, kubectl, docker, ssm, leave empty or @profile&#34;)
_raise_val_err(&#34;Pick between ssh, telnet, kubectl, docker, ssm, leave empty or @profile&#34;)
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
return True
def profile_port_validation(self, answers, current, regex = &#34;(^[0-9]*$)&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick a port between 1-65535, @profile o leave empty&#34;)
_raise_val_err(&#34;Pick a port between 1-65535, @profile o leave empty&#34;)
try:
port = int(current)
except ValueError:
port = 0
if current != &#34;&#34; and not 1 &lt;= int(port) &lt;= 65535:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick a port between 1-65535 or leave empty&#34;)
_raise_val_err(&#34;Pick a port between 1-65535 or leave empty&#34;)
return True
def port_validation(self, answers, current, regex = &#34;(^[0-9]*$|^@.+$)&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick a port between 1-65535, @profile or leave empty&#34;)
_raise_val_err(&#34;Pick a port between 1-65535, @profile or leave empty&#34;)
try:
port = int(current)
except ValueError:
port = 0
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
elif current != &#34;&#34; and not 1 &lt;= int(port) &lt;= 65535:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick a port between 1-65535, @profile o leave empty&#34;)
_raise_val_err(&#34;Pick a port between 1-65535, @profile o leave empty&#34;)
return True
def pass_validation(self, answers, current, regex = &#34;(^@.+$)&#34;):
profiles = current.split(&#34;,&#34;)
for i in profiles:
if not re.match(regex, i) or i[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(i))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(i))
return True
def tags_validation(self, answers, current):
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
elif current != &#34;&#34;:
isdict = False
try:
@@ -123,7 +123,7 @@ el.replaceWith(d);
except Exception:
pass
if not isinstance (isdict, dict):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Tags should be a python dictionary.&#34;.format(current))
_raise_val_err(&#34;Tags should be a python dictionary.&#34;.format(current))
return True
def profile_tags_validation(self, answers, current):
@@ -134,36 +134,36 @@ el.replaceWith(d);
except Exception:
pass
if not isinstance (isdict, dict):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Tags should be a python dictionary.&#34;.format(current))
_raise_val_err(&#34;Tags should be a python dictionary.&#34;.format(current))
return True
def jumphost_validation(self, answers, current):
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
elif current != &#34;&#34;:
if current not in self.app.nodes_list:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Node {} don&#39;t exist.&#34;.format(current))
_raise_val_err(&#34;Node {} don&#39;t exist.&#34;.format(current))
return True
def profile_jumphost_validation(self, answers, current):
if current != &#34;&#34;:
if current not in self.app.nodes_list:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Node {} don&#39;t exist.&#34;.format(current))
_raise_val_err(&#34;Node {} don&#39;t exist.&#34;.format(current))
return True
def default_validation(self, answers, current):
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
return True
def bulk_node_validation(self, answers, current, regex = &#34;^[0-9a-zA-Z_.,$#-]+$&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Host cannot be empty&#34;)
_raise_val_err(&#34;Host cannot be empty&#34;)
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
return True
def bulk_folder_validation(self, answers, current):
@@ -176,19 +176,19 @@ el.replaceWith(d);
matches = list(filter(lambda k: k == candidate, self.app.folders))
if current != &#34;&#34; and len(matches) == 0:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Location {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Location {} don&#39;t exist&#34;.format(current))
return True
def bulk_host_validation(self, answers, current, regex = &#34;^.+$&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Host cannot be empty&#34;)
_raise_val_err(&#34;Host cannot be empty&#34;)
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
hosts = current.split(&#34;,&#34;)
nodes = answers[&#34;ids&#34;].split(&#34;,&#34;)
if len(hosts) &gt; 1 and len(hosts) != len(nodes):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Hosts list should be the same length of nodes list&#34;)
_raise_val_err(&#34;Hosts list should be the same length of nodes list&#34;)
return True</code></pre>
</details>
<div class="desc"></div>
@@ -212,7 +212,7 @@ el.replaceWith(d);
matches = list(filter(lambda k: k == candidate, self.app.folders))
if current != &#34;&#34; and len(matches) == 0:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Location {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Location {} don&#39;t exist&#34;.format(current))
return True</code></pre>
</details>
<div class="desc"></div>
@@ -227,14 +227,14 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def bulk_host_validation(self, answers, current, regex = &#34;^.+$&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Host cannot be empty&#34;)
_raise_val_err(&#34;Host cannot be empty&#34;)
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
hosts = current.split(&#34;,&#34;)
nodes = answers[&#34;ids&#34;].split(&#34;,&#34;)
if len(hosts) &gt; 1 and len(hosts) != len(nodes):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Hosts list should be the same length of nodes list&#34;)
_raise_val_err(&#34;Hosts list should be the same length of nodes list&#34;)
return True</code></pre>
</details>
<div class="desc"></div>
@@ -249,10 +249,10 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def bulk_node_validation(self, answers, current, regex = &#34;^[0-9a-zA-Z_.,$#-]+$&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Host cannot be empty&#34;)
_raise_val_err(&#34;Host cannot be empty&#34;)
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
return True</code></pre>
</details>
<div class="desc"></div>
@@ -268,7 +268,7 @@ el.replaceWith(d);
<pre><code class="python">def default_validation(self, answers, current):
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
return True</code></pre>
</details>
<div class="desc"></div>
@@ -283,10 +283,10 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def host_validation(self, answers, current, regex = &#34;^.+$&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Host cannot be empty&#34;)
_raise_val_err(&#34;Host cannot be empty&#34;)
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
return True</code></pre>
</details>
<div class="desc"></div>
@@ -302,10 +302,10 @@ el.replaceWith(d);
<pre><code class="python">def jumphost_validation(self, answers, current):
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
elif current != &#34;&#34;:
if current not in self.app.nodes_list:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Node {} don&#39;t exist.&#34;.format(current))
_raise_val_err(&#34;Node {} don&#39;t exist.&#34;.format(current))
return True</code></pre>
</details>
<div class="desc"></div>
@@ -322,7 +322,7 @@ el.replaceWith(d);
profiles = current.split(&#34;,&#34;)
for i in profiles:
if not re.match(regex, i) or i[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(i))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(i))
return True</code></pre>
</details>
<div class="desc"></div>
@@ -337,16 +337,16 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def port_validation(self, answers, current, regex = &#34;(^[0-9]*$|^@.+$)&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick a port between 1-65535, @profile or leave empty&#34;)
_raise_val_err(&#34;Pick a port between 1-65535, @profile or leave empty&#34;)
try:
port = int(current)
except ValueError:
port = 0
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
elif current != &#34;&#34; and not 1 &lt;= int(port) &lt;= 65535:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick a port between 1-65535, @profile o leave empty&#34;)
_raise_val_err(&#34;Pick a port between 1-65535, @profile o leave empty&#34;)
return True</code></pre>
</details>
<div class="desc"></div>
@@ -362,7 +362,7 @@ el.replaceWith(d);
<pre><code class="python">def profile_jumphost_validation(self, answers, current):
if current != &#34;&#34;:
if current not in self.app.nodes_list:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Node {} don&#39;t exist.&#34;.format(current))
_raise_val_err(&#34;Node {} don&#39;t exist.&#34;.format(current))
return True</code></pre>
</details>
<div class="desc"></div>
@@ -377,13 +377,13 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def profile_port_validation(self, answers, current, regex = &#34;(^[0-9]*$)&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick a port between 1-65535, @profile o leave empty&#34;)
_raise_val_err(&#34;Pick a port between 1-65535, @profile o leave empty&#34;)
try:
port = int(current)
except ValueError:
port = 0
if current != &#34;&#34; and not 1 &lt;= int(port) &lt;= 65535:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick a port between 1-65535 or leave empty&#34;)
_raise_val_err(&#34;Pick a port between 1-65535 or leave empty&#34;)
return True</code></pre>
</details>
<div class="desc"></div>
@@ -398,7 +398,7 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def profile_protocol_validation(self, answers, current, regex = &#34;(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$)&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick between ssh, telnet, kubectl, docker, ssm or leave empty&#34;)
_raise_val_err(&#34;Pick between ssh, telnet, kubectl, docker, ssm or leave empty&#34;)
return True</code></pre>
</details>
<div class="desc"></div>
@@ -419,7 +419,7 @@ el.replaceWith(d);
except Exception:
pass
if not isinstance (isdict, dict):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Tags should be a python dictionary.&#34;.format(current))
_raise_val_err(&#34;Tags should be a python dictionary.&#34;.format(current))
return True</code></pre>
</details>
<div class="desc"></div>
@@ -434,10 +434,10 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def protocol_validation(self, answers, current, regex = &#34;(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$|^@.+$)&#34;):
if not re.match(regex, current):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Pick between ssh, telnet, kubectl, docker, ssm, leave empty or @profile&#34;)
_raise_val_err(&#34;Pick between ssh, telnet, kubectl, docker, ssm, leave empty or @profile&#34;)
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
return True</code></pre>
</details>
<div class="desc"></div>
@@ -453,7 +453,7 @@ el.replaceWith(d);
<pre><code class="python">def tags_validation(self, answers, current):
if current.startswith(&#34;@&#34;):
if current[1:] not in self.app.profiles:
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Profile {} don&#39;t exist&#34;.format(current))
_raise_val_err(&#34;Profile {} don&#39;t exist&#34;.format(current))
elif current != &#34;&#34;:
isdict = False
try:
@@ -461,7 +461,7 @@ el.replaceWith(d);
except Exception:
pass
if not isinstance (isdict, dict):
raise inquirer.errors.ValidationError(&#34;&#34;, reason=&#34;Tags should be a python dictionary.&#34;.format(current))
_raise_val_err(&#34;Tags should be a python dictionary.&#34;.format(current))
return True</code></pre>
</details>
<div class="desc"></div>
+296 -2
View File
@@ -153,6 +153,21 @@ el.replaceWith(d);
request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
response_serializer=connpy__pb2.SSOProvidersResponse.SerializeToString,
),
&#39;create_api_token&#39;: grpc.unary_unary_rpc_method_handler(
servicer.create_api_token,
request_deserializer=connpy__pb2.CreateApiTokenRequest.FromString,
response_serializer=connpy__pb2.CreateApiTokenResponse.SerializeToString,
),
&#39;list_api_tokens&#39;: grpc.unary_unary_rpc_method_handler(
servicer.list_api_tokens,
request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
response_serializer=connpy__pb2.ListApiTokensResponse.SerializeToString,
),
&#39;revoke_api_token&#39;: grpc.unary_unary_rpc_method_handler(
servicer.revoke_api_token,
request_deserializer=connpy__pb2.RevokeApiTokenRequest.FromString,
response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
&#39;connpy.AuthService&#39;, rpc_method_handlers)
@@ -1779,6 +1794,87 @@ def predict_execution_results(request,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def create_api_token(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,
&#39;/connpy.AuthService/create_api_token&#39;,
connpy__pb2.CreateApiTokenRequest.SerializeToString,
connpy__pb2.CreateApiTokenResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def list_api_tokens(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,
&#39;/connpy.AuthService/list_api_tokens&#39;,
google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
connpy__pb2.ListApiTokensResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def revoke_api_token(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,
&#39;/connpy.AuthService/revoke_api_token&#39;,
connpy__pb2.RevokeApiTokenRequest.SerializeToString,
google_dot_protobuf_dot_empty__pb2.Empty.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>
@@ -1821,6 +1917,43 @@ def change_password(request,
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthService.create_api_token"><code class="name flex">
<span>def <span class="ident">create_api_token</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 create_api_token(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,
&#39;/connpy.AuthService/create_api_token&#39;,
connpy__pb2.CreateApiTokenRequest.SerializeToString,
connpy__pb2.CreateApiTokenResponse.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.connpy_pb2_grpc.AuthService.get_sso_providers"><code class="name flex">
<span>def <span class="ident">get_sso_providers</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>
@@ -1858,6 +1991,43 @@ def get_sso_providers(request,
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthService.list_api_tokens"><code class="name flex">
<span>def <span class="ident">list_api_tokens</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 list_api_tokens(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,
&#39;/connpy.AuthService/list_api_tokens&#39;,
google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
connpy__pb2.ListApiTokensResponse.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.connpy_pb2_grpc.AuthService.login"><code class="name flex">
<span>def <span class="ident">login</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>
@@ -1932,6 +2102,43 @@ def login_sso(request,
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthService.revoke_api_token"><code class="name flex">
<span>def <span class="ident">revoke_api_token</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 revoke_api_token(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,
&#39;/connpy.AuthService/revoke_api_token&#39;,
connpy__pb2.RevokeApiTokenRequest.SerializeToString,
google_dot_protobuf_dot_empty__pb2.Empty.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.connpy_pb2_grpc.AuthServiceServicer"><code class="flex name class">
@@ -1964,6 +2171,24 @@ def login_sso(request,
raise NotImplementedError(&#39;Method not implemented!&#39;)
def get_sso_providers(self, request, context):
&#34;&#34;&#34;Missing associated documentation comment in .proto file.&#34;&#34;&#34;
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details(&#39;Method not implemented!&#39;)
raise NotImplementedError(&#39;Method not implemented!&#39;)
def create_api_token(self, request, context):
&#34;&#34;&#34;Missing associated documentation comment in .proto file.&#34;&#34;&#34;
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details(&#39;Method not implemented!&#39;)
raise NotImplementedError(&#39;Method not implemented!&#39;)
def list_api_tokens(self, request, context):
&#34;&#34;&#34;Missing associated documentation comment in .proto file.&#34;&#34;&#34;
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details(&#39;Method not implemented!&#39;)
raise NotImplementedError(&#39;Method not implemented!&#39;)
def revoke_api_token(self, request, context):
&#34;&#34;&#34;Missing associated documentation comment in .proto file.&#34;&#34;&#34;
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details(&#39;Method not implemented!&#39;)
@@ -1992,6 +2217,22 @@ def login_sso(request,
</details>
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.create_api_token"><code class="name flex">
<span>def <span class="ident">create_api_token</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 create_api_token(self, request, context):
&#34;&#34;&#34;Missing associated documentation comment in .proto file.&#34;&#34;&#34;
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details(&#39;Method not implemented!&#39;)
raise NotImplementedError(&#39;Method not implemented!&#39;)</code></pre>
</details>
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.get_sso_providers"><code class="name flex">
<span>def <span class="ident">get_sso_providers</span></span>(<span>self, request, context)</span>
</code></dt>
@@ -2008,6 +2249,22 @@ def login_sso(request,
</details>
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.list_api_tokens"><code class="name flex">
<span>def <span class="ident">list_api_tokens</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 list_api_tokens(self, request, context):
&#34;&#34;&#34;Missing associated documentation comment in .proto file.&#34;&#34;&#34;
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details(&#39;Method not implemented!&#39;)
raise NotImplementedError(&#39;Method not implemented!&#39;)</code></pre>
</details>
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.login"><code class="name flex">
<span>def <span class="ident">login</span></span>(<span>self, request, context)</span>
</code></dt>
@@ -2040,6 +2297,22 @@ def login_sso(request,
</details>
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.revoke_api_token"><code class="name flex">
<span>def <span class="ident">revoke_api_token</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 revoke_api_token(self, request, context):
&#34;&#34;&#34;Missing associated documentation comment in .proto file.&#34;&#34;&#34;
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details(&#39;Method not implemented!&#39;)
raise NotImplementedError(&#39;Method not implemented!&#39;)</code></pre>
</details>
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceStub"><code class="flex name class">
@@ -2079,6 +2352,21 @@ def login_sso(request,
&#39;/connpy.AuthService/get_sso_providers&#39;,
request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
response_deserializer=connpy__pb2.SSOProvidersResponse.FromString,
_registered_method=True)
self.create_api_token = channel.unary_unary(
&#39;/connpy.AuthService/create_api_token&#39;,
request_serializer=connpy__pb2.CreateApiTokenRequest.SerializeToString,
response_deserializer=connpy__pb2.CreateApiTokenResponse.FromString,
_registered_method=True)
self.list_api_tokens = channel.unary_unary(
&#39;/connpy.AuthService/list_api_tokens&#39;,
request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
response_deserializer=connpy__pb2.ListApiTokensResponse.FromString,
_registered_method=True)
self.revoke_api_token = channel.unary_unary(
&#39;/connpy.AuthService/revoke_api_token&#39;,
request_serializer=connpy__pb2.RevokeApiTokenRequest.SerializeToString,
response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
_registered_method=True)</code></pre>
</details>
<div class="desc"><p>Missing associated documentation comment in .proto file.</p>
@@ -6510,20 +6798,26 @@ def stop_api(request,
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthService" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthService">AuthService</a></code></h4>
<ul class="">
<ul class="two-column">
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthService.change_password" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthService.change_password">change_password</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthService.create_api_token" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthService.create_api_token">create_api_token</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthService.get_sso_providers" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthService.get_sso_providers">get_sso_providers</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthService.list_api_tokens" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthService.list_api_tokens">list_api_tokens</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthService.login" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthService.login">login</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthService.login_sso" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthService.login_sso">login_sso</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthService.revoke_api_token" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthService.revoke_api_token">revoke_api_token</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer">AuthServiceServicer</a></code></h4>
<ul class="">
<ul class="two-column">
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.change_password" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.change_password">change_password</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.create_api_token" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.create_api_token">create_api_token</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.get_sso_providers" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.get_sso_providers">get_sso_providers</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.list_api_tokens" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.list_api_tokens">list_api_tokens</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.login" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.login">login</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.login_sso" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.login_sso">login_sso</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.revoke_api_token" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.revoke_api_token">revoke_api_token</a></code></li>
</ul>
</li>
<li>
+129 -8
View File
@@ -514,6 +514,8 @@ def service(self):
return self._unauthenticated_handler(handler_call_details, &#34;Authorization token is missing&#34;)
username = self.registry.user_service.verify_jwt(token)
if not username and token.startswith(&#34;cnp_pat_&#34;):
username = self.registry.user_service.verify_api_token(token)
if not username:
return self._unauthenticated_handler(handler_call_details, &#34;Invalid or expired token&#34;)
@@ -628,6 +630,8 @@ def service(self):
return self._unauthenticated_handler(handler_call_details, &#34;Authorization token is missing&#34;)
username = self.registry.user_service.verify_jwt(token)
if not username and token.startswith(&#34;cnp_pat_&#34;):
username = self.registry.user_service.verify_api_token(token)
if not username:
return self._unauthenticated_handler(handler_call_details, &#34;Invalid or expired token&#34;)
@@ -834,6 +838,58 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
except ValueError as e:
context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
return Empty()
@handle_errors
def create_api_token(self, request, context):
username = _current_user.get()
if not username:
context.abort(grpc.StatusCode.UNAUTHENTICATED, &#34;Authentication required&#34;)
try:
expires_in_days = request.expires_in_days if request.expires_in_days &gt; 0 else None
result = self.registry.user_service.create_api_token(
username, request.name, expires_in_days=expires_in_days
)
except ValueError as e:
context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
return connpy_pb2.CreateApiTokenResponse(
token_id=result[&#34;token_id&#34;],
raw_token=result[&#34;raw_token&#34;],
name=result[&#34;name&#34;],
)
@handle_errors
def list_api_tokens(self, request, context):
username = _current_user.get()
if not username:
context.abort(grpc.StatusCode.UNAUTHENTICATED, &#34;Authentication required&#34;)
tokens = self.registry.user_service.list_api_tokens(username)
token_infos = [
connpy_pb2.ApiTokenInfo(
token_id=t[&#34;token_id&#34;],
name=t.get(&#34;name&#34;) or &#34;&#34;,
token_prefix=t.get(&#34;token_prefix&#34;) or &#34;&#34;,
created_at=t.get(&#34;created_at&#34;) or &#34;&#34;,
last_used_at=t.get(&#34;last_used_at&#34;) or &#34;&#34;,
expires_at=t.get(&#34;expires_at&#34;) or &#34;&#34;,
)
for t in tokens
]
return connpy_pb2.ListApiTokensResponse(tokens=token_infos)
@handle_errors
def revoke_api_token(self, request, context):
username = _current_user.get()
if not username:
context.abort(grpc.StatusCode.UNAUTHENTICATED, &#34;Authentication required&#34;)
removed = self.registry.user_service.revoke_api_token(username, request.token_id)
if not removed:
context.abort(grpc.StatusCode.NOT_FOUND, f&#34;Token &#39;{request.token_id}&#39; not found&#34;)
return Empty()</code></pre>
</details>
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
@@ -846,9 +902,12 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
<li><code><b><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer">AuthServiceServicer</a></b></code>:
<ul class="hlist">
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.change_password" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.change_password">change_password</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.create_api_token" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.create_api_token">create_api_token</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.get_sso_providers" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.get_sso_providers">get_sso_providers</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.list_api_tokens" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.list_api_tokens">list_api_tokens</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.login" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.login">login</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.login_sso" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.login_sso">login_sso</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.revoke_api_token" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.revoke_api_token">revoke_api_token</a></code></li>
</ul>
</li>
</ul>
@@ -1496,10 +1555,59 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
raw_bytes = str(raw_bytes).encode()
from connpy.utils import log_cleaner
last_line = log_cleaner(raw_bytes.decode(errors=&#39;replace&#39;)).split(&#39;\n&#39;)[-1].strip()
cleaned_buffer = log_cleaner(raw_bytes.decode(errors=&#39;replace&#39;))
last_line = cleaned_buffer.split(&#39;\n&#39;)[-1].strip() if cleaned_buffer.strip() else &#34;(prompt)&#34;
blocks = service.build_context_blocks(raw_bytes, n.cmd_byte_positions, node_info, last_line=last_line)
node_info[&#34;context_blocks&#34;] = blocks
total_cmds = len(blocks)
total_lines = len(cleaned_buffer.split(&#39;\n&#39;))
if not hasattr(remote_stream, &#39;copilot_state&#39;) or remote_stream.copilot_state is None:
remote_stream.copilot_state = {}
session_state = remote_stream.copilot_state
if isinstance(node_info, dict):
for k, v in node_info.items():
if k in (&#39;context_mode&#39;, &#39;context_cmd&#39;, &#39;context_lines&#39;, &#39;persona&#39;, &#39;trust&#39;, &#39;os&#39;, &#39;prompt&#39;):
session_state[k] = v
saved_mode = session_state.get(&#39;context_mode&#39;, 0)
saved_cmd = session_state.get(&#39;context_cmd&#39;, 1)
saved_lines = session_state.get(&#39;context_lines&#39;, 50)
last_total_cmds = session_state.get(&#39;last_total_cmds&#39;, None)
last_total_lines = session_state.get(&#39;last_total_lines&#39;, None)
is_range = saved_mode in (0, &#39;RANGE&#39;, &#39;range&#39;)
is_lines = saved_mode in (2, &#39;LINES&#39;, &#39;lines&#39;)
is_single = saved_mode in (1, &#39;SINGLE&#39;, &#39;single&#39;)
if is_range or is_single:
if last_total_cmds is not None and total_cmds &gt; last_total_cmds and saved_cmd &gt; 1:
new_cmds = total_cmds - last_total_cmds
initial_cmd = saved_cmd + new_cmds
else:
initial_cmd = saved_cmd
initial_lines = saved_lines
elif is_lines:
if last_total_lines is not None and total_lines &gt; last_total_lines and saved_lines &gt; 50:
new_lines = total_lines - last_total_lines
initial_lines = saved_lines + new_lines
else:
initial_lines = saved_lines
initial_cmd = saved_cmd
else:
initial_cmd = saved_cmd
initial_lines = saved_lines
session_state[&#39;context_cmd&#39;] = max(1, initial_cmd)
session_state[&#39;context_lines&#39;] = max(1, initial_lines)
session_state[&#39;last_total_cmds&#39;] = total_cmds
session_state[&#39;last_total_lines&#39;] = total_lines
node_info.update(session_state)
node_info[&#39;context_cmd&#39;] = min(session_state[&#39;context_cmd&#39;], max(1, total_cmds))
node_info[&#39;context_lines&#39;] = min(session_state[&#39;context_lines&#39;], max(1, total_lines))
node_info_json = json.dumps(node_info)
# Convert buffer to string if it&#39;s bytes for the preview
@@ -1544,6 +1652,17 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
if req_session_id and req_session_id != copilot_session_id:
continue # Ignore stale request from a previous session
merged_node_info_str = req_data.get(&#34;node_info_json&#34;, &#34;&#34;)
if merged_node_info_str:
try:
merged_node_info = json.loads(merged_node_info_str)
node_info.update(merged_node_info)
# Sync context state from frontend into session_state for persistence
for k in (&#39;context_mode&#39;, &#39;context_cmd&#39;, &#39;context_lines&#39;):
if k in merged_node_info:
session_state[k] = merged_node_info[k]
except: pass
if &#34;question&#34; not in req_data or not req_data[&#34;question&#34;] or req_data[&#34;question&#34;] == &#34;CANCEL&#34; or req_data.get(&#34;action&#34;) in (&#34;cancel&#34;, &#34;web_cancel&#34;):
if req_data.get(&#34;action&#34;) == &#34;web_cancel&#34;:
os.write(child_fd, b&#39;\x05&#39;)
@@ -1551,13 +1670,6 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
os.write(child_fd, b&#39;\x15\r&#39;)
return
question = req_data[&#34;question&#34;]
merged_node_info_str = req_data.get(&#34;node_info_json&#34;, &#34;&#34;)
if merged_node_info_str:
try:
merged_node_info = json.loads(merged_node_info_str)
node_info.update(merged_node_info)
except: pass
context_buffer = req_data.get(&#34;context_buffer&#34;, &#34;&#34;)
if context_buffer.startswith(&#39;{&#34;context_start_pos&#34;&#39;):
@@ -1620,6 +1732,15 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
if not action_data: return
action = action_data.get(&#34;action&#34;, &#34;cancel&#34;)
merged_node_info_str = action_data.get(&#34;node_info_json&#34;, &#34;&#34;)
if merged_node_info_str:
try:
merged_node_info = json.loads(merged_node_info_str)
for k in (&#39;context_mode&#39;, &#39;context_cmd&#39;, &#39;context_lines&#39;):
if k in merged_node_info:
session_state[k] = merged_node_info[k]
except: pass
if action == &#34;continue&#34;:
continue # Loop back for next question
+94 -1
View File
@@ -864,7 +864,37 @@ Call-Future's exception value will be an RpcError.</p></div>
@handle_errors
def change_password(self, old_password, new_password):
req = connpy_pb2.ChangePasswordRequest(old_password=old_password, new_password=new_password)
self.stub.change_password(req)</code></pre>
self.stub.change_password(req)
@handle_errors
def create_api_token(self, name, expires_in_days=0):
req = connpy_pb2.CreateApiTokenRequest(name=name, expires_in_days=expires_in_days)
resp = self.stub.create_api_token(req)
return {
&#34;token_id&#34;: resp.token_id,
&#34;raw_token&#34;: resp.raw_token,
&#34;name&#34;: resp.name,
}
@handle_errors
def list_api_tokens(self):
resp = self.stub.list_api_tokens(Empty())
return [
{
&#34;token_id&#34;: t.token_id,
&#34;name&#34;: t.name,
&#34;token_prefix&#34;: t.token_prefix,
&#34;created_at&#34;: t.created_at,
&#34;last_used_at&#34;: t.last_used_at,
&#34;expires_at&#34;: t.expires_at,
}
for t in resp.tokens
]
@handle_errors
def revoke_api_token(self, token_id):
req = connpy_pb2.RevokeApiTokenRequest(token_id=token_id)
self.stub.revoke_api_token(req)</code></pre>
</details>
<div class="desc"></div>
<h3>Methods</h3>
@@ -884,6 +914,51 @@ def change_password(self, old_password, new_password):
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.stubs.AuthStub.create_api_token"><code class="name flex">
<span>def <span class="ident">create_api_token</span></span>(<span>self, name, expires_in_days=0)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@handle_errors
def create_api_token(self, name, expires_in_days=0):
req = connpy_pb2.CreateApiTokenRequest(name=name, expires_in_days=expires_in_days)
resp = self.stub.create_api_token(req)
return {
&#34;token_id&#34;: resp.token_id,
&#34;raw_token&#34;: resp.raw_token,
&#34;name&#34;: resp.name,
}</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.stubs.AuthStub.list_api_tokens"><code class="name flex">
<span>def <span class="ident">list_api_tokens</span></span>(<span>self)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@handle_errors
def list_api_tokens(self):
resp = self.stub.list_api_tokens(Empty())
return [
{
&#34;token_id&#34;: t.token_id,
&#34;name&#34;: t.name,
&#34;token_prefix&#34;: t.token_prefix,
&#34;created_at&#34;: t.created_at,
&#34;last_used_at&#34;: t.last_used_at,
&#34;expires_at&#34;: t.expires_at,
}
for t in resp.tokens
]</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.stubs.AuthStub.login"><code class="name flex">
<span>def <span class="ident">login</span></span>(<span>self, username, password)</span>
</code></dt>
@@ -904,6 +979,21 @@ def login(self, username, password):
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.stubs.AuthStub.revoke_api_token"><code class="name flex">
<span>def <span class="ident">revoke_api_token</span></span>(<span>self, token_id)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@handle_errors
def revoke_api_token(self, token_id):
req = connpy_pb2.RevokeApiTokenRequest(token_id=token_id)
self.stub.revoke_api_token(req)</code></pre>
</details>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.stubs.ConfigStub"><code class="flex name class">
@@ -2785,7 +2875,10 @@ def stop_api(self):
<h4><code><a title="connpy.grpc_layer.stubs.AuthStub" href="#connpy.grpc_layer.stubs.AuthStub">AuthStub</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.stubs.AuthStub.change_password" href="#connpy.grpc_layer.stubs.AuthStub.change_password">change_password</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AuthStub.create_api_token" href="#connpy.grpc_layer.stubs.AuthStub.create_api_token">create_api_token</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AuthStub.list_api_tokens" href="#connpy.grpc_layer.stubs.AuthStub.list_api_tokens">list_api_tokens</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AuthStub.login" href="#connpy.grpc_layer.stubs.AuthStub.login">login</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AuthStub.revoke_api_token" href="#connpy.grpc_layer.stubs.AuthStub.revoke_api_token">revoke_api_token</a></code></li>
</ul>
</li>
<li>
+109 -2624
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -322,13 +322,13 @@ el.replaceWith(d);
is_mock = True
def __init__(self, config):
from ..core import node, nodes
from ..ai import ai
from ..connapp import DeferredAIProxy
from ..services.provider import ServiceProvider
self.config = config
self.node = node
self.nodes = nodes
self.ai = ai
self.ai = DeferredAIProxy()
self.services = ServiceProvider(config, mode=&#34;local&#34;)
@@ -645,13 +645,13 @@ el.replaceWith(d);
is_mock = True
def __init__(self, config):
from ..core import node, nodes
from ..ai import ai
from ..connapp import DeferredAIProxy
from ..services.provider import ServiceProvider
self.config = config
self.node = node
self.nodes = nodes
self.ai = ai
self.ai = DeferredAIProxy()
self.services = ServiceProvider(config, mode=&#34;local&#34;)
+177 -16
View File
@@ -79,6 +79,12 @@ el.replaceWith(d);
self.mode = mode
self.config = config
self.remote_host = remote_host
self._system = None
self._execution = None
self._import_export = None
self._sync = None
self._users = None
self._ai = None
if mode == &#34;local&#34;:
self._init_local()
@@ -92,35 +98,20 @@ el.replaceWith(d);
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
from .context_service import ContextService
from .sync_service import SyncService
from .user_service import UserService
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)
self.context = ContextService(self.config)
self.sync = SyncService(self.config)
self.users = UserService(self.config.defaultdir)
def _init_remote(self):
# Allow ConfigService to work locally so the user can revert the mode
from .config_service import ConfigService
from .context_service import ContextService
from .sync_service import SyncService
self.config_svc = ConfigService(self.config)
self.context = ContextService(self.config)
self.sync = SyncService(self.config)
self.users = None
if not self.remote_host:
raise InvalidConfigurationError(&#34;Remote host must be specified in remote mode&#34;)
@@ -134,6 +125,9 @@ el.replaceWith(d);
)
def get_token():
env_token = os.environ.get(&#34;CONNPY_TOKEN&#34;)
if env_token:
return env_token
token_path = os.path.join(self.config.defaultdir, &#34;.token&#34;)
if os.path.exists(token_path):
try:
@@ -159,9 +153,168 @@ el.replaceWith(d);
self.system = SystemStub(channel, remote_host=self.remote_host)
self.execution = ExecutionStub(channel, remote_host=self.remote_host)
self.import_export = ImportExportStub(channel, remote_host=self.remote_host)
self.auth = AuthStub(channel, remote_host=self.remote_host)</code></pre>
self.auth = AuthStub(channel, remote_host=self.remote_host)
@property
def system(self):
if self._system is None and self.mode == &#34;local&#34;:
from .system_service import SystemService
self._system = SystemService(self.config)
return self._system
@system.setter
def system(self, value):
self._system = value
@property
def execution(self):
if self._execution is None and self.mode == &#34;local&#34;:
from .execution_service import ExecutionService
self._execution = ExecutionService(self.config)
return self._execution
@execution.setter
def execution(self, value):
self._execution = value
@property
def import_export(self):
if self._import_export is None and self.mode == &#34;local&#34;:
from .import_export_service import ImportExportService
self._import_export = ImportExportService(self.config)
return self._import_export
@import_export.setter
def import_export(self, value):
self._import_export = value
@property
def sync(self):
if self._sync is None:
from .sync_service import SyncService
self._sync = SyncService(self.config)
return self._sync
@sync.setter
def sync(self, value):
self._sync = value
@property
def users(self):
if self._users is None and self.mode == &#34;local&#34;:
from .user_service import UserService
self._users = UserService(self.config.defaultdir)
return self._users
@users.setter
def users(self, value):
self._users = value
@property
def ai(self):
if self._ai is None and self.mode == &#34;local&#34;:
from .ai_service import AIService
self._ai = AIService(self.config)
return self._ai
@ai.setter
def ai(self, value):
self._ai = value</code></pre>
</details>
<div class="desc"><p>Dynamic service backend. Transparently provides local or remote services.</p></div>
<h3>Instance variables</h3>
<dl>
<dt id="connpy.services.provider.ServiceProvider.ai"><code class="name">prop <span class="ident">ai</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def ai(self):
if self._ai is None and self.mode == &#34;local&#34;:
from .ai_service import AIService
self._ai = AIService(self.config)
return self._ai</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.services.provider.ServiceProvider.execution"><code class="name">prop <span class="ident">execution</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def execution(self):
if self._execution is None and self.mode == &#34;local&#34;:
from .execution_service import ExecutionService
self._execution = ExecutionService(self.config)
return self._execution</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.services.provider.ServiceProvider.import_export"><code class="name">prop <span class="ident">import_export</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def import_export(self):
if self._import_export is None and self.mode == &#34;local&#34;:
from .import_export_service import ImportExportService
self._import_export = ImportExportService(self.config)
return self._import_export</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.services.provider.ServiceProvider.sync"><code class="name">prop <span class="ident">sync</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def sync(self):
if self._sync is None:
from .sync_service import SyncService
self._sync = SyncService(self.config)
return self._sync</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.services.provider.ServiceProvider.system"><code class="name">prop <span class="ident">system</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def system(self):
if self._system is None and self.mode == &#34;local&#34;:
from .system_service import SystemService
self._system = SystemService(self.config)
return self._system</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.services.provider.ServiceProvider.users"><code class="name">prop <span class="ident">users</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def users(self):
if self._users is None and self.mode == &#34;local&#34;:
from .user_service import UserService
self._users = UserService(self.config.defaultdir)
return self._users</code></pre>
</details>
<div class="desc"></div>
</dd>
</dl>
</dd>
</dl>
</section>
@@ -183,6 +336,14 @@ el.replaceWith(d);
</li>
<li>
<h4><code><a title="connpy.services.provider.ServiceProvider" href="#connpy.services.provider.ServiceProvider">ServiceProvider</a></code></h4>
<ul class="two-column">
<li><code><a title="connpy.services.provider.ServiceProvider.ai" href="#connpy.services.provider.ServiceProvider.ai">ai</a></code></li>
<li><code><a title="connpy.services.provider.ServiceProvider.execution" href="#connpy.services.provider.ServiceProvider.execution">execution</a></code></li>
<li><code><a title="connpy.services.provider.ServiceProvider.import_export" href="#connpy.services.provider.ServiceProvider.import_export">import_export</a></code></li>
<li><code><a title="connpy.services.provider.ServiceProvider.sync" href="#connpy.services.provider.ServiceProvider.sync">sync</a></code></li>
<li><code><a title="connpy.services.provider.ServiceProvider.system" href="#connpy.services.provider.ServiceProvider.system">system</a></code></li>
<li><code><a title="connpy.services.provider.ServiceProvider.users" href="#connpy.services.provider.ServiceProvider.users">users</a></code></li>
</ul>
</li>
</ul>
</li>
+14
View File
@@ -82,6 +82,7 @@ el.replaceWith(d);
def login(self):
&#34;&#34;&#34;Authenticate with Google Drive.&#34;&#34;&#34;
Credentials, Request, _, RefreshError, InstalledAppFlow, _, _, _ = _get_google_libs()
creds = None
if os.path.exists(self.token_file):
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
@@ -119,6 +120,7 @@ el.replaceWith(d);
def get_credentials(self):
&#34;&#34;&#34;Get valid credentials, refreshing if necessary.&#34;&#34;&#34;
Credentials, Request, _, RefreshError, _, _, _, _ = _get_google_libs()
if os.path.exists(self.token_file):
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
else:
@@ -136,6 +138,7 @@ el.replaceWith(d);
def check_login_status(self):
&#34;&#34;&#34;Check if logged in to Google Drive.&#34;&#34;&#34;
Credentials, Request, _, RefreshError, _, _, _, _ = _get_google_libs()
if os.path.exists(self.token_file):
creds = Credentials.from_authorized_user_file(self.token_file)
if creds and creds.expired and creds.refresh_token:
@@ -148,6 +151,7 @@ el.replaceWith(d);
def list_backups(self):
&#34;&#34;&#34;List files in Google Drive appDataFolder.&#34;&#34;&#34;
_, _, build, _, _, _, _, HttpError = _get_google_libs()
creds = self.get_credentials()
if not creds:
printer.error(&#34;Not logged in to Google Drive.&#34;)
@@ -206,6 +210,7 @@ el.replaceWith(d);
def upload_file(self, file_path, timestamp):
&#34;&#34;&#34;Internal method to upload to Drive.&#34;&#34;&#34;
_, _, build, _, _, MediaFileUpload, _, _ = _get_google_libs()
creds = self.get_credentials()
if not creds: return False
@@ -231,6 +236,7 @@ el.replaceWith(d);
def delete_backup(self, file_id):
&#34;&#34;&#34;Delete a backup from Drive.&#34;&#34;&#34;
_, _, build, _, _, _, _, _ = _get_google_libs()
creds = self.get_credentials()
if not creds: return False
try:
@@ -264,6 +270,7 @@ el.replaceWith(d);
def download_file(self, file_id, dest):
&#34;&#34;&#34;Internal method to download from Drive.&#34;&#34;&#34;
_, _, build, _, _, _, MediaIoBaseDownload, _ = _get_google_libs()
creds = self.get_credentials()
if not creds: return False
try:
@@ -508,6 +515,7 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def check_login_status(self):
&#34;&#34;&#34;Check if logged in to Google Drive.&#34;&#34;&#34;
Credentials, Request, _, RefreshError, _, _, _, _ = _get_google_libs()
if os.path.exists(self.token_file):
creds = Credentials.from_authorized_user_file(self.token_file)
if creds and creds.expired and creds.refresh_token:
@@ -570,6 +578,7 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def delete_backup(self, file_id):
&#34;&#34;&#34;Delete a backup from Drive.&#34;&#34;&#34;
_, _, build, _, _, _, _, _ = _get_google_libs()
creds = self.get_credentials()
if not creds: return False
try:
@@ -592,6 +601,7 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def download_file(self, file_id, dest):
&#34;&#34;&#34;Internal method to download from Drive.&#34;&#34;&#34;
_, _, build, _, _, _, MediaIoBaseDownload, _ = _get_google_libs()
creds = self.get_credentials()
if not creds: return False
try:
@@ -619,6 +629,7 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def get_credentials(self):
&#34;&#34;&#34;Get valid credentials, refreshing if necessary.&#34;&#34;&#34;
Credentials, Request, _, RefreshError, _, _, _, _ = _get_google_libs()
if os.path.exists(self.token_file):
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
else:
@@ -646,6 +657,7 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def list_backups(self):
&#34;&#34;&#34;List files in Google Drive appDataFolder.&#34;&#34;&#34;
_, _, build, _, _, _, _, HttpError = _get_google_libs()
creds = self.get_credentials()
if not creds:
printer.error(&#34;Not logged in to Google Drive.&#34;)
@@ -684,6 +696,7 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def login(self):
&#34;&#34;&#34;Authenticate with Google Drive.&#34;&#34;&#34;
Credentials, Request, _, RefreshError, InstalledAppFlow, _, _, _ = _get_google_libs()
creds = None
if os.path.exists(self.token_file):
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
@@ -890,6 +903,7 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def upload_file(self, file_path, timestamp):
&#34;&#34;&#34;Internal method to upload to Drive.&#34;&#34;&#34;
_, _, build, _, _, MediaFileUpload, _, _ = _get_google_libs()
creds = self.get_credentials()
if not creds: return False
+302 -1
View File
@@ -63,6 +63,9 @@ el.replaceWith(d);
# Ensure users directory exists
os.makedirs(self.users_dir, exist_ok=True)
# Reverse index cache: token_hash -&gt; (username, token_id)
self._token_index: dict[str, tuple[str, str]] = {}
def _load_registry(self) -&gt; dict:
&#34;&#34;&#34;Loads registry from file. If it doesn&#39;t exist, initializes it with a new JWT secret.&#34;&#34;&#34;
@@ -107,6 +110,16 @@ el.replaceWith(d);
pass
raise e
def _build_token_index(self, registry: dict) -&gt; dict[str, tuple[str, str]]:
&#34;&#34;&#34;Builds a reverse index of token_hash -&gt; (username, token_id) for O(1) PAT lookup.&#34;&#34;&#34;
index = {}
for username, user_data in registry.get(&#34;users&#34;, {}).items():
for token_id, token_meta in user_data.get(&#34;api_tokens&#34;, {}).items():
token_hash = token_meta.get(&#34;token_hash&#34;)
if token_hash:
index[token_hash] = (username, token_id)
return index
def create_user(self, username, password, config_path=None) -&gt; dict:
&#34;&#34;&#34;Creates a new user with bcrypt-hashed credentials.
@@ -282,7 +295,129 @@ el.replaceWith(d);
payload = jwt.decode(token, secret, algorithms=[&#34;HS256&#34;])
return payload.get(&#34;sub&#34;)
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError, KeyError):
return None</code></pre>
return None
# --- Personal Access Token (PAT) Management ---
def create_api_token(self, username: str, name: str, expires_in_days: int | None = None) -&gt; dict:
&#34;&#34;&#34;Creates a Personal Access Token for the user.
Returns the raw token ONCE. Only the SHA-256 hash is persisted.
&#34;&#34;&#34;
if not name or not isinstance(name, str):
raise ValueError(&#34;Token name cannot be empty&#34;)
registry = self._load_registry()
if username not in registry[&#34;users&#34;]:
raise ValueError(f&#34;User &#39;{username}&#39; not found&#34;)
user_data = registry[&#34;users&#34;][username]
if &#34;api_tokens&#34; not in user_data:
user_data[&#34;api_tokens&#34;] = {}
# Generate cryptographically secure token with recognizable prefix
raw_secret = secrets.token_hex(32)
raw_token = f&#34;cnp_pat_{raw_secret}&#34;
token_hash = hashlib.sha256(raw_token.encode(&#34;utf-8&#34;)).hexdigest()
token_id = f&#34;tok_{secrets.token_hex(4)}&#34;
now = datetime.datetime.now(datetime.timezone.utc)
expires_at = None
if expires_in_days and expires_in_days &gt; 0:
expires_at = (now + datetime.timedelta(days=expires_in_days)).isoformat()
user_data[&#34;api_tokens&#34;][token_id] = {
&#34;name&#34;: name,
&#34;token_hash&#34;: token_hash,
&#34;token_prefix&#34;: raw_token[:16],
&#34;created_at&#34;: now.isoformat(),
&#34;last_used_at&#34;: None,
&#34;expires_at&#34;: expires_at,
}
self._save_registry(registry)
self._token_index = self._build_token_index(registry)
return {
&#34;token_id&#34;: token_id,
&#34;raw_token&#34;: raw_token,
&#34;name&#34;: name,
}
def list_api_tokens(self, username: str) -&gt; list[dict]:
&#34;&#34;&#34;Lists all active API tokens for a user (without sensitive data).&#34;&#34;&#34;
registry = self._load_registry()
if username not in registry[&#34;users&#34;]:
raise ValueError(f&#34;User &#39;{username}&#39; not found&#34;)
tokens = registry[&#34;users&#34;][username].get(&#34;api_tokens&#34;, {})
return [
{
&#34;token_id&#34;: tid,
&#34;name&#34;: meta.get(&#34;name&#34;),
&#34;token_prefix&#34;: meta.get(&#34;token_prefix&#34;),
&#34;created_at&#34;: meta.get(&#34;created_at&#34;),
&#34;last_used_at&#34;: meta.get(&#34;last_used_at&#34;),
&#34;expires_at&#34;: meta.get(&#34;expires_at&#34;),
}
for tid, meta in tokens.items()
]
def revoke_api_token(self, username: str, token_id: str) -&gt; bool:
&#34;&#34;&#34;Revokes (deletes) a specific API token. Returns True if found and removed.&#34;&#34;&#34;
registry = self._load_registry()
if username not in registry[&#34;users&#34;]:
raise ValueError(f&#34;User &#39;{username}&#39; not found&#34;)
tokens = registry[&#34;users&#34;][username].get(&#34;api_tokens&#34;, {})
if token_id not in tokens:
return False
del tokens[token_id]
self._save_registry(registry)
self._token_index = self._build_token_index(registry)
return True
def verify_api_token(self, raw_token: str) -&gt; str | None:
&#34;&#34;&#34;Validates a PAT by hashing it and looking up the reverse index.
Returns username if valid and not expired, None otherwise.
&#34;&#34;&#34;
token_hash = hashlib.sha256(raw_token.encode(&#34;utf-8&#34;)).hexdigest()
# Rebuild index if empty (cold start or after process restart)
if not self._token_index:
registry = self._load_registry()
self._token_index = self._build_token_index(registry)
match = self._token_index.get(token_hash)
if not match:
return None
username, token_id = match
# Validate token still exists and check expiration
registry = self._load_registry()
user_data = registry.get(&#34;users&#34;, {}).get(username, {})
token_meta = user_data.get(&#34;api_tokens&#34;, {}).get(token_id)
if not token_meta:
# Token was revoked between index build and now
self._token_index = self._build_token_index(registry)
return None
# Check expiration
expires_at = token_meta.get(&#34;expires_at&#34;)
if expires_at:
exp_dt = datetime.datetime.fromisoformat(expires_at)
if datetime.datetime.now(datetime.timezone.utc) &gt; exp_dt:
return None
# Update last_used_at
token_meta[&#34;last_used_at&#34;] = datetime.datetime.now(datetime.timezone.utc).isoformat()
self._save_registry(registry)
return username</code></pre>
</details>
<div class="desc"></div>
<h3>Methods</h3>
@@ -356,6 +491,62 @@ el.replaceWith(d);
</details>
<div class="desc"><p>Verifies old password and updates registry with new hashed password.</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.create_api_token"><code class="name flex">
<span>def <span class="ident">create_api_token</span></span>(<span>self, username: str, name: str, expires_in_days: int | None = None) > dict</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def create_api_token(self, username: str, name: str, expires_in_days: int | None = None) -&gt; dict:
&#34;&#34;&#34;Creates a Personal Access Token for the user.
Returns the raw token ONCE. Only the SHA-256 hash is persisted.
&#34;&#34;&#34;
if not name or not isinstance(name, str):
raise ValueError(&#34;Token name cannot be empty&#34;)
registry = self._load_registry()
if username not in registry[&#34;users&#34;]:
raise ValueError(f&#34;User &#39;{username}&#39; not found&#34;)
user_data = registry[&#34;users&#34;][username]
if &#34;api_tokens&#34; not in user_data:
user_data[&#34;api_tokens&#34;] = {}
# Generate cryptographically secure token with recognizable prefix
raw_secret = secrets.token_hex(32)
raw_token = f&#34;cnp_pat_{raw_secret}&#34;
token_hash = hashlib.sha256(raw_token.encode(&#34;utf-8&#34;)).hexdigest()
token_id = f&#34;tok_{secrets.token_hex(4)}&#34;
now = datetime.datetime.now(datetime.timezone.utc)
expires_at = None
if expires_in_days and expires_in_days &gt; 0:
expires_at = (now + datetime.timedelta(days=expires_in_days)).isoformat()
user_data[&#34;api_tokens&#34;][token_id] = {
&#34;name&#34;: name,
&#34;token_hash&#34;: token_hash,
&#34;token_prefix&#34;: raw_token[:16],
&#34;created_at&#34;: now.isoformat(),
&#34;last_used_at&#34;: None,
&#34;expires_at&#34;: expires_at,
}
self._save_registry(registry)
self._token_index = self._build_token_index(registry)
return {
&#34;token_id&#34;: token_id,
&#34;raw_token&#34;: raw_token,
&#34;name&#34;: name,
}</code></pre>
</details>
<div class="desc"><p>Creates a Personal Access Token for the user.</p>
<p>Returns the raw token ONCE. Only the SHA-256 hash is persisted.</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.create_user"><code class="name flex">
<span>def <span class="ident">create_user</span></span>(<span>self, username, password, config_path=None) > dict</span>
</code></dt>
@@ -514,6 +705,35 @@ Mode B: config_path set -&gt; Reuses existing directory after validating its str
</details>
<div class="desc"><p>Retrieves raw metadata for a specific user.</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.list_api_tokens"><code class="name flex">
<span>def <span class="ident">list_api_tokens</span></span>(<span>self, username: str) > list[dict]</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def list_api_tokens(self, username: str) -&gt; list[dict]:
&#34;&#34;&#34;Lists all active API tokens for a user (without sensitive data).&#34;&#34;&#34;
registry = self._load_registry()
if username not in registry[&#34;users&#34;]:
raise ValueError(f&#34;User &#39;{username}&#39; not found&#34;)
tokens = registry[&#34;users&#34;][username].get(&#34;api_tokens&#34;, {})
return [
{
&#34;token_id&#34;: tid,
&#34;name&#34;: meta.get(&#34;name&#34;),
&#34;token_prefix&#34;: meta.get(&#34;token_prefix&#34;),
&#34;created_at&#34;: meta.get(&#34;created_at&#34;),
&#34;last_used_at&#34;: meta.get(&#34;last_used_at&#34;),
&#34;expires_at&#34;: meta.get(&#34;expires_at&#34;),
}
for tid, meta in tokens.items()
]</code></pre>
</details>
<div class="desc"><p>Lists all active API tokens for a user (without sensitive data).</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.list_users"><code class="name flex">
<span>def <span class="ident">list_users</span></span>(<span>self) > list[dict]</span>
</code></dt>
@@ -536,6 +756,83 @@ Mode B: config_path set -&gt; Reuses existing directory after validating its str
</details>
<div class="desc"><p>Lists all registered users with metadata.</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.revoke_api_token"><code class="name flex">
<span>def <span class="ident">revoke_api_token</span></span>(<span>self, username: str, token_id: str) > bool</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def revoke_api_token(self, username: str, token_id: str) -&gt; bool:
&#34;&#34;&#34;Revokes (deletes) a specific API token. Returns True if found and removed.&#34;&#34;&#34;
registry = self._load_registry()
if username not in registry[&#34;users&#34;]:
raise ValueError(f&#34;User &#39;{username}&#39; not found&#34;)
tokens = registry[&#34;users&#34;][username].get(&#34;api_tokens&#34;, {})
if token_id not in tokens:
return False
del tokens[token_id]
self._save_registry(registry)
self._token_index = self._build_token_index(registry)
return True</code></pre>
</details>
<div class="desc"><p>Revokes (deletes) a specific API token. Returns True if found and removed.</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.verify_api_token"><code class="name flex">
<span>def <span class="ident">verify_api_token</span></span>(<span>self, raw_token: str) > str | None</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def verify_api_token(self, raw_token: str) -&gt; str | None:
&#34;&#34;&#34;Validates a PAT by hashing it and looking up the reverse index.
Returns username if valid and not expired, None otherwise.
&#34;&#34;&#34;
token_hash = hashlib.sha256(raw_token.encode(&#34;utf-8&#34;)).hexdigest()
# Rebuild index if empty (cold start or after process restart)
if not self._token_index:
registry = self._load_registry()
self._token_index = self._build_token_index(registry)
match = self._token_index.get(token_hash)
if not match:
return None
username, token_id = match
# Validate token still exists and check expiration
registry = self._load_registry()
user_data = registry.get(&#34;users&#34;, {}).get(username, {})
token_meta = user_data.get(&#34;api_tokens&#34;, {}).get(token_id)
if not token_meta:
# Token was revoked between index build and now
self._token_index = self._build_token_index(registry)
return None
# Check expiration
expires_at = token_meta.get(&#34;expires_at&#34;)
if expires_at:
exp_dt = datetime.datetime.fromisoformat(expires_at)
if datetime.datetime.now(datetime.timezone.utc) &gt; exp_dt:
return None
# Update last_used_at
token_meta[&#34;last_used_at&#34;] = datetime.datetime.now(datetime.timezone.utc).isoformat()
self._save_registry(registry)
return username</code></pre>
</details>
<div class="desc"><p>Validates a PAT by hashing it and looking up the reverse index.</p>
<p>Returns username if valid and not expired, None otherwise.</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.verify_jwt"><code class="name flex">
<span>def <span class="ident">verify_jwt</span></span>(<span>self, token) > str | None</span>
</code></dt>
@@ -579,11 +876,15 @@ Mode B: config_path set -&gt; Reuses existing directory after validating its str
<li><code><a title="connpy.services.user_service.UserService.admin_change_password" href="#connpy.services.user_service.UserService.admin_change_password">admin_change_password</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.authenticate" href="#connpy.services.user_service.UserService.authenticate">authenticate</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.change_password" href="#connpy.services.user_service.UserService.change_password">change_password</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.create_api_token" href="#connpy.services.user_service.UserService.create_api_token">create_api_token</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.create_user" href="#connpy.services.user_service.UserService.create_user">create_user</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.delete_user" href="#connpy.services.user_service.UserService.delete_user">delete_user</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.generate_jwt" href="#connpy.services.user_service.UserService.generate_jwt">generate_jwt</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.get_user" href="#connpy.services.user_service.UserService.get_user">get_user</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.list_api_tokens" href="#connpy.services.user_service.UserService.list_api_tokens">list_api_tokens</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.list_users" href="#connpy.services.user_service.UserService.list_users">list_users</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.revoke_api_token" href="#connpy.services.user_service.UserService.revoke_api_token">revoke_api_token</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.verify_api_token" href="#connpy.services.user_service.UserService.verify_api_token">verify_api_token</a></code></li>
<li><code><a title="connpy.services.user_service.UserService.verify_jwt" href="#connpy.services.user_service.UserService.verify_jwt">verify_jwt</a></code></li>
</ul>
</li>
+4
View File
@@ -376,6 +376,8 @@ Handles terminal raw mode, async I/O, and SIGWINCH signals.</p></div>
})
if getattr(req, &#34;copilot_action&#34;, &#34;&#34;):
copilot_msg[&#34;action&#34;] = req.copilot_action
if getattr(req, &#34;copilot_node_info_json&#34;, &#34;&#34;):
copilot_msg[&#34;node_info_json&#34;] = req.copilot_node_info_json
if copilot_msg:
self._loop.call_soon_threadsafe(self.copilot_queue.put_nowait, copilot_msg)
@@ -454,6 +456,8 @@ Bridges the blocking gRPC iterators with the async _async_interact_loop.</p></di
})
if getattr(req, &#34;copilot_action&#34;, &#34;&#34;):
copilot_msg[&#34;action&#34;] = req.copilot_action
if getattr(req, &#34;copilot_node_info_json&#34;, &#34;&#34;):
copilot_msg[&#34;node_info_json&#34;] = req.copilot_node_info_json
if copilot_msg:
self._loop.call_soon_threadsafe(self.copilot_queue.put_nowait, copilot_msg)