perf(core): implement lazy loading for heavy services, Google SDK and CLI forms
- Defer instantiation of heavy services (SyncService, UserService, ImportExportService, SystemService, ExecutionService) using cached properties in ServiceProvider and module __getattr__ in services. - Defer Google Drive SDK imports in sync_service.py using module-level __getattr__ and _get_google_libs() helper while retaining mock patchability. - Lazy load inquirer, Forms, Validators and Blessed theme adapters in connpy/cli/* to prevent loading CLI form dependencies at startup. - Clean up unused top-level service imports in connapp.py.
This commit is contained in:
+5
-1
@@ -1,5 +1,4 @@
|
|||||||
import ast
|
import ast
|
||||||
import inquirer
|
|
||||||
from .validators import Validators
|
from .validators import Validators
|
||||||
|
|
||||||
class Forms:
|
class Forms:
|
||||||
@@ -8,6 +7,7 @@ class Forms:
|
|||||||
self.validators = Validators(app)
|
self.validators = Validators(app)
|
||||||
|
|
||||||
def questions_edit(self):
|
def questions_edit(self):
|
||||||
|
import inquirer
|
||||||
questions = []
|
questions = []
|
||||||
questions.append(inquirer.Confirm("host", message="Edit Hostname/IP?"))
|
questions.append(inquirer.Confirm("host", message="Edit Hostname/IP?"))
|
||||||
questions.append(inquirer.Confirm("protocol", message="Edit Protocol/app?"))
|
questions.append(inquirer.Confirm("protocol", message="Edit Protocol/app?"))
|
||||||
@@ -21,6 +21,7 @@ class Forms:
|
|||||||
return inquirer.prompt(questions)
|
return inquirer.prompt(questions)
|
||||||
|
|
||||||
def questions_nodes(self, unique, uniques=None, edit=None):
|
def questions_nodes(self, unique, uniques=None, edit=None):
|
||||||
|
import inquirer
|
||||||
try:
|
try:
|
||||||
defaults = self.app.services.nodes.get_node_details(unique)
|
defaults = self.app.services.nodes.get_node_details(unique)
|
||||||
if "tags" not in defaults:
|
if "tags" not in defaults:
|
||||||
@@ -98,6 +99,7 @@ class Forms:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def questions_profiles(self, unique, edit=None):
|
def questions_profiles(self, unique, edit=None):
|
||||||
|
import inquirer
|
||||||
try:
|
try:
|
||||||
defaults = self.app.services.profiles.get_profile(unique, resolve=False)
|
defaults = self.app.services.profiles.get_profile(unique, resolve=False)
|
||||||
if "tags" not in defaults:
|
if "tags" not in defaults:
|
||||||
@@ -163,6 +165,7 @@ class Forms:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def questions_bulk(self, nodes="", hosts=""):
|
def questions_bulk(self, nodes="", hosts=""):
|
||||||
|
import inquirer
|
||||||
questions = []
|
questions = []
|
||||||
questions.append(inquirer.Text("ids", message="add a comma separated list of nodes to add", default=nodes, validate=self.validators.bulk_node_validation))
|
questions.append(inquirer.Text("ids", message="add a comma separated list of nodes to add", default=nodes, validate=self.validators.bulk_node_validation))
|
||||||
questions.append(inquirer.Text("location", message="Add a @folder, @subfolder@folder or leave empty", validate=self.validators.bulk_folder_validation))
|
questions.append(inquirer.Text("location", message="Add a @folder, @subfolder@folder or leave empty", validate=self.validators.bulk_folder_validation))
|
||||||
@@ -200,6 +203,7 @@ class Forms:
|
|||||||
|
|
||||||
def mcp_wizard(self, mcp_servers):
|
def mcp_wizard(self, mcp_servers):
|
||||||
"""Interactive wizard to manage MCP servers."""
|
"""Interactive wizard to manage MCP servers."""
|
||||||
|
import inquirer
|
||||||
from .helpers import theme
|
from .helpers import theme
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
import inquirer
|
|
||||||
from inquirer.themes import Default, term
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pyfzf.pyfzf import FzfPrompt
|
from pyfzf.pyfzf import FzfPrompt
|
||||||
@@ -9,6 +7,7 @@ except ImportError:
|
|||||||
|
|
||||||
def hex_to_blessed(hex_str):
|
def hex_to_blessed(hex_str):
|
||||||
"""Convert hex color string to blessed/ansi format."""
|
"""Convert hex color string to blessed/ansi format."""
|
||||||
|
from inquirer.themes import term
|
||||||
if not hex_str or not isinstance(hex_str, str):
|
if not hex_str or not isinstance(hex_str, str):
|
||||||
return term.normal
|
return term.normal
|
||||||
|
|
||||||
@@ -42,8 +41,11 @@ def hex_to_blessed(hex_str):
|
|||||||
except:
|
except:
|
||||||
return prefix + term.normal
|
return prefix + term.normal
|
||||||
|
|
||||||
# Custom inquirer theme matching connpy colors
|
def get_theme():
|
||||||
class ConnpyTheme(Default):
|
"""Returns a fresh instance of the theme with current colors."""
|
||||||
|
from inquirer.themes import Default, term
|
||||||
|
|
||||||
|
class ConnpyTheme(Default):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
try:
|
try:
|
||||||
@@ -61,8 +63,6 @@ class ConnpyTheme(Default):
|
|||||||
self.List.selection_color = term.bold_cyan
|
self.List.selection_color = term.bold_cyan
|
||||||
self.List.selection_cursor = ">"
|
self.List.selection_cursor = ">"
|
||||||
|
|
||||||
def get_theme():
|
|
||||||
"""Returns a fresh instance of the theme with current colors."""
|
|
||||||
return ConnpyTheme()
|
return ConnpyTheme()
|
||||||
|
|
||||||
class ThemeProxy:
|
class ThemeProxy:
|
||||||
@@ -126,6 +126,7 @@ def choose(app, list_, name, action):
|
|||||||
else:
|
else:
|
||||||
return answer[0]
|
return answer[0]
|
||||||
else:
|
else:
|
||||||
|
import inquirer
|
||||||
questions = [inquirer.List(name, message="Pick {} to {}:".format(name,action), choices=list_, carousel=True)]
|
questions = [inquirer.List(name, message="Pick {} to {}:".format(name,action), choices=list_, carousel=True)]
|
||||||
answer = inquirer.prompt(questions, theme=theme)
|
answer = inquirer.prompt(questions, theme=theme)
|
||||||
if answer == None:
|
if answer == None:
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import inquirer
|
|
||||||
from .. import printer
|
from .. import printer
|
||||||
from ..services.exceptions import ConnpyError
|
from ..services.exceptions import ConnpyError
|
||||||
from .forms import Forms
|
|
||||||
|
|
||||||
class ImportExportHandler:
|
class ImportExportHandler:
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.forms = Forms(app)
|
self._forms = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forms(self):
|
||||||
|
if self._forms is None:
|
||||||
|
from .forms import Forms
|
||||||
|
self._forms = Forms(self.app)
|
||||||
|
return self._forms
|
||||||
|
|
||||||
|
@forms.setter
|
||||||
|
def forms(self, value):
|
||||||
|
self._forms = value
|
||||||
|
|
||||||
def dispatch_import(self, args):
|
def dispatch_import(self, args):
|
||||||
file_path = args.data[0]
|
file_path = args.data[0]
|
||||||
try:
|
try:
|
||||||
printer.warning("This could overwrite your current configuration!")
|
printer.warning("This could overwrite your current configuration!")
|
||||||
|
import inquirer
|
||||||
question = [inquirer.Confirm("import", message=f"Are you sure you want to import {file_path}?")]
|
question = [inquirer.Confirm("import", message=f"Are you sure you want to import {file_path}?")]
|
||||||
confirm = inquirer.prompt(question)
|
confirm = inquirer.prompt(question)
|
||||||
if confirm == None or not confirm["import"]:
|
if confirm == None or not confirm["import"]:
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import sys
|
import sys
|
||||||
import yaml
|
import yaml
|
||||||
import inquirer
|
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
|
||||||
from .. import printer
|
from .. import printer
|
||||||
from ..services.exceptions import ConnpyError, InvalidConfigurationError
|
from ..services.exceptions import ConnpyError, InvalidConfigurationError
|
||||||
from .helpers import choose
|
from .helpers import choose
|
||||||
from .forms import Forms
|
|
||||||
from .help_text import get_instructions
|
from .help_text import get_instructions
|
||||||
|
|
||||||
class NodeHandler:
|
class NodeHandler:
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.forms = Forms(app)
|
self._forms = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forms(self):
|
||||||
|
if self._forms is None:
|
||||||
|
from .forms import Forms
|
||||||
|
self._forms = Forms(self.app)
|
||||||
|
return self._forms
|
||||||
|
|
||||||
|
@forms.setter
|
||||||
|
def forms(self, value):
|
||||||
|
self._forms = value
|
||||||
|
|
||||||
def _filter_exact_match(self, matches, query):
|
def _filter_exact_match(self, matches, query):
|
||||||
if not query or len(matches) <= 1:
|
if not query or len(matches) <= 1:
|
||||||
@@ -100,6 +109,7 @@ class NodeHandler:
|
|||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
printer.info(f"Removing: {matches}")
|
printer.info(f"Removing: {matches}")
|
||||||
|
import inquirer
|
||||||
question = [inquirer.Confirm("delete", message="Are you sure you want to continue?")]
|
question = [inquirer.Confirm("delete", message="Are you sure you want to continue?")]
|
||||||
confirm = inquirer.prompt(question)
|
confirm = inquirer.prompt(question)
|
||||||
if confirm == None or not confirm["delete"]:
|
if confirm == None or not confirm["delete"]:
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
import sys
|
import sys
|
||||||
import yaml
|
import yaml
|
||||||
import inquirer
|
|
||||||
|
|
||||||
from .. import printer
|
from .. import printer
|
||||||
from ..services.exceptions import ConnpyError, ProfileNotFoundError
|
from ..services.exceptions import ConnpyError, ProfileNotFoundError
|
||||||
from .forms import Forms
|
|
||||||
|
|
||||||
class ProfileHandler:
|
class ProfileHandler:
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.forms = Forms(app)
|
self._forms = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forms(self):
|
||||||
|
if self._forms is None:
|
||||||
|
from .forms import Forms
|
||||||
|
self._forms = Forms(self.app)
|
||||||
|
return self._forms
|
||||||
|
|
||||||
|
@forms.setter
|
||||||
|
def forms(self, value):
|
||||||
|
self._forms = value
|
||||||
|
|
||||||
def dispatch(self, args):
|
def dispatch(self, args):
|
||||||
if not self.app.case:
|
if not self.app.case:
|
||||||
@@ -29,6 +38,7 @@ class ProfileHandler:
|
|||||||
printer.error("Can't delete default profile")
|
printer.error("Can't delete default profile")
|
||||||
sys.exit(6)
|
sys.exit(6)
|
||||||
|
|
||||||
|
import inquirer
|
||||||
question = [inquirer.Confirm("delete", message=f"Are you sure you want to delete {name}?")]
|
question = [inquirer.Confirm("delete", message=f"Are you sure you want to delete {name}?")]
|
||||||
confirm = inquirer.prompt(question)
|
confirm = inquirer.prompt(question)
|
||||||
if confirm == None or not confirm["delete"]:
|
if confirm == None or not confirm["delete"]:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import sys
|
import sys
|
||||||
import yaml
|
import yaml
|
||||||
import inquirer
|
|
||||||
from .. import printer
|
from .. import printer
|
||||||
|
|
||||||
class SSOHandler:
|
class SSOHandler:
|
||||||
@@ -40,6 +39,7 @@ class SSOHandler:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def add_provider(self, args):
|
def add_provider(self, args):
|
||||||
|
import inquirer
|
||||||
provider = args.provider
|
provider = args.provider
|
||||||
sso = self.app.config.config.get("sso", {})
|
sso = self.app.config.config.get("sso", {})
|
||||||
providers = sso.setdefault("providers", {})
|
providers = sso.setdefault("providers", {})
|
||||||
@@ -113,6 +113,7 @@ class SSOHandler:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Confirm delete
|
# Confirm delete
|
||||||
|
import inquirer
|
||||||
questions = [inquirer.Confirm("confirm", message=f"Are you sure you want to delete SSO Provider '{provider}'?", default=False)]
|
questions = [inquirer.Confirm("confirm", message=f"Are you sure you want to delete SSO Provider '{provider}'?", default=False)]
|
||||||
answers = inquirer.prompt(questions)
|
answers = inquirer.prompt(questions)
|
||||||
if not answers or not answers["confirm"]:
|
if not answers or not answers["confirm"]:
|
||||||
|
|||||||
+28
-25
@@ -1,6 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
import ast
|
import ast
|
||||||
import inquirer
|
|
||||||
|
def _raise_val_err(reason):
|
||||||
|
import inquirer
|
||||||
|
raise inquirer.errors.ValidationError("", reason=reason)
|
||||||
|
|
||||||
class Validators:
|
class Validators:
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
@@ -8,61 +11,61 @@ class Validators:
|
|||||||
|
|
||||||
def host_validation(self, answers, current, regex = "^.+$"):
|
def host_validation(self, answers, current, regex = "^.+$"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Host cannot be empty")
|
_raise_val_err("Host cannot be empty")
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
_raise_val_err("Profile {} don't exist".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$)"):
|
def profile_protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$)"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker, ssm or leave empty")
|
_raise_val_err("Pick between ssh, telnet, kubectl, docker, ssm or leave empty")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$|^@.+$)"):
|
def protocol_validation(self, answers, current, regex = "(^ssh$|^telnet$|^kubectl$|^docker$|^ssm$|^$|^@.+$)"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick between ssh, telnet, kubectl, docker, ssm, leave empty or @profile")
|
_raise_val_err("Pick between ssh, telnet, kubectl, docker, ssm, leave empty or @profile")
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
_raise_val_err("Profile {} don't exist".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def profile_port_validation(self, answers, current, regex = "(^[0-9]*$)"):
|
def profile_port_validation(self, answers, current, regex = "(^[0-9]*$)"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick a port between 1-65535, @profile o leave empty")
|
_raise_val_err("Pick a port between 1-65535, @profile o leave empty")
|
||||||
try:
|
try:
|
||||||
port = int(current)
|
port = int(current)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
port = 0
|
port = 0
|
||||||
if current != "" and not 1 <= int(port) <= 65535:
|
if current != "" and not 1 <= int(port) <= 65535:
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick a port between 1-65535 or leave empty")
|
_raise_val_err("Pick a port between 1-65535 or leave empty")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def port_validation(self, answers, current, regex = "(^[0-9]*$|^@.+$)"):
|
def port_validation(self, answers, current, regex = "(^[0-9]*$|^@.+$)"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick a port between 1-65535, @profile or leave empty")
|
_raise_val_err("Pick a port between 1-65535, @profile or leave empty")
|
||||||
try:
|
try:
|
||||||
port = int(current)
|
port = int(current)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
port = 0
|
port = 0
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
_raise_val_err("Profile {} don't exist".format(current))
|
||||||
elif current != "" and not 1 <= int(port) <= 65535:
|
elif current != "" and not 1 <= int(port) <= 65535:
|
||||||
raise inquirer.errors.ValidationError("", reason="Pick a port between 1-65535, @profile o leave empty")
|
_raise_val_err("Pick a port between 1-65535, @profile o leave empty")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def pass_validation(self, answers, current, regex = "(^@.+$)"):
|
def pass_validation(self, answers, current, regex = "(^@.+$)"):
|
||||||
profiles = current.split(",")
|
profiles = current.split(",")
|
||||||
for i in profiles:
|
for i in profiles:
|
||||||
if not re.match(regex, i) or i[1:] not in self.app.profiles:
|
if not re.match(regex, i) or i[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(i))
|
_raise_val_err("Profile {} don't exist".format(i))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def tags_validation(self, answers, current):
|
def tags_validation(self, answers, current):
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
_raise_val_err("Profile {} don't exist".format(current))
|
||||||
elif current != "":
|
elif current != "":
|
||||||
isdict = False
|
isdict = False
|
||||||
try:
|
try:
|
||||||
@@ -70,7 +73,7 @@ class Validators:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if not isinstance (isdict, dict):
|
if not isinstance (isdict, dict):
|
||||||
raise inquirer.errors.ValidationError("", reason="Tags should be a python dictionary.".format(current))
|
_raise_val_err("Tags should be a python dictionary.".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def profile_tags_validation(self, answers, current):
|
def profile_tags_validation(self, answers, current):
|
||||||
@@ -81,36 +84,36 @@ class Validators:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if not isinstance (isdict, dict):
|
if not isinstance (isdict, dict):
|
||||||
raise inquirer.errors.ValidationError("", reason="Tags should be a python dictionary.".format(current))
|
_raise_val_err("Tags should be a python dictionary.".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def jumphost_validation(self, answers, current):
|
def jumphost_validation(self, answers, current):
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
_raise_val_err("Profile {} don't exist".format(current))
|
||||||
elif current != "":
|
elif current != "":
|
||||||
if current not in self.app.nodes_list:
|
if current not in self.app.nodes_list:
|
||||||
raise inquirer.errors.ValidationError("", reason="Node {} don't exist.".format(current))
|
_raise_val_err("Node {} don't exist.".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def profile_jumphost_validation(self, answers, current):
|
def profile_jumphost_validation(self, answers, current):
|
||||||
if current != "":
|
if current != "":
|
||||||
if current not in self.app.nodes_list:
|
if current not in self.app.nodes_list:
|
||||||
raise inquirer.errors.ValidationError("", reason="Node {} don't exist.".format(current))
|
_raise_val_err("Node {} don't exist.".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def default_validation(self, answers, current):
|
def default_validation(self, answers, current):
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
_raise_val_err("Profile {} don't exist".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def bulk_node_validation(self, answers, current, regex = "^[0-9a-zA-Z_.,$#-]+$"):
|
def bulk_node_validation(self, answers, current, regex = "^[0-9a-zA-Z_.,$#-]+$"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Host cannot be empty")
|
_raise_val_err("Host cannot be empty")
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
_raise_val_err("Profile {} don't exist".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def bulk_folder_validation(self, answers, current):
|
def bulk_folder_validation(self, answers, current):
|
||||||
@@ -123,17 +126,17 @@ class Validators:
|
|||||||
|
|
||||||
matches = list(filter(lambda k: k == candidate, self.app.folders))
|
matches = list(filter(lambda k: k == candidate, self.app.folders))
|
||||||
if current != "" and len(matches) == 0:
|
if current != "" and len(matches) == 0:
|
||||||
raise inquirer.errors.ValidationError("", reason="Location {} don't exist".format(current))
|
_raise_val_err("Location {} don't exist".format(current))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def bulk_host_validation(self, answers, current, regex = "^.+$"):
|
def bulk_host_validation(self, answers, current, regex = "^.+$"):
|
||||||
if not re.match(regex, current):
|
if not re.match(regex, current):
|
||||||
raise inquirer.errors.ValidationError("", reason="Host cannot be empty")
|
_raise_val_err("Host cannot be empty")
|
||||||
if current.startswith("@"):
|
if current.startswith("@"):
|
||||||
if current[1:] not in self.app.profiles:
|
if current[1:] not in self.app.profiles:
|
||||||
raise inquirer.errors.ValidationError("", reason="Profile {} don't exist".format(current))
|
_raise_val_err("Profile {} don't exist".format(current))
|
||||||
hosts = current.split(",")
|
hosts = current.split(",")
|
||||||
nodes = answers["ids"].split(",")
|
nodes = answers["ids"].split(",")
|
||||||
if len(hosts) > 1 and len(hosts) != len(nodes):
|
if len(hosts) > 1 and len(hosts) != len(nodes):
|
||||||
raise inquirer.errors.ValidationError("", reason="Hosts list should be the same length of nodes list")
|
_raise_val_err("Hosts list should be the same length of nodes list")
|
||||||
return True
|
return True
|
||||||
|
|||||||
+1
-6
@@ -13,12 +13,7 @@ from .api import start_api,stop_api,debug_api
|
|||||||
from .ai import ai
|
from .ai import ai
|
||||||
|
|
||||||
from .plugins import Plugins
|
from .plugins import Plugins
|
||||||
from .services import (
|
from .services.exceptions import ConnpyError, ProfileNotFoundError, ReservedNameError
|
||||||
NodeService, ProfileService, ConfigService,
|
|
||||||
PluginService, AIService, SystemService,
|
|
||||||
ExecutionService, ImportExportService, ConnpyError,
|
|
||||||
ProfileNotFoundError, ReservedNameError
|
|
||||||
)
|
|
||||||
|
|
||||||
from rich_argparse import RichHelpFormatter
|
from rich_argparse import RichHelpFormatter
|
||||||
# Bridge rich-argparse with our design system
|
# Bridge rich-argparse with our design system
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .node_service import NodeService
|
from .node_service import NodeService
|
||||||
from .profile_service import ProfileService
|
from .profile_service import ProfileService
|
||||||
from .execution_service import ExecutionService
|
|
||||||
from .import_export_service import ImportExportService
|
|
||||||
from .ai_service import AIService
|
from .ai_service import AIService
|
||||||
from .plugin_service import PluginService
|
from .plugin_service import PluginService
|
||||||
from .config_service import ConfigService
|
from .config_service import ConfigService
|
||||||
from .system_service import SystemService
|
|
||||||
|
def __getattr__(name: str):
|
||||||
|
if name == "ExecutionService":
|
||||||
|
from .execution_service import ExecutionService
|
||||||
|
return ExecutionService
|
||||||
|
elif name == "ImportExportService":
|
||||||
|
from .import_export_service import ImportExportService
|
||||||
|
return ImportExportService
|
||||||
|
elif name == "SystemService":
|
||||||
|
from .system_service import SystemService
|
||||||
|
return SystemService
|
||||||
|
elif name == "SyncService":
|
||||||
|
from .sync_service import SyncService
|
||||||
|
return SyncService
|
||||||
|
elif name == "UserService":
|
||||||
|
from .user_service import UserService
|
||||||
|
return UserService
|
||||||
|
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'NodeService',
|
'NodeService',
|
||||||
@@ -17,6 +32,8 @@ __all__ = [
|
|||||||
'PluginService',
|
'PluginService',
|
||||||
'ConfigService',
|
'ConfigService',
|
||||||
'SystemService',
|
'SystemService',
|
||||||
|
'SyncService',
|
||||||
|
'UserService',
|
||||||
'ConnpyError',
|
'ConnpyError',
|
||||||
'NodeNotFoundError',
|
'NodeNotFoundError',
|
||||||
'NodeAlreadyExistsError',
|
'NodeAlreadyExistsError',
|
||||||
|
|||||||
+60
-13
@@ -14,6 +14,11 @@ class ServiceProvider:
|
|||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.config = config
|
self.config = config
|
||||||
self.remote_host = remote_host
|
self.remote_host = remote_host
|
||||||
|
self._system = None
|
||||||
|
self._execution = None
|
||||||
|
self._import_export = None
|
||||||
|
self._sync = None
|
||||||
|
self._users = None
|
||||||
|
|
||||||
if mode == "local":
|
if mode == "local":
|
||||||
self._init_local()
|
self._init_local()
|
||||||
@@ -28,34 +33,21 @@ class ServiceProvider:
|
|||||||
from .config_service import ConfigService
|
from .config_service import ConfigService
|
||||||
from .plugin_service import PluginService
|
from .plugin_service import PluginService
|
||||||
from .ai_service import AIService
|
from .ai_service import AIService
|
||||||
from .system_service import SystemService
|
|
||||||
from .execution_service import ExecutionService
|
|
||||||
from .import_export_service import ImportExportService
|
|
||||||
from .context_service import ContextService
|
from .context_service import ContextService
|
||||||
from .sync_service import SyncService
|
|
||||||
from .user_service import UserService
|
|
||||||
|
|
||||||
self.nodes = NodeService(self.config)
|
self.nodes = NodeService(self.config)
|
||||||
self.profiles = ProfileService(self.config)
|
self.profiles = ProfileService(self.config)
|
||||||
self.config_svc = ConfigService(self.config)
|
self.config_svc = ConfigService(self.config)
|
||||||
self.plugins = PluginService(self.config)
|
self.plugins = PluginService(self.config)
|
||||||
self.ai = AIService(self.config)
|
self.ai = AIService(self.config)
|
||||||
self.system = SystemService(self.config)
|
|
||||||
self.execution = ExecutionService(self.config)
|
|
||||||
self.import_export = ImportExportService(self.config)
|
|
||||||
self.context = ContextService(self.config)
|
self.context = ContextService(self.config)
|
||||||
self.sync = SyncService(self.config)
|
|
||||||
self.users = UserService(self.config.defaultdir)
|
|
||||||
|
|
||||||
def _init_remote(self):
|
def _init_remote(self):
|
||||||
# Allow ConfigService to work locally so the user can revert the mode
|
# Allow ConfigService to work locally so the user can revert the mode
|
||||||
from .config_service import ConfigService
|
from .config_service import ConfigService
|
||||||
from .context_service import ContextService
|
from .context_service import ContextService
|
||||||
from .sync_service import SyncService
|
|
||||||
self.config_svc = ConfigService(self.config)
|
self.config_svc = ConfigService(self.config)
|
||||||
self.context = ContextService(self.config)
|
self.context = ContextService(self.config)
|
||||||
self.sync = SyncService(self.config)
|
|
||||||
self.users = None
|
|
||||||
|
|
||||||
if not self.remote_host:
|
if not self.remote_host:
|
||||||
raise InvalidConfigurationError("Remote host must be specified in remote mode")
|
raise InvalidConfigurationError("Remote host must be specified in remote mode")
|
||||||
@@ -98,3 +90,58 @@ class ServiceProvider:
|
|||||||
self.execution = ExecutionStub(channel, remote_host=self.remote_host)
|
self.execution = ExecutionStub(channel, remote_host=self.remote_host)
|
||||||
self.import_export = ImportExportStub(channel, remote_host=self.remote_host)
|
self.import_export = ImportExportStub(channel, remote_host=self.remote_host)
|
||||||
self.auth = AuthStub(channel, remote_host=self.remote_host)
|
self.auth = AuthStub(channel, remote_host=self.remote_host)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def system(self):
|
||||||
|
if self._system is None and self.mode == "local":
|
||||||
|
from .system_service import SystemService
|
||||||
|
self._system = SystemService(self.config)
|
||||||
|
return self._system
|
||||||
|
|
||||||
|
@system.setter
|
||||||
|
def system(self, value):
|
||||||
|
self._system = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def execution(self):
|
||||||
|
if self._execution is None and self.mode == "local":
|
||||||
|
from .execution_service import ExecutionService
|
||||||
|
self._execution = ExecutionService(self.config)
|
||||||
|
return self._execution
|
||||||
|
|
||||||
|
@execution.setter
|
||||||
|
def execution(self, value):
|
||||||
|
self._execution = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def import_export(self):
|
||||||
|
if self._import_export is None and self.mode == "local":
|
||||||
|
from .import_export_service import ImportExportService
|
||||||
|
self._import_export = ImportExportService(self.config)
|
||||||
|
return self._import_export
|
||||||
|
|
||||||
|
@import_export.setter
|
||||||
|
def import_export(self, value):
|
||||||
|
self._import_export = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sync(self):
|
||||||
|
if self._sync is None:
|
||||||
|
from .sync_service import SyncService
|
||||||
|
self._sync = SyncService(self.config)
|
||||||
|
return self._sync
|
||||||
|
|
||||||
|
@sync.setter
|
||||||
|
def sync(self, value):
|
||||||
|
self._sync = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def users(self):
|
||||||
|
if self._users is None and self.mode == "local":
|
||||||
|
from .user_service import UserService
|
||||||
|
self._users = UserService(self.config.defaultdir)
|
||||||
|
return self._users
|
||||||
|
|
||||||
|
@users.setter
|
||||||
|
def users(self, value):
|
||||||
|
self._users = value
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import sys
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -6,13 +7,46 @@ import io
|
|||||||
import yaml
|
import yaml
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from google.oauth2.credentials import Credentials
|
|
||||||
from google.auth.transport.requests import Request
|
def __getattr__(name: str):
|
||||||
from googleapiclient.discovery import build
|
if name == "Credentials":
|
||||||
from google.auth.exceptions import RefreshError
|
from google.oauth2.credentials import Credentials
|
||||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
return Credentials
|
||||||
from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
elif name == "Request":
|
||||||
from googleapiclient.errors import HttpError
|
from google.auth.transport.requests import Request
|
||||||
|
return Request
|
||||||
|
elif name == "build":
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
return build
|
||||||
|
elif name == "RefreshError":
|
||||||
|
from google.auth.exceptions import RefreshError
|
||||||
|
return RefreshError
|
||||||
|
elif name == "InstalledAppFlow":
|
||||||
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
|
return InstalledAppFlow
|
||||||
|
elif name == "MediaFileUpload":
|
||||||
|
from googleapiclient.http import MediaFileUpload
|
||||||
|
return MediaFileUpload
|
||||||
|
elif name == "MediaIoBaseDownload":
|
||||||
|
from googleapiclient.http import MediaIoBaseDownload
|
||||||
|
return MediaIoBaseDownload
|
||||||
|
elif name == "HttpError":
|
||||||
|
from googleapiclient.errors import HttpError
|
||||||
|
return HttpError
|
||||||
|
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
||||||
|
|
||||||
|
def _get_google_libs():
|
||||||
|
mod = sys.modules[__name__]
|
||||||
|
return (
|
||||||
|
getattr(mod, "Credentials"),
|
||||||
|
getattr(mod, "Request"),
|
||||||
|
getattr(mod, "build"),
|
||||||
|
getattr(mod, "RefreshError"),
|
||||||
|
getattr(mod, "InstalledAppFlow"),
|
||||||
|
getattr(mod, "MediaFileUpload"),
|
||||||
|
getattr(mod, "MediaIoBaseDownload"),
|
||||||
|
getattr(mod, "HttpError"),
|
||||||
|
)
|
||||||
|
|
||||||
from .base import BaseService
|
from .base import BaseService
|
||||||
from .. import printer
|
from .. import printer
|
||||||
@@ -44,6 +78,7 @@ class SyncService(BaseService):
|
|||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
"""Authenticate with Google Drive."""
|
"""Authenticate with Google Drive."""
|
||||||
|
Credentials, Request, _, RefreshError, InstalledAppFlow, _, _, _ = _get_google_libs()
|
||||||
creds = None
|
creds = None
|
||||||
if os.path.exists(self.token_file):
|
if os.path.exists(self.token_file):
|
||||||
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
|
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
|
||||||
@@ -81,6 +116,7 @@ class SyncService(BaseService):
|
|||||||
|
|
||||||
def get_credentials(self):
|
def get_credentials(self):
|
||||||
"""Get valid credentials, refreshing if necessary."""
|
"""Get valid credentials, refreshing if necessary."""
|
||||||
|
Credentials, Request, _, RefreshError, _, _, _, _ = _get_google_libs()
|
||||||
if os.path.exists(self.token_file):
|
if os.path.exists(self.token_file):
|
||||||
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
|
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
|
||||||
else:
|
else:
|
||||||
@@ -98,6 +134,7 @@ class SyncService(BaseService):
|
|||||||
|
|
||||||
def check_login_status(self):
|
def check_login_status(self):
|
||||||
"""Check if logged in to Google Drive."""
|
"""Check if logged in to Google Drive."""
|
||||||
|
Credentials, Request, _, RefreshError, _, _, _, _ = _get_google_libs()
|
||||||
if os.path.exists(self.token_file):
|
if os.path.exists(self.token_file):
|
||||||
creds = Credentials.from_authorized_user_file(self.token_file)
|
creds = Credentials.from_authorized_user_file(self.token_file)
|
||||||
if creds and creds.expired and creds.refresh_token:
|
if creds and creds.expired and creds.refresh_token:
|
||||||
@@ -110,6 +147,7 @@ class SyncService(BaseService):
|
|||||||
|
|
||||||
def list_backups(self):
|
def list_backups(self):
|
||||||
"""List files in Google Drive appDataFolder."""
|
"""List files in Google Drive appDataFolder."""
|
||||||
|
_, _, build, _, _, _, _, HttpError = _get_google_libs()
|
||||||
creds = self.get_credentials()
|
creds = self.get_credentials()
|
||||||
if not creds:
|
if not creds:
|
||||||
printer.error("Not logged in to Google Drive.")
|
printer.error("Not logged in to Google Drive.")
|
||||||
@@ -168,6 +206,7 @@ class SyncService(BaseService):
|
|||||||
|
|
||||||
def upload_file(self, file_path, timestamp):
|
def upload_file(self, file_path, timestamp):
|
||||||
"""Internal method to upload to Drive."""
|
"""Internal method to upload to Drive."""
|
||||||
|
_, _, build, _, _, MediaFileUpload, _, _ = _get_google_libs()
|
||||||
creds = self.get_credentials()
|
creds = self.get_credentials()
|
||||||
if not creds: return False
|
if not creds: return False
|
||||||
|
|
||||||
@@ -193,6 +232,7 @@ class SyncService(BaseService):
|
|||||||
|
|
||||||
def delete_backup(self, file_id):
|
def delete_backup(self, file_id):
|
||||||
"""Delete a backup from Drive."""
|
"""Delete a backup from Drive."""
|
||||||
|
_, _, build, _, _, _, _, _ = _get_google_libs()
|
||||||
creds = self.get_credentials()
|
creds = self.get_credentials()
|
||||||
if not creds: return False
|
if not creds: return False
|
||||||
try:
|
try:
|
||||||
@@ -226,6 +266,7 @@ class SyncService(BaseService):
|
|||||||
|
|
||||||
def download_file(self, file_id, dest):
|
def download_file(self, file_id, dest):
|
||||||
"""Internal method to download from Drive."""
|
"""Internal method to download from Drive."""
|
||||||
|
_, _, build, _, _, _, MediaIoBaseDownload, _ = _get_google_libs()
|
||||||
creds = self.get_credentials()
|
creds = self.get_credentials()
|
||||||
if not creds: return False
|
if not creds: return False
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user