new version and docs

This commit is contained in:
2026-05-28 18:22:00 -03:00
parent cf866d782a
commit e52d300cf1
48 changed files with 3910 additions and 387 deletions
+1 -1
View File
@@ -1 +1 @@
__version__ = "6.0.0b12"
__version__ = "6.0.0b13"
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.ai_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>
@@ -666,7 +666,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.api_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>
@@ -193,7 +193,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.config_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>
@@ -551,7 +551,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.context_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>
@@ -249,7 +249,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.forms 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>
@@ -690,7 +690,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.help_text 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>
@@ -303,7 +303,7 @@ tasks:
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.helpers 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>
@@ -333,7 +333,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.import_export_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>
@@ -272,7 +272,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+12 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli 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>
@@ -72,6 +72,10 @@ el.replaceWith(d);
<dd>
<div class="desc"></div>
</dd>
<dt><code class="name"><a title="connpy.cli.login_handler" href="login_handler.html">connpy.cli.login_handler</a></code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt><code class="name"><a title="connpy.cli.node_handler" href="node_handler.html">connpy.cli.node_handler</a></code></dt>
<dd>
<div class="desc"></div>
@@ -96,6 +100,10 @@ el.replaceWith(d);
<dd>
<div class="desc"></div>
</dd>
<dt><code class="name"><a title="connpy.cli.user_handler" href="user_handler.html">connpy.cli.user_handler</a></code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt><code class="name"><a title="connpy.cli.validators" href="validators.html">connpy.cli.validators</a></code></dt>
<dd>
<div class="desc"></div>
@@ -129,12 +137,14 @@ el.replaceWith(d);
<li><code><a title="connpy.cli.help_text" href="help_text.html">connpy.cli.help_text</a></code></li>
<li><code><a title="connpy.cli.helpers" href="helpers.html">connpy.cli.helpers</a></code></li>
<li><code><a title="connpy.cli.import_export_handler" href="import_export_handler.html">connpy.cli.import_export_handler</a></code></li>
<li><code><a title="connpy.cli.login_handler" href="login_handler.html">connpy.cli.login_handler</a></code></li>
<li><code><a title="connpy.cli.node_handler" href="node_handler.html">connpy.cli.node_handler</a></code></li>
<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.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>
<li><code><a title="connpy.cli.user_handler" href="user_handler.html">connpy.cli.user_handler</a></code></li>
<li><code><a title="connpy.cli.validators" href="validators.html">connpy.cli.validators</a></code></li>
</ul>
</li>
@@ -142,7 +152,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+408
View File
@@ -0,0 +1,408 @@
<!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.login_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.login_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.login_handler.LoginHandler"><code class="flex name class">
<span>class <span class="ident">LoginHandler</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 LoginHandler:
def __init__(self, app):
self.app = app
def dispatch(self, args):
action = getattr(args, &#34;action&#34;, None)
if action == &#34;login&#34;:
return self.login(args)
elif action == &#34;logout&#34;:
return self.logout(args)
else:
printer.error(f&#34;Unknown action: {action}&#34;)
sys.exit(1)
def login(self, args):
if getattr(args, &#34;status&#34;, False):
return self.show_status()
if self.app.services.mode != &#34;remote&#34;:
printer.warning(&#34;Note: Your current configuration is set to local mode. Logging in will save credentials, but they will only apply when service-mode is set to &#39;remote&#39;.&#34;)
username = getattr(args, &#34;username&#34;, None)
if not username:
try:
username = input(&#34;Username: &#34;).strip()
if not username:
printer.error(&#34;Username cannot be empty.&#34;)
sys.exit(1)
except (KeyboardInterrupt, EOFError):
printer.warning(&#34;\nOperation cancelled.&#34;)
sys.exit(130)
try:
password = getpass.getpass(&#34;Password: &#34;)
if not password:
printer.error(&#34;Password cannot be empty.&#34;)
sys.exit(1)
except (KeyboardInterrupt, EOFError):
printer.warning(&#34;\nOperation cancelled.&#34;)
sys.exit(130)
# Make the gRPC login call via self.app.services.auth stub
# We need to make sure auth is initialized in remote mode.
# If we are in local mode, self.app.services.auth is not initialized on ServiceProvider.
# Let&#39;s instantiate it dynamically if it&#39;s not present.
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:
channel = grpc.insecure_channel(remote_host)
auth_service = AuthStub(channel, remote_host=remote_host)
except Exception as e:
printer.error(f&#34;Failed to connect to remote server for login: {e}&#34;)
sys.exit(1)
try:
res = auth_service.login(username, password)
token = res[&#34;token&#34;]
# Save token to ~/.config/conn/.token
token_path = os.path.join(self.app.config.defaultdir, &#34;.token&#34;)
with open(token_path, &#34;w&#34;) as f:
f.write(token)
os.chmod(token_path, 0o600)
printer.success(f&#34;Logged in successfully as &#39;{username}&#39;. Session expires in 8 hours.&#34;)
except ConnpyError as e:
printer.error(f&#34;Login failed: {e}&#34;)
sys.exit(1)
except Exception as e:
printer.error(f&#34;Login failed with unexpected error: {e}&#34;)
sys.exit(1)
def logout(self, args):
token_path = os.path.join(self.app.config.defaultdir, &#34;.token&#34;)
if os.path.exists(token_path):
try:
os.remove(token_path)
printer.success(&#34;Logged out successfully. Local session cleared.&#34;)
except Exception as e:
printer.error(f&#34;Failed to clear session: {e}&#34;)
sys.exit(1)
else:
printer.info(&#34;No active session found (already logged out).&#34;)
def show_status(self):
import base64
import json
import datetime
token_path = os.path.join(self.app.config.defaultdir, &#34;.token&#34;)
if not os.path.exists(token_path):
printer.warning(&#34;No active session found. You can log in using &#39;connpy login&#39;.&#34;)
return
try:
with open(token_path, &#34;r&#34;) as f:
token = f.read().strip()
parts = token.split(&#34;.&#34;)
if len(parts) != 3:
printer.error(&#34;Invalid local session token format.&#34;)
return
payload_b64 = parts[1]
payload_b64 += &#34;=&#34; * ((4 - len(payload_b64) % 4) % 4)
payload_bytes = base64.urlsafe_b64decode(payload_b64)
payload = json.loads(payload_bytes.decode(&#34;utf-8&#34;))
username = payload.get(&#34;sub&#34;)
exp = payload.get(&#34;exp&#34;)
if not exp:
printer.success(f&#34;Active session as &#39;{username}&#39; (Indefinite expiration).&#34;)
return
now = datetime.datetime.now(datetime.timezone.utc).timestamp()
if now &gt; exp:
printer.error(&#34;Session has expired. Please log in again using &#39;connpy login&#39;.&#34;)
return
remaining = exp - now
hours = int(remaining // 3600)
minutes = int((remaining % 3600) // 60)
printer.success(f&#34;Logged in as &#39;{username}&#39;&#34;)
printer.info(f&#34;Time remaining: {hours}h {minutes}m&#34;)
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>
</details>
<div class="desc"></div>
<h3>Methods</h3>
<dl>
<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>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def dispatch(self, args):
action = getattr(args, &#34;action&#34;, None)
if action == &#34;login&#34;:
return self.login(args)
elif action == &#34;logout&#34;:
return self.logout(args)
else:
printer.error(f&#34;Unknown action: {action}&#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>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def login(self, args):
if getattr(args, &#34;status&#34;, False):
return self.show_status()
if self.app.services.mode != &#34;remote&#34;:
printer.warning(&#34;Note: Your current configuration is set to local mode. Logging in will save credentials, but they will only apply when service-mode is set to &#39;remote&#39;.&#34;)
username = getattr(args, &#34;username&#34;, None)
if not username:
try:
username = input(&#34;Username: &#34;).strip()
if not username:
printer.error(&#34;Username cannot be empty.&#34;)
sys.exit(1)
except (KeyboardInterrupt, EOFError):
printer.warning(&#34;\nOperation cancelled.&#34;)
sys.exit(130)
try:
password = getpass.getpass(&#34;Password: &#34;)
if not password:
printer.error(&#34;Password cannot be empty.&#34;)
sys.exit(1)
except (KeyboardInterrupt, EOFError):
printer.warning(&#34;\nOperation cancelled.&#34;)
sys.exit(130)
# Make the gRPC login call via self.app.services.auth stub
# We need to make sure auth is initialized in remote mode.
# If we are in local mode, self.app.services.auth is not initialized on ServiceProvider.
# Let&#39;s instantiate it dynamically if it&#39;s not present.
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:
channel = grpc.insecure_channel(remote_host)
auth_service = AuthStub(channel, remote_host=remote_host)
except Exception as e:
printer.error(f&#34;Failed to connect to remote server for login: {e}&#34;)
sys.exit(1)
try:
res = auth_service.login(username, password)
token = res[&#34;token&#34;]
# Save token to ~/.config/conn/.token
token_path = os.path.join(self.app.config.defaultdir, &#34;.token&#34;)
with open(token_path, &#34;w&#34;) as f:
f.write(token)
os.chmod(token_path, 0o600)
printer.success(f&#34;Logged in successfully as &#39;{username}&#39;. Session expires in 8 hours.&#34;)
except ConnpyError as e:
printer.error(f&#34;Login failed: {e}&#34;)
sys.exit(1)
except Exception as e:
printer.error(f&#34;Login failed with unexpected error: {e}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.login_handler.LoginHandler.logout"><code class="name flex">
<span>def <span class="ident">logout</span></span>(<span>self, args)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def logout(self, args):
token_path = os.path.join(self.app.config.defaultdir, &#34;.token&#34;)
if os.path.exists(token_path):
try:
os.remove(token_path)
printer.success(&#34;Logged out successfully. Local session cleared.&#34;)
except Exception as e:
printer.error(f&#34;Failed to clear session: {e}&#34;)
sys.exit(1)
else:
printer.info(&#34;No active session found (already logged out).&#34;)</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>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def show_status(self):
import base64
import json
import datetime
token_path = os.path.join(self.app.config.defaultdir, &#34;.token&#34;)
if not os.path.exists(token_path):
printer.warning(&#34;No active session found. You can log in using &#39;connpy login&#39;.&#34;)
return
try:
with open(token_path, &#34;r&#34;) as f:
token = f.read().strip()
parts = token.split(&#34;.&#34;)
if len(parts) != 3:
printer.error(&#34;Invalid local session token format.&#34;)
return
payload_b64 = parts[1]
payload_b64 += &#34;=&#34; * ((4 - len(payload_b64) % 4) % 4)
payload_bytes = base64.urlsafe_b64decode(payload_b64)
payload = json.loads(payload_bytes.decode(&#34;utf-8&#34;))
username = payload.get(&#34;sub&#34;)
exp = payload.get(&#34;exp&#34;)
if not exp:
printer.success(f&#34;Active session as &#39;{username}&#39; (Indefinite expiration).&#34;)
return
now = datetime.datetime.now(datetime.timezone.utc).timestamp()
if now &gt; exp:
printer.error(&#34;Session has expired. Please log in again using &#39;connpy login&#39;.&#34;)
return
remaining = exp - now
hours = int(remaining // 3600)
minutes = int((remaining % 3600) // 60)
printer.success(f&#34;Logged in as &#39;{username}&#39;&#34;)
printer.info(f&#34;Time remaining: {hours}h {minutes}m&#34;)
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>
</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.login_handler.LoginHandler" href="#connpy.cli.login_handler.LoginHandler">LoginHandler</a></code></h4>
<ul class="">
<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.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.show_status" href="#connpy.cli.login_handler.LoginHandler.show_status">show_status</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>
+47 -12
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.node_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>
@@ -60,6 +60,23 @@ el.replaceWith(d);
self.app = app
self.forms = Forms(app)
def _filter_exact_match(self, matches, query):
if not query or len(matches) &lt;= 1:
return matches
exact_matches = []
for m in matches:
if self.app.case:
if m == query:
exact_matches.append(m)
else:
if m.lower() == query.lower():
exact_matches.append(m)
if len(exact_matches) == 1:
return exact_matches
return matches
def dispatch(self, args):
if not self.app.case and args.data != None:
args.data = args.data.lower()
@@ -85,6 +102,7 @@ el.replaceWith(d);
else:
try:
matches = self.app.services.nodes.list_nodes(args.data)
matches = self._filter_exact_match(matches, args.data)
except Exception:
matches = []
@@ -119,6 +137,7 @@ el.replaceWith(d);
matches = self.app.services.nodes.list_folders(args.data)
else:
matches = self.app.services.nodes.list_nodes(args.data)
matches = self._filter_exact_match(matches, args.data)
except Exception:
matches = []
@@ -133,8 +152,9 @@ el.replaceWith(d);
sys.exit(7)
try:
for item in matches:
self.app.services.nodes.delete_node(item, is_folder=is_folder)
for i, item in enumerate(matches):
save_on_last = (i == len(matches) - 1)
self.app.services.nodes.delete_node(item, is_folder=is_folder, save=save_on_last)
if len(matches) == 1:
printer.success(f&#34;{matches[0]} deleted successfully&#34;)
@@ -190,6 +210,7 @@ el.replaceWith(d);
try:
matches = self.app.services.nodes.list_nodes(args.data)
matches = self._filter_exact_match(matches, args.data)
except Exception:
matches = []
@@ -217,6 +238,7 @@ el.replaceWith(d);
try:
matches = self.app.services.nodes.list_nodes(args.data)
matches = self._filter_exact_match(matches, args.data)
except Exception:
matches = []
@@ -255,7 +277,7 @@ el.replaceWith(d);
self.app.services.nodes.update_node(matches[0], updatenode)
printer.success(f&#34;{args.data} edited successfully&#34;)
else:
editcount = 0
changed_items = []
for k in matches:
updated_item = self.app.services.nodes.explode_unique(k)
updated_item[&#34;type&#34;] = &#34;connection&#34;
@@ -268,8 +290,12 @@ el.replaceWith(d);
updated_item[key] = updatenode[key]
if this_item_changed:
editcount += 1
self.app.services.nodes.update_node(k, updated_item)
changed_items.append((k, updated_item))
editcount = len(changed_items)
for i, (k, updated_item) in enumerate(changed_items):
save_on_last = (i == editcount - 1)
self.app.services.nodes.update_node(k, updated_item, save=save_on_last)
if editcount == 0:
printer.info(&#34;Nothing to do here&#34;)
@@ -354,6 +380,7 @@ el.replaceWith(d);
else:
try:
matches = self.app.services.nodes.list_nodes(args.data)
matches = self._filter_exact_match(matches, args.data)
except Exception:
matches = []
@@ -398,6 +425,7 @@ el.replaceWith(d);
matches = self.app.services.nodes.list_folders(args.data)
else:
matches = self.app.services.nodes.list_nodes(args.data)
matches = self._filter_exact_match(matches, args.data)
except Exception:
matches = []
@@ -412,8 +440,9 @@ el.replaceWith(d);
sys.exit(7)
try:
for item in matches:
self.app.services.nodes.delete_node(item, is_folder=is_folder)
for i, item in enumerate(matches):
save_on_last = (i == len(matches) - 1)
self.app.services.nodes.delete_node(item, is_folder=is_folder, save=save_on_last)
if len(matches) == 1:
printer.success(f&#34;{matches[0]} deleted successfully&#34;)
@@ -456,6 +485,7 @@ el.replaceWith(d);
try:
matches = self.app.services.nodes.list_nodes(args.data)
matches = self._filter_exact_match(matches, args.data)
except Exception:
matches = []
@@ -494,7 +524,7 @@ el.replaceWith(d);
self.app.services.nodes.update_node(matches[0], updatenode)
printer.success(f&#34;{args.data} edited successfully&#34;)
else:
editcount = 0
changed_items = []
for k in matches:
updated_item = self.app.services.nodes.explode_unique(k)
updated_item[&#34;type&#34;] = &#34;connection&#34;
@@ -507,8 +537,12 @@ el.replaceWith(d);
updated_item[key] = updatenode[key]
if this_item_changed:
editcount += 1
self.app.services.nodes.update_node(k, updated_item)
changed_items.append((k, updated_item))
editcount = len(changed_items)
for i, (k, updated_item) in enumerate(changed_items):
save_on_last = (i == editcount - 1)
self.app.services.nodes.update_node(k, updated_item, save=save_on_last)
if editcount == 0:
printer.info(&#34;Nothing to do here&#34;)
@@ -535,6 +569,7 @@ el.replaceWith(d);
try:
matches = self.app.services.nodes.list_nodes(args.data)
matches = self._filter_exact_match(matches, args.data)
except Exception:
matches = []
@@ -606,7 +641,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.plugin_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>
@@ -385,7 +385,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.profile_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>
@@ -314,7 +314,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+72 -6
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.run_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>
@@ -68,6 +68,17 @@ el.replaceWith(d);
def node_run(self, args):
nodes_filter = args.data[0]
# Resolve and filter nodes through context-aware list_nodes
try:
matched_nodes = self.app.services.nodes.list_nodes(nodes_filter)
except Exception:
matched_nodes = []
if not matched_nodes:
printer.error(f&#34;No nodes found matching filter: {nodes_filter}&#34;)
sys.exit(2)
commands = [&#34; &#34;.join(args.data[1:])]
try:
@@ -84,7 +95,7 @@ el.replaceWith(d);
printer.test_panel(unique, node_output, node_status, node_result)
results = self.app.services.execution.test_commands(
nodes_filter=nodes_filter,
nodes_filter=matched_nodes,
commands=commands,
expected=args.test_expected,
on_node_complete=_on_node_complete
@@ -101,7 +112,7 @@ el.replaceWith(d);
printer.node_panel(unique, node_output, node_status)
results = self.app.services.execution.run_commands(
nodes_filter=nodes_filter,
nodes_filter=matched_nodes,
commands=commands,
on_node_complete=_on_node_complete
)
@@ -151,6 +162,28 @@ el.replaceWith(d);
folder = output_cfg if output_cfg not in [None, &#34;stdout&#34;] else None
prompt = options.get(&#34;prompt&#34;)
# Resolve and filter nodes through context-aware list_nodes
try:
if isinstance(nodelist, str):
resolved_nodes = self.app.services.nodes.list_nodes(nodelist)
elif isinstance(nodelist, list):
resolved_nodes = []
for item in nodelist:
matches = self.app.services.nodes.list_nodes(item)
for m in matches:
if m not in resolved_nodes:
resolved_nodes.append(m)
else:
resolved_nodes = []
except Exception:
resolved_nodes = []
if not resolved_nodes:
printer.error(f&#34;[{name}] No nodes found matching filter: {nodelist}&#34;)
sys.exit(11)
nodelist = resolved_nodes
try:
header_printed = False
if action == &#34;run&#34;:
@@ -242,6 +275,28 @@ el.replaceWith(d);
folder = output_cfg if output_cfg not in [None, &#34;stdout&#34;] else None
prompt = options.get(&#34;prompt&#34;)
# Resolve and filter nodes through context-aware list_nodes
try:
if isinstance(nodelist, str):
resolved_nodes = self.app.services.nodes.list_nodes(nodelist)
elif isinstance(nodelist, list):
resolved_nodes = []
for item in nodelist:
matches = self.app.services.nodes.list_nodes(item)
for m in matches:
if m not in resolved_nodes:
resolved_nodes.append(m)
else:
resolved_nodes = []
except Exception:
resolved_nodes = []
if not resolved_nodes:
printer.error(f&#34;[{name}] No nodes found matching filter: {nodelist}&#34;)
sys.exit(11)
nodelist = resolved_nodes
try:
header_printed = False
if action == &#34;run&#34;:
@@ -333,6 +388,17 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def node_run(self, args):
nodes_filter = args.data[0]
# Resolve and filter nodes through context-aware list_nodes
try:
matched_nodes = self.app.services.nodes.list_nodes(nodes_filter)
except Exception:
matched_nodes = []
if not matched_nodes:
printer.error(f&#34;No nodes found matching filter: {nodes_filter}&#34;)
sys.exit(2)
commands = [&#34; &#34;.join(args.data[1:])]
try:
@@ -349,7 +415,7 @@ el.replaceWith(d);
printer.test_panel(unique, node_output, node_status, node_result)
results = self.app.services.execution.test_commands(
nodes_filter=nodes_filter,
nodes_filter=matched_nodes,
commands=commands,
expected=args.test_expected,
on_node_complete=_on_node_complete
@@ -366,7 +432,7 @@ el.replaceWith(d);
printer.node_panel(unique, node_output, node_status)
results = self.app.services.execution.run_commands(
nodes_filter=nodes_filter,
nodes_filter=matched_nodes,
commands=commands,
on_node_complete=_on_node_complete
)
@@ -454,7 +520,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.sync_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>
@@ -427,7 +427,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.terminal_ui 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>
@@ -1017,7 +1017,7 @@ on_ai_call: async function(active_buffer, question) -&gt; result_dict</p></div>
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+522
View File
@@ -0,0 +1,522 @@
<!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.user_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.user_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.user_handler.UserHandler"><code class="flex name class">
<span>class <span class="ident">UserHandler</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 UserHandler:
def __init__(self, app):
self.app = app
def dispatch(self, args):
if self.app.services.mode == &#34;remote&#34;:
printer.error(&#34;User management commands are only available in local/server-side mode.&#34;)
sys.exit(1)
# Parse actions from argparse mutually exclusive options
if getattr(args, &#34;add&#34;, None):
args.action = &#34;add&#34;
args.username = args.add[0]
elif getattr(args, &#34;delete&#34;, None):
args.action = &#34;del&#34;
args.username = args.delete[0]
elif getattr(args, &#34;list&#34;, False):
args.action = &#34;list&#34;
elif getattr(args, &#34;show&#34;, None):
args.action = &#34;show&#34;
args.username = args.show[0]
elif getattr(args, &#34;regen_password&#34;, None):
args.action = &#34;regen_password&#34;
args.username = args.regen_password[0]
action = getattr(args, &#34;action&#34;, None)
if action == &#34;add&#34;:
return self.add_user(args)
elif action == &#34;del&#34;:
return self.delete_user(args)
elif action == &#34;list&#34;:
return self.list_users(args)
elif action == &#34;show&#34;:
return self.show_user(args)
elif action == &#34;regen_password&#34;:
return self.regen_password(args)
else:
printer.error(f&#34;Unknown action: {action}&#34;)
sys.exit(1)
def add_user(self, args):
username = getattr(args, &#34;username&#34;, None)
if not username:
printer.error(&#34;Username is required. Usage: connpy user --add &lt;username&gt;&#34;)
sys.exit(1)
custom_path = getattr(args, &#34;path&#34;, None)
if custom_path:
custom_path = custom_path[0] if isinstance(custom_path, list) else custom_path
try:
password = getpass.getpass(&#34;Enter password for new user: &#34;)
if not password:
printer.error(&#34;Password cannot be empty.&#34;)
sys.exit(1)
confirm = getpass.getpass(&#34;Confirm password: &#34;)
if password != confirm:
printer.error(&#34;Passwords do not match.&#34;)
sys.exit(1)
except (KeyboardInterrupt, EOFError):
printer.warning(&#34;\nOperation cancelled.&#34;)
sys.exit(130)
try:
self.app.services.users.create_user(username, password, config_path=custom_path)
printer.success(f&#34;User &#39;{username}&#39; created successfully.&#34;)
except ConnpyError as e:
printer.error(str(e))
sys.exit(1)
except ValueError as e:
printer.error(str(e))
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to create user: {e}&#34;)
sys.exit(1)
def delete_user(self, args):
username = getattr(args, &#34;username&#34;, None)
if not username:
printer.error(&#34;Username is required. Usage: connpy user --del &lt;username&gt;&#34;)
sys.exit(1)
try:
self.app.services.users.delete_user(username)
printer.success(f&#34;User &#39;{username}&#39; deleted successfully.&#34;)
except ConnpyError as e:
printer.error(str(e))
sys.exit(1)
except ValueError as e:
printer.error(str(e))
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to delete user: {e}&#34;)
sys.exit(1)
def list_users(self, args):
try:
users = self.app.services.users.list_users()
if not users:
printer.warning(&#34;No users registered.&#34;)
return
# Format custom config path, falling back to computed default path instead of null/None
formatted_users = []
for u in users:
formatted_u = u.copy()
if not formatted_u.get(&#34;config_path&#34;):
formatted_u[&#34;config_path&#34;] = os.path.join(self.app.services.users.users_dir, formatted_u[&#34;username&#34;])
formatted_users.append(formatted_u)
yaml_str = yaml.dump(formatted_users, sort_keys=False, default_flow_style=False)
printer.data(&#34;Registered Users&#34;, yaml_str)
except Exception as e:
printer.error(f&#34;Failed to list users: {e}&#34;)
sys.exit(1)
def show_user(self, args):
username = getattr(args, &#34;username&#34;, None)
if not username:
printer.error(&#34;Username is required. Usage: connpy user --show &lt;username&gt;&#34;)
sys.exit(1)
try:
user = self.app.services.users.get_user(username)
if not user:
printer.error(f&#34;User &#39;{username}&#39; not found.&#34;)
sys.exit(1)
# Hide the password hash from the CLI output for safety
safe_user = {k: v for k, v in user.items() if k != &#34;password_hash&#34;}
if not safe_user.get(&#34;config_path&#34;):
safe_user[&#34;config_path&#34;] = os.path.join(self.app.services.users.users_dir, username)
yaml_str = yaml.dump(safe_user, sort_keys=False, default_flow_style=False)
printer.data(f&#34;User: {username}&#34;, yaml_str)
except ValueError as e:
printer.error(str(e))
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to retrieve user details: {e}&#34;)
sys.exit(1)
def regen_password(self, args):
username = getattr(args, &#34;username&#34;, None)
if not username:
printer.error(&#34;Username is required. Usage: connpy user --regen-password &lt;username&gt;&#34;)
sys.exit(1)
try:
user = self.app.services.users.get_user(username)
if not user:
printer.error(f&#34;User &#39;{username}&#39; not found.&#34;)
sys.exit(1)
except ValueError as e:
printer.error(str(e))
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to retrieve user details: {e}&#34;)
sys.exit(1)
try:
new_password = getpass.getpass(&#34;Enter new password: &#34;)
if not new_password:
printer.error(&#34;Password cannot be empty.&#34;)
sys.exit(1)
confirm = getpass.getpass(&#34;Confirm new password: &#34;)
if new_password != confirm:
printer.error(&#34;Passwords do not match.&#34;)
sys.exit(1)
except (KeyboardInterrupt, EOFError):
printer.warning(&#34;\nOperation cancelled.&#34;)
sys.exit(130)
try:
self.app.services.users.admin_change_password(username, new_password)
printer.success(f&#34;Password for user &#39;{username}&#39; regenerated successfully.&#34;)
except ValueError as e:
printer.error(str(e))
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to regenerate password: {e}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
<h3>Methods</h3>
<dl>
<dt id="connpy.cli.user_handler.UserHandler.add_user"><code class="name flex">
<span>def <span class="ident">add_user</span></span>(<span>self, args)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def add_user(self, args):
username = getattr(args, &#34;username&#34;, None)
if not username:
printer.error(&#34;Username is required. Usage: connpy user --add &lt;username&gt;&#34;)
sys.exit(1)
custom_path = getattr(args, &#34;path&#34;, None)
if custom_path:
custom_path = custom_path[0] if isinstance(custom_path, list) else custom_path
try:
password = getpass.getpass(&#34;Enter password for new user: &#34;)
if not password:
printer.error(&#34;Password cannot be empty.&#34;)
sys.exit(1)
confirm = getpass.getpass(&#34;Confirm password: &#34;)
if password != confirm:
printer.error(&#34;Passwords do not match.&#34;)
sys.exit(1)
except (KeyboardInterrupt, EOFError):
printer.warning(&#34;\nOperation cancelled.&#34;)
sys.exit(130)
try:
self.app.services.users.create_user(username, password, config_path=custom_path)
printer.success(f&#34;User &#39;{username}&#39; created successfully.&#34;)
except ConnpyError as e:
printer.error(str(e))
sys.exit(1)
except ValueError as e:
printer.error(str(e))
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to create user: {e}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.user_handler.UserHandler.delete_user"><code class="name flex">
<span>def <span class="ident">delete_user</span></span>(<span>self, args)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def delete_user(self, args):
username = getattr(args, &#34;username&#34;, None)
if not username:
printer.error(&#34;Username is required. Usage: connpy user --del &lt;username&gt;&#34;)
sys.exit(1)
try:
self.app.services.users.delete_user(username)
printer.success(f&#34;User &#39;{username}&#39; deleted successfully.&#34;)
except ConnpyError as e:
printer.error(str(e))
sys.exit(1)
except ValueError as e:
printer.error(str(e))
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to delete user: {e}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.user_handler.UserHandler.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):
if self.app.services.mode == &#34;remote&#34;:
printer.error(&#34;User management commands are only available in local/server-side mode.&#34;)
sys.exit(1)
# Parse actions from argparse mutually exclusive options
if getattr(args, &#34;add&#34;, None):
args.action = &#34;add&#34;
args.username = args.add[0]
elif getattr(args, &#34;delete&#34;, None):
args.action = &#34;del&#34;
args.username = args.delete[0]
elif getattr(args, &#34;list&#34;, False):
args.action = &#34;list&#34;
elif getattr(args, &#34;show&#34;, None):
args.action = &#34;show&#34;
args.username = args.show[0]
elif getattr(args, &#34;regen_password&#34;, None):
args.action = &#34;regen_password&#34;
args.username = args.regen_password[0]
action = getattr(args, &#34;action&#34;, None)
if action == &#34;add&#34;:
return self.add_user(args)
elif action == &#34;del&#34;:
return self.delete_user(args)
elif action == &#34;list&#34;:
return self.list_users(args)
elif action == &#34;show&#34;:
return self.show_user(args)
elif action == &#34;regen_password&#34;:
return self.regen_password(args)
else:
printer.error(f&#34;Unknown action: {action}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.user_handler.UserHandler.list_users"><code class="name flex">
<span>def <span class="ident">list_users</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_users(self, args):
try:
users = self.app.services.users.list_users()
if not users:
printer.warning(&#34;No users registered.&#34;)
return
# Format custom config path, falling back to computed default path instead of null/None
formatted_users = []
for u in users:
formatted_u = u.copy()
if not formatted_u.get(&#34;config_path&#34;):
formatted_u[&#34;config_path&#34;] = os.path.join(self.app.services.users.users_dir, formatted_u[&#34;username&#34;])
formatted_users.append(formatted_u)
yaml_str = yaml.dump(formatted_users, sort_keys=False, default_flow_style=False)
printer.data(&#34;Registered Users&#34;, yaml_str)
except Exception as e:
printer.error(f&#34;Failed to list users: {e}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.user_handler.UserHandler.regen_password"><code class="name flex">
<span>def <span class="ident">regen_password</span></span>(<span>self, args)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def regen_password(self, args):
username = getattr(args, &#34;username&#34;, None)
if not username:
printer.error(&#34;Username is required. Usage: connpy user --regen-password &lt;username&gt;&#34;)
sys.exit(1)
try:
user = self.app.services.users.get_user(username)
if not user:
printer.error(f&#34;User &#39;{username}&#39; not found.&#34;)
sys.exit(1)
except ValueError as e:
printer.error(str(e))
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to retrieve user details: {e}&#34;)
sys.exit(1)
try:
new_password = getpass.getpass(&#34;Enter new password: &#34;)
if not new_password:
printer.error(&#34;Password cannot be empty.&#34;)
sys.exit(1)
confirm = getpass.getpass(&#34;Confirm new password: &#34;)
if new_password != confirm:
printer.error(&#34;Passwords do not match.&#34;)
sys.exit(1)
except (KeyboardInterrupt, EOFError):
printer.warning(&#34;\nOperation cancelled.&#34;)
sys.exit(130)
try:
self.app.services.users.admin_change_password(username, new_password)
printer.success(f&#34;Password for user &#39;{username}&#39; regenerated successfully.&#34;)
except ValueError as e:
printer.error(str(e))
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to regenerate password: {e}&#34;)
sys.exit(1)</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.cli.user_handler.UserHandler.show_user"><code class="name flex">
<span>def <span class="ident">show_user</span></span>(<span>self, args)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def show_user(self, args):
username = getattr(args, &#34;username&#34;, None)
if not username:
printer.error(&#34;Username is required. Usage: connpy user --show &lt;username&gt;&#34;)
sys.exit(1)
try:
user = self.app.services.users.get_user(username)
if not user:
printer.error(f&#34;User &#39;{username}&#39; not found.&#34;)
sys.exit(1)
# Hide the password hash from the CLI output for safety
safe_user = {k: v for k, v in user.items() if k != &#34;password_hash&#34;}
if not safe_user.get(&#34;config_path&#34;):
safe_user[&#34;config_path&#34;] = os.path.join(self.app.services.users.users_dir, username)
yaml_str = yaml.dump(safe_user, sort_keys=False, default_flow_style=False)
printer.data(f&#34;User: {username}&#34;, yaml_str)
except ValueError as e:
printer.error(str(e))
sys.exit(1)
except Exception as e:
printer.error(f&#34;Failed to retrieve user details: {e}&#34;)
sys.exit(1)</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.user_handler.UserHandler" href="#connpy.cli.user_handler.UserHandler">UserHandler</a></code></h4>
<ul class="two-column">
<li><code><a title="connpy.cli.user_handler.UserHandler.add_user" href="#connpy.cli.user_handler.UserHandler.add_user">add_user</a></code></li>
<li><code><a title="connpy.cli.user_handler.UserHandler.delete_user" href="#connpy.cli.user_handler.UserHandler.delete_user">delete_user</a></code></li>
<li><code><a title="connpy.cli.user_handler.UserHandler.dispatch" href="#connpy.cli.user_handler.UserHandler.dispatch">dispatch</a></code></li>
<li><code><a title="connpy.cli.user_handler.UserHandler.list_users" href="#connpy.cli.user_handler.UserHandler.list_users">list_users</a></code></li>
<li><code><a title="connpy.cli.user_handler.UserHandler.regen_password" href="#connpy.cli.user_handler.UserHandler.regen_password">regen_password</a></code></li>
<li><code><a title="connpy.cli.user_handler.UserHandler.show_user" href="#connpy.cli.user_handler.UserHandler.show_user">show_user</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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.cli.validators 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>
@@ -508,7 +508,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.grpc_layer.connpy_pb2 API documentation</title>
<meta name="description" content="Generated protocol buffer code.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -61,7 +61,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+293 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.grpc_layer.connpy_pb2_grpc API documentation</title>
<meta name="description" content="Client and server classes corresponding to protobuf-defined services.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -108,6 +108,34 @@ el.replaceWith(d);
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.add_AuthServiceServicer_to_server"><code class="name flex">
<span>def <span class="ident">add_AuthServiceServicer_to_server</span></span>(<span>servicer, server)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def add_AuthServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
&#39;login&#39;: grpc.unary_unary_rpc_method_handler(
servicer.login,
request_deserializer=connpy__pb2.LoginRequest.FromString,
response_serializer=connpy__pb2.LoginResponse.SerializeToString,
),
&#39;change_password&#39;: grpc.unary_unary_rpc_method_handler(
servicer.change_password,
request_deserializer=connpy__pb2.ChangePasswordRequest.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)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers(&#39;connpy.AuthService&#39;, rpc_method_handlers)</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.add_ConfigServiceServicer_to_server"><code class="name flex">
<span>def <span class="ident">add_ConfigServiceServicer_to_server</span></span>(<span>servicer, server)</span>
</code></dt>
@@ -1341,6 +1369,251 @@ def load_session_data(request,
<dd>A grpc.Channel.</dd>
</dl></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthService"><code class="flex name class">
<span>class <span class="ident">AuthService</span></span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class AuthService(object):
&#34;&#34;&#34;Missing associated documentation comment in .proto file.&#34;&#34;&#34;
@staticmethod
def login(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/login&#39;,
connpy__pb2.LoginRequest.SerializeToString,
connpy__pb2.LoginResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def change_password(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/change_password&#39;,
connpy__pb2.ChangePasswordRequest.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>
<h3>Static methods</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthService.change_password"><code class="name flex">
<span>def <span class="ident">change_password</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 change_password(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/change_password&#39;,
connpy__pb2.ChangePasswordRequest.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>
<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>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@staticmethod
def login(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/login&#39;,
connpy__pb2.LoginRequest.SerializeToString,
connpy__pb2.LoginResponse.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">
<span>class <span class="ident">AuthServiceServicer</span></span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class AuthServiceServicer(object):
&#34;&#34;&#34;Missing associated documentation comment in .proto file.&#34;&#34;&#34;
def login(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 change_password(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>
<h3>Subclasses</h3>
<ul class="hlist">
<li><a title="connpy.grpc_layer.server.AuthServicer" href="server.html#connpy.grpc_layer.server.AuthServicer">AuthServicer</a></li>
</ul>
<h3>Methods</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.change_password"><code class="name flex">
<span>def <span class="ident">change_password</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 change_password(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>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def login(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">
<span>class <span class="ident">AuthServiceStub</span></span>
<span>(</span><span>channel)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class AuthServiceStub(object):
&#34;&#34;&#34;Missing associated documentation comment in .proto file.&#34;&#34;&#34;
def __init__(self, channel):
&#34;&#34;&#34;Constructor.
Args:
channel: A grpc.Channel.
&#34;&#34;&#34;
self.login = channel.unary_unary(
&#39;/connpy.AuthService/login&#39;,
request_serializer=connpy__pb2.LoginRequest.SerializeToString,
response_deserializer=connpy__pb2.LoginResponse.FromString,
_registered_method=True)
self.change_password = channel.unary_unary(
&#39;/connpy.AuthService/change_password&#39;,
request_serializer=connpy__pb2.ChangePasswordRequest.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>
<p>Constructor.</p>
<h2 id="args">Args</h2>
<dl>
<dt><strong><code>channel</code></strong></dt>
<dd>A grpc.Channel.</dd>
</dl></div>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2_grpc.ConfigService"><code class="flex name class">
<span>class <span class="ident">ConfigService</span></span>
</code></dt>
@@ -5802,6 +6075,7 @@ def stop_api(request,
<li><h3><a href="#header-functions">Functions</a></h3>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.add_AIServiceServicer_to_server" href="#connpy.grpc_layer.connpy_pb2_grpc.add_AIServiceServicer_to_server">add_AIServiceServicer_to_server</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.add_AuthServiceServicer_to_server" href="#connpy.grpc_layer.connpy_pb2_grpc.add_AuthServiceServicer_to_server">add_AuthServiceServicer_to_server</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.add_ConfigServiceServicer_to_server" href="#connpy.grpc_layer.connpy_pb2_grpc.add_ConfigServiceServicer_to_server">add_ConfigServiceServicer_to_server</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.add_ExecutionServiceServicer_to_server" href="#connpy.grpc_layer.connpy_pb2_grpc.add_ExecutionServiceServicer_to_server">add_ExecutionServiceServicer_to_server</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.add_ImportExportServiceServicer_to_server" href="#connpy.grpc_layer.connpy_pb2_grpc.add_ImportExportServiceServicer_to_server">add_ImportExportServiceServicer_to_server</a></code></li>
@@ -5845,6 +6119,23 @@ def stop_api(request,
<h4><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceStub" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceStub">AIServiceStub</a></code></h4>
</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="">
<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.login" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthService.login">login</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="">
<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.login" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceServicer.login">login</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AuthServiceStub" href="#connpy.grpc_layer.connpy_pb2_grpc.AuthServiceStub">AuthServiceStub</a></code></h4>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ConfigService" href="#connpy.grpc_layer.connpy_pb2_grpc.ConfigService">ConfigService</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ConfigService.apply_theme_from_file" href="#connpy.grpc_layer.connpy_pb2_grpc.ConfigService.apply_theme_from_file">apply_theme_from_file</a></code></li>
@@ -6029,7 +6320,7 @@ def stop_api(request,
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+7 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.grpc_layer API documentation</title>
<meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -64,6 +64,10 @@ el.replaceWith(d);
<dd>
<div class="desc"></div>
</dd>
<dt><code class="name"><a title="connpy.grpc_layer.user_registry" href="user_registry.html">connpy.grpc_layer.user_registry</a></code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt><code class="name"><a title="connpy.grpc_layer.utils" href="utils.html">connpy.grpc_layer.utils</a></code></dt>
<dd>
<div class="desc"></div>
@@ -95,6 +99,7 @@ el.replaceWith(d);
<li><code><a title="connpy.grpc_layer.remote_plugin_pb2_grpc" href="remote_plugin_pb2_grpc.html">connpy.grpc_layer.remote_plugin_pb2_grpc</a></code></li>
<li><code><a title="connpy.grpc_layer.server" href="server.html">connpy.grpc_layer.server</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs" href="stubs.html">connpy.grpc_layer.stubs</a></code></li>
<li><code><a title="connpy.grpc_layer.user_registry" href="user_registry.html">connpy.grpc_layer.user_registry</a></code></li>
<li><code><a title="connpy.grpc_layer.utils" href="utils.html">connpy.grpc_layer.utils</a></code></li>
</ul>
</li>
@@ -102,7 +107,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.grpc_layer.remote_plugin_pb2 API documentation</title>
<meta name="description" content="Generated protocol buffer code.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -62,7 +62,7 @@ el.replaceWith(d);
<dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.IdRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"><p>The type of the None singleton.</p></div>
<div class="desc"></div>
</dd>
</dl>
</dd>
@@ -81,7 +81,7 @@ el.replaceWith(d);
<dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.OutputChunk.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"><p>The type of the None singleton.</p></div>
<div class="desc"></div>
</dd>
</dl>
</dd>
@@ -100,7 +100,7 @@ el.replaceWith(d);
<dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"><p>The type of the None singleton.</p></div>
<div class="desc"></div>
</dd>
</dl>
</dd>
@@ -119,7 +119,7 @@ el.replaceWith(d);
<dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.StringResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"><p>The type of the None singleton.</p></div>
<div class="desc"></div>
</dd>
</dl>
</dd>
@@ -168,7 +168,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.grpc_layer.remote_plugin_pb2_grpc API documentation</title>
<meta name="description" content="Client and server classes corresponding to protobuf-defined services.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -366,7 +366,7 @@ def invoke_plugin(request,
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
File diff suppressed because it is too large Load Diff
+325 -14
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.grpc_layer.stubs 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>
@@ -272,9 +272,6 @@ el.replaceWith(d);
from ..printer import connpy_theme, get_original_stdout
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
stable_console.print(Rule(style=alias))
elif not full_content and final_result.get(&#34;response&#34;):
# If nothing streamed but we have response (e.g. error or direct guide)
printer.console.print(Panel(Markdown(final_result[&#34;response&#34;]), title=title, border_style=alias, expand=False))
break
except Exception as e:
# Check if it was a gRPC error that we should let handle_errors catch
@@ -517,9 +514,6 @@ def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debu
from ..printer import connpy_theme, get_original_stdout
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
stable_console.print(Rule(style=alias))
elif not full_content and final_result.get(&#34;response&#34;):
# If nothing streamed but we have response (e.g. error or direct guide)
printer.console.print(Panel(Markdown(final_result[&#34;response&#34;]), title=title, border_style=alias, expand=False))
break
except Exception as e:
# Check if it was a gRPC error that we should let handle_errors catch
@@ -652,6 +646,303 @@ def load_session_data(self, session_id):
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.stubs.AuthClientInterceptor"><code class="flex name class">
<span>class <span class="ident">AuthClientInterceptor</span></span>
<span>(</span><span>token_provider)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class AuthClientInterceptor(grpc.UnaryUnaryClientInterceptor,
grpc.UnaryStreamClientInterceptor,
grpc.StreamUnaryClientInterceptor,
grpc.StreamStreamClientInterceptor):
def __init__(self, token_provider):
self.token_provider = token_provider
def _add_metadata(self, client_call_details):
token = self.token_provider()
if not token:
return client_call_details
metadata = []
if client_call_details.metadata:
metadata = list(client_call_details.metadata)
# Check if already present to avoid duplicates
if not any(k.lower() == &#34;authorization&#34; for k, v in metadata):
metadata.append((&#34;authorization&#34;, f&#34;Bearer {token}&#34;))
return _ClientCallDetails(
method=client_call_details.method,
timeout=client_call_details.timeout,
metadata=metadata,
credentials=client_call_details.credentials,
wait_for_ready=client_call_details.wait_for_ready,
compression=client_call_details.compression,
)
def intercept_unary_unary(self, continuation, client_call_details, request):
new_details = self._add_metadata(client_call_details)
return continuation(new_details, request)
def intercept_unary_stream(self, continuation, client_call_details, request):
new_details = self._add_metadata(client_call_details)
return continuation(new_details, request)
def intercept_stream_unary(self, continuation, client_call_details, request_iterator):
new_details = self._add_metadata(client_call_details)
return continuation(new_details, request_iterator)
def intercept_stream_stream(self, continuation, client_call_details, request_iterator):
new_details = self._add_metadata(client_call_details)
return continuation(new_details, request_iterator)</code></pre>
</details>
<div class="desc"><p>Affords intercepting unary-unary invocations.</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>grpc.UnaryUnaryClientInterceptor</li>
<li>grpc.UnaryStreamClientInterceptor</li>
<li>grpc.StreamUnaryClientInterceptor</li>
<li>grpc.StreamStreamClientInterceptor</li>
<li>abc.ABC</li>
</ul>
<h3>Methods</h3>
<dl>
<dt id="connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_stream_stream"><code class="name flex">
<span>def <span class="ident">intercept_stream_stream</span></span>(<span>self, continuation, client_call_details, request_iterator)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def intercept_stream_stream(self, continuation, client_call_details, request_iterator):
new_details = self._add_metadata(client_call_details)
return continuation(new_details, request_iterator)</code></pre>
</details>
<div class="desc"><p>Intercepts a stream-stream invocation.</p>
<h2 id="args">Args</h2>
<dl>
<dt><strong><code>continuation</code></strong></dt>
<dd>A function that proceeds with the invocation by
executing the next interceptor in chain or invoking the
actual RPC on the underlying Channel. It is the interceptor's
responsibility to call it if it decides to move the RPC forward.
The interceptor can use
<code>response_iterator = continuation(client_call_details, request_iterator)</code>
to continue with the RPC. <code>continuation</code> returns an object that is
both a Call for the RPC and an iterator for response values.
Drawing response values from the returned Call-iterator may
raise RpcError indicating termination of the RPC with non-OK
status.</dd>
<dt><strong><code>client_call_details</code></strong></dt>
<dd>A ClientCallDetails object describing the
outgoing RPC.</dd>
<dt><strong><code>request_iterator</code></strong></dt>
<dd>An iterator that yields request values for the RPC.</dd>
</dl>
<h2 id="returns">Returns</h2>
<p>An object that is both a Call for the RPC and an iterator of
response values. Drawing response values from the returned
Call-iterator may raise RpcError indicating termination of
the RPC with non-OK status. This object <em>should</em> also fulfill the
Future interface, though it may not.</p></div>
</dd>
<dt id="connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_stream_unary"><code class="name flex">
<span>def <span class="ident">intercept_stream_unary</span></span>(<span>self, continuation, client_call_details, request_iterator)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def intercept_stream_unary(self, continuation, client_call_details, request_iterator):
new_details = self._add_metadata(client_call_details)
return continuation(new_details, request_iterator)</code></pre>
</details>
<div class="desc"><p>Intercepts a stream-unary invocation asynchronously.</p>
<h2 id="args">Args</h2>
<dl>
<dt><strong><code>continuation</code></strong></dt>
<dd>A function that proceeds with the invocation by
executing the next interceptor in chain or invoking the
actual RPC on the underlying Channel. It is the interceptor's
responsibility to call it if it decides to move the RPC forward.
The interceptor can use
<code>response_future = continuation(client_call_details, request_iterator)</code>
to continue with the RPC. <code>continuation</code> returns an object that is
both a Call for the RPC and a Future. In the event of RPC completion,
the return Call-Future's result value will be the response message
of the RPC. Should the event terminate with non-OK status, the
returned Call-Future's exception value will be an RpcError.</dd>
<dt><strong><code>client_call_details</code></strong></dt>
<dd>A ClientCallDetails object describing the
outgoing RPC.</dd>
<dt><strong><code>request_iterator</code></strong></dt>
<dd>An iterator that yields request values for the RPC.</dd>
</dl>
<h2 id="returns">Returns</h2>
<p>An object that is both a Call for the RPC and a Future.
In the event of RPC completion, the return Call-Future's
result value will be the response message of the RPC.
Should the event terminate with non-OK status, the returned
Call-Future's exception value will be an RpcError.</p></div>
</dd>
<dt id="connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_unary_stream"><code class="name flex">
<span>def <span class="ident">intercept_unary_stream</span></span>(<span>self, continuation, client_call_details, request)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def intercept_unary_stream(self, continuation, client_call_details, request):
new_details = self._add_metadata(client_call_details)
return continuation(new_details, request)</code></pre>
</details>
<div class="desc"><p>Intercepts a unary-stream invocation.</p>
<h2 id="args">Args</h2>
<dl>
<dt><strong><code>continuation</code></strong></dt>
<dd>A function that proceeds with the invocation by
executing the next interceptor in chain or invoking the
actual RPC on the underlying Channel. It is the interceptor's
responsibility to call it if it decides to move the RPC forward.
The interceptor can use
<code>response_iterator = continuation(client_call_details, request)</code>
to continue with the RPC. <code>continuation</code> returns an object that is
both a Call for the RPC and an iterator for response values.
Drawing response values from the returned Call-iterator may
raise RpcError indicating termination of the RPC with non-OK
status.</dd>
<dt><strong><code>client_call_details</code></strong></dt>
<dd>A ClientCallDetails object describing the
outgoing RPC.</dd>
<dt><strong><code>request</code></strong></dt>
<dd>The request value for the RPC.</dd>
</dl>
<h2 id="returns">Returns</h2>
<p>An object that is both a Call for the RPC and an iterator of
response values. Drawing response values from the returned
Call-iterator may raise RpcError indicating termination of
the RPC with non-OK status. This object <em>should</em> also fulfill the
Future interface, though it may not.</p></div>
</dd>
<dt id="connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_unary_unary"><code class="name flex">
<span>def <span class="ident">intercept_unary_unary</span></span>(<span>self, continuation, client_call_details, request)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def intercept_unary_unary(self, continuation, client_call_details, request):
new_details = self._add_metadata(client_call_details)
return continuation(new_details, request)</code></pre>
</details>
<div class="desc"><p>Intercepts a unary-unary invocation asynchronously.</p>
<h2 id="args">Args</h2>
<dl>
<dt><strong><code>continuation</code></strong></dt>
<dd>A function that proceeds with the invocation by
executing the next interceptor in chain or invoking the
actual RPC on the underlying Channel. It is the interceptor's
responsibility to call it if it decides to move the RPC forward.
The interceptor can use
<code>response_future = continuation(client_call_details, request)</code>
to continue with the RPC. <code>continuation</code> returns an object that is
both a Call for the RPC and a Future. In the event of RPC
completion, the return Call-Future's result value will be
the response message of the RPC. Should the event terminate
with non-OK status, the returned Call-Future's exception value
will be an RpcError.</dd>
<dt><strong><code>client_call_details</code></strong></dt>
<dd>A ClientCallDetails object describing the
outgoing RPC.</dd>
<dt><strong><code>request</code></strong></dt>
<dd>The request value for the RPC.</dd>
</dl>
<h2 id="returns">Returns</h2>
<p>An object that is both a Call for the RPC and a Future.
In the event of RPC completion, the return Call-Future's
result value will be the response message of the RPC.
Should the event terminate with non-OK status, the returned
Call-Future's exception value will be an RpcError.</p></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.stubs.AuthStub"><code class="flex name class">
<span>class <span class="ident">AuthStub</span></span>
<span>(</span><span>channel, remote_host)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class AuthStub:
def __init__(self, channel, remote_host):
self.stub = connpy_pb2_grpc.AuthServiceStub(channel)
self.remote_host = remote_host
@handle_errors
def login(self, username, password):
req = connpy_pb2.LoginRequest(username=username, password=password)
resp = self.stub.login(req)
return {
&#34;token&#34;: resp.token,
&#34;username&#34;: resp.username,
&#34;expires_at&#34;: resp.expires_at
}
@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>
</details>
<div class="desc"></div>
<h3>Methods</h3>
<dl>
<dt id="connpy.grpc_layer.stubs.AuthStub.change_password"><code class="name flex">
<span>def <span class="ident">change_password</span></span>(<span>self, old_password, new_password)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@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>
</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>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@handle_errors
def login(self, username, password):
req = connpy_pb2.LoginRequest(username=username, password=password)
resp = self.stub.login(req)
return {
&#34;token&#34;: resp.token,
&#34;username&#34;: resp.username,
&#34;expires_at&#34;: resp.expires_at
}</code></pre>
</details>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.stubs.ConfigStub"><code class="flex name class">
<span>class <span class="ident">ConfigStub</span></span>
<span>(</span><span>channel, remote_host)</span>
@@ -1467,15 +1758,17 @@ def set_reserved_names(self, names):
self._trigger_local_cache_sync()
@handle_errors
def update_node(self, unique_id, data):
def update_node(self, unique_id, data, save=True):
req = connpy_pb2.NodeRequest(id=unique_id, data=to_struct(data), is_folder=False)
self.stub.update_node(req)
if save:
self._trigger_local_cache_sync()
@handle_errors
def delete_node(self, unique_id, is_folder=False):
def delete_node(self, unique_id, is_folder=False, save=True):
req = connpy_pb2.DeleteRequest(id=unique_id, is_folder=is_folder)
self.stub.delete_node(req)
if save:
self._trigger_local_cache_sync()
@handle_errors
@@ -1857,7 +2150,7 @@ def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.stubs.NodeStub.delete_node"><code class="name flex">
<span>def <span class="ident">delete_node</span></span>(<span>self, unique_id, is_folder=False)</span>
<span>def <span class="ident">delete_node</span></span>(<span>self, unique_id, is_folder=False, save=True)</span>
</code></dt>
<dd>
<details class="source">
@@ -1865,9 +2158,10 @@ def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
<span>Expand source code</span>
</summary>
<pre><code class="python">@handle_errors
def delete_node(self, unique_id, is_folder=False):
def delete_node(self, unique_id, is_folder=False, save=True):
req = connpy_pb2.DeleteRequest(id=unique_id, is_folder=is_folder)
self.stub.delete_node(req)
if save:
self._trigger_local_cache_sync()</code></pre>
</details>
<div class="desc"></div>
@@ -2028,7 +2322,7 @@ def set_reserved_names(self, names):
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.stubs.NodeStub.update_node"><code class="name flex">
<span>def <span class="ident">update_node</span></span>(<span>self, unique_id, data)</span>
<span>def <span class="ident">update_node</span></span>(<span>self, unique_id, data, save=True)</span>
</code></dt>
<dd>
<details class="source">
@@ -2036,9 +2330,10 @@ def set_reserved_names(self, names):
<span>Expand source code</span>
</summary>
<pre><code class="python">@handle_errors
def update_node(self, unique_id, data):
def update_node(self, unique_id, data, save=True):
req = connpy_pb2.NodeRequest(id=unique_id, data=to_struct(data), is_folder=False)
self.stub.update_node(req)
if save:
self._trigger_local_cache_sync()</code></pre>
</details>
<div class="desc"></div>
@@ -2532,6 +2827,22 @@ def stop_api(self):
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.stubs.AuthClientInterceptor" href="#connpy.grpc_layer.stubs.AuthClientInterceptor">AuthClientInterceptor</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_stream_stream" href="#connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_stream_stream">intercept_stream_stream</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_stream_unary" href="#connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_stream_unary">intercept_stream_unary</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_unary_stream" href="#connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_unary_stream">intercept_unary_stream</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_unary_unary" href="#connpy.grpc_layer.stubs.AuthClientInterceptor.intercept_unary_unary">intercept_unary_unary</a></code></li>
</ul>
</li>
<li>
<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.login" href="#connpy.grpc_layer.stubs.AuthStub.login">login</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.stubs.ConfigStub" href="#connpy.grpc_layer.stubs.ConfigStub">ConfigStub</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.stubs.ConfigStub.encrypt_password" href="#connpy.grpc_layer.stubs.ConfigStub.encrypt_password">encrypt_password</a></code></li>
@@ -2618,7 +2929,7 @@ def stop_api(self):
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+295
View File
@@ -0,0 +1,295 @@
<!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.grpc_layer.user_registry API documentation</title>
<meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/typography.min.css" integrity="sha512-Y1DYSb995BAfxobCkKepB1BqJJTPrOp3zPL74AWFugHHmmdcvO+C48WLrUOlhGMc0QG7AE3f7gmvvcrmX2fDoA==" crossorigin>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" crossorigin>
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:1.5em;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:2em 0 .50em 0}h3{font-size:1.4em;margin:1.6em 0 .7em 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .2s ease-in-out}a:visited{color:#503}a:hover{color:#b62}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900;font-weight:bold}pre code{font-size:.8em;line-height:1.4em;padding:1em;display:block}code{background:#f3f3f3;font-family:"DejaVu Sans Mono",monospace;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source > summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible;min-width:max-content}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em 1em;margin:1em 0}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul ul{padding-left:1em}.toc > ul > li{margin-top:.5em}}</style>
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin></script>
<script>window.addEventListener('DOMContentLoaded', () => {
hljs.configure({languages: ['bash', 'css', 'diff', 'graphql', 'ini', 'javascript', 'json', 'plaintext', 'python', 'python-repl', 'rust', 'shell', 'sql', 'typescript', 'xml', 'yaml']});
hljs.highlightAll();
/* Collapse source docstrings */
setTimeout(() => {
[...document.querySelectorAll('.hljs.language-python > .hljs-string')]
.filter(el => el.innerHTML.length > 200 && ['"""', "'''"].includes(el.innerHTML.substring(0, 3)))
.forEach(el => {
let d = document.createElement('details');
d.classList.add('hljs-string');
d.innerHTML = '<summary>"""</summary>' + el.innerHTML.substring(3);
el.replaceWith(d);
});
}, 100);
})</script>
</head>
<body>
<main>
<article id="content">
<header>
<h1 class="title">Module <code>connpy.grpc_layer.user_registry</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.grpc_layer.user_registry.UserRegistry"><code class="flex name class">
<span>class <span class="ident">UserRegistry</span></span>
<span>(</span><span>server_config_dir)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class UserRegistry:
&#34;&#34;&#34;Holds per-user ServiceProviders in memory, thread-safe with hot-reloading.&#34;&#34;&#34;
def __init__(self, server_config_dir):
self.server_config_dir = os.path.abspath(server_config_dir)
self.user_service = UserService(self.server_config_dir)
self._providers = {} # username → ServiceProvider
self._mtimes = {} # username → last loaded mtime (float)
self._lock = threading.Lock()
# Load shared/global config
self._shared_conf_file = os.path.join(self.server_config_dir, &#34;config.yaml&#34;)
if os.path.exists(self._shared_conf_file):
self._shared_config = configfile(conf=self._shared_conf_file)
self._shared_mtime = os.path.getmtime(self._shared_conf_file)
else:
self._shared_config = None
self._shared_mtime = 0.0
def _refresh_shared(self):
&#34;&#34;&#34;Hot-reload shared config if the file changed on disk.&#34;&#34;&#34;
if not os.path.exists(self._shared_conf_file):
return
current_mtime = os.path.getmtime(self._shared_conf_file)
if current_mtime &gt; self._shared_mtime:
try:
self._shared_config = configfile(conf=self._shared_conf_file)
self._shared_mtime = current_mtime
# Clear all user providers so they pick up the new shared config
self._providers.clear()
self._mtimes.clear()
except Exception as e:
from connpy import printer
printer.warning(f&#34;Failed to reload shared config: {e}&#34;)
def get_provider(self, username) -&gt; ServiceProvider:
&#34;&#34;&#34;Get, lazy-load, or hot-reload a user&#39;s full ServiceProvider.&#34;&#34;&#34;
with self._lock:
# Refresh shared/global config if it has changed
self._refresh_shared()
# 1. Resolve physical path of the user&#39;s config.yaml file
user_data = self.user_service.get_user(username)
config_path = user_data.get(&#34;config_path&#34;)
if config_path:
conf_file = os.path.join(config_path, &#34;config.yaml&#34;)
else:
conf_file = os.path.join(self.server_config_dir, &#34;users&#34;, username, &#34;config.yaml&#34;)
# 2. Retrieve actual modification time in disk
current_mtime = os.path.getmtime(conf_file) if os.path.exists(conf_file) else 0.0
# 3. Validate if initial load or hot-reload is required
if username not in self._providers or self._mtimes.get(username, 0.0) &lt; current_mtime:
old_provider = self._providers.get(username)
try:
# Attempt a fresh configuration load
config = configfile(conf=conf_file, shared_config=self._shared_config)
new_provider = ServiceProvider(config, mode=&#34;local&#34;)
# Successfully loaded, clean up the old provider
if old_provider:
self._providers.pop(username, None)
if hasattr(old_provider, &#34;close&#34;):
try:
old_provider.close()
except Exception:
pass
self._providers[username] = new_provider
self._mtimes[username] = current_mtime
except Exception as e:
# Log warning but fallback to the old stable provider in memory if available
from connpy import printer
printer.warning(f&#34;Failed to hot-reload config for user &#39;{username}&#39; (file may be corrupt/incomplete): {e}&#34;)
if old_provider:
# Keep serving with the old cached instance to ensure service continuity
self._mtimes[username] = current_mtime
else:
# No fallback exists, propagate the exception
raise e
return self._providers[username]
def has_users(self) -&gt; bool:
&#34;&#34;&#34;Check if any users are registered (enables auth enforcement).&#34;&#34;&#34;
return bool(self.user_service.list_users())
def evict(self, username):
&#34;&#34;&#34;Remove and cleanly shut down cached provider (after delete or password change).&#34;&#34;&#34;
with self._lock:
provider = self._providers.pop(username, None)
self._mtimes.pop(username, None)
if provider:
# Explicit cleanup of user-scoped resources if custom close/cleanup exists
if hasattr(provider, &#34;close&#34;):
try:
provider.close()
except Exception:
pass</code></pre>
</details>
<div class="desc"><p>Holds per-user ServiceProviders in memory, thread-safe with hot-reloading.</p></div>
<h3>Methods</h3>
<dl>
<dt id="connpy.grpc_layer.user_registry.UserRegistry.evict"><code class="name flex">
<span>def <span class="ident">evict</span></span>(<span>self, username)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def evict(self, username):
&#34;&#34;&#34;Remove and cleanly shut down cached provider (after delete or password change).&#34;&#34;&#34;
with self._lock:
provider = self._providers.pop(username, None)
self._mtimes.pop(username, None)
if provider:
# Explicit cleanup of user-scoped resources if custom close/cleanup exists
if hasattr(provider, &#34;close&#34;):
try:
provider.close()
except Exception:
pass</code></pre>
</details>
<div class="desc"><p>Remove and cleanly shut down cached provider (after delete or password change).</p></div>
</dd>
<dt id="connpy.grpc_layer.user_registry.UserRegistry.get_provider"><code class="name flex">
<span>def <span class="ident">get_provider</span></span>(<span>self, username) > <a title="connpy.services.provider.ServiceProvider" href="../services/provider.html#connpy.services.provider.ServiceProvider">ServiceProvider</a></span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def get_provider(self, username) -&gt; ServiceProvider:
&#34;&#34;&#34;Get, lazy-load, or hot-reload a user&#39;s full ServiceProvider.&#34;&#34;&#34;
with self._lock:
# Refresh shared/global config if it has changed
self._refresh_shared()
# 1. Resolve physical path of the user&#39;s config.yaml file
user_data = self.user_service.get_user(username)
config_path = user_data.get(&#34;config_path&#34;)
if config_path:
conf_file = os.path.join(config_path, &#34;config.yaml&#34;)
else:
conf_file = os.path.join(self.server_config_dir, &#34;users&#34;, username, &#34;config.yaml&#34;)
# 2. Retrieve actual modification time in disk
current_mtime = os.path.getmtime(conf_file) if os.path.exists(conf_file) else 0.0
# 3. Validate if initial load or hot-reload is required
if username not in self._providers or self._mtimes.get(username, 0.0) &lt; current_mtime:
old_provider = self._providers.get(username)
try:
# Attempt a fresh configuration load
config = configfile(conf=conf_file, shared_config=self._shared_config)
new_provider = ServiceProvider(config, mode=&#34;local&#34;)
# Successfully loaded, clean up the old provider
if old_provider:
self._providers.pop(username, None)
if hasattr(old_provider, &#34;close&#34;):
try:
old_provider.close()
except Exception:
pass
self._providers[username] = new_provider
self._mtimes[username] = current_mtime
except Exception as e:
# Log warning but fallback to the old stable provider in memory if available
from connpy import printer
printer.warning(f&#34;Failed to hot-reload config for user &#39;{username}&#39; (file may be corrupt/incomplete): {e}&#34;)
if old_provider:
# Keep serving with the old cached instance to ensure service continuity
self._mtimes[username] = current_mtime
else:
# No fallback exists, propagate the exception
raise e
return self._providers[username]</code></pre>
</details>
<div class="desc"><p>Get, lazy-load, or hot-reload a user's full ServiceProvider.</p></div>
</dd>
<dt id="connpy.grpc_layer.user_registry.UserRegistry.has_users"><code class="name flex">
<span>def <span class="ident">has_users</span></span>(<span>self) > bool</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def has_users(self) -&gt; bool:
&#34;&#34;&#34;Check if any users are registered (enables auth enforcement).&#34;&#34;&#34;
return bool(self.user_service.list_users())</code></pre>
</details>
<div class="desc"><p>Check if any users are registered (enables auth enforcement).</p></div>
</dd>
</dl>
</dd>
</dl>
</section>
</article>
<nav id="sidebar">
<div class="toc">
<ul></ul>
</div>
<ul id="index">
<li><h3>Super-module</h3>
<ul>
<li><code><a title="connpy.grpc_layer" href="index.html">connpy.grpc_layer</a></code></li>
</ul>
</li>
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
<li>
<h4><code><a title="connpy.grpc_layer.user_registry.UserRegistry" href="#connpy.grpc_layer.user_registry.UserRegistry">UserRegistry</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.user_registry.UserRegistry.evict" href="#connpy.grpc_layer.user_registry.UserRegistry.evict">evict</a></code></li>
<li><code><a title="connpy.grpc_layer.user_registry.UserRegistry.get_provider" href="#connpy.grpc_layer.user_registry.UserRegistry.get_provider">get_provider</a></code></li>
<li><code><a title="connpy.grpc_layer.user_registry.UserRegistry.has_users" href="#connpy.grpc_layer.user_registry.UserRegistry.has_users">has_users</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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.grpc_layer.utils API documentation</title>
<meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -138,7 +138,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+103 -27
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy API documentation</title>
<meta name="description" content="&lt;p align=&#34;center&#34;&gt;
&lt;img src=&#34;https://nginx.gederico.dynu.net/images/CONNPY-resized.png&#34; alt=&#34;App Logo&#34;&gt;
@@ -51,8 +51,12 @@ el.replaceWith(d);
<h2 id="ai-copilot-new-in-v6">🤖 AI Copilot (New in v6)</h2>
<p>The AI Copilot is deeply integrated into your terminal workflow:
- <strong>Terminal Context Awareness</strong>: The Copilot can "see" your screen output, helping you diagnose errors or analyze command results in real-time.
- <strong>Dynamic Context Selection</strong>: Flexibly select single, range, or line-based terminal blocks to feed the Copilot, filtering out interactive scrolling garbage automatically (e.g., Cisco IOS/XR scrolling, paginators).
- <strong>Hybrid Multi-Agent System</strong>: Automatically escalates complex tasks between the <strong>Network Engineer</strong> (execution) and the <strong>Network Architect</strong> (strategy).
- <strong>MCP Integration</strong>: Dynamically load tools from external providers (6WIND, AWS, etc.) via the Model Context Protocol.
- <strong>Flexible Auth &amp; Keyless AI</strong>: Support for advanced LiteLLM credentials (<code>--engineer-auth</code> / <code>--architect-auth</code>) allowing keyless local models (Ollama), cloud engines (Vertex AI), or custom endpoints.
- <strong>Enhanced Session Management</strong>: Uniquely generated sessions, robust pagination, and interactive styling translating prompt themes directly to terminal escapes.
- <strong>Semantic Prompt Integration</strong>: Emit standard OSC prompt sequences (<code>]133;B</code>) for real-time remote/web front-end command tracking.
- <strong>Interactive Chat</strong>: Launch with <code>conn <a title="connpy.ai" href="#connpy.ai">ai</a></code> for a collaborative troubleshooting session.</p>
<h2 id="core-features">Core Features</h2>
<ul>
@@ -642,8 +646,11 @@ class ai:
self.interrupted = False
# 1. Cargar configuración genérica
aiconfig = self.config.config.get(&#34;ai&#34;, {})
# 1. Cargar configuración genérica con herencia/merge global
if hasattr(self.config, &#34;get_effective_setting&#34;):
aiconfig = self.config.get_effective_setting(&#34;ai&#34;, {})
else:
aiconfig = self.config.config.get(&#34;ai&#34;, {}) if hasattr(self.config, &#34;config&#34;) else {}
# Modelos (Prioridad: Argumento -&gt; Config -&gt; Default)
self.engineer_model = engineer_model or aiconfig.get(&#34;engineer_model&#34;) or &#34;gemini/gemini-3.1-flash-lite&#34;
@@ -1534,10 +1541,12 @@ class ai:
@MethodHook
def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=False, stream=True, session_id=None, chunk_callback=None):
soft_limit_warned = False
is_engineer_keyless = &#34;vertex&#34; in self.engineer_model.lower() or &#34;ollama&#34; in self.engineer_model.lower() or &#34;local&#34; in self.engineer_model.lower()
if not self.engineer_key and not self.engineer_auth and not is_engineer_keyless:
raise ValueError(&#34;Engineer API key or authentication not configured. Use &#39;connpy config --engineer-auth &lt;auth&gt;&#39; to set it.&#34;)
if chat_history is None: chat_history = []
# Load session if provided and history is empty
@@ -2144,7 +2153,7 @@ Node: {node_name}&#34;&#34;&#34;
<dl>
<dt id="connpy.ai.SAFE_COMMANDS"><code class="name">var <span class="ident">SAFE_COMMANDS</span></code></dt>
<dd>
<div class="desc"><p>The type of the None singleton.</p></div>
<div class="desc"></div>
</dd>
</dl>
<h3>Instance variables</h3>
@@ -2479,10 +2488,12 @@ Node: {node_name}&#34;&#34;&#34;
</summary>
<pre><code class="python">@MethodHook
def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=False, stream=True, session_id=None, chunk_callback=None):
soft_limit_warned = False
is_engineer_keyless = &#34;vertex&#34; in self.engineer_model.lower() or &#34;ollama&#34; in self.engineer_model.lower() or &#34;local&#34; in self.engineer_model.lower()
if not self.engineer_key and not self.engineer_auth and not is_engineer_keyless:
raise ValueError(&#34;Engineer API key or authentication not configured. Use &#39;connpy config --engineer-auth &lt;auth&gt;&#39; to set it.&#34;)
if chat_history is None: chat_history = []
# Load session if provided and history is empty
@@ -3184,7 +3195,7 @@ def confirm(self, user_input): return True</code></pre>
</dd>
<dt id="connpy.configfile"><code class="flex name class">
<span>class <span class="ident">configfile</span></span>
<span>(</span><span>conf=None, key=None)</span>
<span>(</span><span>conf=None, key=None, shared_config=None)</span>
</code></dt>
<dd>
<details class="source">
@@ -3217,7 +3228,8 @@ class configfile:
passwords.
&#39;&#39;&#39;
def __init__(self, conf = None, key = None):
def __init__(self, conf = None, key = None, shared_config = None):
self._shared_config = shared_config
&#39;&#39;&#39;
### Optional Parameters:
@@ -3323,6 +3335,42 @@ class configfile:
self._generate_nodes_cache()
def get_effective_setting(self, key, default=None):
&#34;&#34;&#34;Get config setting with shared fallback for inheritable keys.&#34;&#34;&#34;
val = self.config.get(key)
if key == &#34;ai&#34;:
if val is not None:
if self._shared_config:
import copy
# Deep merge: shared as base, user overrides
base = copy.deepcopy(self._shared_config.config.get(key, {}))
if isinstance(base, dict) and isinstance(val, dict):
# Credential isolation:
# If user defines engineer credentials, discard shared ones
if &#34;engineer_api_key&#34; in val or &#34;engineer_auth&#34; in val:
base.pop(&#34;engineer_api_key&#34;, None)
base.pop(&#34;engineer_auth&#34;, None)
# If user defines architect credentials, discard shared ones
if &#34;architect_api_key&#34; in val or &#34;architect_auth&#34; in val:
base.pop(&#34;architect_api_key&#34;, None)
base.pop(&#34;architect_auth&#34;, None)
# Recursive update for inner dictionaries (like mcp_servers or model details)
def deep_merge(d1, d2):
for k, v in d2.items():
if isinstance(v, dict) and k in d1 and isinstance(d1[k], dict):
deep_merge(d1[k], v)
else:
d1[k] = copy.deepcopy(v)
deep_merge(base, val)
return base
return val
elif self._shared_config:
return self._shared_config.config.get(key, default)
return val if val is not None else default
def _validate_config(self, data):
&#34;&#34;&#34;Verify config data has the required structure.&#34;&#34;&#34;
if not isinstance(data, dict):
@@ -3663,7 +3711,8 @@ class configfile:
else:
printer.error(&#34;Filter must be a string or a list of strings&#34;)
sys.exit(1)
nodes = [item for item in nodes if any(re.search(pattern, item) for pattern in flat_filter)]
flags = re.IGNORECASE if not self.config.get(&#34;case&#34;, False) else 0
nodes = [item for item in nodes if any(re.search(pattern, item, flags) for pattern in flat_filter)]
return nodes
@MethodHook
@@ -3786,13 +3835,6 @@ class configfile:
- publickey (obj): Object containing the public key to decrypt
passwords.
</code></pre>
<h3 id="optional-parameters">Optional Parameters:</h3>
<pre><code>- conf (str): Path/file to config file. If left empty default
path is ~/.config/conn/config.yaml
- key (str): Path/file to RSA key file. If left empty default
path is ~/.config/conn/.osk
</code></pre></div>
<h3>Methods</h3>
<dl>
@@ -3844,6 +3886,51 @@ def encrypt(self, password, keyfile=None):
<pre><code>str: Encrypted password.
</code></pre></div>
</dd>
<dt id="connpy.configfile.get_effective_setting"><code class="name flex">
<span>def <span class="ident">get_effective_setting</span></span>(<span>self, key, default=None)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def get_effective_setting(self, key, default=None):
&#34;&#34;&#34;Get config setting with shared fallback for inheritable keys.&#34;&#34;&#34;
val = self.config.get(key)
if key == &#34;ai&#34;:
if val is not None:
if self._shared_config:
import copy
# Deep merge: shared as base, user overrides
base = copy.deepcopy(self._shared_config.config.get(key, {}))
if isinstance(base, dict) and isinstance(val, dict):
# Credential isolation:
# If user defines engineer credentials, discard shared ones
if &#34;engineer_api_key&#34; in val or &#34;engineer_auth&#34; in val:
base.pop(&#34;engineer_api_key&#34;, None)
base.pop(&#34;engineer_auth&#34;, None)
# If user defines architect credentials, discard shared ones
if &#34;architect_api_key&#34; in val or &#34;architect_auth&#34; in val:
base.pop(&#34;architect_api_key&#34;, None)
base.pop(&#34;architect_auth&#34;, None)
# Recursive update for inner dictionaries (like mcp_servers or model details)
def deep_merge(d1, d2):
for k, v in d2.items():
if isinstance(v, dict) and k in d1 and isinstance(d1[k], dict):
deep_merge(d1[k], v)
else:
d1[k] = copy.deepcopy(v)
deep_merge(base, val)
return base
return val
elif self._shared_config:
return self._shared_config.config.get(key, default)
return val if val is not None else default</code></pre>
</details>
<div class="desc"><p>Get config setting with shared fallback for inheritable keys.</p></div>
</dd>
<dt id="connpy.configfile.getitem"><code class="name flex">
<span>def <span class="ident">getitem</span></span>(<span>self, unique, keys=None, extract=False)</span>
</code></dt>
@@ -5000,18 +5087,6 @@ class node:
cmd += f&#34; {self.options}&#34;
return cmd
@MethodHook
def _generate_ssm_cmd(self):
region = self.tags.get(&#34;region&#34;, &#34;&#34;) if isinstance(self.tags, dict) else &#34;&#34;
profile = self.tags.get(&#34;profile&#34;, &#34;&#34;) if isinstance(self.tags, dict) else &#34;&#34;
cmd = f&#34;aws ssm start-session --target {self.host}&#34;
if region:
cmd += f&#34; --region {region}&#34;
if profile:
cmd += f&#34; --profile {profile}&#34;
if self.options:
cmd += f&#34; {self.options}&#34;
return cmd
@MethodHook
def _get_cmd(self):
@@ -6358,6 +6433,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
<h4><code><a title="connpy.configfile" href="#connpy.configfile">configfile</a></code></h4>
<ul class="">
<li><code><a title="connpy.configfile.encrypt" href="#connpy.configfile.encrypt">encrypt</a></code></li>
<li><code><a title="connpy.configfile.get_effective_setting" href="#connpy.configfile.get_effective_setting">get_effective_setting</a></code></li>
<li><code><a title="connpy.configfile.getitem" href="#connpy.configfile.getitem">getitem</a></code></li>
<li><code><a title="connpy.configfile.getitems" href="#connpy.configfile.getitems">getitems</a></code></li>
</ul>
@@ -6384,7 +6460,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+10 -4
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.mcp_client 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>
@@ -86,7 +86,10 @@ el.replaceWith(d);
all_llm_tools = []
try:
mcp_config = self.config.config.get(&#34;ai&#34;, {}).get(&#34;mcp_servers&#34;, {})
if hasattr(self.config, &#34;get_effective_setting&#34;):
mcp_config = self.config.get_effective_setting(&#34;ai&#34;, {}).get(&#34;mcp_servers&#34;, {})
else:
mcp_config = self.config.config.get(&#34;ai&#34;, {}).get(&#34;mcp_servers&#34;, {}) if hasattr(self.config, &#34;config&#34;) else {}
except Exception:
return []
@@ -260,7 +263,10 @@ el.replaceWith(d);
all_llm_tools = []
try:
mcp_config = self.config.config.get(&#34;ai&#34;, {}).get(&#34;mcp_servers&#34;, {})
if hasattr(self.config, &#34;get_effective_setting&#34;):
mcp_config = self.config.get_effective_setting(&#34;ai&#34;, {}).get(&#34;mcp_servers&#34;, {})
else:
mcp_config = self.config.config.get(&#34;ai&#34;, {}).get(&#34;mcp_servers&#34;, {}) if hasattr(self.config, &#34;config&#34;) else {}
except Exception:
return []
@@ -343,7 +349,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.proto 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>
@@ -60,7 +60,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+10 -4
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.ai_service 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>
@@ -359,7 +359,10 @@ el.replaceWith(d);
def list_mcp_servers(self) -&gt; dict:
&#34;&#34;&#34;Get the configured MCP servers.&#34;&#34;&#34;
ai_settings = self.config.config.get(&#34;ai&#34;, {})
if hasattr(self.config, &#34;get_effective_setting&#34;):
ai_settings = self.config.get_effective_setting(&#34;ai&#34;, {})
else:
ai_settings = self.config.config.get(&#34;ai&#34;, {}) if hasattr(self.config, &#34;config&#34;) else {}
return ai_settings.get(&#34;mcp_servers&#34;, {})
def load_session_data(self, session_id):
@@ -669,7 +672,10 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def list_mcp_servers(self) -&gt; dict:
&#34;&#34;&#34;Get the configured MCP servers.&#34;&#34;&#34;
ai_settings = self.config.config.get(&#34;ai&#34;, {})
if hasattr(self.config, &#34;get_effective_setting&#34;):
ai_settings = self.config.get_effective_setting(&#34;ai&#34;, {})
else:
ai_settings = self.config.config.get(&#34;ai&#34;, {}) if hasattr(self.config, &#34;config&#34;) else {}
return ai_settings.get(&#34;mcp_servers&#34;, {})</code></pre>
</details>
<div class="desc"><p>Get the configured MCP servers.</p></div>
@@ -826,7 +832,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.base 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>
@@ -152,7 +152,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.config_service 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>
@@ -319,7 +319,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.context_service 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>
@@ -370,7 +370,7 @@ def current_context(self) -&gt; str:
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.exceptions 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>
@@ -268,7 +268,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.execution_service 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>
@@ -449,7 +449,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.import_export_service 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>
@@ -361,7 +361,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+230 -86
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services 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>
@@ -92,6 +92,10 @@ el.replaceWith(d);
<dd>
<div class="desc"></div>
</dd>
<dt><code class="name"><a title="connpy.services.user_service" href="user_service.html">connpy.services.user_service</a></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</section>
<section>
@@ -414,7 +418,10 @@ el.replaceWith(d);
def list_mcp_servers(self) -&gt; dict:
&#34;&#34;&#34;Get the configured MCP servers.&#34;&#34;&#34;
ai_settings = self.config.config.get(&#34;ai&#34;, {})
if hasattr(self.config, &#34;get_effective_setting&#34;):
ai_settings = self.config.get_effective_setting(&#34;ai&#34;, {})
else:
ai_settings = self.config.config.get(&#34;ai&#34;, {}) if hasattr(self.config, &#34;config&#34;) else {}
return ai_settings.get(&#34;mcp_servers&#34;, {})
def load_session_data(self, session_id):
@@ -724,7 +731,10 @@ el.replaceWith(d);
</summary>
<pre><code class="python">def list_mcp_servers(self) -&gt; dict:
&#34;&#34;&#34;Get the configured MCP servers.&#34;&#34;&#34;
ai_settings = self.config.config.get(&#34;ai&#34;, {})
if hasattr(self.config, &#34;get_effective_setting&#34;):
ai_settings = self.config.get_effective_setting(&#34;ai&#34;, {})
else:
ai_settings = self.config.config.get(&#34;ai&#34;, {}) if hasattr(self.config, &#34;config&#34;) else {}
return ai_settings.get(&#34;mcp_servers&#34;, {})</code></pre>
</details>
<div class="desc"><p>Get the configured MCP servers.</p></div>
@@ -2008,7 +2018,7 @@ el.replaceWith(d);
self.config._connections_add(**data)
self.config._saveconfig(self.config.file)
def update_node(self, unique_id, data):
def update_node(self, unique_id, data, save=True):
&#34;&#34;&#34;Explicitly update an existing node.&#34;&#34;&#34;
all_nodes = self.config._getallnodes()
if unique_id not in all_nodes:
@@ -2022,9 +2032,10 @@ el.replaceWith(d);
# config._connections_add actually handles updates if ID exists correctly
self.config._connections_add(**data)
if save:
self.config._saveconfig(self.config.file)
def delete_node(self, unique_id, is_folder=False):
def delete_node(self, unique_id, is_folder=False, save=True):
&#34;&#34;&#34;Logic for deleting a node or folder.&#34;&#34;&#34;
if is_folder:
uniques = self.config._explode_unique(unique_id)
@@ -2037,6 +2048,7 @@ el.replaceWith(d);
raise NodeNotFoundError(f&#34;Node &#39;{unique_id}&#39; not found or invalid.&#34;)
self.config._connections_del(**uniques)
if save:
self.config._saveconfig(self.config.file)
def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
@@ -2267,14 +2279,14 @@ el.replaceWith(d);
<div class="desc"><p>Interact with a node directly.</p></div>
</dd>
<dt id="connpy.services.NodeService.delete_node"><code class="name flex">
<span>def <span class="ident">delete_node</span></span>(<span>self, unique_id, is_folder=False)</span>
<span>def <span class="ident">delete_node</span></span>(<span>self, unique_id, is_folder=False, save=True)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def delete_node(self, unique_id, is_folder=False):
<pre><code class="python">def delete_node(self, unique_id, is_folder=False, save=True):
&#34;&#34;&#34;Logic for deleting a node or folder.&#34;&#34;&#34;
if is_folder:
uniques = self.config._explode_unique(unique_id)
@@ -2287,6 +2299,7 @@ el.replaceWith(d);
raise NodeNotFoundError(f&#34;Node &#39;{unique_id}&#39; not found or invalid.&#34;)
self.config._connections_del(**uniques)
if save:
self.config._saveconfig(self.config.file)</code></pre>
</details>
<div class="desc"><p>Logic for deleting a node or folder.</p></div>
@@ -2496,14 +2509,14 @@ el.replaceWith(d);
<div class="desc"><p>Move or copy a node.</p></div>
</dd>
<dt id="connpy.services.NodeService.update_node"><code class="name flex">
<span>def <span class="ident">update_node</span></span>(<span>self, unique_id, data)</span>
<span>def <span class="ident">update_node</span></span>(<span>self, unique_id, data, save=True)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def update_node(self, unique_id, data):
<pre><code class="python">def update_node(self, unique_id, data, save=True):
&#34;&#34;&#34;Explicitly update an existing node.&#34;&#34;&#34;
all_nodes = self.config._getallnodes()
if unique_id not in all_nodes:
@@ -2517,6 +2530,7 @@ el.replaceWith(d);
# config._connections_add actually handles updates if ID exists correctly
self.config._connections_add(**data)
if save:
self.config._saveconfig(self.config.file)</code></pre>
</details>
<div class="desc"><p>Explicitly update an existing node.</p></div>
@@ -2568,16 +2582,47 @@ el.replaceWith(d);
<pre><code class="python">class PluginService(BaseService):
&#34;&#34;&#34;Business logic for enabling, disabling, and listing plugins.&#34;&#34;&#34;
def _get_plugin_path(self, name, include_disabled=True):
&#34;&#34;&#34;Resolves the physical path of a plugin by name. Priority: user, shared/global, core.&#34;&#34;&#34;
import os
# 1. User directory
user_dir = os.path.join(self.config.defaultdir, &#34;plugins&#34;)
if os.path.exists(user_dir):
p_file = os.path.join(user_dir, f&#34;{name}.py&#34;)
if os.path.exists(p_file):
return p_file, &#34;user&#34;, True
if include_disabled:
bkp_file = os.path.join(user_dir, f&#34;{name}.py.bkp&#34;)
if os.path.exists(bkp_file):
return bkp_file, &#34;user&#34;, False
# 2. Shared/Global directory
if hasattr(self.config, &#34;_shared_config&#34;) and self.config._shared_config:
shared_dir = os.path.join(self.config._shared_config.defaultdir, &#34;plugins&#34;)
if os.path.exists(shared_dir):
p_file = os.path.join(shared_dir, f&#34;{name}.py&#34;)
if os.path.exists(p_file):
return p_file, &#34;shared&#34;, True
if include_disabled:
bkp_file = os.path.join(shared_dir, f&#34;{name}.py.bkp&#34;)
if os.path.exists(bkp_file):
return bkp_file, &#34;shared&#34;, False
# 3. Core plugins
core_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), &#34;..&#34;, &#34;core_plugins&#34;)
p_file = os.path.join(core_dir, f&#34;{name}.py&#34;)
if os.path.exists(p_file):
return p_file, &#34;core&#34;, True
return None, None, False
def list_plugins(self):
&#34;&#34;&#34;List all core and user-defined plugins with their status and hash.&#34;&#34;&#34;
import os
import hashlib
# Check for user plugins directory
plugin_dir = os.path.join(self.config.defaultdir, &#34;plugins&#34;)
# Check for core plugins directory
core_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), &#34;..&#34;, &#34;core_plugins&#34;)
all_plugin_info = {}
def get_hash(path):
@@ -2587,12 +2632,35 @@ el.replaceWith(d);
except Exception:
return &#34;&#34;
# User plugins
if os.path.exists(plugin_dir):
for f in os.listdir(plugin_dir):
# 1. Scan core plugins (lowest priority)
core_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), &#34;..&#34;, &#34;core_plugins&#34;)
if os.path.exists(core_dir):
for f in os.listdir(core_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(plugin_dir, f)
path = os.path.join(core_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
# 2. Scan shared plugins (medium priority)
if hasattr(self.config, &#34;_shared_config&#34;) and self.config._shared_config:
shared_dir = os.path.join(self.config._shared_config.defaultdir, &#34;plugins&#34;)
if os.path.exists(shared_dir):
for f in os.listdir(shared_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(shared_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
elif f.endswith(&#34;.py.bkp&#34;):
name = f[:-7]
all_plugin_info[name] = {&#34;enabled&#34;: False}
# 3. Scan user plugins (highest priority)
user_dir = os.path.join(self.config.defaultdir, &#34;plugins&#34;)
if os.path.exists(user_dir):
for f in os.listdir(user_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(user_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
elif f.endswith(&#34;.py.bkp&#34;):
name = f[:-7]
@@ -2600,6 +2668,7 @@ el.replaceWith(d);
return all_plugin_info
def add_plugin(self, name, source_file, update=False):
&#34;&#34;&#34;Add or update a plugin from a local file.&#34;&#34;&#34;
import os
@@ -2680,6 +2749,10 @@ el.replaceWith(d);
raise InvalidConfigurationError(f&#34;Failed to delete plugin file &#39;{f}&#39;: {e}&#34;)
if not deleted:
# If not deleted from user directory, check if it&#39;s in shared or core
path, origin, enabled = self._get_plugin_path(name, include_disabled=True)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
raise InvalidConfigurationError(&#34;Global and core plugins are read-only and cannot be deleted by users.&#34;)
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)
def enable_plugin(self, name):
@@ -2688,51 +2761,80 @@ el.replaceWith(d);
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
disabled_file = f&#34;{plugin_file}.bkp&#34;
if os.path.exists(plugin_file):
return False # Already enabled
if not os.path.exists(disabled_file):
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)
if os.path.exists(disabled_file):
# Check if it is a shadow bkp file (0 bytes shadowing shared/core)
is_shadow = False
if os.path.getsize(disabled_file) == 0:
# Resolve without the local bkp file to verify if shared/core has it
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
is_shadow = True
if is_shadow:
# Remove shadow file to restore inheritance
try:
os.remove(disabled_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to remove shadow file &#39;{disabled_file}&#39;: {e}&#34;)
else:
try:
os.rename(disabled_file, plugin_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to enable plugin &#39;{name}&#39;: {e}&#34;)
if os.path.exists(plugin_file):
return False # Already enabled
# If it doesn&#39;t exist locally, check if it&#39;s already an active shared/core plugin
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
return False # Already active/enabled through inheritance
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)
def disable_plugin(self, name):
&#34;&#34;&#34;Deactivate a plugin by renaming it to a backup file.&#34;&#34;&#34;
import os
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
disabled_file = f&#34;{plugin_file}.bkp&#34;
if os.path.exists(disabled_file):
return False # Already disabled
if not os.path.exists(plugin_file):
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found or is a core plugin.&#34;)
if os.path.exists(plugin_file):
# Regular user-level plugin exists. Rename to bkp
try:
os.rename(plugin_file, disabled_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to disable plugin &#39;{name}&#39;: {e}&#34;)
if os.path.exists(disabled_file):
return False # Already disabled
# Check if it exists in shared or core
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
# Shadow disable it by creating an empty .py.bkp in user plugins dir
plugin_dir = os.path.dirname(plugin_file)
os.makedirs(plugin_dir, exist_ok=True)
try:
with open(disabled_file, &#34;w&#34;) as f:
f.write(&#34;&#34;)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to create shadow disable file: {e}&#34;)
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found or is already disabled.&#34;)
def get_plugin_source(self, name):
import os
from ..services.exceptions import InvalidConfigurationError
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
core_path = os.path.dirname(os.path.realpath(__file__)) + f&#34;/../core_plugins/{name}.py&#34;
if os.path.exists(plugin_file):
target = plugin_file
elif os.path.exists(core_path):
target = core_path
else:
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if not path:
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found&#34;)
with open(target, &#34;r&#34;) as f:
with open(path, &#34;r&#34;) as f:
return f.read()
def invoke_plugin(self, name, args_dict):
@@ -2772,17 +2874,12 @@ el.replaceWith(d);
p_manager = Plugins()
import os
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
core_path = os.path.dirname(os.path.realpath(__file__)) + f&#34;/../core_plugins/{name}.py&#34;
if os.path.exists(plugin_file):
target = plugin_file
elif os.path.exists(core_path):
target = core_path
else:
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if not path:
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found&#34;)
module = p_manager._import_from_path(target)
module = p_manager._import_from_path(path)
parser = module.Parser().parser if hasattr(module, &#34;Parser&#34;) else None
if &#34;__func_name__&#34; in args_dict and hasattr(module, args_dict[&#34;__func_name__&#34;]):
@@ -2935,6 +3032,10 @@ el.replaceWith(d);
raise InvalidConfigurationError(f&#34;Failed to delete plugin file &#39;{f}&#39;: {e}&#34;)
if not deleted:
# If not deleted from user directory, check if it&#39;s in shared or core
path, origin, enabled = self._get_plugin_path(name, include_disabled=True)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
raise InvalidConfigurationError(&#34;Global and core plugins are read-only and cannot be deleted by users.&#34;)
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)</code></pre>
</details>
<div class="desc"><p>Remove a plugin file permanently.</p></div>
@@ -2953,17 +3054,31 @@ el.replaceWith(d);
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
disabled_file = f&#34;{plugin_file}.bkp&#34;
if os.path.exists(disabled_file):
return False # Already disabled
if not os.path.exists(plugin_file):
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found or is a core plugin.&#34;)
if os.path.exists(plugin_file):
# Regular user-level plugin exists. Rename to bkp
try:
os.rename(plugin_file, disabled_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to disable plugin &#39;{name}&#39;: {e}&#34;)</code></pre>
raise InvalidConfigurationError(f&#34;Failed to disable plugin &#39;{name}&#39;: {e}&#34;)
if os.path.exists(disabled_file):
return False # Already disabled
# Check if it exists in shared or core
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
# Shadow disable it by creating an empty .py.bkp in user plugins dir
plugin_dir = os.path.dirname(plugin_file)
os.makedirs(plugin_dir, exist_ok=True)
try:
with open(disabled_file, &#34;w&#34;) as f:
f.write(&#34;&#34;)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to create shadow disable file: {e}&#34;)
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found or is already disabled.&#34;)</code></pre>
</details>
<div class="desc"><p>Deactivate a plugin by renaming it to a backup file.</p></div>
</dd>
@@ -2981,17 +3096,38 @@ el.replaceWith(d);
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
disabled_file = f&#34;{plugin_file}.bkp&#34;
if os.path.exists(plugin_file):
return False # Already enabled
if not os.path.exists(disabled_file):
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)
if os.path.exists(disabled_file):
# Check if it is a shadow bkp file (0 bytes shadowing shared/core)
is_shadow = False
if os.path.getsize(disabled_file) == 0:
# Resolve without the local bkp file to verify if shared/core has it
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
is_shadow = True
if is_shadow:
# Remove shadow file to restore inheritance
try:
os.remove(disabled_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to remove shadow file &#39;{disabled_file}&#39;: {e}&#34;)
else:
try:
os.rename(disabled_file, plugin_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to enable plugin &#39;{name}&#39;: {e}&#34;)</code></pre>
raise InvalidConfigurationError(f&#34;Failed to enable plugin &#39;{name}&#39;: {e}&#34;)
if os.path.exists(plugin_file):
return False # Already enabled
# If it doesn&#39;t exist locally, check if it&#39;s already an active shared/core plugin
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
return False # Already active/enabled through inheritance
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)</code></pre>
</details>
<div class="desc"><p>Activate a plugin by renaming its backup file.</p></div>
</dd>
@@ -3007,17 +3143,11 @@ el.replaceWith(d);
import os
from ..services.exceptions import InvalidConfigurationError
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
core_path = os.path.dirname(os.path.realpath(__file__)) + f&#34;/../core_plugins/{name}.py&#34;
if os.path.exists(plugin_file):
target = plugin_file
elif os.path.exists(core_path):
target = core_path
else:
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if not path:
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found&#34;)
with open(target, &#34;r&#34;) as f:
with open(path, &#34;r&#34;) as f:
return f.read()</code></pre>
</details>
<div class="desc"></div>
@@ -3067,17 +3197,12 @@ el.replaceWith(d);
p_manager = Plugins()
import os
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
core_path = os.path.dirname(os.path.realpath(__file__)) + f&#34;/../core_plugins/{name}.py&#34;
if os.path.exists(plugin_file):
target = plugin_file
elif os.path.exists(core_path):
target = core_path
else:
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if not path:
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found&#34;)
module = p_manager._import_from_path(target)
module = p_manager._import_from_path(path)
parser = module.Parser().parser if hasattr(module, &#34;Parser&#34;) else None
if &#34;__func_name__&#34; in args_dict and hasattr(module, args_dict[&#34;__func_name__&#34;]):
@@ -3146,11 +3271,6 @@ el.replaceWith(d);
import os
import hashlib
# Check for user plugins directory
plugin_dir = os.path.join(self.config.defaultdir, &#34;plugins&#34;)
# Check for core plugins directory
core_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), &#34;..&#34;, &#34;core_plugins&#34;)
all_plugin_info = {}
def get_hash(path):
@@ -3160,12 +3280,35 @@ el.replaceWith(d);
except Exception:
return &#34;&#34;
# User plugins
if os.path.exists(plugin_dir):
for f in os.listdir(plugin_dir):
# 1. Scan core plugins (lowest priority)
core_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), &#34;..&#34;, &#34;core_plugins&#34;)
if os.path.exists(core_dir):
for f in os.listdir(core_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(plugin_dir, f)
path = os.path.join(core_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
# 2. Scan shared plugins (medium priority)
if hasattr(self.config, &#34;_shared_config&#34;) and self.config._shared_config:
shared_dir = os.path.join(self.config._shared_config.defaultdir, &#34;plugins&#34;)
if os.path.exists(shared_dir):
for f in os.listdir(shared_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(shared_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
elif f.endswith(&#34;.py.bkp&#34;):
name = f[:-7]
all_plugin_info[name] = {&#34;enabled&#34;: False}
# 3. Scan user plugins (highest priority)
user_dir = os.path.join(self.config.defaultdir, &#34;plugins&#34;)
if os.path.exists(user_dir):
for f in os.listdir(user_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(user_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
elif f.endswith(&#34;.py.bkp&#34;):
name = f[:-7]
@@ -3854,6 +3997,7 @@ el.replaceWith(d);
<li><code><a title="connpy.services.provider" href="provider.html">connpy.services.provider</a></code></li>
<li><code><a title="connpy.services.sync_service" href="sync_service.html">connpy.services.sync_service</a></code></li>
<li><code><a title="connpy.services.system_service" href="system_service.html">connpy.services.system_service</a></code></li>
<li><code><a title="connpy.services.user_service" href="user_service.html">connpy.services.user_service</a></code></li>
</ul>
</li>
<li><h3><a href="#header-classes">Classes</a></h3>
@@ -3984,7 +4128,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+12 -8
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.node_service 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>
@@ -198,7 +198,7 @@ el.replaceWith(d);
self.config._connections_add(**data)
self.config._saveconfig(self.config.file)
def update_node(self, unique_id, data):
def update_node(self, unique_id, data, save=True):
&#34;&#34;&#34;Explicitly update an existing node.&#34;&#34;&#34;
all_nodes = self.config._getallnodes()
if unique_id not in all_nodes:
@@ -212,9 +212,10 @@ el.replaceWith(d);
# config._connections_add actually handles updates if ID exists correctly
self.config._connections_add(**data)
if save:
self.config._saveconfig(self.config.file)
def delete_node(self, unique_id, is_folder=False):
def delete_node(self, unique_id, is_folder=False, save=True):
&#34;&#34;&#34;Logic for deleting a node or folder.&#34;&#34;&#34;
if is_folder:
uniques = self.config._explode_unique(unique_id)
@@ -227,6 +228,7 @@ el.replaceWith(d);
raise NodeNotFoundError(f&#34;Node &#39;{unique_id}&#39; not found or invalid.&#34;)
self.config._connections_del(**uniques)
if save:
self.config._saveconfig(self.config.file)
def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
@@ -457,14 +459,14 @@ el.replaceWith(d);
<div class="desc"><p>Interact with a node directly.</p></div>
</dd>
<dt id="connpy.services.node_service.NodeService.delete_node"><code class="name flex">
<span>def <span class="ident">delete_node</span></span>(<span>self, unique_id, is_folder=False)</span>
<span>def <span class="ident">delete_node</span></span>(<span>self, unique_id, is_folder=False, save=True)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def delete_node(self, unique_id, is_folder=False):
<pre><code class="python">def delete_node(self, unique_id, is_folder=False, save=True):
&#34;&#34;&#34;Logic for deleting a node or folder.&#34;&#34;&#34;
if is_folder:
uniques = self.config._explode_unique(unique_id)
@@ -477,6 +479,7 @@ el.replaceWith(d);
raise NodeNotFoundError(f&#34;Node &#39;{unique_id}&#39; not found or invalid.&#34;)
self.config._connections_del(**uniques)
if save:
self.config._saveconfig(self.config.file)</code></pre>
</details>
<div class="desc"><p>Logic for deleting a node or folder.</p></div>
@@ -686,14 +689,14 @@ el.replaceWith(d);
<div class="desc"><p>Move or copy a node.</p></div>
</dd>
<dt id="connpy.services.node_service.NodeService.update_node"><code class="name flex">
<span>def <span class="ident">update_node</span></span>(<span>self, unique_id, data)</span>
<span>def <span class="ident">update_node</span></span>(<span>self, unique_id, data, save=True)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def update_node(self, unique_id, data):
<pre><code class="python">def update_node(self, unique_id, data, save=True):
&#34;&#34;&#34;Explicitly update an existing node.&#34;&#34;&#34;
all_nodes = self.config._getallnodes()
if unique_id not in all_nodes:
@@ -707,6 +710,7 @@ el.replaceWith(d);
# config._connections_add actually handles updates if ID exists correctly
self.config._connections_add(**data)
if save:
self.config._saveconfig(self.config.file)</code></pre>
</details>
<div class="desc"><p>Explicitly update an existing node.</p></div>
@@ -786,7 +790,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+207 -78
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.plugin_service 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>
@@ -58,16 +58,47 @@ el.replaceWith(d);
<pre><code class="python">class PluginService(BaseService):
&#34;&#34;&#34;Business logic for enabling, disabling, and listing plugins.&#34;&#34;&#34;
def _get_plugin_path(self, name, include_disabled=True):
&#34;&#34;&#34;Resolves the physical path of a plugin by name. Priority: user, shared/global, core.&#34;&#34;&#34;
import os
# 1. User directory
user_dir = os.path.join(self.config.defaultdir, &#34;plugins&#34;)
if os.path.exists(user_dir):
p_file = os.path.join(user_dir, f&#34;{name}.py&#34;)
if os.path.exists(p_file):
return p_file, &#34;user&#34;, True
if include_disabled:
bkp_file = os.path.join(user_dir, f&#34;{name}.py.bkp&#34;)
if os.path.exists(bkp_file):
return bkp_file, &#34;user&#34;, False
# 2. Shared/Global directory
if hasattr(self.config, &#34;_shared_config&#34;) and self.config._shared_config:
shared_dir = os.path.join(self.config._shared_config.defaultdir, &#34;plugins&#34;)
if os.path.exists(shared_dir):
p_file = os.path.join(shared_dir, f&#34;{name}.py&#34;)
if os.path.exists(p_file):
return p_file, &#34;shared&#34;, True
if include_disabled:
bkp_file = os.path.join(shared_dir, f&#34;{name}.py.bkp&#34;)
if os.path.exists(bkp_file):
return bkp_file, &#34;shared&#34;, False
# 3. Core plugins
core_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), &#34;..&#34;, &#34;core_plugins&#34;)
p_file = os.path.join(core_dir, f&#34;{name}.py&#34;)
if os.path.exists(p_file):
return p_file, &#34;core&#34;, True
return None, None, False
def list_plugins(self):
&#34;&#34;&#34;List all core and user-defined plugins with their status and hash.&#34;&#34;&#34;
import os
import hashlib
# Check for user plugins directory
plugin_dir = os.path.join(self.config.defaultdir, &#34;plugins&#34;)
# Check for core plugins directory
core_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), &#34;..&#34;, &#34;core_plugins&#34;)
all_plugin_info = {}
def get_hash(path):
@@ -77,12 +108,35 @@ el.replaceWith(d);
except Exception:
return &#34;&#34;
# User plugins
if os.path.exists(plugin_dir):
for f in os.listdir(plugin_dir):
# 1. Scan core plugins (lowest priority)
core_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), &#34;..&#34;, &#34;core_plugins&#34;)
if os.path.exists(core_dir):
for f in os.listdir(core_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(plugin_dir, f)
path = os.path.join(core_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
# 2. Scan shared plugins (medium priority)
if hasattr(self.config, &#34;_shared_config&#34;) and self.config._shared_config:
shared_dir = os.path.join(self.config._shared_config.defaultdir, &#34;plugins&#34;)
if os.path.exists(shared_dir):
for f in os.listdir(shared_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(shared_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
elif f.endswith(&#34;.py.bkp&#34;):
name = f[:-7]
all_plugin_info[name] = {&#34;enabled&#34;: False}
# 3. Scan user plugins (highest priority)
user_dir = os.path.join(self.config.defaultdir, &#34;plugins&#34;)
if os.path.exists(user_dir):
for f in os.listdir(user_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(user_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
elif f.endswith(&#34;.py.bkp&#34;):
name = f[:-7]
@@ -90,6 +144,7 @@ el.replaceWith(d);
return all_plugin_info
def add_plugin(self, name, source_file, update=False):
&#34;&#34;&#34;Add or update a plugin from a local file.&#34;&#34;&#34;
import os
@@ -170,6 +225,10 @@ el.replaceWith(d);
raise InvalidConfigurationError(f&#34;Failed to delete plugin file &#39;{f}&#39;: {e}&#34;)
if not deleted:
# If not deleted from user directory, check if it&#39;s in shared or core
path, origin, enabled = self._get_plugin_path(name, include_disabled=True)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
raise InvalidConfigurationError(&#34;Global and core plugins are read-only and cannot be deleted by users.&#34;)
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)
def enable_plugin(self, name):
@@ -178,51 +237,80 @@ el.replaceWith(d);
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
disabled_file = f&#34;{plugin_file}.bkp&#34;
if os.path.exists(plugin_file):
return False # Already enabled
if not os.path.exists(disabled_file):
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)
if os.path.exists(disabled_file):
# Check if it is a shadow bkp file (0 bytes shadowing shared/core)
is_shadow = False
if os.path.getsize(disabled_file) == 0:
# Resolve without the local bkp file to verify if shared/core has it
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
is_shadow = True
if is_shadow:
# Remove shadow file to restore inheritance
try:
os.remove(disabled_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to remove shadow file &#39;{disabled_file}&#39;: {e}&#34;)
else:
try:
os.rename(disabled_file, plugin_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to enable plugin &#39;{name}&#39;: {e}&#34;)
if os.path.exists(plugin_file):
return False # Already enabled
# If it doesn&#39;t exist locally, check if it&#39;s already an active shared/core plugin
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
return False # Already active/enabled through inheritance
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)
def disable_plugin(self, name):
&#34;&#34;&#34;Deactivate a plugin by renaming it to a backup file.&#34;&#34;&#34;
import os
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
disabled_file = f&#34;{plugin_file}.bkp&#34;
if os.path.exists(disabled_file):
return False # Already disabled
if not os.path.exists(plugin_file):
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found or is a core plugin.&#34;)
if os.path.exists(plugin_file):
# Regular user-level plugin exists. Rename to bkp
try:
os.rename(plugin_file, disabled_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to disable plugin &#39;{name}&#39;: {e}&#34;)
if os.path.exists(disabled_file):
return False # Already disabled
# Check if it exists in shared or core
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
# Shadow disable it by creating an empty .py.bkp in user plugins dir
plugin_dir = os.path.dirname(plugin_file)
os.makedirs(plugin_dir, exist_ok=True)
try:
with open(disabled_file, &#34;w&#34;) as f:
f.write(&#34;&#34;)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to create shadow disable file: {e}&#34;)
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found or is already disabled.&#34;)
def get_plugin_source(self, name):
import os
from ..services.exceptions import InvalidConfigurationError
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
core_path = os.path.dirname(os.path.realpath(__file__)) + f&#34;/../core_plugins/{name}.py&#34;
if os.path.exists(plugin_file):
target = plugin_file
elif os.path.exists(core_path):
target = core_path
else:
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if not path:
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found&#34;)
with open(target, &#34;r&#34;) as f:
with open(path, &#34;r&#34;) as f:
return f.read()
def invoke_plugin(self, name, args_dict):
@@ -262,17 +350,12 @@ el.replaceWith(d);
p_manager = Plugins()
import os
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
core_path = os.path.dirname(os.path.realpath(__file__)) + f&#34;/../core_plugins/{name}.py&#34;
if os.path.exists(plugin_file):
target = plugin_file
elif os.path.exists(core_path):
target = core_path
else:
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if not path:
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found&#34;)
module = p_manager._import_from_path(target)
module = p_manager._import_from_path(path)
parser = module.Parser().parser if hasattr(module, &#34;Parser&#34;) else None
if &#34;__func_name__&#34; in args_dict and hasattr(module, args_dict[&#34;__func_name__&#34;]):
@@ -425,6 +508,10 @@ el.replaceWith(d);
raise InvalidConfigurationError(f&#34;Failed to delete plugin file &#39;{f}&#39;: {e}&#34;)
if not deleted:
# If not deleted from user directory, check if it&#39;s in shared or core
path, origin, enabled = self._get_plugin_path(name, include_disabled=True)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
raise InvalidConfigurationError(&#34;Global and core plugins are read-only and cannot be deleted by users.&#34;)
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)</code></pre>
</details>
<div class="desc"><p>Remove a plugin file permanently.</p></div>
@@ -443,17 +530,31 @@ el.replaceWith(d);
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
disabled_file = f&#34;{plugin_file}.bkp&#34;
if os.path.exists(disabled_file):
return False # Already disabled
if not os.path.exists(plugin_file):
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found or is a core plugin.&#34;)
if os.path.exists(plugin_file):
# Regular user-level plugin exists. Rename to bkp
try:
os.rename(plugin_file, disabled_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to disable plugin &#39;{name}&#39;: {e}&#34;)</code></pre>
raise InvalidConfigurationError(f&#34;Failed to disable plugin &#39;{name}&#39;: {e}&#34;)
if os.path.exists(disabled_file):
return False # Already disabled
# Check if it exists in shared or core
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
# Shadow disable it by creating an empty .py.bkp in user plugins dir
plugin_dir = os.path.dirname(plugin_file)
os.makedirs(plugin_dir, exist_ok=True)
try:
with open(disabled_file, &#34;w&#34;) as f:
f.write(&#34;&#34;)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to create shadow disable file: {e}&#34;)
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found or is already disabled.&#34;)</code></pre>
</details>
<div class="desc"><p>Deactivate a plugin by renaming it to a backup file.</p></div>
</dd>
@@ -471,17 +572,38 @@ el.replaceWith(d);
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
disabled_file = f&#34;{plugin_file}.bkp&#34;
if os.path.exists(plugin_file):
return False # Already enabled
if not os.path.exists(disabled_file):
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)
if os.path.exists(disabled_file):
# Check if it is a shadow bkp file (0 bytes shadowing shared/core)
is_shadow = False
if os.path.getsize(disabled_file) == 0:
# Resolve without the local bkp file to verify if shared/core has it
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
is_shadow = True
if is_shadow:
# Remove shadow file to restore inheritance
try:
os.remove(disabled_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to remove shadow file &#39;{disabled_file}&#39;: {e}&#34;)
else:
try:
os.rename(disabled_file, plugin_file)
return True
except OSError as e:
raise InvalidConfigurationError(f&#34;Failed to enable plugin &#39;{name}&#39;: {e}&#34;)</code></pre>
raise InvalidConfigurationError(f&#34;Failed to enable plugin &#39;{name}&#39;: {e}&#34;)
if os.path.exists(plugin_file):
return False # Already enabled
# If it doesn&#39;t exist locally, check if it&#39;s already an active shared/core plugin
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if origin in [&#34;shared&#34;, &#34;core&#34;]:
return False # Already active/enabled through inheritance
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found.&#34;)</code></pre>
</details>
<div class="desc"><p>Activate a plugin by renaming its backup file.</p></div>
</dd>
@@ -497,17 +619,11 @@ el.replaceWith(d);
import os
from ..services.exceptions import InvalidConfigurationError
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
core_path = os.path.dirname(os.path.realpath(__file__)) + f&#34;/../core_plugins/{name}.py&#34;
if os.path.exists(plugin_file):
target = plugin_file
elif os.path.exists(core_path):
target = core_path
else:
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if not path:
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found&#34;)
with open(target, &#34;r&#34;) as f:
with open(path, &#34;r&#34;) as f:
return f.read()</code></pre>
</details>
<div class="desc"></div>
@@ -557,17 +673,12 @@ el.replaceWith(d);
p_manager = Plugins()
import os
plugin_file = os.path.join(self.config.defaultdir, &#34;plugins&#34;, f&#34;{name}.py&#34;)
core_path = os.path.dirname(os.path.realpath(__file__)) + f&#34;/../core_plugins/{name}.py&#34;
if os.path.exists(plugin_file):
target = plugin_file
elif os.path.exists(core_path):
target = core_path
else:
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
if not path:
raise InvalidConfigurationError(f&#34;Plugin &#39;{name}&#39; not found&#34;)
module = p_manager._import_from_path(target)
module = p_manager._import_from_path(path)
parser = module.Parser().parser if hasattr(module, &#34;Parser&#34;) else None
if &#34;__func_name__&#34; in args_dict and hasattr(module, args_dict[&#34;__func_name__&#34;]):
@@ -636,11 +747,6 @@ el.replaceWith(d);
import os
import hashlib
# Check for user plugins directory
plugin_dir = os.path.join(self.config.defaultdir, &#34;plugins&#34;)
# Check for core plugins directory
core_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), &#34;..&#34;, &#34;core_plugins&#34;)
all_plugin_info = {}
def get_hash(path):
@@ -650,12 +756,35 @@ el.replaceWith(d);
except Exception:
return &#34;&#34;
# User plugins
if os.path.exists(plugin_dir):
for f in os.listdir(plugin_dir):
# 1. Scan core plugins (lowest priority)
core_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), &#34;..&#34;, &#34;core_plugins&#34;)
if os.path.exists(core_dir):
for f in os.listdir(core_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(plugin_dir, f)
path = os.path.join(core_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
# 2. Scan shared plugins (medium priority)
if hasattr(self.config, &#34;_shared_config&#34;) and self.config._shared_config:
shared_dir = os.path.join(self.config._shared_config.defaultdir, &#34;plugins&#34;)
if os.path.exists(shared_dir):
for f in os.listdir(shared_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(shared_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
elif f.endswith(&#34;.py.bkp&#34;):
name = f[:-7]
all_plugin_info[name] = {&#34;enabled&#34;: False}
# 3. Scan user plugins (highest priority)
user_dir = os.path.join(self.config.defaultdir, &#34;plugins&#34;)
if os.path.exists(user_dir):
for f in os.listdir(user_dir):
if f.endswith(&#34;.py&#34;):
name = f[:-3]
path = os.path.join(user_dir, f)
all_plugin_info[name] = {&#34;enabled&#34;: True, &#34;hash&#34;: get_hash(path)}
elif f.endswith(&#34;.py.bkp&#34;):
name = f[:-7]
@@ -709,7 +838,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.profile_service 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>
@@ -429,7 +429,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+30 -4
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.provider 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>
@@ -98,6 +98,7 @@ el.replaceWith(d);
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)
@@ -109,6 +110,7 @@ el.replaceWith(d);
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
@@ -118,14 +120,37 @@ el.replaceWith(d);
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;)
import grpc
from ..grpc_layer.stubs import NodeStub, ProfileStub, PluginStub, AIStub, ExecutionStub, ImportExportStub, SystemStub
import os
from ..grpc_layer.stubs import (
NodeStub, ProfileStub, PluginStub, AIStub,
ExecutionStub, ImportExportStub, SystemStub,
ConfigStub, AuthClientInterceptor, AuthStub
)
def get_token():
token_path = os.path.join(self.config.defaultdir, &#34;.token&#34;)
if os.path.exists(token_path):
try:
with open(token_path, &#34;r&#34;) as f:
return f.read().strip()
except Exception:
pass
return None
channel = grpc.insecure_channel(self.remote_host)
interceptor = AuthClientInterceptor(get_token)
channel = grpc.intercept_channel(channel, interceptor)
# Surgical fix: Keep ConfigService local for mode/theme management,
# but delegate encryption to the server stub.
config_remote = ConfigStub(channel, remote_host=self.remote_host)
self.config_svc.encrypt_password = config_remote.encrypt_password
self.nodes = NodeStub(channel, remote_host=self.remote_host, config=self.config)
self.profiles = ProfileStub(channel, remote_host=self.remote_host, node_stub=self.nodes)
@@ -133,7 +158,8 @@ el.replaceWith(d);
self.ai = AIStub(channel, remote_host=self.remote_host)
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)</code></pre>
self.import_export = ImportExportStub(channel, remote_host=self.remote_host)
self.auth = AuthStub(channel, remote_host=self.remote_host)</code></pre>
</details>
<div class="desc"><p>Dynamic service backend. Transparently provides local or remote services.</p></div>
</dd>
@@ -164,7 +190,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.sync_service 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>
@@ -964,7 +964,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.services.system_service 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>
@@ -325,7 +325,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+595
View File
@@ -0,0 +1,595 @@
<!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.services.user_service 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.services.user_service</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.services.user_service.UserService"><code class="flex name class">
<span>class <span class="ident">UserService</span></span>
<span>(</span><span>config_dir)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class UserService:
def __init__(self, config_dir):
self.config_dir = os.path.abspath(config_dir)
self.users_dir = os.path.join(self.config_dir, &#34;users&#34;)
self.registry_file = os.path.join(self.users_dir, &#34;registry.yaml&#34;)
# Ensure users directory exists
os.makedirs(self.users_dir, exist_ok=True)
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;
if not os.path.exists(self.registry_file):
registry = {
&#34;jwt_secret&#34;: secrets.token_hex(32),
&#34;users&#34;: {}
}
self._save_registry(registry)
return registry
try:
with open(self.registry_file, &#34;r&#34;) as f:
registry = yaml.safe_load(f) or {}
except Exception:
registry = {}
if not isinstance(registry, dict):
registry = {}
if &#34;jwt_secret&#34; not in registry:
registry[&#34;jwt_secret&#34;] = secrets.token_hex(32)
if &#34;users&#34; not in registry or not isinstance(registry[&#34;users&#34;], dict):
registry[&#34;users&#34;] = {}
return registry
def _save_registry(self, data: dict):
&#34;&#34;&#34;Safely saves registry structure to registry.yaml.&#34;&#34;&#34;
tmp_file = self.registry_file + &#34;.tmp&#34;
try:
with open(tmp_file, &#34;w&#34;) as f:
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
os.replace(tmp_file, self.registry_file)
os.chmod(self.registry_file, 0o600)
except Exception as e:
if os.path.exists(tmp_file):
try:
os.remove(tmp_file)
except OSError:
pass
raise e
def create_user(self, username, password, config_path=None) -&gt; dict:
&#34;&#34;&#34;Creates a new user with bcrypt-hashed credentials.
Mode A: config_path=None (fresh user) -&gt; Generates config.yaml and .osk key.
Mode B: config_path set -&gt; Reuses existing directory after validating its structure.
&#34;&#34;&#34;
if not username or not isinstance(username, str):
raise ValueError(&#34;Username cannot be empty&#34;)
if not re.match(r&#34;^[a-zA-Z0-9_-]+$&#34;, username):
raise ValueError(&#34;Username must contain only alphanumeric characters, dashes, or underscores&#34;)
if not password or not isinstance(password, str):
raise ValueError(&#34;Password cannot be empty&#34;)
registry = self._load_registry()
if username in registry[&#34;users&#34;]:
raise ValueError(f&#34;User &#39;{username}&#39; already exists&#34;)
# Resolve path and initialize configuration
if config_path is None:
user_dir = os.path.join(self.users_dir, username)
os.makedirs(user_dir, exist_ok=True)
# Create subdirs for plugins and sessions
os.makedirs(os.path.join(user_dir, &#34;plugins&#34;), exist_ok=True)
os.makedirs(os.path.join(user_dir, &#34;ai_sessions&#34;), exist_ok=True)
# Create default config.yaml &amp; .osk key via configfile
conf_file = os.path.join(user_dir, &#34;config.yaml&#34;)
configfile(conf=conf_file)
stored_config_path = None
else:
abs_config_path = os.path.abspath(config_path)
os.makedirs(abs_config_path, exist_ok=True)
# Create subdirs for plugins and sessions in the custom path
os.makedirs(os.path.join(abs_config_path, &#34;plugins&#34;), exist_ok=True)
os.makedirs(os.path.join(abs_config_path, &#34;ai_sessions&#34;), exist_ok=True)
# Create default config.yaml &amp; .osk key via configfile if config.yaml is not present
conf_file = os.path.join(abs_config_path, &#34;config.yaml&#34;)
if not os.path.exists(conf_file):
configfile(conf=conf_file)
stored_config_path = abs_config_path
# Hash password securely
password_hash = bcrypt.hashpw(password.encode(&#34;utf-8&#34;), bcrypt.gensalt()).decode(&#34;utf-8&#34;)
user_entry = {
&#34;password_hash&#34;: password_hash,
&#34;config_path&#34;: stored_config_path,
&#34;created&#34;: datetime.datetime.now(datetime.timezone.utc).isoformat()
}
registry[&#34;users&#34;][username] = user_entry
self._save_registry(registry)
return {
&#34;username&#34;: username,
&#34;config_path&#34;: stored_config_path,
&#34;created&#34;: user_entry[&#34;created&#34;]
}
def delete_user(self, username):
&#34;&#34;&#34;Removes user from the registry and cleans up config directory if server-managed.&#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;)
user_data = registry[&#34;users&#34;][username]
config_path = user_data.get(&#34;config_path&#34;)
if config_path is None:
user_dir = os.path.join(self.users_dir, username)
if os.path.exists(user_dir):
shutil.rmtree(user_dir, ignore_errors=True)
del registry[&#34;users&#34;][username]
self._save_registry(registry)
def list_users(self) -&gt; list[dict]:
&#34;&#34;&#34;Lists all registered users with metadata.&#34;&#34;&#34;
registry = self._load_registry()
return [
{
&#34;username&#34;: name,
&#34;config_path&#34;: data.get(&#34;config_path&#34;),
&#34;created&#34;: data.get(&#34;created&#34;)
}
for name, data in registry.get(&#34;users&#34;, {}).items()
]
def get_user(self, username) -&gt; dict:
&#34;&#34;&#34;Retrieves raw metadata for a specific user.&#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;)
data = registry[&#34;users&#34;][username]
return {
&#34;username&#34;: username,
&#34;config_path&#34;: data.get(&#34;config_path&#34;),
&#34;created&#34;: data.get(&#34;created&#34;),
&#34;password_hash&#34;: data.get(&#34;password_hash&#34;)
}
def change_password(self, username, old_password, new_password):
&#34;&#34;&#34;Verifies old password and updates registry with new hashed password.&#34;&#34;&#34;
if not new_password or not isinstance(new_password, str):
raise ValueError(&#34;New password 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 not bcrypt.checkpw(old_password.encode(&#34;utf-8&#34;), user_data[&#34;password_hash&#34;].encode(&#34;utf-8&#34;)):
raise ValueError(&#34;Invalid credentials&#34;)
# Update hash
user_data[&#34;password_hash&#34;] = bcrypt.hashpw(new_password.encode(&#34;utf-8&#34;), bcrypt.gensalt()).decode(&#34;utf-8&#34;)
self._save_registry(registry)
def admin_change_password(self, username, new_password):
&#34;&#34;&#34;Administrative password override (does not require old password).&#34;&#34;&#34;
if not new_password or not isinstance(new_password, str):
raise ValueError(&#34;New password 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]
user_data[&#34;password_hash&#34;] = bcrypt.hashpw(new_password.encode(&#34;utf-8&#34;), bcrypt.gensalt()).decode(&#34;utf-8&#34;)
self._save_registry(registry)
def authenticate(self, username, password) -&gt; bool:
&#34;&#34;&#34;Verifies if the credentials are valid using bcrypt.&#34;&#34;&#34;
registry = self._load_registry()
if username not in registry[&#34;users&#34;]:
return False
user_data = registry[&#34;users&#34;][username]
return bcrypt.checkpw(password.encode(&#34;utf-8&#34;), user_data[&#34;password_hash&#34;].encode(&#34;utf-8&#34;))
def generate_jwt(self, username) -&gt; str:
&#34;&#34;&#34;Generates a secure JSON Web Token for the user expiring in 8 hours.&#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;)
expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=8)
payload = {
&#34;sub&#34;: username,
&#34;exp&#34;: expiration
}
token = jwt.encode(payload, registry[&#34;jwt_secret&#34;], algorithm=&#34;HS256&#34;)
if isinstance(token, bytes):
token = token.decode(&#34;utf-8&#34;)
return token
def verify_jwt(self, token) -&gt; str | None:
&#34;&#34;&#34;Decodes JWT and returns username if token is valid and unexpired.&#34;&#34;&#34;
registry = self._load_registry()
try:
payload = jwt.decode(token, registry[&#34;jwt_secret&#34;], algorithms=[&#34;HS256&#34;])
return payload.get(&#34;sub&#34;)
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError, KeyError):
return None</code></pre>
</details>
<div class="desc"></div>
<h3>Methods</h3>
<dl>
<dt id="connpy.services.user_service.UserService.admin_change_password"><code class="name flex">
<span>def <span class="ident">admin_change_password</span></span>(<span>self, username, new_password)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def admin_change_password(self, username, new_password):
&#34;&#34;&#34;Administrative password override (does not require old password).&#34;&#34;&#34;
if not new_password or not isinstance(new_password, str):
raise ValueError(&#34;New password 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]
user_data[&#34;password_hash&#34;] = bcrypt.hashpw(new_password.encode(&#34;utf-8&#34;), bcrypt.gensalt()).decode(&#34;utf-8&#34;)
self._save_registry(registry)</code></pre>
</details>
<div class="desc"><p>Administrative password override (does not require old password).</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.authenticate"><code class="name flex">
<span>def <span class="ident">authenticate</span></span>(<span>self, username, password) > bool</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def authenticate(self, username, password) -&gt; bool:
&#34;&#34;&#34;Verifies if the credentials are valid using bcrypt.&#34;&#34;&#34;
registry = self._load_registry()
if username not in registry[&#34;users&#34;]:
return False
user_data = registry[&#34;users&#34;][username]
return bcrypt.checkpw(password.encode(&#34;utf-8&#34;), user_data[&#34;password_hash&#34;].encode(&#34;utf-8&#34;))</code></pre>
</details>
<div class="desc"><p>Verifies if the credentials are valid using bcrypt.</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.change_password"><code class="name flex">
<span>def <span class="ident">change_password</span></span>(<span>self, username, old_password, new_password)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def change_password(self, username, old_password, new_password):
&#34;&#34;&#34;Verifies old password and updates registry with new hashed password.&#34;&#34;&#34;
if not new_password or not isinstance(new_password, str):
raise ValueError(&#34;New password 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 not bcrypt.checkpw(old_password.encode(&#34;utf-8&#34;), user_data[&#34;password_hash&#34;].encode(&#34;utf-8&#34;)):
raise ValueError(&#34;Invalid credentials&#34;)
# Update hash
user_data[&#34;password_hash&#34;] = bcrypt.hashpw(new_password.encode(&#34;utf-8&#34;), bcrypt.gensalt()).decode(&#34;utf-8&#34;)
self._save_registry(registry)</code></pre>
</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_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>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def create_user(self, username, password, config_path=None) -&gt; dict:
&#34;&#34;&#34;Creates a new user with bcrypt-hashed credentials.
Mode A: config_path=None (fresh user) -&gt; Generates config.yaml and .osk key.
Mode B: config_path set -&gt; Reuses existing directory after validating its structure.
&#34;&#34;&#34;
if not username or not isinstance(username, str):
raise ValueError(&#34;Username cannot be empty&#34;)
if not re.match(r&#34;^[a-zA-Z0-9_-]+$&#34;, username):
raise ValueError(&#34;Username must contain only alphanumeric characters, dashes, or underscores&#34;)
if not password or not isinstance(password, str):
raise ValueError(&#34;Password cannot be empty&#34;)
registry = self._load_registry()
if username in registry[&#34;users&#34;]:
raise ValueError(f&#34;User &#39;{username}&#39; already exists&#34;)
# Resolve path and initialize configuration
if config_path is None:
user_dir = os.path.join(self.users_dir, username)
os.makedirs(user_dir, exist_ok=True)
# Create subdirs for plugins and sessions
os.makedirs(os.path.join(user_dir, &#34;plugins&#34;), exist_ok=True)
os.makedirs(os.path.join(user_dir, &#34;ai_sessions&#34;), exist_ok=True)
# Create default config.yaml &amp; .osk key via configfile
conf_file = os.path.join(user_dir, &#34;config.yaml&#34;)
configfile(conf=conf_file)
stored_config_path = None
else:
abs_config_path = os.path.abspath(config_path)
os.makedirs(abs_config_path, exist_ok=True)
# Create subdirs for plugins and sessions in the custom path
os.makedirs(os.path.join(abs_config_path, &#34;plugins&#34;), exist_ok=True)
os.makedirs(os.path.join(abs_config_path, &#34;ai_sessions&#34;), exist_ok=True)
# Create default config.yaml &amp; .osk key via configfile if config.yaml is not present
conf_file = os.path.join(abs_config_path, &#34;config.yaml&#34;)
if not os.path.exists(conf_file):
configfile(conf=conf_file)
stored_config_path = abs_config_path
# Hash password securely
password_hash = bcrypt.hashpw(password.encode(&#34;utf-8&#34;), bcrypt.gensalt()).decode(&#34;utf-8&#34;)
user_entry = {
&#34;password_hash&#34;: password_hash,
&#34;config_path&#34;: stored_config_path,
&#34;created&#34;: datetime.datetime.now(datetime.timezone.utc).isoformat()
}
registry[&#34;users&#34;][username] = user_entry
self._save_registry(registry)
return {
&#34;username&#34;: username,
&#34;config_path&#34;: stored_config_path,
&#34;created&#34;: user_entry[&#34;created&#34;]
}</code></pre>
</details>
<div class="desc"><p>Creates a new user with bcrypt-hashed credentials.</p>
<p>Mode A: config_path=None (fresh user) -&gt; Generates config.yaml and .osk key.
Mode B: config_path set -&gt; Reuses existing directory after validating its structure.</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.delete_user"><code class="name flex">
<span>def <span class="ident">delete_user</span></span>(<span>self, username)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def delete_user(self, username):
&#34;&#34;&#34;Removes user from the registry and cleans up config directory if server-managed.&#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;)
user_data = registry[&#34;users&#34;][username]
config_path = user_data.get(&#34;config_path&#34;)
if config_path is None:
user_dir = os.path.join(self.users_dir, username)
if os.path.exists(user_dir):
shutil.rmtree(user_dir, ignore_errors=True)
del registry[&#34;users&#34;][username]
self._save_registry(registry)</code></pre>
</details>
<div class="desc"><p>Removes user from the registry and cleans up config directory if server-managed.</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.generate_jwt"><code class="name flex">
<span>def <span class="ident">generate_jwt</span></span>(<span>self, username) > str</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def generate_jwt(self, username) -&gt; str:
&#34;&#34;&#34;Generates a secure JSON Web Token for the user expiring in 8 hours.&#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;)
expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=8)
payload = {
&#34;sub&#34;: username,
&#34;exp&#34;: expiration
}
token = jwt.encode(payload, registry[&#34;jwt_secret&#34;], algorithm=&#34;HS256&#34;)
if isinstance(token, bytes):
token = token.decode(&#34;utf-8&#34;)
return token</code></pre>
</details>
<div class="desc"><p>Generates a secure JSON Web Token for the user expiring in 8 hours.</p></div>
</dd>
<dt id="connpy.services.user_service.UserService.get_user"><code class="name flex">
<span>def <span class="ident">get_user</span></span>(<span>self, username) > dict</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def get_user(self, username) -&gt; dict:
&#34;&#34;&#34;Retrieves raw metadata for a specific user.&#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;)
data = registry[&#34;users&#34;][username]
return {
&#34;username&#34;: username,
&#34;config_path&#34;: data.get(&#34;config_path&#34;),
&#34;created&#34;: data.get(&#34;created&#34;),
&#34;password_hash&#34;: data.get(&#34;password_hash&#34;)
}</code></pre>
</details>
<div class="desc"><p>Retrieves raw metadata for a specific user.</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>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def list_users(self) -&gt; list[dict]:
&#34;&#34;&#34;Lists all registered users with metadata.&#34;&#34;&#34;
registry = self._load_registry()
return [
{
&#34;username&#34;: name,
&#34;config_path&#34;: data.get(&#34;config_path&#34;),
&#34;created&#34;: data.get(&#34;created&#34;)
}
for name, data in registry.get(&#34;users&#34;, {}).items()
]</code></pre>
</details>
<div class="desc"><p>Lists all registered users with metadata.</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>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def verify_jwt(self, token) -&gt; str | None:
&#34;&#34;&#34;Decodes JWT and returns username if token is valid and unexpired.&#34;&#34;&#34;
registry = self._load_registry()
try:
payload = jwt.decode(token, registry[&#34;jwt_secret&#34;], algorithms=[&#34;HS256&#34;])
return payload.get(&#34;sub&#34;)
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError, KeyError):
return None</code></pre>
</details>
<div class="desc"><p>Decodes JWT and returns username if token is valid and unexpired.</p></div>
</dd>
</dl>
</dd>
</dl>
</section>
</article>
<nav id="sidebar">
<div class="toc">
<ul></ul>
</div>
<ul id="index">
<li><h3>Super-module</h3>
<ul>
<li><code><a title="connpy.services" href="index.html">connpy.services</a></code></li>
</ul>
</li>
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
<li>
<h4><code><a title="connpy.services.user_service.UserService" href="#connpy.services.user_service.UserService">UserService</a></code></h4>
<ul class="">
<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_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_users" href="#connpy.services.user_service.UserService.list_users">list_users</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>
</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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.tunnels 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>
@@ -545,7 +545,7 @@ Bridges the blocking gRPC iterators with the async _async_interact_loop.</p></di
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.6">
<meta name="generator" content="pdoc3 0.11.5">
<title>connpy.utils API documentation</title>
<meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -147,7 +147,7 @@ el.replaceWith(d);
</nav>
</main>
<footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
<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>