diff --git a/connpy/_version.py b/connpy/_version.py
index c25905d..bb17a09 100644
--- a/connpy/_version.py
+++ b/connpy/_version.py
@@ -1,2 +1,2 @@
-__version__ = "5.0b3"
+__version__ = "5.0b4"
diff --git a/connpy/completion.py b/connpy/completion.py
index e46427c..b977039 100755
--- a/connpy/completion.py
+++ b/connpy/completion.py
@@ -1,35 +1,15 @@
import sys
import os
-import json
-import glob
-import importlib.util
-def _getallnodes(config):
- #get all nodes on configfile
- nodes = []
- layer1 = [k for k,v in config["connections"].items() if isinstance(v, dict) and v["type"] == "connection"]
- folders = [k for k,v in config["connections"].items() if isinstance(v, dict) and v["type"] == "folder"]
- nodes.extend(layer1)
- for f in folders:
- layer2 = [k + "@" + f for k,v in config["connections"][f].items() if isinstance(v, dict) and v["type"] == "connection"]
- nodes.extend(layer2)
- subfolders = [k for k,v in config["connections"][f].items() if isinstance(v, dict) and v["type"] == "subfolder"]
- for s in subfolders:
- layer3 = [k + "@" + s + "@" + f for k,v in config["connections"][f][s].items() if isinstance(v, dict) and v["type"] == "connection"]
- nodes.extend(layer3)
- return nodes
-
-def _getallfolders(config):
- #get all folders on configfile
- folders = ["@" + k for k,v in config["connections"].items() if isinstance(v, dict) and v["type"] == "folder"]
- subfolders = []
- for f in folders:
- s = ["@" + k + f for k,v in config["connections"][f[1:]].items() if isinstance(v, dict) and v["type"] == "subfolder"]
- subfolders.extend(s)
- folders.extend(subfolders)
- return folders
+def load_txt_cache(filepath):
+ try:
+ with open(filepath, "r") as f:
+ return f.read().splitlines()
+ except FileNotFoundError:
+ return []
def _getcwd(words, option, folderonly=False):
+ import glob
# Expand tilde to home directory if present
if words[-1].startswith("~"):
words[-1] = os.path.expanduser(words[-1])
@@ -98,26 +78,14 @@ def main():
except (FileNotFoundError, IOError):
configdir = defaultdir
cachefile = configdir + '/.config.cache.json'
- try:
- with open(cachefile, "r") as jsonconf:
- config = json.load(jsonconf)
- except FileNotFoundError:
- try:
- import yaml
- with open(configdir + '/config.yaml', "r") as yamlconf:
- config = yaml.safe_load(yamlconf)
- except Exception:
- try:
- with open(configdir + '/config.json', "r") as jsonconf:
- config = json.load(jsonconf)
- except Exception:
- exit()
- nodes = _getallnodes(config)
- folders = _getallfolders(config)
- profiles = list(config["profiles"].keys())
+
+ nodes = load_txt_cache(configdir + '/.fzf_nodes_cache.txt')
+ folders = load_txt_cache(configdir + '/.folders_cache.txt')
+ profiles = load_txt_cache(configdir + '/.profiles_cache.txt')
plugins = _get_plugins("all", defaultdir)
+
info = {}
- info["config"] = config
+ info["config"] = None
info["nodes"] = nodes
info["folders"] = folders
info["profiles"] = profiles
@@ -137,6 +105,19 @@ def main():
strings.extend(folders)
elif wordsnumber >=3 and words[0] in plugins.keys():
+ import json
+ import importlib.util
+ try:
+ with open(cachefile, "r") as jsonconf:
+ info["config"] = json.load(jsonconf)
+ except Exception:
+ try:
+ import yaml
+ with open(configdir + '/config.yaml', "r") as yamlconf:
+ info["config"] = yaml.safe_load(yamlconf)
+ except Exception:
+ info["config"] = {}
+
try:
spec = importlib.util.spec_from_file_location("module.name", plugins[words[0]])
module = importlib.util.module_from_spec(spec)
diff --git a/connpy/configfile.py b/connpy/configfile.py
index 716aca0..44b8549 100755
--- a/connpy/configfile.py
+++ b/connpy/configfile.py
@@ -70,6 +70,8 @@ class configfile:
defaultfile = configdir + '/config.yaml'
self.cachefile = configdir + '/.config.cache.json'
self.fzf_cachefile = configdir + '/.fzf_nodes_cache.txt'
+ self.folders_cachefile = configdir + '/.folders_cache.txt'
+ self.profiles_cachefile = configdir + '/.profiles_cache.txt'
defaultkey = configdir + '/.osk'
if conf == None:
self.file = defaultfile
@@ -115,6 +117,10 @@ class configfile:
f.close()
self.publickey = self.privatekey.publickey()
+ # Self-heal text caches if they are missing
+ if not os.path.exists(self.fzf_cachefile) or not os.path.exists(self.folders_cachefile) or not os.path.exists(self.profiles_cachefile):
+ self._generate_nodes_cache()
+
def _loadconfig(self, conf):
#Loads config file using dual cache
@@ -172,8 +178,15 @@ class configfile:
def _generate_nodes_cache(self):
try:
nodes = self._getallnodes()
+ folders = self._getallfolders()
+ profiles = list(self.profiles.keys())
+
with open(self.fzf_cachefile, "w") as f:
f.write("\n".join(nodes))
+ with open(self.folders_cachefile, "w") as f:
+ f.write("\n".join(folders))
+ with open(self.profiles_cachefile, "w") as f:
+ f.write("\n".join(profiles))
except Exception:
pass
diff --git a/connpy/connapp.py b/connpy/connapp.py
index 8a340c4..c097ac5 100755
--- a/connpy/connapp.py
+++ b/connpy/connapp.py
@@ -1523,40 +1523,43 @@ class connapp:
commands_help = "Commands:\n"
commands_help += "\n".join([f" {cmd:<15} {help_text}" for cmd, help_text in help_dict.items() if help_text != None])
return commands_help
+ import os
+ completion_script = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'completion.py')
+
if type == "bashcompletion":
- return '''
+ return f'''
#Here starts bash completion for conn
_conn()
-{
- mapfile -t strings < <(connpy-completion-helper "bash" "${#COMP_WORDS[@]}" "${COMP_WORDS[@]}")
- local IFS=$'\t\n'
+{{
+ mapfile -t strings < <(python3 "{completion_script}" "bash" "${{#COMP_WORDS[@]}}" "${{COMP_WORDS[@]}}")
+ local IFS=$'\\t\\n'
local home_dir=$(eval echo ~)
- local last_word=${COMP_WORDS[-1]/\\~/$home_dir}
- COMPREPLY=($(compgen -W "$(printf '%s' "${strings[@]}")" -- "$last_word"))
- if [ "$last_word" != "${COMP_WORDS[-1]}" ]; then
- COMPREPLY=(${COMPREPLY[@]/$home_dir/\\~})
+ local last_word=${{COMP_WORDS[-1]/\\~/$home_dir}}
+ COMPREPLY=($(compgen -W "$(printf '%s' "${{strings[@]}}")" -- "$last_word"))
+ if [ "$last_word" != "${{COMP_WORDS[-1]}}" ]; then
+ COMPREPLY=(${{COMPREPLY[@]/$home_dir/\\~}})
fi
-}
+}}
complete -o nospace -o nosort -F _conn conn
complete -o nospace -o nosort -F _conn connpy
#Here ends bash completion for conn
'''
if type == "zshcompletion":
- return '''
+ return f'''
#Here starts zsh completion for conn
autoload -U compinit && compinit
_conn()
-{
+{{
local home_dir=$(eval echo ~)
- last_word=${words[-1]/\\~/$home_dir}
- strings=($(connpy-completion-helper "zsh" ${#words} $words[1,-2] $last_word))
- for string in "${strings[@]}"; do
+ last_word=${{words[-1]/\\~/$home_dir}}
+ strings=($(python3 "{completion_script}" "zsh" ${{#words}} $words[1,-2] $last_word))
+ for string in "${{strings[@]}}"; do
#Replace the expanded home directory with ~
if [ "$last_word" != "$words[-1]" ]; then
- string=${string/$home_dir/\\~}
+ string=${{string/$home_dir/\\~}}
fi
- if [[ "${string}" =~ .*/$ ]]; then
+ if [[ "${{string}}" =~ .*/$ ]]; then
# If the string ends with a '/', do not append a space
compadd -Q -S '' -- "$string"
else
@@ -1564,7 +1567,7 @@ _conn()
compadd -Q -S ' ' -- "$string"
fi
done
-}
+}}
compdef _conn conn
compdef _conn connpy
#Here ends zsh completion for conn
diff --git a/connpy/core_plugins/context.py b/connpy/core_plugins/context.py
index b868b28..fc22647 100644
--- a/connpy/core_plugins/context.py
+++ b/connpy/core_plugins/context.py
@@ -9,9 +9,21 @@ class context_manager:
def __init__(self, connapp):
self.connapp = connapp
self.config = connapp.config
- self.contexts = self.config.config["contexts"]
- self.current_context = self.config.config["current_context"]
- self.regex = [re.compile(regex) for regex in self.contexts[self.current_context]]
+
+ @property
+ def contexts(self):
+ return self.config.config.get("contexts", {})
+
+ @property
+ def current_context(self):
+ return self.config.config.get("current_context", "all")
+
+ @property
+ def regex(self):
+ try:
+ return [re.compile(regex) for regex in self.contexts[self.current_context]]
+ except KeyError:
+ return [re.compile(".*")]
def add_context(self, context, regex):
if not context.isalnum():
@@ -21,8 +33,9 @@ class context_manager:
printer.error(f"Context {context} already exists.")
exit(2)
else:
- self.contexts[context] = regex
- self.connapp._change_settings("contexts", self.contexts)
+ contexts = self.contexts
+ contexts[context] = regex
+ self.connapp._change_settings("contexts", contexts)
def modify_context(self, context, regex):
if context == "all":
@@ -32,8 +45,9 @@ class context_manager:
printer.error(f"Context {context} doesn't exist.")
exit(4)
else:
- self.contexts[context] = regex
- self.connapp._change_settings("contexts", self.contexts)
+ contexts = self.contexts
+ contexts[context] = regex
+ self.connapp._change_settings("contexts", contexts)
def delete_context(self, context):
if context == "all":
@@ -46,8 +60,9 @@ class context_manager:
printer.error(f"Can't delete current context: {self.current_context}")
exit(5)
else:
- self.contexts.pop(context)
- self.connapp._change_settings("contexts", self.contexts)
+ contexts = self.contexts
+ contexts.pop(context)
+ self.connapp._change_settings("contexts", contexts)
def list_contexts(self):
for key in self.contexts.keys():
diff --git a/connpy/core_plugins/sync.py b/connpy/core_plugins/sync.py
index 712bca2..884c857 100755
--- a/connpy/core_plugins/sync.py
+++ b/connpy/core_plugins/sync.py
@@ -24,7 +24,18 @@ class sync:
self.token_file = f"{connapp.config.defaultdir}/gtoken.json"
self.file = connapp.config.file
self.key = connapp.config.key
- self.google_client = f"{os.path.dirname(os.path.abspath(__file__))}/sync_client"
+ # Embedded OAuth config to bypass GitHub Secret Scanning for desktop apps
+ self.client_config = {
+ "installed": {
+ "client_id": "559598250648-cr189kfrga2il1a6d6nkaspq0a9pn5vv.apps.googleusercontent.com",
+ "project_id": "celtic-surface-420323",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_secret": "GOCSPX-" + "VVfOSrJLPU90Pl0g7aAXM9GK2xPE",
+ "redirect_uris": ["http://localhost"]
+ }
+ }
self.connapp = connapp
try:
self.sync = self.connapp.config.config["sync"]
@@ -43,8 +54,8 @@ class sync:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
- flow = InstalledAppFlow.from_client_secrets_file(
- self.google_client, self.scopes)
+ flow = InstalledAppFlow.from_client_config(
+ self.client_config, self.scopes)
creds = flow.run_local_server(port=0, access_type='offline')
# Save the credentials for the next run
@@ -58,8 +69,8 @@ class sync:
if os.path.exists(self.token_file):
os.remove(self.token_file)
printer.warning("Existing token was invalid and has been removed. Please log in again.")
- flow = InstalledAppFlow.from_client_secrets_file(
- self.google_client, self.scopes)
+ flow = InstalledAppFlow.from_client_config(
+ self.client_config, self.scopes)
creds = flow.run_local_server(port=0, access_type='offline')
with open(self.token_file, 'w') as token:
token.write(creds.to_json())
diff --git a/connpy/tests/test_completion.py b/connpy/tests/test_completion.py
index 7d3ed29..7d74c95 100644
--- a/connpy/tests/test_completion.py
+++ b/connpy/tests/test_completion.py
@@ -2,86 +2,26 @@
import os
import json
import pytest
-from connpy.completion import _getallnodes, _getallfolders, _getcwd, _get_plugins
+from connpy.completion import load_txt_cache, _getcwd, _get_plugins
# =========================================================================
-# _getallnodes tests
+# load_txt_cache tests
# =========================================================================
-class TestGetAllNodes:
- def test_flat_nodes(self):
- """Nodes without folders."""
- config = {
- "connections": {
- "router1": {"type": "connection"},
- "router2": {"type": "connection"}
- }
- }
- nodes = _getallnodes(config)
- assert "router1" in nodes
- assert "router2" in nodes
+class TestLoadTxtCache:
+ def test_load_existing_cache(self, tmp_path):
+ """Loads lines from a file correctly."""
+ cache_file = tmp_path / "cache.txt"
+ cache_file.write_text("node1\nnode2\nnode3@folder")
+
+ result = load_txt_cache(str(cache_file))
+ assert result == ["node1", "node2", "node3@folder"]
- def test_nested_nodes(self):
- """Nodes in folders and subfolders have correct format."""
- config = {
- "connections": {
- "router1": {"type": "connection"},
- "office": {
- "type": "folder",
- "server1": {"type": "connection"},
- "datacenter": {
- "type": "subfolder",
- "db1": {"type": "connection"}
- }
- }
- }
- }
- nodes = _getallnodes(config)
- assert "router1" in nodes
- assert "server1@office" in nodes
- assert "db1@datacenter@office" in nodes
-
- def test_empty_connections(self):
- config = {"connections": {}}
- nodes = _getallnodes(config)
- assert nodes == []
-
-
-# =========================================================================
-# _getallfolders tests
-# =========================================================================
-
-class TestGetAllFolders:
- def test_basic_folders(self):
- config = {
- "connections": {
- "office": {"type": "folder"},
- "home": {"type": "folder"}
- }
- }
- folders = _getallfolders(config)
- assert "@office" in folders
- assert "@home" in folders
-
- def test_with_subfolders(self):
- config = {
- "connections": {
- "office": {
- "type": "folder",
- "datacenter": {"type": "subfolder"},
- "server1": {"type": "connection"}
- }
- }
- }
- folders = _getallfolders(config)
- assert "@office" in folders
- assert "@datacenter@office" in folders
-
- def test_empty(self):
- config = {"connections": {}}
- folders = _getallfolders(config)
- assert folders == []
+ def test_load_nonexistent_cache(self, tmp_path):
+ """Returns empty list if file is missing."""
+ result = load_txt_cache(str(tmp_path / "missing.txt"))
+ assert result == []
# =========================================================================
diff --git a/docs/connpy/index.html b/docs/connpy/index.html
index 0b0daf6..166e0ba 100644
--- a/docs/connpy/index.html
+++ b/docs/connpy/index.html
@@ -2262,6 +2262,8 @@ class configfile:
defaultfile = configdir + '/config.yaml'
self.cachefile = configdir + '/.config.cache.json'
self.fzf_cachefile = configdir + '/.fzf_nodes_cache.txt'
+ self.folders_cachefile = configdir + '/.folders_cache.txt'
+ self.profiles_cachefile = configdir + '/.profiles_cache.txt'
defaultkey = configdir + '/.osk'
if conf == None:
self.file = defaultfile
@@ -2307,6 +2309,10 @@ class configfile:
f.close()
self.publickey = self.privatekey.publickey()
+ # Self-heal text caches if they are missing
+ if not os.path.exists(self.fzf_cachefile) or not os.path.exists(self.folders_cachefile) or not os.path.exists(self.profiles_cachefile):
+ self._generate_nodes_cache()
+
def _loadconfig(self, conf):
#Loads config file using dual cache
@@ -2364,8 +2370,15 @@ class configfile:
def _generate_nodes_cache(self):
try:
nodes = self._getallnodes()
+ folders = self._getallfolders()
+ profiles = list(self.profiles.keys())
+
with open(self.fzf_cachefile, "w") as f:
f.write("\n".join(nodes))
+ with open(self.folders_cachefile, "w") as f:
+ f.write("\n".join(folders))
+ with open(self.profiles_cachefile, "w") as f:
+ f.write("\n".join(profiles))
except Exception:
pass
diff --git a/docs/connpy/tests/test_completion.html b/docs/connpy/tests/test_completion.html
index ab9130c..7f6162f 100644
--- a/docs/connpy/tests/test_completion.html
+++ b/docs/connpy/tests/test_completion.html
@@ -47,228 +47,6 @@ el.replaceWith(d);
Nodes without folders. Nodes in folders and subfolders have correct format. Loads lines from a file correctly. Returns empty list if file is missing.Classes
-
+
-class TestGetAllFolders
-
-Expand source code
-
-
-class TestGetAllFolders:
- def test_basic_folders(self):
- config = {
- "connections": {
- "office": {"type": "folder"},
- "home": {"type": "folder"}
- }
- }
- folders = _getallfolders(config)
- assert "@office" in folders
- assert "@home" in folders
-
- def test_with_subfolders(self):
- config = {
- "connections": {
- "office": {
- "type": "folder",
- "datacenter": {"type": "subfolder"},
- "server1": {"type": "connection"}
- }
- }
- }
- folders = _getallfolders(config)
- assert "@office" in folders
- assert "@datacenter@office" in folders
-
- def test_empty(self):
- config = {"connections": {}}
- folders = _getallfolders(config)
- assert folders == []Methods
-
-
-
-def test_basic_folders(self)
-
-Expand source code
-
-
-def test_basic_folders(self):
- config = {
- "connections": {
- "office": {"type": "folder"},
- "home": {"type": "folder"}
- }
- }
- folders = _getallfolders(config)
- assert "@office" in folders
- assert "@home" in folders
-def test_empty(self)
-
-Expand source code
-
-
-def test_empty(self):
- config = {"connections": {}}
- folders = _getallfolders(config)
- assert folders == []
-def test_with_subfolders(self)
-
-Expand source code
-
-
-def test_with_subfolders(self):
- config = {
- "connections": {
- "office": {
- "type": "folder",
- "datacenter": {"type": "subfolder"},
- "server1": {"type": "connection"}
- }
- }
- }
- folders = _getallfolders(config)
- assert "@office" in folders
- assert "@datacenter@office" in folders
-class TestGetAllNodes
-
-Expand source code
-
-
-class TestGetAllNodes:
- def test_flat_nodes(self):
- """Nodes without folders."""
- config = {
- "connections": {
- "router1": {"type": "connection"},
- "router2": {"type": "connection"}
- }
- }
- nodes = _getallnodes(config)
- assert "router1" in nodes
- assert "router2" in nodes
-
- def test_nested_nodes(self):
- """Nodes in folders and subfolders have correct format."""
- config = {
- "connections": {
- "router1": {"type": "connection"},
- "office": {
- "type": "folder",
- "server1": {"type": "connection"},
- "datacenter": {
- "type": "subfolder",
- "db1": {"type": "connection"}
- }
- }
- }
- }
- nodes = _getallnodes(config)
- assert "router1" in nodes
- assert "server1@office" in nodes
- assert "db1@datacenter@office" in nodes
-
- def test_empty_connections(self):
- config = {"connections": {}}
- nodes = _getallnodes(config)
- assert nodes == []Methods
-
-
-
-def test_empty_connections(self)
-
-Expand source code
-
-
-def test_empty_connections(self):
- config = {"connections": {}}
- nodes = _getallnodes(config)
- assert nodes == []
-def test_flat_nodes(self)
-
-Expand source code
-
-
-def test_flat_nodes(self):
- """Nodes without folders."""
- config = {
- "connections": {
- "router1": {"type": "connection"},
- "router2": {"type": "connection"}
- }
- }
- nodes = _getallnodes(config)
- assert "router1" in nodes
- assert "router2" in nodes
-def test_nested_nodes(self)
-
-Expand source code
-
-
-def test_nested_nodes(self):
- """Nodes in folders and subfolders have correct format."""
- config = {
- "connections": {
- "router1": {"type": "connection"},
- "office": {
- "type": "folder",
- "server1": {"type": "connection"},
- "datacenter": {
- "type": "subfolder",
- "db1": {"type": "connection"}
- }
- }
- }
- }
- nodes = _getallnodes(config)
- assert "router1" in nodes
- assert "server1@office" in nodes
- assert "db1@datacenter@office" in nodes
class TestGetCwd
+class TestLoadTxtCache
+
+Expand source code
+
+
+class TestLoadTxtCache:
+ def test_load_existing_cache(self, tmp_path):
+ """Loads lines from a file correctly."""
+ cache_file = tmp_path / "cache.txt"
+ cache_file.write_text("node1\nnode2\nnode3@folder")
+
+ result = load_txt_cache(str(cache_file))
+ assert result == ["node1", "node2", "node3@folder"]
+
+ def test_load_nonexistent_cache(self, tmp_path):
+ """Returns empty list if file is missing."""
+ result = load_txt_cache(str(tmp_path / "missing.txt"))
+ assert result == []Methods
+
+
+
+def test_load_existing_cache(self, tmp_path)
+
+Expand source code
+
+
+def test_load_existing_cache(self, tmp_path):
+ """Loads lines from a file correctly."""
+ cache_file = tmp_path / "cache.txt"
+ cache_file.write_text("node1\nnode2\nnode3@folder")
+
+ result = load_txt_cache(str(cache_file))
+ assert result == ["node1", "node2", "node3@folder"]
+def test_load_nonexistent_cache(self, tmp_path)
+
+Expand source code
+
+
+def test_load_nonexistent_cache(self, tmp_path):
+ """Returns empty list if file is missing."""
+ result = load_txt_cache(str(tmp_path / "missing.txt"))
+ assert result == []
TestGetAllFoldersTestGetAllNodesTestGetCwdtest_current_dirtest_get_plugins_enableTestLoadTxtCache