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:
2026-08-11 17:06:40 -03:00
parent fe189885d4
commit bf5bddda5a
11 changed files with 225 additions and 86 deletions
+5 -1
View File
@@ -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:
+22 -21
View File
@@ -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,27 +41,28 @@ def hex_to_blessed(hex_str):
except: except:
return prefix + term.normal return prefix + term.normal
# Custom inquirer theme matching connpy colors
class ConnpyTheme(Default):
def __init__(self):
super().__init__()
try:
from ..printer import _global_active_styles
# Use user_prompt as primary accent, fallback to info/cyan
accent = _global_active_styles.get("user_prompt", _global_active_styles.get("info", "cyan"))
accent_color = hex_to_blessed(accent)
self.Question.mark_color = accent_color
self.List.selection_color = accent_color
self.List.selection_cursor = ">"
except:
# Absolute fallback to standard cyan
self.Question.mark_color = term.cyan
self.List.selection_color = term.bold_cyan
self.List.selection_cursor = ">"
def get_theme(): def get_theme():
"""Returns a fresh instance of the theme with current colors.""" """Returns a fresh instance of the theme with current colors."""
from inquirer.themes import Default, term
class ConnpyTheme(Default):
def __init__(self):
super().__init__()
try:
from ..printer import _global_active_styles
# Use user_prompt as primary accent, fallback to info/cyan
accent = _global_active_styles.get("user_prompt", _global_active_styles.get("info", "cyan"))
accent_color = hex_to_blessed(accent)
self.Question.mark_color = accent_color
self.List.selection_color = accent_color
self.List.selection_cursor = ">"
except:
# Absolute fallback to standard cyan
self.Question.mark_color = term.cyan
self.List.selection_color = term.bold_cyan
self.List.selection_cursor = ">"
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:
+13 -3
View File
@@ -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"]:
+13 -3
View File
@@ -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"]:
+13 -3
View File
@@ -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"]:
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+20 -3
View File
@@ -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
View File
@@ -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
+48 -7
View File
@@ -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: