new version and docs
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, "action", None)
|
||||
if action == "login":
|
||||
return self.login(args)
|
||||
elif action == "logout":
|
||||
return self.logout(args)
|
||||
else:
|
||||
printer.error(f"Unknown action: {action}")
|
||||
sys.exit(1)
|
||||
|
||||
def login(self, args):
|
||||
if getattr(args, "status", False):
|
||||
return self.show_status()
|
||||
|
||||
if self.app.services.mode != "remote":
|
||||
printer.warning("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 'remote'.")
|
||||
|
||||
username = getattr(args, "username", None)
|
||||
if not username:
|
||||
try:
|
||||
username = input("Username: ").strip()
|
||||
if not username:
|
||||
printer.error("Username cannot be empty.")
|
||||
sys.exit(1)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.warning("\nOperation cancelled.")
|
||||
sys.exit(130)
|
||||
|
||||
try:
|
||||
password = getpass.getpass("Password: ")
|
||||
if not password:
|
||||
printer.error("Password cannot be empty.")
|
||||
sys.exit(1)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.warning("\nOperation cancelled.")
|
||||
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's instantiate it dynamically if it's not present.
|
||||
auth_service = getattr(self.app.services, "auth", 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("remote_host")
|
||||
if not remote_host:
|
||||
printer.error("Remote host is not configured. Run 'connpy config --remote HOST:PORT' first.")
|
||||
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"Failed to connect to remote server for login: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
res = auth_service.login(username, password)
|
||||
token = res["token"]
|
||||
|
||||
# Save token to ~/.config/conn/.token
|
||||
token_path = os.path.join(self.app.config.defaultdir, ".token")
|
||||
with open(token_path, "w") as f:
|
||||
f.write(token)
|
||||
os.chmod(token_path, 0o600)
|
||||
|
||||
printer.success(f"Logged in successfully as '{username}'. Session expires in 8 hours.")
|
||||
except ConnpyError as e:
|
||||
printer.error(f"Login failed: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
printer.error(f"Login failed with unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def logout(self, args):
|
||||
token_path = os.path.join(self.app.config.defaultdir, ".token")
|
||||
if os.path.exists(token_path):
|
||||
try:
|
||||
os.remove(token_path)
|
||||
printer.success("Logged out successfully. Local session cleared.")
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to clear session: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
printer.info("No active session found (already logged out).")
|
||||
|
||||
def show_status(self):
|
||||
import base64
|
||||
import json
|
||||
import datetime
|
||||
|
||||
token_path = os.path.join(self.app.config.defaultdir, ".token")
|
||||
if not os.path.exists(token_path):
|
||||
printer.warning("No active session found. You can log in using 'connpy login'.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(token_path, "r") as f:
|
||||
token = f.read().strip()
|
||||
|
||||
parts = token.split(".")
|
||||
if len(parts) != 3:
|
||||
printer.error("Invalid local session token format.")
|
||||
return
|
||||
|
||||
payload_b64 = parts[1]
|
||||
payload_b64 += "=" * ((4 - len(payload_b64) % 4) % 4)
|
||||
payload_bytes = base64.urlsafe_b64decode(payload_b64)
|
||||
payload = json.loads(payload_bytes.decode("utf-8"))
|
||||
|
||||
username = payload.get("sub")
|
||||
exp = payload.get("exp")
|
||||
|
||||
if not exp:
|
||||
printer.success(f"Active session as '{username}' (Indefinite expiration).")
|
||||
return
|
||||
|
||||
now = datetime.datetime.now(datetime.timezone.utc).timestamp()
|
||||
if now > exp:
|
||||
printer.error("Session has expired. Please log in again using 'connpy login'.")
|
||||
return
|
||||
|
||||
remaining = exp - now
|
||||
hours = int(remaining // 3600)
|
||||
minutes = int((remaining % 3600) // 60)
|
||||
|
||||
printer.success(f"Logged in as '{username}'")
|
||||
printer.info(f"Time remaining: {hours}h {minutes}m")
|
||||
|
||||
exp_dt = datetime.datetime.fromtimestamp(exp, datetime.timezone.utc)
|
||||
printer.info(f"Expires at: {exp_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to check local session status: {e}")</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, "action", None)
|
||||
if action == "login":
|
||||
return self.login(args)
|
||||
elif action == "logout":
|
||||
return self.logout(args)
|
||||
else:
|
||||
printer.error(f"Unknown action: {action}")
|
||||
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, "status", False):
|
||||
return self.show_status()
|
||||
|
||||
if self.app.services.mode != "remote":
|
||||
printer.warning("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 'remote'.")
|
||||
|
||||
username = getattr(args, "username", None)
|
||||
if not username:
|
||||
try:
|
||||
username = input("Username: ").strip()
|
||||
if not username:
|
||||
printer.error("Username cannot be empty.")
|
||||
sys.exit(1)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.warning("\nOperation cancelled.")
|
||||
sys.exit(130)
|
||||
|
||||
try:
|
||||
password = getpass.getpass("Password: ")
|
||||
if not password:
|
||||
printer.error("Password cannot be empty.")
|
||||
sys.exit(1)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.warning("\nOperation cancelled.")
|
||||
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's instantiate it dynamically if it's not present.
|
||||
auth_service = getattr(self.app.services, "auth", 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("remote_host")
|
||||
if not remote_host:
|
||||
printer.error("Remote host is not configured. Run 'connpy config --remote HOST:PORT' first.")
|
||||
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"Failed to connect to remote server for login: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
res = auth_service.login(username, password)
|
||||
token = res["token"]
|
||||
|
||||
# Save token to ~/.config/conn/.token
|
||||
token_path = os.path.join(self.app.config.defaultdir, ".token")
|
||||
with open(token_path, "w") as f:
|
||||
f.write(token)
|
||||
os.chmod(token_path, 0o600)
|
||||
|
||||
printer.success(f"Logged in successfully as '{username}'. Session expires in 8 hours.")
|
||||
except ConnpyError as e:
|
||||
printer.error(f"Login failed: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
printer.error(f"Login failed with unexpected error: {e}")
|
||||
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, ".token")
|
||||
if os.path.exists(token_path):
|
||||
try:
|
||||
os.remove(token_path)
|
||||
printer.success("Logged out successfully. Local session cleared.")
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to clear session: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
printer.info("No active session found (already logged out).")</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, ".token")
|
||||
if not os.path.exists(token_path):
|
||||
printer.warning("No active session found. You can log in using 'connpy login'.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(token_path, "r") as f:
|
||||
token = f.read().strip()
|
||||
|
||||
parts = token.split(".")
|
||||
if len(parts) != 3:
|
||||
printer.error("Invalid local session token format.")
|
||||
return
|
||||
|
||||
payload_b64 = parts[1]
|
||||
payload_b64 += "=" * ((4 - len(payload_b64) % 4) % 4)
|
||||
payload_bytes = base64.urlsafe_b64decode(payload_b64)
|
||||
payload = json.loads(payload_bytes.decode("utf-8"))
|
||||
|
||||
username = payload.get("sub")
|
||||
exp = payload.get("exp")
|
||||
|
||||
if not exp:
|
||||
printer.success(f"Active session as '{username}' (Indefinite expiration).")
|
||||
return
|
||||
|
||||
now = datetime.datetime.now(datetime.timezone.utc).timestamp()
|
||||
if now > exp:
|
||||
printer.error("Session has expired. Please log in again using 'connpy login'.")
|
||||
return
|
||||
|
||||
remaining = exp - now
|
||||
hours = int(remaining // 3600)
|
||||
minutes = int((remaining % 3600) // 60)
|
||||
|
||||
printer.success(f"Logged in as '{username}'")
|
||||
printer.info(f"Time remaining: {hours}h {minutes}m")
|
||||
|
||||
exp_dt = datetime.datetime.fromtimestamp(exp, datetime.timezone.utc)
|
||||
printer.info(f"Expires at: {exp_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to check local session status: {e}")</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>
|
||||
@@ -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) <= 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"{matches[0]} deleted successfully")
|
||||
@@ -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"{args.data} edited successfully")
|
||||
else:
|
||||
editcount = 0
|
||||
changed_items = []
|
||||
for k in matches:
|
||||
updated_item = self.app.services.nodes.explode_unique(k)
|
||||
updated_item["type"] = "connection"
|
||||
@@ -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("Nothing to do here")
|
||||
@@ -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"{matches[0]} deleted successfully")
|
||||
@@ -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"{args.data} edited successfully")
|
||||
else:
|
||||
editcount = 0
|
||||
changed_items = []
|
||||
for k in matches:
|
||||
updated_item = self.app.services.nodes.explode_unique(k)
|
||||
updated_item["type"] = "connection"
|
||||
@@ -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("Nothing to do here")
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"No nodes found matching filter: {nodes_filter}")
|
||||
sys.exit(2)
|
||||
|
||||
commands = [" ".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, "stdout"] else None
|
||||
prompt = options.get("prompt")
|
||||
|
||||
# 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"[{name}] No nodes found matching filter: {nodelist}")
|
||||
sys.exit(11)
|
||||
|
||||
nodelist = resolved_nodes
|
||||
|
||||
try:
|
||||
header_printed = False
|
||||
if action == "run":
|
||||
@@ -242,6 +275,28 @@ el.replaceWith(d);
|
||||
folder = output_cfg if output_cfg not in [None, "stdout"] else None
|
||||
prompt = options.get("prompt")
|
||||
|
||||
# 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"[{name}] No nodes found matching filter: {nodelist}")
|
||||
sys.exit(11)
|
||||
|
||||
nodelist = resolved_nodes
|
||||
|
||||
try:
|
||||
header_printed = False
|
||||
if action == "run":
|
||||
@@ -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"No nodes found matching filter: {nodes_filter}")
|
||||
sys.exit(2)
|
||||
|
||||
commands = [" ".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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) -> 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>
|
||||
|
||||
@@ -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 == "remote":
|
||||
printer.error("User management commands are only available in local/server-side mode.")
|
||||
sys.exit(1)
|
||||
|
||||
# Parse actions from argparse mutually exclusive options
|
||||
if getattr(args, "add", None):
|
||||
args.action = "add"
|
||||
args.username = args.add[0]
|
||||
elif getattr(args, "delete", None):
|
||||
args.action = "del"
|
||||
args.username = args.delete[0]
|
||||
elif getattr(args, "list", False):
|
||||
args.action = "list"
|
||||
elif getattr(args, "show", None):
|
||||
args.action = "show"
|
||||
args.username = args.show[0]
|
||||
elif getattr(args, "regen_password", None):
|
||||
args.action = "regen_password"
|
||||
args.username = args.regen_password[0]
|
||||
|
||||
action = getattr(args, "action", None)
|
||||
|
||||
if action == "add":
|
||||
return self.add_user(args)
|
||||
elif action == "del":
|
||||
return self.delete_user(args)
|
||||
elif action == "list":
|
||||
return self.list_users(args)
|
||||
elif action == "show":
|
||||
return self.show_user(args)
|
||||
elif action == "regen_password":
|
||||
return self.regen_password(args)
|
||||
else:
|
||||
printer.error(f"Unknown action: {action}")
|
||||
sys.exit(1)
|
||||
|
||||
def add_user(self, args):
|
||||
username = getattr(args, "username", None)
|
||||
if not username:
|
||||
printer.error("Username is required. Usage: connpy user --add <username>")
|
||||
sys.exit(1)
|
||||
|
||||
custom_path = getattr(args, "path", None)
|
||||
if custom_path:
|
||||
custom_path = custom_path[0] if isinstance(custom_path, list) else custom_path
|
||||
|
||||
try:
|
||||
password = getpass.getpass("Enter password for new user: ")
|
||||
if not password:
|
||||
printer.error("Password cannot be empty.")
|
||||
sys.exit(1)
|
||||
confirm = getpass.getpass("Confirm password: ")
|
||||
if password != confirm:
|
||||
printer.error("Passwords do not match.")
|
||||
sys.exit(1)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.warning("\nOperation cancelled.")
|
||||
sys.exit(130)
|
||||
|
||||
try:
|
||||
self.app.services.users.create_user(username, password, config_path=custom_path)
|
||||
printer.success(f"User '{username}' created successfully.")
|
||||
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"Failed to create user: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def delete_user(self, args):
|
||||
username = getattr(args, "username", None)
|
||||
if not username:
|
||||
printer.error("Username is required. Usage: connpy user --del <username>")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
self.app.services.users.delete_user(username)
|
||||
printer.success(f"User '{username}' deleted successfully.")
|
||||
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"Failed to delete user: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def list_users(self, args):
|
||||
try:
|
||||
users = self.app.services.users.list_users()
|
||||
if not users:
|
||||
printer.warning("No users registered.")
|
||||
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("config_path"):
|
||||
formatted_u["config_path"] = os.path.join(self.app.services.users.users_dir, formatted_u["username"])
|
||||
formatted_users.append(formatted_u)
|
||||
|
||||
yaml_str = yaml.dump(formatted_users, sort_keys=False, default_flow_style=False)
|
||||
printer.data("Registered Users", yaml_str)
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to list users: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def show_user(self, args):
|
||||
username = getattr(args, "username", None)
|
||||
if not username:
|
||||
printer.error("Username is required. Usage: connpy user --show <username>")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
user = self.app.services.users.get_user(username)
|
||||
if not user:
|
||||
printer.error(f"User '{username}' not found.")
|
||||
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 != "password_hash"}
|
||||
if not safe_user.get("config_path"):
|
||||
safe_user["config_path"] = 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"User: {username}", yaml_str)
|
||||
except ValueError as e:
|
||||
printer.error(str(e))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to retrieve user details: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def regen_password(self, args):
|
||||
username = getattr(args, "username", None)
|
||||
if not username:
|
||||
printer.error("Username is required. Usage: connpy user --regen-password <username>")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
user = self.app.services.users.get_user(username)
|
||||
if not user:
|
||||
printer.error(f"User '{username}' not found.")
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
printer.error(str(e))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to retrieve user details: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
new_password = getpass.getpass("Enter new password: ")
|
||||
if not new_password:
|
||||
printer.error("Password cannot be empty.")
|
||||
sys.exit(1)
|
||||
confirm = getpass.getpass("Confirm new password: ")
|
||||
if new_password != confirm:
|
||||
printer.error("Passwords do not match.")
|
||||
sys.exit(1)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.warning("\nOperation cancelled.")
|
||||
sys.exit(130)
|
||||
|
||||
try:
|
||||
self.app.services.users.admin_change_password(username, new_password)
|
||||
printer.success(f"Password for user '{username}' regenerated successfully.")
|
||||
except ValueError as e:
|
||||
printer.error(str(e))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to regenerate password: {e}")
|
||||
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, "username", None)
|
||||
if not username:
|
||||
printer.error("Username is required. Usage: connpy user --add <username>")
|
||||
sys.exit(1)
|
||||
|
||||
custom_path = getattr(args, "path", None)
|
||||
if custom_path:
|
||||
custom_path = custom_path[0] if isinstance(custom_path, list) else custom_path
|
||||
|
||||
try:
|
||||
password = getpass.getpass("Enter password for new user: ")
|
||||
if not password:
|
||||
printer.error("Password cannot be empty.")
|
||||
sys.exit(1)
|
||||
confirm = getpass.getpass("Confirm password: ")
|
||||
if password != confirm:
|
||||
printer.error("Passwords do not match.")
|
||||
sys.exit(1)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.warning("\nOperation cancelled.")
|
||||
sys.exit(130)
|
||||
|
||||
try:
|
||||
self.app.services.users.create_user(username, password, config_path=custom_path)
|
||||
printer.success(f"User '{username}' created successfully.")
|
||||
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"Failed to create user: {e}")
|
||||
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, "username", None)
|
||||
if not username:
|
||||
printer.error("Username is required. Usage: connpy user --del <username>")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
self.app.services.users.delete_user(username)
|
||||
printer.success(f"User '{username}' deleted successfully.")
|
||||
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"Failed to delete user: {e}")
|
||||
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 == "remote":
|
||||
printer.error("User management commands are only available in local/server-side mode.")
|
||||
sys.exit(1)
|
||||
|
||||
# Parse actions from argparse mutually exclusive options
|
||||
if getattr(args, "add", None):
|
||||
args.action = "add"
|
||||
args.username = args.add[0]
|
||||
elif getattr(args, "delete", None):
|
||||
args.action = "del"
|
||||
args.username = args.delete[0]
|
||||
elif getattr(args, "list", False):
|
||||
args.action = "list"
|
||||
elif getattr(args, "show", None):
|
||||
args.action = "show"
|
||||
args.username = args.show[0]
|
||||
elif getattr(args, "regen_password", None):
|
||||
args.action = "regen_password"
|
||||
args.username = args.regen_password[0]
|
||||
|
||||
action = getattr(args, "action", None)
|
||||
|
||||
if action == "add":
|
||||
return self.add_user(args)
|
||||
elif action == "del":
|
||||
return self.delete_user(args)
|
||||
elif action == "list":
|
||||
return self.list_users(args)
|
||||
elif action == "show":
|
||||
return self.show_user(args)
|
||||
elif action == "regen_password":
|
||||
return self.regen_password(args)
|
||||
else:
|
||||
printer.error(f"Unknown action: {action}")
|
||||
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("No users registered.")
|
||||
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("config_path"):
|
||||
formatted_u["config_path"] = os.path.join(self.app.services.users.users_dir, formatted_u["username"])
|
||||
formatted_users.append(formatted_u)
|
||||
|
||||
yaml_str = yaml.dump(formatted_users, sort_keys=False, default_flow_style=False)
|
||||
printer.data("Registered Users", yaml_str)
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to list users: {e}")
|
||||
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, "username", None)
|
||||
if not username:
|
||||
printer.error("Username is required. Usage: connpy user --regen-password <username>")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
user = self.app.services.users.get_user(username)
|
||||
if not user:
|
||||
printer.error(f"User '{username}' not found.")
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
printer.error(str(e))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to retrieve user details: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
new_password = getpass.getpass("Enter new password: ")
|
||||
if not new_password:
|
||||
printer.error("Password cannot be empty.")
|
||||
sys.exit(1)
|
||||
confirm = getpass.getpass("Confirm new password: ")
|
||||
if new_password != confirm:
|
||||
printer.error("Passwords do not match.")
|
||||
sys.exit(1)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.warning("\nOperation cancelled.")
|
||||
sys.exit(130)
|
||||
|
||||
try:
|
||||
self.app.services.users.admin_change_password(username, new_password)
|
||||
printer.success(f"Password for user '{username}' regenerated successfully.")
|
||||
except ValueError as e:
|
||||
printer.error(str(e))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to regenerate password: {e}")
|
||||
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, "username", None)
|
||||
if not username:
|
||||
printer.error("Username is required. Usage: connpy user --show <username>")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
user = self.app.services.users.get_user(username)
|
||||
if not user:
|
||||
printer.error(f"User '{username}' not found.")
|
||||
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 != "password_hash"}
|
||||
if not safe_user.get("config_path"):
|
||||
safe_user["config_path"] = 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"User: {username}", yaml_str)
|
||||
except ValueError as e:
|
||||
printer.error(str(e))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to retrieve user details: {e}")
|
||||
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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
'login': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.login,
|
||||
request_deserializer=connpy__pb2.LoginRequest.FromString,
|
||||
response_serializer=connpy__pb2.LoginResponse.SerializeToString,
|
||||
),
|
||||
'change_password': 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(
|
||||
'connpy.AuthService', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
server.add_registered_method_handlers('connpy.AuthService', 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):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
@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,
|
||||
'/connpy.AuthService/login',
|
||||
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,
|
||||
'/connpy.AuthService/change_password',
|
||||
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,
|
||||
'/connpy.AuthService/change_password',
|
||||
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,
|
||||
'/connpy.AuthService/login',
|
||||
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):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def login(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def change_password(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')</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):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')</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):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')</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):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.login = channel.unary_unary(
|
||||
'/connpy.AuthService/login',
|
||||
request_serializer=connpy__pb2.LoginRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.LoginResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.change_password = channel.unary_unary(
|
||||
'/connpy.AuthService/change_password',
|
||||
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>
|
||||
|
||||
@@ -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
@@ -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("response"):
|
||||
# If nothing streamed but we have response (e.g. error or direct guide)
|
||||
printer.console.print(Panel(Markdown(final_result["response"]), 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("response"):
|
||||
# If nothing streamed but we have response (e.g. error or direct guide)
|
||||
printer.console.print(Panel(Markdown(final_result["response"]), 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() == "authorization" for k, v in metadata):
|
||||
metadata.append(("authorization", f"Bearer {token}"))
|
||||
|
||||
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 {
|
||||
"token": resp.token,
|
||||
"username": resp.username,
|
||||
"expires_at": 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 {
|
||||
"token": resp.token,
|
||||
"username": resp.username,
|
||||
"expires_at": 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,16 +1758,18 @@ 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)
|
||||
self._trigger_local_cache_sync()
|
||||
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)
|
||||
self._trigger_local_cache_sync()
|
||||
if save:
|
||||
self._trigger_local_cache_sync()
|
||||
|
||||
@handle_errors
|
||||
def move_node(self, src_id, dst_id, copy=False):
|
||||
@@ -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,10 +2158,11 @@ 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)
|
||||
self._trigger_local_cache_sync()</code></pre>
|
||||
if save:
|
||||
self._trigger_local_cache_sync()</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
@@ -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,10 +2330,11 @@ 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)
|
||||
self._trigger_local_cache_sync()</code></pre>
|
||||
if save:
|
||||
self._trigger_local_cache_sync()</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
"""Holds per-user ServiceProviders in memory, thread-safe with hot-reloading."""
|
||||
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, "config.yaml")
|
||||
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):
|
||||
"""Hot-reload shared config if the file changed on disk."""
|
||||
if not os.path.exists(self._shared_conf_file):
|
||||
return
|
||||
current_mtime = os.path.getmtime(self._shared_conf_file)
|
||||
if current_mtime > 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"Failed to reload shared config: {e}")
|
||||
|
||||
def get_provider(self, username) -> ServiceProvider:
|
||||
"""Get, lazy-load, or hot-reload a user's full ServiceProvider."""
|
||||
with self._lock:
|
||||
# Refresh shared/global config if it has changed
|
||||
self._refresh_shared()
|
||||
|
||||
# 1. Resolve physical path of the user's config.yaml file
|
||||
user_data = self.user_service.get_user(username)
|
||||
config_path = user_data.get("config_path")
|
||||
if config_path:
|
||||
conf_file = os.path.join(config_path, "config.yaml")
|
||||
else:
|
||||
conf_file = os.path.join(self.server_config_dir, "users", username, "config.yaml")
|
||||
|
||||
# 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) < 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="local")
|
||||
|
||||
# Successfully loaded, clean up the old provider
|
||||
if old_provider:
|
||||
self._providers.pop(username, None)
|
||||
if hasattr(old_provider, "close"):
|
||||
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"Failed to hot-reload config for user '{username}' (file may be corrupt/incomplete): {e}")
|
||||
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) -> bool:
|
||||
"""Check if any users are registered (enables auth enforcement)."""
|
||||
return bool(self.user_service.list_users())
|
||||
|
||||
def evict(self, username):
|
||||
"""Remove and cleanly shut down cached provider (after delete or password change)."""
|
||||
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, "close"):
|
||||
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):
|
||||
"""Remove and cleanly shut down cached provider (after delete or password change)."""
|
||||
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, "close"):
|
||||
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) -> ServiceProvider:
|
||||
"""Get, lazy-load, or hot-reload a user's full ServiceProvider."""
|
||||
with self._lock:
|
||||
# Refresh shared/global config if it has changed
|
||||
self._refresh_shared()
|
||||
|
||||
# 1. Resolve physical path of the user's config.yaml file
|
||||
user_data = self.user_service.get_user(username)
|
||||
config_path = user_data.get("config_path")
|
||||
if config_path:
|
||||
conf_file = os.path.join(config_path, "config.yaml")
|
||||
else:
|
||||
conf_file = os.path.join(self.server_config_dir, "users", username, "config.yaml")
|
||||
|
||||
# 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) < 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="local")
|
||||
|
||||
# Successfully loaded, clean up the old provider
|
||||
if old_provider:
|
||||
self._providers.pop(username, None)
|
||||
if hasattr(old_provider, "close"):
|
||||
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"Failed to hot-reload config for user '{username}' (file may be corrupt/incomplete): {e}")
|
||||
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) -> bool:
|
||||
"""Check if any users are registered (enables auth enforcement)."""
|
||||
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>
|
||||
@@ -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
@@ -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="<p align="center">
|
||||
<img src="https://nginx.gederico.dynu.net/images/CONNPY-resized.png" alt="App Logo">
|
||||
@@ -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 & 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("ai", {})
|
||||
# 1. Cargar configuración genérica con herencia/merge global
|
||||
if hasattr(self.config, "get_effective_setting"):
|
||||
aiconfig = self.config.get_effective_setting("ai", {})
|
||||
else:
|
||||
aiconfig = self.config.config.get("ai", {}) if hasattr(self.config, "config") else {}
|
||||
|
||||
# Modelos (Prioridad: Argumento -> Config -> Default)
|
||||
self.engineer_model = engineer_model or aiconfig.get("engineer_model") or "gemini/gemini-3.1-flash-lite"
|
||||
@@ -1534,9 +1541,11 @@ 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 = "vertex" in self.engineer_model.lower() or "ollama" in self.engineer_model.lower() or "local" in self.engineer_model.lower()
|
||||
if not self.engineer_key and not self.engineer_auth and not is_engineer_keyless:
|
||||
raise ValueError("Engineer API key or authentication not configured. Use 'connpy config --engineer-auth <auth>' to set it.")
|
||||
|
||||
|
||||
if chat_history is None: chat_history = []
|
||||
|
||||
@@ -2144,7 +2153,7 @@ Node: {node_name}"""
|
||||
<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,9 +2488,11 @@ Node: {node_name}"""
|
||||
</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 = "vertex" in self.engineer_model.lower() or "ollama" in self.engineer_model.lower() or "local" in self.engineer_model.lower()
|
||||
if not self.engineer_key and not self.engineer_auth and not is_engineer_keyless:
|
||||
raise ValueError("Engineer API key or authentication not configured. Use 'connpy config --engineer-auth <auth>' to set it.")
|
||||
|
||||
|
||||
if chat_history is None: chat_history = []
|
||||
|
||||
@@ -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.
|
||||
'''
|
||||
|
||||
def __init__(self, conf = None, key = None):
|
||||
def __init__(self, conf = None, key = None, shared_config = None):
|
||||
self._shared_config = shared_config
|
||||
'''
|
||||
|
||||
### Optional Parameters:
|
||||
@@ -3323,6 +3335,42 @@ class configfile:
|
||||
self._generate_nodes_cache()
|
||||
|
||||
|
||||
def get_effective_setting(self, key, default=None):
|
||||
"""Get config setting with shared fallback for inheritable keys."""
|
||||
val = self.config.get(key)
|
||||
if key == "ai":
|
||||
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 "engineer_api_key" in val or "engineer_auth" in val:
|
||||
base.pop("engineer_api_key", None)
|
||||
base.pop("engineer_auth", None)
|
||||
# If user defines architect credentials, discard shared ones
|
||||
if "architect_api_key" in val or "architect_auth" in val:
|
||||
base.pop("architect_api_key", None)
|
||||
base.pop("architect_auth", 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):
|
||||
"""Verify config data has the required structure."""
|
||||
if not isinstance(data, dict):
|
||||
@@ -3663,7 +3711,8 @@ class configfile:
|
||||
else:
|
||||
printer.error("Filter must be a string or a list of strings")
|
||||
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("case", 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):
|
||||
"""Get config setting with shared fallback for inheritable keys."""
|
||||
val = self.config.get(key)
|
||||
if key == "ai":
|
||||
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 "engineer_api_key" in val or "engineer_auth" in val:
|
||||
base.pop("engineer_api_key", None)
|
||||
base.pop("engineer_auth", None)
|
||||
# If user defines architect credentials, discard shared ones
|
||||
if "architect_api_key" in val or "architect_auth" in val:
|
||||
base.pop("architect_api_key", None)
|
||||
base.pop("architect_auth", 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" {self.options}"
|
||||
return cmd
|
||||
|
||||
@MethodHook
|
||||
def _generate_ssm_cmd(self):
|
||||
region = self.tags.get("region", "") if isinstance(self.tags, dict) else ""
|
||||
profile = self.tags.get("profile", "") if isinstance(self.tags, dict) else ""
|
||||
cmd = f"aws ssm start-session --target {self.host}"
|
||||
if region:
|
||||
cmd += f" --region {region}"
|
||||
if profile:
|
||||
cmd += f" --profile {profile}"
|
||||
if self.options:
|
||||
cmd += f" {self.options}"
|
||||
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>
|
||||
|
||||
@@ -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("ai", {}).get("mcp_servers", {})
|
||||
if hasattr(self.config, "get_effective_setting"):
|
||||
mcp_config = self.config.get_effective_setting("ai", {}).get("mcp_servers", {})
|
||||
else:
|
||||
mcp_config = self.config.config.get("ai", {}).get("mcp_servers", {}) if hasattr(self.config, "config") else {}
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
@@ -260,7 +263,10 @@ el.replaceWith(d);
|
||||
|
||||
all_llm_tools = []
|
||||
try:
|
||||
mcp_config = self.config.config.get("ai", {}).get("mcp_servers", {})
|
||||
if hasattr(self.config, "get_effective_setting"):
|
||||
mcp_config = self.config.get_effective_setting("ai", {}).get("mcp_servers", {})
|
||||
else:
|
||||
mcp_config = self.config.config.get("ai", {}).get("mcp_servers", {}) if hasattr(self.config, "config") 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) -> dict:
|
||||
"""Get the configured MCP servers."""
|
||||
ai_settings = self.config.config.get("ai", {})
|
||||
if hasattr(self.config, "get_effective_setting"):
|
||||
ai_settings = self.config.get_effective_setting("ai", {})
|
||||
else:
|
||||
ai_settings = self.config.config.get("ai", {}) if hasattr(self.config, "config") else {}
|
||||
return ai_settings.get("mcp_servers", {})
|
||||
|
||||
def load_session_data(self, session_id):
|
||||
@@ -669,7 +672,10 @@ el.replaceWith(d);
|
||||
</summary>
|
||||
<pre><code class="python">def list_mcp_servers(self) -> dict:
|
||||
"""Get the configured MCP servers."""
|
||||
ai_settings = self.config.config.get("ai", {})
|
||||
if hasattr(self.config, "get_effective_setting"):
|
||||
ai_settings = self.config.get_effective_setting("ai", {})
|
||||
else:
|
||||
ai_settings = self.config.config.get("ai", {}) if hasattr(self.config, "config") else {}
|
||||
return ai_settings.get("mcp_servers", {})</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) -> 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+240
-96
@@ -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) -> dict:
|
||||
"""Get the configured MCP servers."""
|
||||
ai_settings = self.config.config.get("ai", {})
|
||||
if hasattr(self.config, "get_effective_setting"):
|
||||
ai_settings = self.config.get_effective_setting("ai", {})
|
||||
else:
|
||||
ai_settings = self.config.config.get("ai", {}) if hasattr(self.config, "config") else {}
|
||||
return ai_settings.get("mcp_servers", {})
|
||||
|
||||
def load_session_data(self, session_id):
|
||||
@@ -724,7 +731,10 @@ el.replaceWith(d);
|
||||
</summary>
|
||||
<pre><code class="python">def list_mcp_servers(self) -> dict:
|
||||
"""Get the configured MCP servers."""
|
||||
ai_settings = self.config.config.get("ai", {})
|
||||
if hasattr(self.config, "get_effective_setting"):
|
||||
ai_settings = self.config.get_effective_setting("ai", {})
|
||||
else:
|
||||
ai_settings = self.config.config.get("ai", {}) if hasattr(self.config, "config") else {}
|
||||
return ai_settings.get("mcp_servers", {})</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):
|
||||
"""Explicitly update an existing node."""
|
||||
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)
|
||||
self.config._saveconfig(self.config.file)
|
||||
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):
|
||||
"""Logic for deleting a node or folder."""
|
||||
if is_folder:
|
||||
uniques = self.config._explode_unique(unique_id)
|
||||
@@ -2037,7 +2048,8 @@ el.replaceWith(d);
|
||||
raise NodeNotFoundError(f"Node '{unique_id}' not found or invalid.")
|
||||
self.config._connections_del(**uniques)
|
||||
|
||||
self.config._saveconfig(self.config.file)
|
||||
if save:
|
||||
self.config._saveconfig(self.config.file)
|
||||
|
||||
def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
|
||||
"""Interact with a node directly."""
|
||||
@@ -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):
|
||||
"""Logic for deleting a node or folder."""
|
||||
if is_folder:
|
||||
uniques = self.config._explode_unique(unique_id)
|
||||
@@ -2287,7 +2299,8 @@ el.replaceWith(d);
|
||||
raise NodeNotFoundError(f"Node '{unique_id}' not found or invalid.")
|
||||
self.config._connections_del(**uniques)
|
||||
|
||||
self.config._saveconfig(self.config.file)</code></pre>
|
||||
if save:
|
||||
self.config._saveconfig(self.config.file)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Logic for deleting a node or folder.</p></div>
|
||||
</dd>
|
||||
@@ -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):
|
||||
"""Explicitly update an existing node."""
|
||||
all_nodes = self.config._getallnodes()
|
||||
if unique_id not in all_nodes:
|
||||
@@ -2517,7 +2530,8 @@ el.replaceWith(d);
|
||||
|
||||
# config._connections_add actually handles updates if ID exists correctly
|
||||
self.config._connections_add(**data)
|
||||
self.config._saveconfig(self.config.file)</code></pre>
|
||||
if save:
|
||||
self.config._saveconfig(self.config.file)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Explicitly update an existing node.</p></div>
|
||||
</dd>
|
||||
@@ -2568,16 +2582,47 @@ el.replaceWith(d);
|
||||
<pre><code class="python">class PluginService(BaseService):
|
||||
"""Business logic for enabling, disabling, and listing plugins."""
|
||||
|
||||
def _get_plugin_path(self, name, include_disabled=True):
|
||||
"""Resolves the physical path of a plugin by name. Priority: user, shared/global, core."""
|
||||
import os
|
||||
|
||||
# 1. User directory
|
||||
user_dir = os.path.join(self.config.defaultdir, "plugins")
|
||||
if os.path.exists(user_dir):
|
||||
p_file = os.path.join(user_dir, f"{name}.py")
|
||||
if os.path.exists(p_file):
|
||||
return p_file, "user", True
|
||||
if include_disabled:
|
||||
bkp_file = os.path.join(user_dir, f"{name}.py.bkp")
|
||||
if os.path.exists(bkp_file):
|
||||
return bkp_file, "user", False
|
||||
|
||||
# 2. Shared/Global directory
|
||||
if hasattr(self.config, "_shared_config") and self.config._shared_config:
|
||||
shared_dir = os.path.join(self.config._shared_config.defaultdir, "plugins")
|
||||
if os.path.exists(shared_dir):
|
||||
p_file = os.path.join(shared_dir, f"{name}.py")
|
||||
if os.path.exists(p_file):
|
||||
return p_file, "shared", True
|
||||
if include_disabled:
|
||||
bkp_file = os.path.join(shared_dir, f"{name}.py.bkp")
|
||||
if os.path.exists(bkp_file):
|
||||
return bkp_file, "shared", False
|
||||
|
||||
# 3. Core plugins
|
||||
core_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "core_plugins")
|
||||
p_file = os.path.join(core_dir, f"{name}.py")
|
||||
if os.path.exists(p_file):
|
||||
return p_file, "core", True
|
||||
|
||||
return None, None, False
|
||||
|
||||
|
||||
def list_plugins(self):
|
||||
"""List all core and user-defined plugins with their status and hash."""
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
# Check for user plugins directory
|
||||
plugin_dir = os.path.join(self.config.defaultdir, "plugins")
|
||||
# Check for core plugins directory
|
||||
core_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "core_plugins")
|
||||
|
||||
all_plugin_info = {}
|
||||
|
||||
def get_hash(path):
|
||||
@@ -2587,12 +2632,35 @@ el.replaceWith(d);
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
# 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__)), "..", "core_plugins")
|
||||
if os.path.exists(core_dir):
|
||||
for f in os.listdir(core_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(plugin_dir, f)
|
||||
path = os.path.join(core_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
|
||||
# 2. Scan shared plugins (medium priority)
|
||||
if hasattr(self.config, "_shared_config") and self.config._shared_config:
|
||||
shared_dir = os.path.join(self.config._shared_config.defaultdir, "plugins")
|
||||
if os.path.exists(shared_dir):
|
||||
for f in os.listdir(shared_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(shared_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
elif f.endswith(".py.bkp"):
|
||||
name = f[:-7]
|
||||
all_plugin_info[name] = {"enabled": False}
|
||||
|
||||
# 3. Scan user plugins (highest priority)
|
||||
user_dir = os.path.join(self.config.defaultdir, "plugins")
|
||||
if os.path.exists(user_dir):
|
||||
for f in os.listdir(user_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(user_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
elif f.endswith(".py.bkp"):
|
||||
name = f[:-7]
|
||||
@@ -2600,6 +2668,7 @@ el.replaceWith(d);
|
||||
|
||||
return all_plugin_info
|
||||
|
||||
|
||||
def add_plugin(self, name, source_file, update=False):
|
||||
"""Add or update a plugin from a local file."""
|
||||
import os
|
||||
@@ -2680,6 +2749,10 @@ el.replaceWith(d);
|
||||
raise InvalidConfigurationError(f"Failed to delete plugin file '{f}': {e}")
|
||||
|
||||
if not deleted:
|
||||
# If not deleted from user directory, check if it's in shared or core
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=True)
|
||||
if origin in ["shared", "core"]:
|
||||
raise InvalidConfigurationError("Global and core plugins are read-only and cannot be deleted by users.")
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")
|
||||
|
||||
def enable_plugin(self, name):
|
||||
@@ -2688,17 +2761,38 @@ el.replaceWith(d);
|
||||
plugin_file = os.path.join(self.config.defaultdir, "plugins", f"{name}.py")
|
||||
disabled_file = f"{plugin_file}.bkp"
|
||||
|
||||
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 ["shared", "core"]:
|
||||
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"Failed to remove shadow file '{disabled_file}': {e}")
|
||||
else:
|
||||
try:
|
||||
os.rename(disabled_file, plugin_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to enable plugin '{name}': {e}")
|
||||
|
||||
if os.path.exists(plugin_file):
|
||||
return False # Already enabled
|
||||
|
||||
if not os.path.exists(disabled_file):
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")
|
||||
# If it doesn't exist locally, check if it's already an active shared/core plugin
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
|
||||
if origin in ["shared", "core"]:
|
||||
return False # Already active/enabled through inheritance
|
||||
|
||||
try:
|
||||
os.rename(disabled_file, plugin_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to enable plugin '{name}': {e}")
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")
|
||||
|
||||
def disable_plugin(self, name):
|
||||
"""Deactivate a plugin by renaming it to a backup file."""
|
||||
@@ -2706,33 +2800,41 @@ el.replaceWith(d);
|
||||
plugin_file = os.path.join(self.config.defaultdir, "plugins", f"{name}.py")
|
||||
disabled_file = f"{plugin_file}.bkp"
|
||||
|
||||
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"Failed to disable plugin '{name}': {e}")
|
||||
|
||||
if os.path.exists(disabled_file):
|
||||
return False # Already disabled
|
||||
|
||||
if not os.path.exists(plugin_file):
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found or is a core plugin.")
|
||||
|
||||
try:
|
||||
os.rename(plugin_file, disabled_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to disable plugin '{name}': {e}")
|
||||
# Check if it exists in shared or core
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
|
||||
if origin in ["shared", "core"]:
|
||||
# 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, "w") as f:
|
||||
f.write("")
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to create shadow disable file: {e}")
|
||||
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found or is already disabled.")
|
||||
|
||||
def get_plugin_source(self, name):
|
||||
import os
|
||||
from ..services.exceptions import InvalidConfigurationError
|
||||
|
||||
plugin_file = os.path.join(self.config.defaultdir, "plugins", f"{name}.py")
|
||||
core_path = os.path.dirname(os.path.realpath(__file__)) + f"/../core_plugins/{name}.py"
|
||||
|
||||
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"Plugin '{name}' not found")
|
||||
|
||||
with open(target, "r") as f:
|
||||
with open(path, "r") 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, "plugins", f"{name}.py")
|
||||
core_path = os.path.dirname(os.path.realpath(__file__)) + f"/../core_plugins/{name}.py"
|
||||
|
||||
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"Plugin '{name}' not found")
|
||||
|
||||
module = p_manager._import_from_path(target)
|
||||
module = p_manager._import_from_path(path)
|
||||
parser = module.Parser().parser if hasattr(module, "Parser") else None
|
||||
|
||||
if "__func_name__" in args_dict and hasattr(module, args_dict["__func_name__"]):
|
||||
@@ -2935,6 +3032,10 @@ el.replaceWith(d);
|
||||
raise InvalidConfigurationError(f"Failed to delete plugin file '{f}': {e}")
|
||||
|
||||
if not deleted:
|
||||
# If not deleted from user directory, check if it's in shared or core
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=True)
|
||||
if origin in ["shared", "core"]:
|
||||
raise InvalidConfigurationError("Global and core plugins are read-only and cannot be deleted by users.")
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")</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, "plugins", f"{name}.py")
|
||||
disabled_file = f"{plugin_file}.bkp"
|
||||
|
||||
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"Failed to disable plugin '{name}': {e}")
|
||||
|
||||
if os.path.exists(disabled_file):
|
||||
return False # Already disabled
|
||||
|
||||
if not os.path.exists(plugin_file):
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found or is a core plugin.")
|
||||
|
||||
try:
|
||||
os.rename(plugin_file, disabled_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to disable plugin '{name}': {e}")</code></pre>
|
||||
# Check if it exists in shared or core
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
|
||||
if origin in ["shared", "core"]:
|
||||
# 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, "w") as f:
|
||||
f.write("")
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to create shadow disable file: {e}")
|
||||
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found or is already disabled.")</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, "plugins", f"{name}.py")
|
||||
disabled_file = f"{plugin_file}.bkp"
|
||||
|
||||
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 ["shared", "core"]:
|
||||
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"Failed to remove shadow file '{disabled_file}': {e}")
|
||||
else:
|
||||
try:
|
||||
os.rename(disabled_file, plugin_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to enable plugin '{name}': {e}")
|
||||
|
||||
if os.path.exists(plugin_file):
|
||||
return False # Already enabled
|
||||
|
||||
if not os.path.exists(disabled_file):
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")
|
||||
# If it doesn't exist locally, check if it's already an active shared/core plugin
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
|
||||
if origin in ["shared", "core"]:
|
||||
return False # Already active/enabled through inheritance
|
||||
|
||||
try:
|
||||
os.rename(disabled_file, plugin_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to enable plugin '{name}': {e}")</code></pre>
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")</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, "plugins", f"{name}.py")
|
||||
core_path = os.path.dirname(os.path.realpath(__file__)) + f"/../core_plugins/{name}.py"
|
||||
|
||||
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"Plugin '{name}' not found")
|
||||
|
||||
with open(target, "r") as f:
|
||||
with open(path, "r") 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, "plugins", f"{name}.py")
|
||||
core_path = os.path.dirname(os.path.realpath(__file__)) + f"/../core_plugins/{name}.py"
|
||||
|
||||
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"Plugin '{name}' not found")
|
||||
|
||||
module = p_manager._import_from_path(target)
|
||||
module = p_manager._import_from_path(path)
|
||||
parser = module.Parser().parser if hasattr(module, "Parser") else None
|
||||
|
||||
if "__func_name__" in args_dict and hasattr(module, args_dict["__func_name__"]):
|
||||
@@ -3146,11 +3271,6 @@ el.replaceWith(d);
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
# Check for user plugins directory
|
||||
plugin_dir = os.path.join(self.config.defaultdir, "plugins")
|
||||
# Check for core plugins directory
|
||||
core_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "core_plugins")
|
||||
|
||||
all_plugin_info = {}
|
||||
|
||||
def get_hash(path):
|
||||
@@ -3160,12 +3280,35 @@ el.replaceWith(d);
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
# 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__)), "..", "core_plugins")
|
||||
if os.path.exists(core_dir):
|
||||
for f in os.listdir(core_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(plugin_dir, f)
|
||||
path = os.path.join(core_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
|
||||
# 2. Scan shared plugins (medium priority)
|
||||
if hasattr(self.config, "_shared_config") and self.config._shared_config:
|
||||
shared_dir = os.path.join(self.config._shared_config.defaultdir, "plugins")
|
||||
if os.path.exists(shared_dir):
|
||||
for f in os.listdir(shared_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(shared_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
elif f.endswith(".py.bkp"):
|
||||
name = f[:-7]
|
||||
all_plugin_info[name] = {"enabled": False}
|
||||
|
||||
# 3. Scan user plugins (highest priority)
|
||||
user_dir = os.path.join(self.config.defaultdir, "plugins")
|
||||
if os.path.exists(user_dir):
|
||||
for f in os.listdir(user_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(user_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
elif f.endswith(".py.bkp"):
|
||||
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>
|
||||
|
||||
@@ -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):
|
||||
"""Explicitly update an existing node."""
|
||||
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)
|
||||
self.config._saveconfig(self.config.file)
|
||||
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):
|
||||
"""Logic for deleting a node or folder."""
|
||||
if is_folder:
|
||||
uniques = self.config._explode_unique(unique_id)
|
||||
@@ -227,7 +228,8 @@ el.replaceWith(d);
|
||||
raise NodeNotFoundError(f"Node '{unique_id}' not found or invalid.")
|
||||
self.config._connections_del(**uniques)
|
||||
|
||||
self.config._saveconfig(self.config.file)
|
||||
if save:
|
||||
self.config._saveconfig(self.config.file)
|
||||
|
||||
def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
|
||||
"""Interact with a node directly."""
|
||||
@@ -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):
|
||||
"""Logic for deleting a node or folder."""
|
||||
if is_folder:
|
||||
uniques = self.config._explode_unique(unique_id)
|
||||
@@ -477,7 +479,8 @@ el.replaceWith(d);
|
||||
raise NodeNotFoundError(f"Node '{unique_id}' not found or invalid.")
|
||||
self.config._connections_del(**uniques)
|
||||
|
||||
self.config._saveconfig(self.config.file)</code></pre>
|
||||
if save:
|
||||
self.config._saveconfig(self.config.file)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Logic for deleting a node or folder.</p></div>
|
||||
</dd>
|
||||
@@ -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):
|
||||
"""Explicitly update an existing node."""
|
||||
all_nodes = self.config._getallnodes()
|
||||
if unique_id not in all_nodes:
|
||||
@@ -707,7 +710,8 @@ el.replaceWith(d);
|
||||
|
||||
# config._connections_add actually handles updates if ID exists correctly
|
||||
self.config._connections_add(**data)
|
||||
self.config._saveconfig(self.config.file)</code></pre>
|
||||
if save:
|
||||
self.config._saveconfig(self.config.file)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Explicitly update an existing node.</p></div>
|
||||
</dd>
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
"""Business logic for enabling, disabling, and listing plugins."""
|
||||
|
||||
def _get_plugin_path(self, name, include_disabled=True):
|
||||
"""Resolves the physical path of a plugin by name. Priority: user, shared/global, core."""
|
||||
import os
|
||||
|
||||
# 1. User directory
|
||||
user_dir = os.path.join(self.config.defaultdir, "plugins")
|
||||
if os.path.exists(user_dir):
|
||||
p_file = os.path.join(user_dir, f"{name}.py")
|
||||
if os.path.exists(p_file):
|
||||
return p_file, "user", True
|
||||
if include_disabled:
|
||||
bkp_file = os.path.join(user_dir, f"{name}.py.bkp")
|
||||
if os.path.exists(bkp_file):
|
||||
return bkp_file, "user", False
|
||||
|
||||
# 2. Shared/Global directory
|
||||
if hasattr(self.config, "_shared_config") and self.config._shared_config:
|
||||
shared_dir = os.path.join(self.config._shared_config.defaultdir, "plugins")
|
||||
if os.path.exists(shared_dir):
|
||||
p_file = os.path.join(shared_dir, f"{name}.py")
|
||||
if os.path.exists(p_file):
|
||||
return p_file, "shared", True
|
||||
if include_disabled:
|
||||
bkp_file = os.path.join(shared_dir, f"{name}.py.bkp")
|
||||
if os.path.exists(bkp_file):
|
||||
return bkp_file, "shared", False
|
||||
|
||||
# 3. Core plugins
|
||||
core_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "core_plugins")
|
||||
p_file = os.path.join(core_dir, f"{name}.py")
|
||||
if os.path.exists(p_file):
|
||||
return p_file, "core", True
|
||||
|
||||
return None, None, False
|
||||
|
||||
|
||||
def list_plugins(self):
|
||||
"""List all core and user-defined plugins with their status and hash."""
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
# Check for user plugins directory
|
||||
plugin_dir = os.path.join(self.config.defaultdir, "plugins")
|
||||
# Check for core plugins directory
|
||||
core_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "core_plugins")
|
||||
|
||||
all_plugin_info = {}
|
||||
|
||||
def get_hash(path):
|
||||
@@ -77,12 +108,35 @@ el.replaceWith(d);
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
# 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__)), "..", "core_plugins")
|
||||
if os.path.exists(core_dir):
|
||||
for f in os.listdir(core_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(plugin_dir, f)
|
||||
path = os.path.join(core_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
|
||||
# 2. Scan shared plugins (medium priority)
|
||||
if hasattr(self.config, "_shared_config") and self.config._shared_config:
|
||||
shared_dir = os.path.join(self.config._shared_config.defaultdir, "plugins")
|
||||
if os.path.exists(shared_dir):
|
||||
for f in os.listdir(shared_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(shared_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
elif f.endswith(".py.bkp"):
|
||||
name = f[:-7]
|
||||
all_plugin_info[name] = {"enabled": False}
|
||||
|
||||
# 3. Scan user plugins (highest priority)
|
||||
user_dir = os.path.join(self.config.defaultdir, "plugins")
|
||||
if os.path.exists(user_dir):
|
||||
for f in os.listdir(user_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(user_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
elif f.endswith(".py.bkp"):
|
||||
name = f[:-7]
|
||||
@@ -90,6 +144,7 @@ el.replaceWith(d);
|
||||
|
||||
return all_plugin_info
|
||||
|
||||
|
||||
def add_plugin(self, name, source_file, update=False):
|
||||
"""Add or update a plugin from a local file."""
|
||||
import os
|
||||
@@ -170,6 +225,10 @@ el.replaceWith(d);
|
||||
raise InvalidConfigurationError(f"Failed to delete plugin file '{f}': {e}")
|
||||
|
||||
if not deleted:
|
||||
# If not deleted from user directory, check if it's in shared or core
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=True)
|
||||
if origin in ["shared", "core"]:
|
||||
raise InvalidConfigurationError("Global and core plugins are read-only and cannot be deleted by users.")
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")
|
||||
|
||||
def enable_plugin(self, name):
|
||||
@@ -178,17 +237,38 @@ el.replaceWith(d);
|
||||
plugin_file = os.path.join(self.config.defaultdir, "plugins", f"{name}.py")
|
||||
disabled_file = f"{plugin_file}.bkp"
|
||||
|
||||
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 ["shared", "core"]:
|
||||
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"Failed to remove shadow file '{disabled_file}': {e}")
|
||||
else:
|
||||
try:
|
||||
os.rename(disabled_file, plugin_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to enable plugin '{name}': {e}")
|
||||
|
||||
if os.path.exists(plugin_file):
|
||||
return False # Already enabled
|
||||
|
||||
if not os.path.exists(disabled_file):
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")
|
||||
# If it doesn't exist locally, check if it's already an active shared/core plugin
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
|
||||
if origin in ["shared", "core"]:
|
||||
return False # Already active/enabled through inheritance
|
||||
|
||||
try:
|
||||
os.rename(disabled_file, plugin_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to enable plugin '{name}': {e}")
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")
|
||||
|
||||
def disable_plugin(self, name):
|
||||
"""Deactivate a plugin by renaming it to a backup file."""
|
||||
@@ -196,33 +276,41 @@ el.replaceWith(d);
|
||||
plugin_file = os.path.join(self.config.defaultdir, "plugins", f"{name}.py")
|
||||
disabled_file = f"{plugin_file}.bkp"
|
||||
|
||||
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"Failed to disable plugin '{name}': {e}")
|
||||
|
||||
if os.path.exists(disabled_file):
|
||||
return False # Already disabled
|
||||
|
||||
if not os.path.exists(plugin_file):
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found or is a core plugin.")
|
||||
|
||||
try:
|
||||
os.rename(plugin_file, disabled_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to disable plugin '{name}': {e}")
|
||||
# Check if it exists in shared or core
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
|
||||
if origin in ["shared", "core"]:
|
||||
# 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, "w") as f:
|
||||
f.write("")
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to create shadow disable file: {e}")
|
||||
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found or is already disabled.")
|
||||
|
||||
def get_plugin_source(self, name):
|
||||
import os
|
||||
from ..services.exceptions import InvalidConfigurationError
|
||||
|
||||
plugin_file = os.path.join(self.config.defaultdir, "plugins", f"{name}.py")
|
||||
core_path = os.path.dirname(os.path.realpath(__file__)) + f"/../core_plugins/{name}.py"
|
||||
|
||||
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"Plugin '{name}' not found")
|
||||
|
||||
with open(target, "r") as f:
|
||||
with open(path, "r") 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, "plugins", f"{name}.py")
|
||||
core_path = os.path.dirname(os.path.realpath(__file__)) + f"/../core_plugins/{name}.py"
|
||||
|
||||
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"Plugin '{name}' not found")
|
||||
|
||||
module = p_manager._import_from_path(target)
|
||||
module = p_manager._import_from_path(path)
|
||||
parser = module.Parser().parser if hasattr(module, "Parser") else None
|
||||
|
||||
if "__func_name__" in args_dict and hasattr(module, args_dict["__func_name__"]):
|
||||
@@ -425,6 +508,10 @@ el.replaceWith(d);
|
||||
raise InvalidConfigurationError(f"Failed to delete plugin file '{f}': {e}")
|
||||
|
||||
if not deleted:
|
||||
# If not deleted from user directory, check if it's in shared or core
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=True)
|
||||
if origin in ["shared", "core"]:
|
||||
raise InvalidConfigurationError("Global and core plugins are read-only and cannot be deleted by users.")
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")</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, "plugins", f"{name}.py")
|
||||
disabled_file = f"{plugin_file}.bkp"
|
||||
|
||||
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"Failed to disable plugin '{name}': {e}")
|
||||
|
||||
if os.path.exists(disabled_file):
|
||||
return False # Already disabled
|
||||
|
||||
if not os.path.exists(plugin_file):
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found or is a core plugin.")
|
||||
|
||||
try:
|
||||
os.rename(plugin_file, disabled_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to disable plugin '{name}': {e}")</code></pre>
|
||||
# Check if it exists in shared or core
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
|
||||
if origin in ["shared", "core"]:
|
||||
# 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, "w") as f:
|
||||
f.write("")
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to create shadow disable file: {e}")
|
||||
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found or is already disabled.")</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, "plugins", f"{name}.py")
|
||||
disabled_file = f"{plugin_file}.bkp"
|
||||
|
||||
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 ["shared", "core"]:
|
||||
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"Failed to remove shadow file '{disabled_file}': {e}")
|
||||
else:
|
||||
try:
|
||||
os.rename(disabled_file, plugin_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to enable plugin '{name}': {e}")
|
||||
|
||||
if os.path.exists(plugin_file):
|
||||
return False # Already enabled
|
||||
|
||||
if not os.path.exists(disabled_file):
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")
|
||||
# If it doesn't exist locally, check if it's already an active shared/core plugin
|
||||
path, origin, enabled = self._get_plugin_path(name, include_disabled=False)
|
||||
if origin in ["shared", "core"]:
|
||||
return False # Already active/enabled through inheritance
|
||||
|
||||
try:
|
||||
os.rename(disabled_file, plugin_file)
|
||||
return True
|
||||
except OSError as e:
|
||||
raise InvalidConfigurationError(f"Failed to enable plugin '{name}': {e}")</code></pre>
|
||||
raise InvalidConfigurationError(f"Plugin '{name}' not found.")</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, "plugins", f"{name}.py")
|
||||
core_path = os.path.dirname(os.path.realpath(__file__)) + f"/../core_plugins/{name}.py"
|
||||
|
||||
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"Plugin '{name}' not found")
|
||||
|
||||
with open(target, "r") as f:
|
||||
with open(path, "r") 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, "plugins", f"{name}.py")
|
||||
core_path = os.path.dirname(os.path.realpath(__file__)) + f"/../core_plugins/{name}.py"
|
||||
|
||||
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"Plugin '{name}' not found")
|
||||
|
||||
module = p_manager._import_from_path(target)
|
||||
module = p_manager._import_from_path(path)
|
||||
parser = module.Parser().parser if hasattr(module, "Parser") else None
|
||||
|
||||
if "__func_name__" in args_dict and hasattr(module, args_dict["__func_name__"]):
|
||||
@@ -636,11 +747,6 @@ el.replaceWith(d);
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
# Check for user plugins directory
|
||||
plugin_dir = os.path.join(self.config.defaultdir, "plugins")
|
||||
# Check for core plugins directory
|
||||
core_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "core_plugins")
|
||||
|
||||
all_plugin_info = {}
|
||||
|
||||
def get_hash(path):
|
||||
@@ -650,12 +756,35 @@ el.replaceWith(d);
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
# 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__)), "..", "core_plugins")
|
||||
if os.path.exists(core_dir):
|
||||
for f in os.listdir(core_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(plugin_dir, f)
|
||||
path = os.path.join(core_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
|
||||
# 2. Scan shared plugins (medium priority)
|
||||
if hasattr(self.config, "_shared_config") and self.config._shared_config:
|
||||
shared_dir = os.path.join(self.config._shared_config.defaultdir, "plugins")
|
||||
if os.path.exists(shared_dir):
|
||||
for f in os.listdir(shared_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(shared_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
elif f.endswith(".py.bkp"):
|
||||
name = f[:-7]
|
||||
all_plugin_info[name] = {"enabled": False}
|
||||
|
||||
# 3. Scan user plugins (highest priority)
|
||||
user_dir = os.path.join(self.config.defaultdir, "plugins")
|
||||
if os.path.exists(user_dir):
|
||||
for f in os.listdir(user_dir):
|
||||
if f.endswith(".py"):
|
||||
name = f[:-3]
|
||||
path = os.path.join(user_dir, f)
|
||||
all_plugin_info[name] = {"enabled": True, "hash": get_hash(path)}
|
||||
elif f.endswith(".py.bkp"):
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,22 +120,46 @@ 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("Remote host must be specified in remote mode")
|
||||
|
||||
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, ".token")
|
||||
if os.path.exists(token_path):
|
||||
try:
|
||||
with open(token_path, "r") 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)
|
||||
self.plugins = PluginStub(channel, remote_host=self.remote_host)
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, "users")
|
||||
self.registry_file = os.path.join(self.users_dir, "registry.yaml")
|
||||
|
||||
# Ensure users directory exists
|
||||
os.makedirs(self.users_dir, exist_ok=True)
|
||||
|
||||
def _load_registry(self) -> dict:
|
||||
"""Loads registry from file. If it doesn't exist, initializes it with a new JWT secret."""
|
||||
if not os.path.exists(self.registry_file):
|
||||
registry = {
|
||||
"jwt_secret": secrets.token_hex(32),
|
||||
"users": {}
|
||||
}
|
||||
self._save_registry(registry)
|
||||
return registry
|
||||
|
||||
try:
|
||||
with open(self.registry_file, "r") as f:
|
||||
registry = yaml.safe_load(f) or {}
|
||||
except Exception:
|
||||
registry = {}
|
||||
|
||||
if not isinstance(registry, dict):
|
||||
registry = {}
|
||||
|
||||
if "jwt_secret" not in registry:
|
||||
registry["jwt_secret"] = secrets.token_hex(32)
|
||||
|
||||
if "users" not in registry or not isinstance(registry["users"], dict):
|
||||
registry["users"] = {}
|
||||
|
||||
return registry
|
||||
|
||||
def _save_registry(self, data: dict):
|
||||
"""Safely saves registry structure to registry.yaml."""
|
||||
tmp_file = self.registry_file + ".tmp"
|
||||
try:
|
||||
with open(tmp_file, "w") 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) -> dict:
|
||||
"""Creates a new user with bcrypt-hashed credentials.
|
||||
|
||||
Mode A: config_path=None (fresh user) -> Generates config.yaml and .osk key.
|
||||
Mode B: config_path set -> Reuses existing directory after validating its structure.
|
||||
"""
|
||||
if not username or not isinstance(username, str):
|
||||
raise ValueError("Username cannot be empty")
|
||||
|
||||
if not re.match(r"^[a-zA-Z0-9_-]+$", username):
|
||||
raise ValueError("Username must contain only alphanumeric characters, dashes, or underscores")
|
||||
|
||||
if not password or not isinstance(password, str):
|
||||
raise ValueError("Password cannot be empty")
|
||||
|
||||
registry = self._load_registry()
|
||||
if username in registry["users"]:
|
||||
raise ValueError(f"User '{username}' already exists")
|
||||
|
||||
# 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, "plugins"), exist_ok=True)
|
||||
os.makedirs(os.path.join(user_dir, "ai_sessions"), exist_ok=True)
|
||||
|
||||
# Create default config.yaml & .osk key via configfile
|
||||
conf_file = os.path.join(user_dir, "config.yaml")
|
||||
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, "plugins"), exist_ok=True)
|
||||
os.makedirs(os.path.join(abs_config_path, "ai_sessions"), exist_ok=True)
|
||||
|
||||
# Create default config.yaml & .osk key via configfile if config.yaml is not present
|
||||
conf_file = os.path.join(abs_config_path, "config.yaml")
|
||||
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("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
|
||||
user_entry = {
|
||||
"password_hash": password_hash,
|
||||
"config_path": stored_config_path,
|
||||
"created": datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
registry["users"][username] = user_entry
|
||||
self._save_registry(registry)
|
||||
|
||||
return {
|
||||
"username": username,
|
||||
"config_path": stored_config_path,
|
||||
"created": user_entry["created"]
|
||||
}
|
||||
|
||||
def delete_user(self, username):
|
||||
"""Removes user from the registry and cleans up config directory if server-managed."""
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
raise ValueError(f"User '{username}' not found")
|
||||
|
||||
user_data = registry["users"][username]
|
||||
config_path = user_data.get("config_path")
|
||||
|
||||
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["users"][username]
|
||||
self._save_registry(registry)
|
||||
|
||||
def list_users(self) -> list[dict]:
|
||||
"""Lists all registered users with metadata."""
|
||||
registry = self._load_registry()
|
||||
return [
|
||||
{
|
||||
"username": name,
|
||||
"config_path": data.get("config_path"),
|
||||
"created": data.get("created")
|
||||
}
|
||||
for name, data in registry.get("users", {}).items()
|
||||
]
|
||||
|
||||
def get_user(self, username) -> dict:
|
||||
"""Retrieves raw metadata for a specific user."""
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
raise ValueError(f"User '{username}' not found")
|
||||
|
||||
data = registry["users"][username]
|
||||
return {
|
||||
"username": username,
|
||||
"config_path": data.get("config_path"),
|
||||
"created": data.get("created"),
|
||||
"password_hash": data.get("password_hash")
|
||||
}
|
||||
|
||||
def change_password(self, username, old_password, new_password):
|
||||
"""Verifies old password and updates registry with new hashed password."""
|
||||
if not new_password or not isinstance(new_password, str):
|
||||
raise ValueError("New password cannot be empty")
|
||||
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
raise ValueError(f"User '{username}' not found")
|
||||
|
||||
user_data = registry["users"][username]
|
||||
if not bcrypt.checkpw(old_password.encode("utf-8"), user_data["password_hash"].encode("utf-8")):
|
||||
raise ValueError("Invalid credentials")
|
||||
|
||||
# Update hash
|
||||
user_data["password_hash"] = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
self._save_registry(registry)
|
||||
|
||||
def admin_change_password(self, username, new_password):
|
||||
"""Administrative password override (does not require old password)."""
|
||||
if not new_password or not isinstance(new_password, str):
|
||||
raise ValueError("New password cannot be empty")
|
||||
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
raise ValueError(f"User '{username}' not found")
|
||||
|
||||
user_data = registry["users"][username]
|
||||
user_data["password_hash"] = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
self._save_registry(registry)
|
||||
|
||||
def authenticate(self, username, password) -> bool:
|
||||
"""Verifies if the credentials are valid using bcrypt."""
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
return False
|
||||
|
||||
user_data = registry["users"][username]
|
||||
return bcrypt.checkpw(password.encode("utf-8"), user_data["password_hash"].encode("utf-8"))
|
||||
|
||||
def generate_jwt(self, username) -> str:
|
||||
"""Generates a secure JSON Web Token for the user expiring in 8 hours."""
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
raise ValueError(f"User '{username}' not found")
|
||||
|
||||
expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=8)
|
||||
payload = {
|
||||
"sub": username,
|
||||
"exp": expiration
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, registry["jwt_secret"], algorithm="HS256")
|
||||
if isinstance(token, bytes):
|
||||
token = token.decode("utf-8")
|
||||
|
||||
return token
|
||||
|
||||
def verify_jwt(self, token) -> str | None:
|
||||
"""Decodes JWT and returns username if token is valid and unexpired."""
|
||||
registry = self._load_registry()
|
||||
try:
|
||||
payload = jwt.decode(token, registry["jwt_secret"], algorithms=["HS256"])
|
||||
return payload.get("sub")
|
||||
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):
|
||||
"""Administrative password override (does not require old password)."""
|
||||
if not new_password or not isinstance(new_password, str):
|
||||
raise ValueError("New password cannot be empty")
|
||||
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
raise ValueError(f"User '{username}' not found")
|
||||
|
||||
user_data = registry["users"][username]
|
||||
user_data["password_hash"] = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
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) -> bool:
|
||||
"""Verifies if the credentials are valid using bcrypt."""
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
return False
|
||||
|
||||
user_data = registry["users"][username]
|
||||
return bcrypt.checkpw(password.encode("utf-8"), user_data["password_hash"].encode("utf-8"))</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):
|
||||
"""Verifies old password and updates registry with new hashed password."""
|
||||
if not new_password or not isinstance(new_password, str):
|
||||
raise ValueError("New password cannot be empty")
|
||||
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
raise ValueError(f"User '{username}' not found")
|
||||
|
||||
user_data = registry["users"][username]
|
||||
if not bcrypt.checkpw(old_password.encode("utf-8"), user_data["password_hash"].encode("utf-8")):
|
||||
raise ValueError("Invalid credentials")
|
||||
|
||||
# Update hash
|
||||
user_data["password_hash"] = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
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) -> dict:
|
||||
"""Creates a new user with bcrypt-hashed credentials.
|
||||
|
||||
Mode A: config_path=None (fresh user) -> Generates config.yaml and .osk key.
|
||||
Mode B: config_path set -> Reuses existing directory after validating its structure.
|
||||
"""
|
||||
if not username or not isinstance(username, str):
|
||||
raise ValueError("Username cannot be empty")
|
||||
|
||||
if not re.match(r"^[a-zA-Z0-9_-]+$", username):
|
||||
raise ValueError("Username must contain only alphanumeric characters, dashes, or underscores")
|
||||
|
||||
if not password or not isinstance(password, str):
|
||||
raise ValueError("Password cannot be empty")
|
||||
|
||||
registry = self._load_registry()
|
||||
if username in registry["users"]:
|
||||
raise ValueError(f"User '{username}' already exists")
|
||||
|
||||
# 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, "plugins"), exist_ok=True)
|
||||
os.makedirs(os.path.join(user_dir, "ai_sessions"), exist_ok=True)
|
||||
|
||||
# Create default config.yaml & .osk key via configfile
|
||||
conf_file = os.path.join(user_dir, "config.yaml")
|
||||
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, "plugins"), exist_ok=True)
|
||||
os.makedirs(os.path.join(abs_config_path, "ai_sessions"), exist_ok=True)
|
||||
|
||||
# Create default config.yaml & .osk key via configfile if config.yaml is not present
|
||||
conf_file = os.path.join(abs_config_path, "config.yaml")
|
||||
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("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||
|
||||
user_entry = {
|
||||
"password_hash": password_hash,
|
||||
"config_path": stored_config_path,
|
||||
"created": datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
registry["users"][username] = user_entry
|
||||
self._save_registry(registry)
|
||||
|
||||
return {
|
||||
"username": username,
|
||||
"config_path": stored_config_path,
|
||||
"created": user_entry["created"]
|
||||
}</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Creates a new user with bcrypt-hashed credentials.</p>
|
||||
<p>Mode A: config_path=None (fresh user) -> Generates config.yaml and .osk key.
|
||||
Mode B: config_path set -> 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):
|
||||
"""Removes user from the registry and cleans up config directory if server-managed."""
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
raise ValueError(f"User '{username}' not found")
|
||||
|
||||
user_data = registry["users"][username]
|
||||
config_path = user_data.get("config_path")
|
||||
|
||||
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["users"][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) -> str:
|
||||
"""Generates a secure JSON Web Token for the user expiring in 8 hours."""
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
raise ValueError(f"User '{username}' not found")
|
||||
|
||||
expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=8)
|
||||
payload = {
|
||||
"sub": username,
|
||||
"exp": expiration
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, registry["jwt_secret"], algorithm="HS256")
|
||||
if isinstance(token, bytes):
|
||||
token = token.decode("utf-8")
|
||||
|
||||
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) -> dict:
|
||||
"""Retrieves raw metadata for a specific user."""
|
||||
registry = self._load_registry()
|
||||
if username not in registry["users"]:
|
||||
raise ValueError(f"User '{username}' not found")
|
||||
|
||||
data = registry["users"][username]
|
||||
return {
|
||||
"username": username,
|
||||
"config_path": data.get("config_path"),
|
||||
"created": data.get("created"),
|
||||
"password_hash": data.get("password_hash")
|
||||
}</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) -> list[dict]:
|
||||
"""Lists all registered users with metadata."""
|
||||
registry = self._load_registry()
|
||||
return [
|
||||
{
|
||||
"username": name,
|
||||
"config_path": data.get("config_path"),
|
||||
"created": data.get("created")
|
||||
}
|
||||
for name, data in registry.get("users", {}).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) -> str | None:
|
||||
"""Decodes JWT and returns username if token is valid and unexpired."""
|
||||
registry = self._load_registry()
|
||||
try:
|
||||
payload = jwt.decode(token, registry["jwt_secret"], algorithms=["HS256"])
|
||||
return payload.get("sub")
|
||||
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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user