From 348cb051827cbea9427ac3ce99e23b72947067e4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Mar 2021 12:52:52 +0100 Subject: [PATCH 01/25] SyncServer - added required methods Implemented required methods in LocalDriveHandler Added progress for LocalDriveHandler --- .../providers/abstract_provider.py | 22 +++++ .../sync_server/providers/local_drive.py | 89 +++++++++++++++---- 2 files changed, 95 insertions(+), 16 deletions(-) diff --git a/pype/modules/sync_server/providers/abstract_provider.py b/pype/modules/sync_server/providers/abstract_provider.py index 9130a06d948..56928e93d8b 100644 --- a/pype/modules/sync_server/providers/abstract_provider.py +++ b/pype/modules/sync_server/providers/abstract_provider.py @@ -71,3 +71,25 @@ def list_folder(self, folder_path): (list) """ pass + + @abstractmethod + def create_folder(self, folder_path): + """ + Create all nonexistent folders and subfolders in 'path'. + + Args: + path (string): absolute path + + Returns: + (string) folder id of lowest subfolder from 'path' + """ + pass + + @abstractmethod + def get_tree(self): + """ + Creates folder structure for providers which do not provide + tree folder structure (GDrive has no accessible tree structure, + only parents and their parents) + """ + pass diff --git a/pype/modules/sync_server/providers/local_drive.py b/pype/modules/sync_server/providers/local_drive.py index 4d16b8b9301..8d816b008a3 100644 --- a/pype/modules/sync_server/providers/local_drive.py +++ b/pype/modules/sync_server/providers/local_drive.py @@ -1,6 +1,8 @@ from __future__ import print_function import os.path import shutil +import threading +import time from pype.api import Logger from .abstract_provider import AbstractProvider @@ -13,29 +15,37 @@ class LocalDriveHandler(AbstractProvider): def is_active(self): return True - def upload_file(self, source_path, target_path, overwrite=True): + def upload_file(self, source_path, target_path, + server, collection, file, representation, site, + overwrite=False, direction="Upload"): """ Copies file from 'source_path' to 'target_path' """ - if os.path.exists(source_path): - if overwrite: - shutil.copy(source_path, target_path) - else: - if os.path.exists(target_path): - raise ValueError("File {} exists, set overwrite". - format(target_path)) + if not os.path.isfile(source_path): + raise FileNotFoundError("Source file {} doesn't exist." + .format(source_path)) + if overwrite: + thread = threading.Thread(target=self._copy, + args=(source_path, target_path)) + thread.start() + self._mark_progress(collection, file, representation, server, + site, source_path, target_path, direction) + else: + if os.path.exists(target_path): + raise ValueError("File {} exists, set overwrite". + format(target_path)) - def download_file(self, source_path, local_path, overwrite=True): + return os.path.basename(target_path) + + def download_file(self, source_path, local_path, + server, collection, file, representation, site, + overwrite=False): """ Download a file form 'source_path' to 'local_path' """ - if os.path.exists(source_path): - if overwrite: - shutil.copy(source_path, local_path) - else: - if os.path.exists(local_path): - raise ValueError("File {} exists, set overwrite". - format(local_path)) + return self.upload_file(source_path, local_path, + server, collection, file, representation, site, + overwrite, direction="Download") def delete_file(self, path): """ @@ -57,3 +67,50 @@ def list_folder(self, folder_path): lst.append(os.path.join(dir_path, name)) return lst + + def create_folder(self, folder_path): + """ + Creates 'folder_path' on local system + + Args: + folder_path (string): absolute path on local (and mounted) disk + + Returns: + (string) - sends back folder_path to denote folder(s) was + created + """ + os.makedirs(folder_path, exist_ok=True) + return folder_path + + def get_tree(self): + return + + def _copy(self, source_path, target_path): + print("copying {}->{}".format(source_path, target_path)) + shutil.copy(source_path, target_path) + + def _mark_progress(self, collection, file, representation, server, site, + source_path, target_path, direction): + """ + Updates progress field in DB by values 0-1. + + Compares file sizes of source and target. + """ + source_file_size = os.path.getsize(source_path) + target_file_size = 0 + last_tick = status_val = None + while source_file_size != target_file_size: + if not last_tick or \ + time.time() - last_tick >= server.LOG_PROGRESS_SEC: + status_val = target_file_size / source_file_size + last_tick = time.time() + log.debug(direction + "ed %d%%." % int(status_val * 100)) + server.update_db(collection=collection, + new_file_id=None, + file=file, + representation=representation, + site=site, + progress=status_val + ) + target_file_size = os.path.getsize(target_path) + time.sleep(0.5) From 3e9c86799de6b6fa0f7dc3d7cfc79948b094fa62 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Mar 2021 12:53:29 +0100 Subject: [PATCH 02/25] SyncServer - fix exceptions when no project configured --- pype/modules/sync_server/tray/app.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/modules/sync_server/tray/app.py b/pype/modules/sync_server/tray/app.py index 2adec8382b8..3dc3e56e7fe 100644 --- a/pype/modules/sync_server/tray/app.py +++ b/pype/modules/sync_server/tray/app.py @@ -29,6 +29,8 @@ -1: 'Not available' } +DUMMY_PROJECT = "No project configured" + class SyncServerWindow(QtWidgets.QDialog): """ @@ -157,6 +159,7 @@ def refresh(self): model = self.project_list.model() model.clear() + project_name = None for project_name in self.sync_server.get_synced_presets().keys(): if self.sync_server.is_paused() or \ self.sync_server.is_project_paused(project_name): @@ -167,7 +170,7 @@ def refresh(self): model.appendRow(QtGui.QStandardItem(icon, project_name)) if len(self.sync_server.get_synced_presets().keys()) == 0: - model.appendRow(QtGui.QStandardItem("No project configured")) + model.appendRow(QtGui.QStandardItem(DUMMY_PROJECT)) self.current_project = self.project_list.currentIndex().data( QtCore.Qt.DisplayRole @@ -176,7 +179,8 @@ def refresh(self): self.current_project = self.project_list.model().item(0). \ data(QtCore.Qt.DisplayRole) - self.local_site = self.sync_server.get_local_site(project_name) + if project_name: + self.local_site = self.sync_server.get_local_site(project_name) def _get_icon(self, status): if not self.icons.get(status): @@ -620,6 +624,8 @@ def __init__(self, sync_server, header, project=None): self.filter = None self._initialized = False + if not self._project or self._project == DUMMY_PROJECT: + return self.sync_server = sync_server # TODO think about admin mode From 720ab0f5616dd009cddee5741452eb2d4047160a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Mar 2021 12:57:11 +0100 Subject: [PATCH 03/25] SyncServer - fixes, refactor --- pype/modules/sync_server/sync_server.py | 110 ++++++++++++------------ 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/pype/modules/sync_server/sync_server.py b/pype/modules/sync_server/sync_server.py index 22dede66d80..32acd55d1d5 100644 --- a/pype/modules/sync_server/sync_server.py +++ b/pype/modules/sync_server/sync_server.py @@ -119,7 +119,8 @@ def initialize(self, module_settings): # some parts of code need to run sequentially, not in async self.lock = None self.connection = None # connection to avalon DB to update state - self.presets = None # settings for all enabled projects for sync + # settings for all enabled projects for sync + self.sync_project_presets = None self.sync_server_thread = None # asyncio requires new thread self.action_show_widget = None @@ -335,15 +336,14 @@ def tray_init(self): if not self.enabled: return - self.presets = None + self.sync_project_presets = None self.lock = threading.Lock() self.connection = AvalonMongoDB() self.connection.install() try: - self.presets = self.get_synced_presets() - self.set_active_sites(self.presets) + self.sync_project_presets = self.get_synced_presets() self.sync_server_thread = SyncServerThread(self) from .tray.app import SyncServerWindow self.widget = SyncServerWindow(self) @@ -355,7 +355,7 @@ def tray_init(self): "There are not set presets for SyncServer OR " "Credentials provided are invalid, " "no syncing possible"). - format(str(self.presets)), exc_info=True) + format(str(self.sync_project_presets)), exc_info=True) self.enabled = False def tray_start(self): @@ -369,7 +369,8 @@ def tray_start(self): Returns: None """ - if self.presets and self.active_sites: + if self.sync_project_presets and \ + self.has_working_sites(self.sync_project_presets): self.sync_server_thread.start() else: log.info("No presets or active providers. " + @@ -431,8 +432,9 @@ def get_synced_presets(self): Returns: (dict): of settings, keys are project names """ - if self.presets: # presets set already, do not call again and again - return self.presets + # presets set already, do not call again and again + if self.sync_project_presets: + return self.sync_project_presets sync_presets = {} if not self.connection: @@ -460,8 +462,9 @@ def get_synced_preset(self, project_name): """ # presets set already, do not call again and again # self.log.debug("project preset {}".format(self.presets)) - if self.presets and self.presets.get(project_name): - return self.presets.get(project_name) + if self.sync_project_presets and \ + self.sync_project_presets.get(project_name): + return self.sync_project_presets.get(project_name) settings = get_project_settings(project_name) sync_settings = settings.get("global")["sync_server"] @@ -474,20 +477,26 @@ def get_synced_preset(self, project_name): return {} - def set_active_sites(self, settings): + def has_working_sites(self, settings): """ - Sets 'self.active_sites' as a dictionary from provided 'settings' + Learn if there is any valid combination of active and remote sites + for any configured project. + + If yes, sync server should be started, if not, it shouldnt be + started unnecessary to hog resources. - Format: - { 'project_name' : ('provider_name', 'site_name') } Args: settings (dict): all enabled project sync setting (sites labesl, retries count etc.) """ - self.active_sites = {} - initiated_handlers = {} + initiated_handlers = {} # handlers cache for project_name, project_setting in settings.items(): + set_sites = {self.get_local_site(project_name): False, + self.get_remote_site(project_name): False} for site_name, config in project_setting.get("sites").items(): + if site_name not in set_sites.keys(): + continue + handler = initiated_handlers.get(config["provider"]) if not handler: handler = lib.factory.get_provider(config["provider"], @@ -496,29 +505,12 @@ def set_active_sites(self, settings): initiated_handlers[config["provider"]] = handler if handler.is_active(): - if not self.active_sites.get('project_name'): - self.active_sites[project_name] = [] + set_sites[site_name] = True + if all(set_sites.values()): + return True - self.active_sites[project_name].append( - (config["provider"], site_name)) - - if not self.active_sites: - log.info("No sync sites active, no working credentials provided") - - def get_active_sites(self, project_name): - """ - Returns active sites (provider configured and able to connect) per - project. - - Args: - project_name (str): used as a key in dict - - Returns: - (dict): - Format: - { 'project_name' : ('provider_name', 'site_name') } - """ - return self.active_sites[project_name] + log.info("No tuple of active-remote sites is active for any project.") + return False def get_local_site(self, project_name): """ @@ -606,8 +598,9 @@ def get_sync_representations(self, collection, active_site, remote_site): ]} ] } - - log.debug("get_sync_representations.query: {}".format(query)) + log.debug("active_site:{} - remote_site:{}".format(active_site, + remote_site)) + log.debug("query: {}".format(query)) representations = self.connection.find(query) return representations @@ -686,9 +679,11 @@ async def upload(self, collection, file, representation, provider_name, # structure should be run in parallel handler = lib.factory.get_provider(provider_name, site_name, tree=tree, presets=preset) - remote_file = self._get_remote_file_path(file, - handler.get_roots_config() - ) + + root_configs = self._get_roots_config(self.sync_project_presets, + collection, + site_name) + remote_file = self._get_remote_file_path(file, root_configs) local_file = self.get_local_file_path(collection, file.get("path", "")) @@ -737,8 +732,11 @@ async def download(self, collection, file, representation, provider_name, with self.lock: handler = lib.factory.get_provider(provider_name, site_name, tree=tree, presets=preset) - remote_file_path = self._get_remote_file_path( - file, handler.get_roots_config()) + + root_configs = self._get_roots_config(self.sync_project_presets, + collection, + site_name) + remote_file_path = self._get_remote_file_path(file, root_configs) local_file_path = self.get_local_file_path(collection, file.get("path", "")) @@ -1072,13 +1070,8 @@ def _remove_local_file(self, collection, representation_id, site_name): format(site_name)) return - handler = None - sites = self.get_active_sites(collection) - for provider_name, provider_site_name in sites: - if provider_site_name == site_name: - handler = lib.factory.get_provider(provider_name, - site_name) - break + provider_name = self.get_provider_for_site(collection, site_name) + handler = lib.factory.get_provider(provider_name, site_name) if handler and isinstance(handler, LocalDriveHandler): query = { @@ -1141,7 +1134,8 @@ def get_loop_delay(self, project_name): Returns: (int): in seconds """ - return int(self.presets[project_name]["config"]["loop_delay"]) + ld = self.sync_project_presets[project_name]["config"]["loop_delay"] + return int(ld) def show_widget(self): """Show dialog to enter credentials""" @@ -1278,12 +1272,20 @@ def _get_retries_arr(self, project_name): Returns: (list) """ - retry_cnt = self.presets[project_name].get("config")["retry_cnt"] + retry_cnt = self.sync_project_presets[project_name].\ + get("config")["retry_cnt"] arr = [i for i in range(int(retry_cnt))] arr.append(None) return arr + def _get_roots_config(self, presets, project_name, site_name): + """ + Returns configured root(s) for 'project_name' and 'site_name' from + settings ('presets') + """ + return presets[project_name]['sites'][site_name]['root'] + class SyncServerThread(threading.Thread): """ From 5faf1543d7acbd32b8095112ffc5de9a5addbe17 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 Mar 2021 17:06:19 +0100 Subject: [PATCH 04/25] SyncServer - use properly get_local_site_id for user's local site in app --- pype/modules/sync_server/tray/app.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pype/modules/sync_server/tray/app.py b/pype/modules/sync_server/tray/app.py index 3dc3e56e7fe..2a67d906b23 100644 --- a/pype/modules/sync_server/tray/app.py +++ b/pype/modules/sync_server/tray/app.py @@ -15,8 +15,7 @@ from bson.objectid import ObjectId from pype.lib import PypeLogger - -import json +from pype.api import get_local_site_id log = PypeLogger().get_logger("SyncServer") @@ -213,8 +212,7 @@ def _on_context_menu(self, point): actions_mapping[action] = self._pause menu.addAction(action) - if self.local_site == self.sync_server.get_my_local_site( - self.project_name): + if self.local_site == self.sync_server.get_my_local_site(): action = QtWidgets.QAction("Clear local project") actions_mapping[action] = self._clear_project menu.addAction(action) @@ -477,7 +475,7 @@ def _unpause(self): def _add_site(self): log.info(self.representation_id) project_name = self.table_view.model()._project - local_site_name = self.sync_server.get_my_local_site(project_name) + local_site_name = self.sync_server.get_my_local_site() try: self.sync_server.add_site( self.table_view.model()._project, @@ -502,13 +500,14 @@ def _remove_site(self): """ log.info("Removing {}".format(self.representation_id)) try: + local_site = get_local_site_id() self.sync_server.remove_site( self.table_view.model()._project, self.representation_id, - 'local_0', + local_site, True ) - self.message_generated.emit("Site local_0 removed") + self.message_generated.emit("Site {} removed".format(local_site)) except ValueError as exp: self.message_generated.emit("Error {}".format(str(exp))) From 02bd3570209fbebe162d01e29b15dffebc9f3902 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 Mar 2021 17:07:46 +0100 Subject: [PATCH 05/25] SyncServer - cleaned up unneeded configuration local_id is accessible by get_local_site_id, shouldnt be visible in Settings studio and local sites should be configured in Local Settings --- .../defaults/project_settings/global.json | 17 +---------------- .../schema_project_syncserver.json | 6 ------ 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/pype/settings/defaults/project_settings/global.json b/pype/settings/defaults/project_settings/global.json index 92ef9bdb28b..ada4a6e17cc 100644 --- a/pype/settings/defaults/project_settings/global.json +++ b/pype/settings/defaults/project_settings/global.json @@ -198,11 +198,10 @@ "sync_server": { "enabled": true, "config": { - "local_id": "local_0", "retry_cnt": "3", "loop_delay": "60", "active_site": "studio", - "remote_site": "gdrive" + "remote_site": "studio" }, "sites": { "gdrive": { @@ -211,20 +210,6 @@ "root": { "work": "" } - }, - "studio": { - "provider": "local_drive", - "credentials_url": "", - "root": { - "work": "" - } - }, - "local_0": { - "provider": "local_drive", - "credentials_url": "", - "root": { - "work": "" - } } } } diff --git a/pype/settings/entities/schemas/projects_schema/schema_project_syncserver.json b/pype/settings/entities/schemas/projects_schema/schema_project_syncserver.json index 4bc3fb6c48e..fd728f39824 100644 --- a/pype/settings/entities/schemas/projects_schema/schema_project_syncserver.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_syncserver.json @@ -17,12 +17,6 @@ "label": "Config", "collapsible": true, "children": [ - - { - "type": "text", - "key": "local_id", - "label": "Local ID" - }, { "type": "text", "key": "retry_cnt", From 1a574198accdc6bb5df13018ccaa90d7bef36d74 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 Mar 2021 17:08:26 +0100 Subject: [PATCH 06/25] SyncServer - use get_local_site_id for local site --- pype/plugins/publish/integrate_new.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/plugins/publish/integrate_new.py b/pype/plugins/publish/integrate_new.py index 14b25b9c46b..63f746da7e6 100644 --- a/pype/plugins/publish/integrate_new.py +++ b/pype/plugins/publish/integrate_new.py @@ -969,6 +969,9 @@ def prepare_file_info(self, path, size=None, file_hash=None, if sync_server_presets["enabled"]: local_site = sync_server_presets["config"].\ get("active_site", "studio").strip() + if local_site == 'local': + local_site = pype.api.get_local_site_id() + remote_site = sync_server_presets["config"].get("remote_site") rec = { From ba9b56fc7abd6f8d7e8ad3083dba418c316928f4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 Mar 2021 17:14:11 +0100 Subject: [PATCH 07/25] SyncServer - added get_active_sites, get_remote_sites methods refactor renamed get_local_site to get_active_site (part of public API) --- pype/modules/sync_server/sync_server.py | 223 ++++++++++++++++-------- pype/modules/sync_server/tray/app.py | 8 +- 2 files changed, 158 insertions(+), 73 deletions(-) diff --git a/pype/modules/sync_server/sync_server.py b/pype/modules/sync_server/sync_server.py index 32acd55d1d5..19fe62c0697 100644 --- a/pype/modules/sync_server/sync_server.py +++ b/pype/modules/sync_server/sync_server.py @@ -1,7 +1,7 @@ from pype.api import ( Anatomy, get_project_settings, - get_current_project_settings) + get_local_site_id) import threading import concurrent.futures @@ -97,6 +97,7 @@ class SyncServer(PypeModule, ITrayModule): # set 0 to no limit REPRESENTATION_LIMIT = 100 DEFAULT_SITE = 'studio' + LOCAL_SITE = 'local' LOG_PROGRESS_SEC = 5 # how often log progress to DB name = "sync_server" @@ -129,7 +130,7 @@ def initialize(self, module_settings): self._paused_representations = set() self._anatomies = {} - # public facing API + """ Start of Public API """ def add_site(self, collection, representation_id, site_name=None): """ Adds new site to representation to be synced. @@ -323,6 +324,68 @@ def is_paused(self): """ Is server paused """ return self._paused + def get_active_sites(self, project_name): + """ + Returns list of active sites for 'project_name'. + + By default it returns ['studio', 'local'], these sites are defaults + and always present even if SyncServer is not enabled. (for publish) + + Used mainly for Local settings for user override. + + Args: + project_name (string): + + Returns: + (list) of strings + """ + sites = [self.DEFAULT_SITE, self.LOCAL_SITE] + + return sites + + def get_remote_sites(self, project_name): + """ + Returns all remote sites configured on 'project_name'. + + If 'project_name' is not enabled for syncing returns []. + + Used by Local setting to allow user choose remote site. + + Args: + project_name (string): + + Returns: + (list) of strings + """ + sync_presets = self.get_synced_presets() + remote_sites = [] + if sync_presets: + for _, sites in self.get_configured_sites(sync_presets, + project_name).items(): + for site, enabled in sites.items(): + if enabled: + remote_sites.append(site) + + return remote_sites + + def get_active_site(self, project_name): + """ + Returns active (mine) site for 'project_name' from settings + """ + active_site = self.get_synced_preset(project_name)['config']\ + ['active_site'] + if active_site == self.LOCAL_SITE: + return get_local_site_id() + return active_site + + def get_remote_site(self, project_name): + """ + Returns remote (theirs) site for 'project_name' from settings + """ + return self.get_synced_preset(project_name)['config']['remote_site'] + + """ End of Public API """ + def connect_with_modules(self, *_a, **kw): return @@ -343,7 +406,7 @@ def tray_init(self): self.connection.install() try: - self.sync_project_presets = self.get_synced_presets() + self.set_sync_project_presets() self.sync_server_thread = SyncServerThread(self) from .tray.app import SyncServerWindow self.widget = SyncServerWindow(self) @@ -426,30 +489,21 @@ def get_anatomy(self, project_name): """ return self._anatomies.get('project_name') or Anatomy(project_name) - def get_synced_presets(self): + def get_synced_presets(self, refresh=False): """ Collects all projects which have enabled syncing and their settings + Args: + refresh (bool): refresh presets from settings - used when user + changes site in Local Settings Returns: (dict): of settings, keys are project names + {'projectA':{enabled: True, sites:{}...} """ # presets set already, do not call again and again - if self.sync_project_presets: - return self.sync_project_presets - - sync_presets = {} - if not self.connection: - self.connection = AvalonMongoDB() - self.connection.install() + if not self.sync_project_presets: + self.set_sync_project_presets() - for collection in self.connection.database.collection_names(False): - sync_settings = self.get_synced_preset(collection) - if sync_settings: - sync_presets[collection] = sync_settings - - if not sync_presets: - log.info("No enabled and configured projects for sync.") - - return sync_presets + return self.sync_project_presets def get_synced_preset(self, project_name): """ Handles pulling sync_server's settings for enabled 'project_name' @@ -477,52 +531,88 @@ def get_synced_preset(self, project_name): return {} - def has_working_sites(self, settings): - """ - Learn if there is any valid combination of active and remote sites - for any configured project. + def set_sync_project_presets(self): + sync_presets = {} + if not self.connection: + self.connection = AvalonMongoDB() + self.connection.install() - If yes, sync server should be started, if not, it shouldnt be - started unnecessary to hog resources. + for collection in self.connection.database.collection_names(False): + sync_settings = self.get_synced_preset(collection) + if sync_settings: + sync_presets[collection] = sync_settings - Args: - settings (dict): all enabled project sync setting (sites labesl, - retries count etc.) + if not sync_presets: + log.info("No enabled and configured projects for sync.") + + self.sync_project_presets = sync_presets + + def get_configured_sites(self, settings, only_project_name=None): + """ + Loops through settings and looks for configured sites and checks + its handlers. + + Args: + settings(dict): dictionary from Settings + only_project_name(string, optional): only interested in + particular project + Returns: + (dict of dict) + {'ProjectA': {'studio':True, 'gdrive':False}} """ initiated_handlers = {} # handlers cache + working_sites_per_project = {} for project_name, project_setting in settings.items(): - set_sites = {self.get_local_site(project_name): False, - self.get_remote_site(project_name): False} - for site_name, config in project_setting.get("sites").items(): - if site_name not in set_sites.keys(): - continue + if only_project_name and project_name != only_project_name: + continue + + if not project_setting.get("enabled"): + continue - handler = initiated_handlers.get(config["provider"]) + configured_sites = {} + for site_name, config in project_setting.get("sites").items(): + handler = initiated_handlers.\ + get((config["provider"], site_name)) if not handler: handler = lib.factory.get_provider(config["provider"], site_name, presets=config) - initiated_handlers[config["provider"]] = handler + initiated_handlers[(config["provider"], site_name)] = \ + handler if handler.is_active(): - set_sites[site_name] = True - if all(set_sites.values()): - return True + configured_sites[site_name] = True - log.info("No tuple of active-remote sites is active for any project.") - return False + working_sites_per_project[project_name] = configured_sites - def get_local_site(self, project_name): - """ - Returns active (mine) site for 'project_name' from settings - """ - return self.get_synced_preset(project_name)['config']['active_site'] + return working_sites_per_project - def get_remote_site(self, project_name): + def has_working_sites(self, settings, only_project_name=None): """ - Returns remote (theirs) site for 'project_name' from settings + Learn if there is any valid combination of active and remote sites + for any configured project. + + Valid means that both handlers are configured correctly + (credentials for remote sites etc.) + + If yes, sync server should be started, if not, it shouldnt be + started unnecessary to hog resources. + + Args: + settings (dict): all enabled project sync setting (sites labesl, + retries count etc.) """ - return self.get_synced_preset(project_name)['config']['remote_site'] + working_sites_per_project = self.get_configured_sites( + settings, only_project_name) + + for project_name in working_sites_per_project.keys(): + if only_project_name and project_name != only_project_name: + continue + if all(working_sites_per_project[project_name].values()): + return True + + log.info("No tuple of active-remote sites is active for project.") + return False def get_provider_for_site(self, project_name, site): """ @@ -743,7 +833,7 @@ async def download(self, collection, file, representation, provider_name, local_folder = os.path.dirname(local_file_path) os.makedirs(local_folder, exist_ok=True) - local_site = self.get_local_site(collection) + local_site = self.get_active_site(collection) loop = asyncio.get_running_loop() file_id = await loop.run_in_executor(None, @@ -905,7 +995,7 @@ def reset_provider_for_file(self, collection, representation_id, raise ValueError("Misconfiguration, only one of side and " + "site_name arguments should be passed.") - local_site = self.get_local_site(collection) + local_site = self.get_active_site(collection) remote_site = self.get_remote_site(collection) if side: @@ -1064,7 +1154,7 @@ def _remove_local_file(self, collection, representation_id, site_name): Returns: only logs, catches IndexError and OSError """ - my_local_site = self.get_my_local_site(collection) + my_local_site = self.get_my_local_site() if my_local_site != site_name: self.log.warning("Cannot remove non local file for {}". format(site_name)) @@ -1109,23 +1199,14 @@ def _remove_local_file(self, collection, representation_id, site_name): self.log.warning(msg) raise ValueError(msg) - def get_my_local_site(self, project_name=None): + def get_my_local_site(self): """ - Returns name of current user local_site + Returns name of current user local_site, its Pype wide. - Args: - project_name (string): Returns: (string) """ - if project_name: - settings = get_project_settings(project_name) - else: - settings = get_current_project_settings() - - sync_server_presets = settings["global"]["sync_server"]["config"] - - return sync_server_presets.get("local_id") + return get_local_site_id() def get_loop_delay(self, project_name): """ @@ -1223,7 +1304,7 @@ def get_local_file_path(self, collection, path): Returns: (string) - absolute path on local system """ - local_active_site = self.get_local_site(collection) + local_active_site = self.get_active_site(collection) sites = self.get_synced_preset(collection)["sites"] root_config = sites[local_active_site]["root"] @@ -1336,20 +1417,24 @@ async def sync_loop(self): while self.is_running and not self.module.is_paused(): import time start_time = None + self.module.set_sync_project_presets() for collection, preset in self.module.get_synced_presets().\ items(): if self.module.is_project_paused(collection): continue + local_site = self.module.get_active_site(collection) + remote_site = self.module.get_remote_site(collection) + if local_site == remote_site: + self.log.debug("Both sites same, skipping") + start_time = time.time() sync_repres = self.module.get_sync_representations( collection, - preset.get('config')["active_site"], - preset.get('config')["remote_site"] + local_site, + remote_site ) - local_site = preset.get('config')["active_site"] - remote_site = preset.get('config')["remote_site"] task_files_to_process = [] files_processed_info = [] # process only unique file paths in one batch diff --git a/pype/modules/sync_server/tray/app.py b/pype/modules/sync_server/tray/app.py index 2a67d906b23..0abc3576e97 100644 --- a/pype/modules/sync_server/tray/app.py +++ b/pype/modules/sync_server/tray/app.py @@ -179,7 +179,7 @@ def refresh(self): data(QtCore.Qt.DisplayRole) if project_name: - self.local_site = self.sync_server.get_local_site(project_name) + self.local_site = self.sync_server.get_active_site(project_name) def _get_icon(self, status): if not self.icons.get(status): @@ -629,7 +629,7 @@ def __init__(self, sync_server, header, project=None): self.sync_server = sync_server # TODO think about admin mode # this is for regular user, always only single local and single remote - self.local_site = self.sync_server.get_local_site(self._project) + self.local_site = self.sync_server.get_active_site(self._project) self.remote_site = self.sync_server.get_remote_site(self._project) self.projection = self.get_default_projection() @@ -876,7 +876,7 @@ def set_project(self, project): project (str): name of project """ self._project = project - self.local_site = self.sync_server.get_local_site(self._project) + self.local_site = self.sync_server.get_active_site(self._project) self.remote_site = self.sync_server.get_remote_site(self._project) self.refresh() @@ -1465,7 +1465,7 @@ def __init__(self, sync_server, header, _id, project=None): self.sync_server = sync_server # TODO think about admin mode # this is for regular user, always only single local and single remote - self.local_site = self.sync_server.get_local_site(self._project) + self.local_site = self.sync_server.get_active_site(self._project) self.remote_site = self.sync_server.get_remote_site(self._project) self.sort = self.DEFAULT_SORT From 9c6544ad0feca0eb5558697c931bc33571665524 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 Mar 2021 17:37:13 +0100 Subject: [PATCH 08/25] SyncServer - add 2 new methods get_active_sites_from_settings , get_remote_sites_from_settings Refactorr --- pype/modules/sync_server/sync_server.py | 292 ++++++++++++++---------- 1 file changed, 168 insertions(+), 124 deletions(-) diff --git a/pype/modules/sync_server/sync_server.py b/pype/modules/sync_server/sync_server.py index 19fe62c0697..b7632153144 100644 --- a/pype/modules/sync_server/sync_server.py +++ b/pype/modules/sync_server/sync_server.py @@ -121,7 +121,7 @@ def initialize(self, module_settings): self.lock = None self.connection = None # connection to avalon DB to update state # settings for all enabled projects for sync - self.sync_project_presets = None + self.sync_project_settings = None self.sync_server_thread = None # asyncio requires new thread self.action_show_widget = None @@ -148,7 +148,7 @@ def add_site(self, collection, representation_id, site_name=None): Returns: throws ValueError if any issue """ - if not self.get_synced_preset(collection): + if not self.get_sync_project_setting(collection): raise ValueError("Project not configured") if not site_name: @@ -175,7 +175,7 @@ def remove_site(self, collection, representation_id, site_name, Returns: throws ValueError if any issue """ - if not self.get_synced_preset(collection): + if not self.get_sync_project_setting(collection): raise ValueError("Project not configured") self.reset_provider_for_file(collection, @@ -328,7 +328,7 @@ def get_active_sites(self, project_name): """ Returns list of active sites for 'project_name'. - By default it returns ['studio', 'local'], these sites are defaults + By default it returns ['studio'], this site is default and always present even if SyncServer is not enabled. (for publish) Used mainly for Local settings for user override. @@ -339,10 +339,44 @@ def get_active_sites(self, project_name): Returns: (list) of strings """ - sites = [self.DEFAULT_SITE, self.LOCAL_SITE] + return self.get_active_sites_from_setting( + get_project_settings(project_name)) + + def get_active_sites_from_setting(self, settings): + """ + List available active sites from incoming 'settings'. Used for + returning 'default' values for Local Settings + + Args: + settings (dict): full settings (global + project) + Returns: + (list) of strings + """ + sync_settings = self._parse_sync_settings_from_settings(settings) + + return self._get_active_sites_from_settings(sync_settings) + + def get_active_site(self, project_name): + """ + Returns active (mine) site for 'project_name' from settings + + Returns: + (string) + """ + active_site = self.get_sync_project_setting(project_name)['config']\ + ['active_site'] + if active_site == self.LOCAL_SITE: + return get_local_site_id() + return active_site + + def _get_active_sites_from_settings(self, sync_settings): + sites = [self.DEFAULT_SITE] + if self.enabled and sync_settings['enabled']: + sites.append(self.LOCAL_SITE) return sites + # remote sites def get_remote_sites(self, project_name): """ Returns all remote sites configured on 'project_name'. @@ -357,32 +391,35 @@ def get_remote_sites(self, project_name): Returns: (list) of strings """ - sync_presets = self.get_synced_presets() - remote_sites = [] - if sync_presets: - for _, sites in self.get_configured_sites(sync_presets, - project_name).items(): - for site, enabled in sites.items(): - if enabled: - remote_sites.append(site) - - return remote_sites + return self.get_remote_sites_from_setting( + get_project_settings(project_name)) - def get_active_site(self, project_name): + def get_remote_sites_from_setting(self, settings): """ - Returns active (mine) site for 'project_name' from settings + Get remote sites for returning 'default' values for Local Settings """ - active_site = self.get_synced_preset(project_name)['config']\ - ['active_site'] - if active_site == self.LOCAL_SITE: - return get_local_site_id() - return active_site + sync_settings = self._parse_sync_settings_from_settings(settings) + + return self._get_remote_sites_from_settings(sync_settings) def get_remote_site(self, project_name): """ Returns remote (theirs) site for 'project_name' from settings """ - return self.get_synced_preset(project_name)['config']['remote_site'] + return self.get_sync_project_setting(project_name)['config']['remote_site'] + + def _get_remote_sites_from_settings(self, sync_settings): + if not self.enabled or not sync_settings['enabled']: + return [] + + remote_sites = [self.DEFAULT_SITE, self.LOCAL_SITE] + if sync_settings: + for site, enabled in self._get_configured_sites_from_setting( + sync_settings).items(): + if enabled: + remote_sites.append(site) + + return list(set(remote_sites)) """ End of Public API """ @@ -399,14 +436,14 @@ def tray_init(self): if not self.enabled: return - self.sync_project_presets = None + self.sync_project_settings = None self.lock = threading.Lock() self.connection = AvalonMongoDB() self.connection.install() try: - self.set_sync_project_presets() + self.set_sync_project_settings() self.sync_server_thread = SyncServerThread(self) from .tray.app import SyncServerWindow self.widget = SyncServerWindow(self) @@ -418,7 +455,7 @@ def tray_init(self): "There are not set presets for SyncServer OR " "Credentials provided are invalid, " "no syncing possible"). - format(str(self.sync_project_presets)), exc_info=True) + format(str(self.sync_project_settings)), exc_info=True) self.enabled = False def tray_start(self): @@ -432,8 +469,7 @@ def tray_start(self): Returns: None """ - if self.sync_project_presets and \ - self.has_working_sites(self.sync_project_presets): + if self.sync_project_settings and self.enabled: self.sync_server_thread.start() else: log.info("No presets or active providers. " + @@ -489,23 +525,45 @@ def get_anatomy(self, project_name): """ return self._anatomies.get('project_name') or Anatomy(project_name) - def get_synced_presets(self, refresh=False): + def set_sync_project_settings(self): + """ + Set sync_project_settings for all projects (caching) + + For performance + """ + sync_project_presets = {} + if not self.connection: + self.connection = AvalonMongoDB() + self.connection.install() + + for collection in self.connection.database.collection_names(False): + sync_settings = self.get_sync_project_setting(collection) + if sync_settings: + sync_project_presets[collection] = sync_settings + + if not sync_project_presets: + log.info("No enabled and configured projects for sync.") + + self.sync_project_settings = sync_project_presets + + def get_sync_project_settings(self, refresh=False): """ Collects all projects which have enabled syncing and their settings Args: refresh (bool): refresh presets from settings - used when user - changes site in Local Settings + changes site in Local Settings or any time up-to-date values + are necessary Returns: (dict): of settings, keys are project names {'projectA':{enabled: True, sites:{}...} """ # presets set already, do not call again and again - if not self.sync_project_presets: - self.set_sync_project_presets() + if refresh or not self.sync_project_settings: + self.set_sync_project_settings() - return self.sync_project_presets + return self.sync_project_settings - def get_synced_preset(self, project_name): + def get_sync_project_setting(self, project_name): """ Handles pulling sync_server's settings for enabled 'project_name' Args: @@ -516,109 +574,79 @@ def get_synced_preset(self, project_name): """ # presets set already, do not call again and again # self.log.debug("project preset {}".format(self.presets)) - if self.sync_project_presets and \ - self.sync_project_presets.get(project_name): - return self.sync_project_presets.get(project_name) + if self.sync_project_settings and \ + self.sync_project_settings.get(project_name): + return self.sync_project_settings.get(project_name) settings = get_project_settings(project_name) - sync_settings = settings.get("global")["sync_server"] + return self._parse_sync_settings_from_settings(settings) + + def site_is_working(self, project_name, site_name): + """ + Confirm that 'site_name' is configured correctly for 'project_name' + Args: + project_name(string): + site_name(string): + Returns + (bool) + """ + if self._get_configured_sites(project_name).get(site_name): + return True + return False + + def _parse_sync_settings_from_settings(self, settings): + """ settings from api.get_project_settings, TOOD rename """ + sync_settings = settings.get("global").get("sync_server") if not sync_settings: - log.info("No project setting for {}, not syncing.". - format(project_name)) + log.info("No project setting not syncing.") return {} if sync_settings.get("enabled"): return sync_settings return {} - def set_sync_project_presets(self): - sync_presets = {} - if not self.connection: - self.connection = AvalonMongoDB() - self.connection.install() - - for collection in self.connection.database.collection_names(False): - sync_settings = self.get_synced_preset(collection) - if sync_settings: - sync_presets[collection] = sync_settings - - if not sync_presets: - log.info("No enabled and configured projects for sync.") - - self.sync_project_presets = sync_presets - - def get_configured_sites(self, settings, only_project_name=None): + def _get_configured_sites(self, project_name): """ Loops through settings and looks for configured sites and checks - its handlers. + its handlers for particular 'project_name'. Args: - settings(dict): dictionary from Settings + project_setting(dict): dictionary from Settings only_project_name(string, optional): only interested in particular project Returns: (dict of dict) {'ProjectA': {'studio':True, 'gdrive':False}} """ - initiated_handlers = {} # handlers cache - working_sites_per_project = {} - for project_name, project_setting in settings.items(): - if only_project_name and project_name != only_project_name: - continue - - if not project_setting.get("enabled"): - continue - - configured_sites = {} - for site_name, config in project_setting.get("sites").items(): - handler = initiated_handlers.\ - get((config["provider"], site_name)) - if not handler: - handler = lib.factory.get_provider(config["provider"], - site_name, - presets=config) - initiated_handlers[(config["provider"], site_name)] = \ - handler - - if handler.is_active(): - configured_sites[site_name] = True - - working_sites_per_project[project_name] = configured_sites - - return working_sites_per_project - - def has_working_sites(self, settings, only_project_name=None): - """ - Learn if there is any valid combination of active and remote sites - for any configured project. - - Valid means that both handlers are configured correctly - (credentials for remote sites etc.) + settings = self.get_sync_project_setting(project_name) + return self._get_configured_sites_from_setting(settings) - If yes, sync server should be started, if not, it shouldnt be - started unnecessary to hog resources. + def _get_configured_sites_from_setting(self, project_setting): + if not project_setting.get("enabled"): + return {} - Args: - settings (dict): all enabled project sync setting (sites labesl, - retries count etc.) - """ - working_sites_per_project = self.get_configured_sites( - settings, only_project_name) + initiated_handlers = {} + configured_sites = {} + for site_name, config in project_setting.get("sites").items(): + handler = initiated_handlers. \ + get((config["provider"], site_name)) + if not handler: + handler = lib.factory.get_provider(config["provider"], + site_name, + presets=config) + initiated_handlers[(config["provider"], site_name)] = \ + handler - for project_name in working_sites_per_project.keys(): - if only_project_name and project_name != only_project_name: - continue - if all(working_sites_per_project[project_name].values()): - return True + if handler.is_active(): + configured_sites[site_name] = True - log.info("No tuple of active-remote sites is active for project.") - return False + return configured_sites def get_provider_for_site(self, project_name, site): """ Return provider name for site. """ - site_preset = self.get_synced_preset(project_name)["sites"].get(site) + site_preset = self.get_sync_project_setting(project_name)["sites"].get(site) if site_preset: return site_preset["provider"] @@ -770,7 +798,7 @@ async def upload(self, collection, file, representation, provider_name, handler = lib.factory.get_provider(provider_name, site_name, tree=tree, presets=preset) - root_configs = self._get_roots_config(self.sync_project_presets, + root_configs = self._get_roots_config(self.sync_project_settings, collection, site_name) remote_file = self._get_remote_file_path(file, root_configs) @@ -823,7 +851,7 @@ async def download(self, collection, file, representation, provider_name, handler = lib.factory.get_provider(provider_name, site_name, tree=tree, presets=preset) - root_configs = self._get_roots_config(self.sync_project_presets, + root_configs = self._get_roots_config(self.sync_project_settings, collection, site_name) remote_file_path = self._get_remote_file_path(file, root_configs) @@ -1200,7 +1228,7 @@ def _remove_local_file(self, collection, representation_id, site_name): raise ValueError(msg) def get_my_local_site(self): - """ + """ TODO remove Returns name of current user local_site, its Pype wide. Returns: @@ -1215,7 +1243,7 @@ def get_loop_delay(self, project_name): Returns: (int): in seconds """ - ld = self.sync_project_presets[project_name]["config"]["loop_delay"] + ld = self.sync_project_settings[project_name]["config"]["loop_delay"] return int(ld) def show_widget(self): @@ -1305,7 +1333,7 @@ def get_local_file_path(self, collection, path): (string) - absolute path on local system """ local_active_site = self.get_active_site(collection) - sites = self.get_synced_preset(collection)["sites"] + sites = self.get_sync_project_setting(collection)["sites"] root_config = sites[local_active_site]["root"] if not root_config.get("root"): @@ -1353,7 +1381,7 @@ def _get_retries_arr(self, project_name): Returns: (list) """ - retry_cnt = self.sync_project_presets[project_name].\ + retry_cnt = self.sync_project_settings[project_name].\ get("config")["retry_cnt"] arr = [i for i in range(int(retry_cnt))] arr.append(None) @@ -1417,18 +1445,14 @@ async def sync_loop(self): while self.is_running and not self.module.is_paused(): import time start_time = None - self.module.set_sync_project_presets() - for collection, preset in self.module.get_synced_presets().\ + self.module.set_sync_project_settings() # clean cache + for collection, preset in self.module.get_sync_project_settings().\ items(): - if self.module.is_project_paused(collection): + start_time = time.time() + local_site, remote_site = self._working_sites(collection) + if not all([local_site, remote_site]): continue - local_site = self.module.get_active_site(collection) - remote_site = self.module.get_remote_site(collection) - if local_site == remote_site: - self.log.debug("Both sites same, skipping") - - start_time = time.time() sync_repres = self.module.get_sync_representations( collection, local_site, @@ -1564,3 +1588,23 @@ async def check_shutdown(self): self.executor.shutdown(wait=True) await asyncio.sleep(0.07) self.loop.stop() + + def _working_sites(self, collection): + if self.module.is_project_paused(collection): + log.debug("Both sites same, skipping") + return None, None + + local_site = self.module.get_active_site(collection) + remote_site = self.module.get_remote_site(collection) + if local_site == remote_site: + log.debug("{}-{} sites same, skipping".format(local_site, + remote_site)) + return None, None + + if not all(self.module.site_is_working(collection, local_site), + self.module.site_is_working(collection, remote_site)): + log.debug("At least one of the sites is not " + + "working properly") + return None, None + + return local_site, remote_site From d16434db0356c80aaaf3b6a6326b392d461e36fa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 Mar 2021 17:40:27 +0100 Subject: [PATCH 09/25] SyncServer - added missed file --- pype/modules/sync_server/tray/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/sync_server/tray/app.py b/pype/modules/sync_server/tray/app.py index 0abc3576e97..3d8535f8551 100644 --- a/pype/modules/sync_server/tray/app.py +++ b/pype/modules/sync_server/tray/app.py @@ -159,7 +159,7 @@ def refresh(self): model.clear() project_name = None - for project_name in self.sync_server.get_synced_presets().keys(): + for project_name in self.sync_server.get_sync_project_settings().keys(): if self.sync_server.is_paused() or \ self.sync_server.is_project_paused(project_name): icon = self._get_icon("paused") @@ -168,7 +168,7 @@ def refresh(self): model.appendRow(QtGui.QStandardItem(icon, project_name)) - if len(self.sync_server.get_synced_presets().keys()) == 0: + if len(self.sync_server.get_sync_project_settings().keys()) == 0: model.appendRow(QtGui.QStandardItem(DUMMY_PROJECT)) self.current_project = self.project_list.currentIndex().data( From 838d0e0c2950b836c364e6ecdd7b8223774c0950 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 17:57:58 +0100 Subject: [PATCH 10/25] modules manager is also passed to local settings widgets --- .../local_settings/projects_widget.py | 28 +++++++++++++------ pype/tools/settings/local_settings/window.py | 6 +++- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/pype/tools/settings/local_settings/projects_widget.py b/pype/tools/settings/local_settings/projects_widget.py index 28765155c24..21ca640fe8d 100644 --- a/pype/tools/settings/local_settings/projects_widget.py +++ b/pype/tools/settings/local_settings/projects_widget.py @@ -223,9 +223,10 @@ def _on_value_change(self): class RootsWidget(QtWidgets.QWidget): - def __init__(self, project_settings, parent): + def __init__(self, modules_manager, project_settings, parent): super(RootsWidget, self).__init__(parent) + self.modules_manager = modules_manager self.project_settings = project_settings self.site_widgets = [] self.local_project_settings = None @@ -294,10 +295,12 @@ def change_project(self, project_name): class _SiteCombobox(QtWidgets.QWidget): input_label = None - def __init__(self, project_settings, parent): + def __init__(self, modules_manager, project_settings, parent): super(_SiteCombobox, self).__init__(parent) self.project_settings = project_settings + self.modules_manager = modules_manager + self.local_project_settings = None self.local_project_settings_orig = None self.project_name = None @@ -601,17 +604,22 @@ def _set_local_settings_value(self, value): class RootSiteWidget(QtWidgets.QWidget): - def __init__(self, project_settings, parent): + def __init__(self, modules_manager, project_settings, parent): self._parent_widget = parent super(RootSiteWidget, self).__init__(parent) + self.modules_manager = modules_manager self.project_settings = project_settings self._project_name = None sites_widget = QtWidgets.QWidget(self) - active_site_widget = AciveSiteCombo(project_settings, sites_widget) - remote_site_widget = RemoteSiteCombo(project_settings, sites_widget) + active_site_widget = AciveSiteCombo( + modules_manager, project_settings, sites_widget + ) + remote_site_widget = RemoteSiteCombo( + modules_manager, project_settings, sites_widget + ) sites_layout = QtWidgets.QHBoxLayout(sites_widget) sites_layout.setContentsMargins(0, 0, 0, 0) @@ -619,7 +627,7 @@ def __init__(self, project_settings, parent): sites_layout.addWidget(remote_site_widget) sites_layout.addWidget(SpacerWidget(self), 1) - roots_widget = RootsWidget(project_settings, self) + roots_widget = RootsWidget(modules_manager, project_settings, self) main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(sites_widget) @@ -669,13 +677,17 @@ class ProjectValue(dict): class ProjectSettingsWidget(QtWidgets.QWidget): - def __init__(self, project_settings, parent): + def __init__(self, modules_manager, project_settings, parent): super(ProjectSettingsWidget, self).__init__(parent) self.local_project_settings = {} + self.modules_manager = modules_manager + projects_widget = _ProjectListWidget(self) - roos_site_widget = RootSiteWidget(project_settings, self) + roos_site_widget = RootSiteWidget( + modules_manager, project_settings, self + ) main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) diff --git a/pype/tools/settings/local_settings/window.py b/pype/tools/settings/local_settings/window.py index 87a276c78cb..c5f5d158316 100644 --- a/pype/tools/settings/local_settings/window.py +++ b/pype/tools/settings/local_settings/window.py @@ -11,6 +11,7 @@ SystemSettings, ProjectSettings ) +from pype.modules import ModulesManager from .widgets import ( SpacerWidget, @@ -37,6 +38,7 @@ def __init__(self, parent=None): self.system_settings = SystemSettings() self.project_settings = ProjectSettings() + self.modules_manager = ModulesManager() self.main_layout = QtWidgets.QVBoxLayout(self) @@ -108,7 +110,9 @@ def _create_project_ui(self): project_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) project_expand_widget.set_content_widget(project_content) - projects_widget = ProjectSettingsWidget(self.project_settings, self) + projects_widget = ProjectSettingsWidget( + self.modules_manager, self.project_settings, self + ) project_layout.addWidget(projects_widget) self.main_layout.addWidget(project_expand_widget) From 5a2aea6677acb3a8cc82baa79b3c08382ddf474c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 18:00:14 +0100 Subject: [PATCH 11/25] added applications of project settings --- pype/settings/lib.py | 111 +++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 35 deletions(-) diff --git a/pype/settings/lib.py b/pype/settings/lib.py index feeeaf3813e..4f112613e82 100644 --- a/pype/settings/lib.py +++ b/pype/settings/lib.py @@ -398,34 +398,23 @@ def apply_local_settings_on_anatomy_settings( if not local_settings: return - local_project_settings = local_settings.get("projects") - if not local_project_settings: - return + local_project_settings = local_settings.get("projects") or {} project_locals = local_project_settings.get(project_name) or {} default_locals = local_project_settings.get(DEFAULT_PROJECT_KEY) or {} - active_site = project_locals.get("active_site") - if not active_site: - active_site = default_locals.get("active_site") - - if not active_site: - project_settings = get_project_settings(project_name) - active_site = ( - project_settings - ["global"] - ["sync_server"] - ["config"] - ["active_site"] - ) + project_settings = get_project_settings(project_name) + active_site = ( + project_settings["global"]["sync_server"]["config"]["active_site"] + ) # QUESTION should raise an exception? if not active_site: return - roots_locals = default_locals.get("roots", {}).get(active_site, {}) + roots_locals = default_locals.get("roots", {}).get(active_site) or {} if project_name != DEFAULT_PROJECT_KEY: roots_locals.update( - project_locals.get("roots", {}).get(active_site, {}) + project_locals.get("roots", {}).get(active_site) or {} ) if not roots_locals: @@ -442,6 +431,45 @@ def apply_local_settings_on_anatomy_settings( ) +def apply_local_settings_on_project_settings( + project_settings, local_settings, project_name +): + """Apply local settings on project settings. + + Args: + project_settings (dict): Data for project settings. + local_settings (dict): Data of local settings. + project_name (str): Name of project for which settings data are. + """ + if not local_settings: + return + + local_project_settings = local_settings.get("projects") + if not local_project_settings: + return + + project_locals = local_project_settings.get(project_name) or {} + default_locals = local_project_settings.get(DEFAULT_PROJECT_KEY) or {} + active_site = ( + project_locals.get("active_site") + or default_locals.get("active_site") + ) + remote_site = ( + project_locals.get("remote_site") + or default_locals.get("remote_site") + ) + + if active_site or remote_site: + sync_server_config = ( + project_settings["global"]["sync_server"]["config"] + ) + if active_site: + sync_server_config["active_site"] = active_site + + if remote_site: + sync_server_config["remote_site"] = active_site + + def get_system_settings(clear_metadata=True): """System settings with applied studio overrides.""" default_values = get_default_settings()[SYSTEM_SETTINGS_KEY] @@ -463,6 +491,8 @@ def get_default_project_settings(clear_metadata=True): result = apply_overrides(default_values, studio_values) if clear_metadata: clear_metadata_from_settings(result) + local_settings = get_local_settings() + apply_local_settings_on_project_settings(result, local_settings, None) return result @@ -485,7 +515,7 @@ def get_default_anatomy_settings(clear_metadata=True): return result -def get_anatomy_settings(project_name, clear_metadata=True): +def get_applied_anatomy_settings(project_name): """Project anatomy data with applied studio and project overrides.""" if not project_name: raise ValueError( @@ -498,23 +528,24 @@ def get_anatomy_settings(project_name, clear_metadata=True): project_name ) - # TODO uncomment and remove hotfix result when overrides of anatomy - # are stored correctly. - # result = apply_overrides(studio_overrides, project_overrides) - result = copy.deepcopy(studio_overrides) - if project_overrides: - for key, value in project_overrides.items(): - result[key] = value - if clear_metadata: - clear_metadata_from_settings(result) - local_settings = get_local_settings() - apply_local_settings_on_anatomy_settings( - result, local_settings, project_name - ) + result = apply_overrides(studio_overrides, project_overrides) + + clear_metadata_from_settings(result) + return result -def get_project_settings(project_name, clear_metadata=True): +def get_anatomy_settings(project_name): + result = get_applied_anatomy_settings(project_name) + + local_settings = get_local_settings() + apply_local_settings_on_anatomy_settings( + result, local_settings, project_name + ) + return result + + +def get_applied_project_settings(project_name): """Project settings with applied studio and project overrides.""" if not project_name: raise ValueError( @@ -528,8 +559,18 @@ def get_project_settings(project_name, clear_metadata=True): ) result = apply_overrides(studio_overrides, project_overrides) - if clear_metadata: - clear_metadata_from_settings(result) + clear_metadata_from_settings(result) + return result + + +def get_project_settings(project_name): + result = get_applied_project_settings(project_name) + + local_settings = get_local_settings() + apply_local_settings_on_project_settings( + result, local_settings, project_name + ) + return result From 459a39fef6b6533f63c8175dbc9f9e8aeb6cb466 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 18:00:38 +0100 Subject: [PATCH 12/25] modified how active sites and remote sites are filled --- .../local_settings/projects_widget.py | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/pype/tools/settings/local_settings/projects_widget.py b/pype/tools/settings/local_settings/projects_widget.py index 21ca640fe8d..4ad964fcf7b 100644 --- a/pype/tools/settings/local_settings/projects_widget.py +++ b/pype/tools/settings/local_settings/projects_widget.py @@ -22,12 +22,6 @@ NOT_SET = type("NOT_SET", (), {})() -def get_active_sites(project_settings): - global_entity = project_settings["project_settings"]["global"] - sites_entity = global_entity["sync_server"]["sites"] - return tuple(sites_entity.keys()) - - class _ProjectListWidget(ProjectListWidget): def on_item_clicked(self, new_index): new_project_name = new_index.data(QtCore.Qt.DisplayRole) @@ -242,6 +236,16 @@ def _clear_widgets(self): self.content_layout.removeItem(item) self.site_widgets = [] + def _get_active_sites(self): + sync_server_module = ( + self.modules_manager.modules_by_name["sync_server"] + ) + if self._project_name is None: + return sync_server_module.get_active_sites_from_setting( + self.project_settings["project_settings"].value + ) + return sync_server_module.get_active_sites(self._project_name) + def refresh(self): self._clear_widgets() @@ -252,7 +256,7 @@ def refresh(self): self.project_settings[PROJECT_ANATOMY_KEY][LOCAL_ROOTS_KEY] ) # Site label - for site_name in get_active_sites(self.project_settings): + for site_name in self._get_active_sites(): site_widget = QtWidgets.QWidget(self) site_layout = QtWidgets.QVBoxLayout(site_widget) @@ -550,7 +554,14 @@ class AciveSiteCombo(_SiteCombobox): input_label = "Active site" def _get_project_sites(self): - return get_active_sites(self.project_settings) + sync_server_module = ( + self.modules_manager.modules_by_name["sync_server"] + ) + if self.project_name is None: + return sync_server_module.get_active_sites_from_setting( + self.project_settings["project_settings"].value + ) + return sync_server_module.get_active_sites(self.project_name) def _get_local_settings_item(self, project_name=None, data=None): if project_name is None: @@ -578,9 +589,14 @@ class RemoteSiteCombo(_SiteCombobox): input_label = "Remote site" def _get_project_sites(self): - global_entity = self.project_settings["project_settings"]["global"] - sites_entity = global_entity["sync_server"]["sites"] - return tuple(sites_entity.keys()) + sync_server_module = ( + self.modules_manager.modules_by_name["sync_server"] + ) + if self.project_name is None: + return sync_server_module.get_remote_sites_from_setting( + self.project_settings["project_settings"].value + ) + return sync_server_module.get_remote_sites(self.project_name) def _get_local_settings_item(self, project_name=None, data=None): if project_name is None: From 40e80232b19f1505072a526906e41249afa4adc3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 18:01:24 +0100 Subject: [PATCH 13/25] fixed typo in method names --- pype/modules/sync_server/sync_server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/modules/sync_server/sync_server.py b/pype/modules/sync_server/sync_server.py index b7632153144..73292b8886c 100644 --- a/pype/modules/sync_server/sync_server.py +++ b/pype/modules/sync_server/sync_server.py @@ -339,10 +339,10 @@ def get_active_sites(self, project_name): Returns: (list) of strings """ - return self.get_active_sites_from_setting( + return self.get_active_sites_from_settings( get_project_settings(project_name)) - def get_active_sites_from_setting(self, settings): + def get_active_sites_from_settings(self, settings): """ List available active sites from incoming 'settings'. Used for returning 'default' values for Local Settings @@ -391,10 +391,10 @@ def get_remote_sites(self, project_name): Returns: (list) of strings """ - return self.get_remote_sites_from_setting( + return self.get_remote_sites_from_settings( get_project_settings(project_name)) - def get_remote_sites_from_setting(self, settings): + def get_remote_sites_from_settings(self, settings): """ Get remote sites for returning 'default' values for Local Settings """ From a469f2d01ab17291648f5b2c3818f01807715441 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 18:08:27 +0100 Subject: [PATCH 14/25] modified order of local settings processing --- pype/settings/lib.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/pype/settings/lib.py b/pype/settings/lib.py index 4f112613e82..59fa6a951c0 100644 --- a/pype/settings/lib.py +++ b/pype/settings/lib.py @@ -400,8 +400,19 @@ def apply_local_settings_on_anatomy_settings( local_project_settings = local_settings.get("projects") or {} + # Get local settings fro default project and project specified in argument project_locals = local_project_settings.get(project_name) or {} default_locals = local_project_settings.get(DEFAULT_PROJECT_KEY) or {} + + # Check for roots existence in local settings first + roots_project_locals = project_locals.get("roots") or {} + roots_default_locals = default_locals.get("roots") or {} + + # Skip rest of processing if roots are not set + if not roots_project_locals and not roots_default_locals: + return + + # Get active site from settings project_settings = get_project_settings(project_name) active_site = ( project_settings["global"]["sync_server"]["config"]["active_site"] @@ -411,12 +422,11 @@ def apply_local_settings_on_anatomy_settings( if not active_site: return - roots_locals = default_locals.get("roots", {}).get(active_site) or {} - if project_name != DEFAULT_PROJECT_KEY: - roots_locals.update( - project_locals.get("roots", {}).get(active_site) or {} - ) - + # Combine roots from local settings + roots_locals = roots_default_locals.get(active_site) or {} + roots_locals.update(roots_project_locals.get(active_site) or {}) + # Skip processing if roots for current active site are not available in + # local settings if not roots_locals: return @@ -436,6 +446,8 @@ def apply_local_settings_on_project_settings( ): """Apply local settings on project settings. + Currently is modifying active site and remote site in sync server. + Args: project_settings (dict): Data for project settings. local_settings (dict): Data of local settings. From c370d5f3402a9d461cce12975508dfecaa3c7322 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 11 Mar 2021 11:51:52 +0100 Subject: [PATCH 15/25] it is possible to pass site_name to get_anatomy_settings --- pype/settings/lib.py | 50 +++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/pype/settings/lib.py b/pype/settings/lib.py index 59fa6a951c0..0c0b9739a65 100644 --- a/pype/settings/lib.py +++ b/pype/settings/lib.py @@ -373,7 +373,7 @@ def apply_local_settings_on_system_settings(system_settings, local_settings): def apply_local_settings_on_anatomy_settings( - anatomy_settings, local_settings, project_name + anatomy_settings, local_settings, project_name, site_name=None ): """Apply local settings on anatomy settings. @@ -400,31 +400,36 @@ def apply_local_settings_on_anatomy_settings( local_project_settings = local_settings.get("projects") or {} - # Get local settings fro default project and project specified in argument - project_locals = local_project_settings.get(project_name) or {} - default_locals = local_project_settings.get(DEFAULT_PROJECT_KEY) or {} - # Check for roots existence in local settings first - roots_project_locals = project_locals.get("roots") or {} - roots_default_locals = default_locals.get("roots") or {} + roots_project_locals = ( + local_project_settings + .get(project_name, {}) + .get("roots", {}) + ) + roots_default_locals = ( + local_project_settings + .get(DEFAULT_PROJECT_KEY, {}) + .get("roots", {}) + ) # Skip rest of processing if roots are not set if not roots_project_locals and not roots_default_locals: return # Get active site from settings - project_settings = get_project_settings(project_name) - active_site = ( - project_settings["global"]["sync_server"]["config"]["active_site"] - ) + if site_name is None: + project_settings = get_project_settings(project_name) + site_name = ( + project_settings["global"]["sync_server"]["config"]["active_site"] + ) # QUESTION should raise an exception? - if not active_site: + if not site_name: return # Combine roots from local settings - roots_locals = roots_default_locals.get(active_site) or {} - roots_locals.update(roots_project_locals.get(active_site) or {}) + roots_locals = roots_default_locals.get(site_name) or {} + roots_locals.update(roots_project_locals.get(site_name) or {}) # Skip processing if roots for current active site are not available in # local settings if not roots_locals: @@ -471,15 +476,12 @@ def apply_local_settings_on_project_settings( or default_locals.get("remote_site") ) - if active_site or remote_site: - sync_server_config = ( - project_settings["global"]["sync_server"]["config"] - ) - if active_site: - sync_server_config["active_site"] = active_site + sync_server_config = project_settings["global"]["sync_server"]["config"] + if active_site: + sync_server_config["active_site"] = active_site - if remote_site: - sync_server_config["remote_site"] = active_site + if remote_site: + sync_server_config["remote_site"] = remote_site def get_system_settings(clear_metadata=True): @@ -547,12 +549,12 @@ def get_applied_anatomy_settings(project_name): return result -def get_anatomy_settings(project_name): +def get_anatomy_settings(project_name, site_name=None): result = get_applied_anatomy_settings(project_name) local_settings = get_local_settings() apply_local_settings_on_anatomy_settings( - result, local_settings, project_name + result, local_settings, project_name, site_name ) return result From ed321845cfcf3bdc60045d37a422e5de0e685961 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 11 Mar 2021 11:52:08 +0100 Subject: [PATCH 16/25] anatomy can have defined site_name on creation --- pype/lib/anatomy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/lib/anatomy.py b/pype/lib/anatomy.py index b754f4fd763..67b8c01a562 100644 --- a/pype/lib/anatomy.py +++ b/pype/lib/anatomy.py @@ -77,7 +77,7 @@ class Anatomy: root_key_regex = re.compile(r"{(root?[^}]+)}") root_name_regex = re.compile(r"root\[([^]]+)\]") - def __init__(self, project_name=None): + def __init__(self, project_name=None, site_name=None): if not project_name: project_name = os.environ.get("AVALON_PROJECT") @@ -89,7 +89,7 @@ def __init__(self, project_name=None): self.project_name = project_name - self._data = get_anatomy_settings(project_name) + self._data = get_anatomy_settings(project_name, site_name) self._templates_obj = Templates(self) self._roots_obj = Roots(self) From 431d5a7ab3d6be158983b11737449d4d2d9fab39 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 11 Mar 2021 12:49:24 +0100 Subject: [PATCH 17/25] fix method name --- .../settings/local_settings/projects_widget.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pype/tools/settings/local_settings/projects_widget.py b/pype/tools/settings/local_settings/projects_widget.py index 4ad964fcf7b..482b67882c4 100644 --- a/pype/tools/settings/local_settings/projects_widget.py +++ b/pype/tools/settings/local_settings/projects_widget.py @@ -240,11 +240,10 @@ def _get_active_sites(self): sync_server_module = ( self.modules_manager.modules_by_name["sync_server"] ) - if self._project_name is None: - return sync_server_module.get_active_sites_from_setting( - self.project_settings["project_settings"].value - ) - return sync_server_module.get_active_sites(self._project_name) + + return sync_server_module.get_active_sites_from_settings( + self.project_settings["project_settings"].value + ) def refresh(self): self._clear_widgets() @@ -558,7 +557,7 @@ def _get_project_sites(self): self.modules_manager.modules_by_name["sync_server"] ) if self.project_name is None: - return sync_server_module.get_active_sites_from_setting( + return sync_server_module.get_active_sites_from_settings( self.project_settings["project_settings"].value ) return sync_server_module.get_active_sites(self.project_name) @@ -593,7 +592,7 @@ def _get_project_sites(self): self.modules_manager.modules_by_name["sync_server"] ) if self.project_name is None: - return sync_server_module.get_remote_sites_from_setting( + return sync_server_module.get_remote_sites_from_settings( self.project_settings["project_settings"].value ) return sync_server_module.get_remote_sites(self.project_name) From fcfd5378303eaf86c87c674f29e98fe6f4930964 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Mar 2021 13:17:28 +0100 Subject: [PATCH 18/25] Fix - get_remote_sites_from_settings does not validate sites Sites could be configured in Local setting, so validation doesn't have sense here Small refactor --- pype/modules/sync_server/sync_server.py | 39 +++++++++++++------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/pype/modules/sync_server/sync_server.py b/pype/modules/sync_server/sync_server.py index 73292b8886c..83d7cc9d7f0 100644 --- a/pype/modules/sync_server/sync_server.py +++ b/pype/modules/sync_server/sync_server.py @@ -369,13 +369,6 @@ def get_active_site(self, project_name): return get_local_site_id() return active_site - def _get_active_sites_from_settings(self, sync_settings): - sites = [self.DEFAULT_SITE] - if self.enabled and sync_settings['enabled']: - sites.append(self.LOCAL_SITE) - - return sites - # remote sites def get_remote_sites(self, project_name): """ @@ -406,7 +399,10 @@ def get_remote_site(self, project_name): """ Returns remote (theirs) site for 'project_name' from settings """ - return self.get_sync_project_setting(project_name)['config']['remote_site'] + return self.get_sync_project_setting(project_name)['config']\ + ['remote_site'] + + """ End of Public API """ def _get_remote_sites_from_settings(self, sync_settings): if not self.enabled or not sync_settings['enabled']: @@ -414,14 +410,16 @@ def _get_remote_sites_from_settings(self, sync_settings): remote_sites = [self.DEFAULT_SITE, self.LOCAL_SITE] if sync_settings: - for site, enabled in self._get_configured_sites_from_setting( - sync_settings).items(): - if enabled: - remote_sites.append(site) + remote_sites.extend(sync_settings.get("sites").keys()) return list(set(remote_sites)) - """ End of Public API """ + def _get_active_sites_from_settings(self, sync_settings): + sites = [self.DEFAULT_SITE] + if self.enabled and sync_settings['enabled']: + sites.append(self.LOCAL_SITE) + + return sites def connect_with_modules(self, *_a, **kw): return @@ -627,7 +625,11 @@ def _get_configured_sites_from_setting(self, project_setting): initiated_handlers = {} configured_sites = {} - for site_name, config in project_setting.get("sites").items(): + default_config = {'provider': 'local_drive'} + all_sites = {self.DEFAULT_SITE: default_config, + self.LOCAL_SITE: default_config} + all_sites.update(project_setting.get("sites")) + for site_name, config in all_sites.items(): handler = initiated_handlers. \ get((config["provider"], site_name)) if not handler: @@ -1601,10 +1603,11 @@ def _working_sites(self, collection): remote_site)) return None, None - if not all(self.module.site_is_working(collection, local_site), - self.module.site_is_working(collection, remote_site)): - log.debug("At least one of the sites is not " + - "working properly") + if not all([self.module.site_is_working(collection, local_site), + self.module.site_is_working(collection, remote_site)]): + log.debug("Some of the sites {} - {} is not ".format(local_site, + remote_site) + + "working properly") return None, None return local_site, remote_site From 83570d4998b96a5d01c878ccced706a5ce08f4b7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Mar 2021 16:20:54 +0100 Subject: [PATCH 19/25] SyncServer - modified resolving of paths for local and remote Fix - status for some failed was incorrectly set to Not available Extended AbstracProvider with new method for resolvments of paths Added defaults sites to configured sites Name refactor --- .../providers/abstract_provider.py | 14 ++ pype/modules/sync_server/providers/gdrive.py | 10 ++ .../sync_server/providers/local_drive.py | 19 ++ pype/modules/sync_server/sync_server.py | 168 ++++++++---------- pype/modules/sync_server/tray/app.py | 40 +++-- 5 files changed, 136 insertions(+), 115 deletions(-) diff --git a/pype/modules/sync_server/providers/abstract_provider.py b/pype/modules/sync_server/providers/abstract_provider.py index 56928e93d8b..001d4c4d50b 100644 --- a/pype/modules/sync_server/providers/abstract_provider.py +++ b/pype/modules/sync_server/providers/abstract_provider.py @@ -93,3 +93,17 @@ def get_tree(self): only parents and their parents) """ pass + + @abstractmethod + def resolve_path(self, path, root_config, anatomy=None): + """ + Replaces root placeholders with appropriate real value from + 'root_configs' (from Settings or Local Settings) or Anatomy + (mainly for 'studio' site) + + Args: + path(string): path with '{root[work]}/...' + root_config(dict): from Settings or Local Settings + anatomy (Anatomy): prepared anatomy object for project + """ + pass diff --git a/pype/modules/sync_server/providers/gdrive.py b/pype/modules/sync_server/providers/gdrive.py index b1411312037..cbd5c1b5270 100644 --- a/pype/modules/sync_server/providers/gdrive.py +++ b/pype/modules/sync_server/providers/gdrive.py @@ -678,6 +678,16 @@ def get_presets(cls): return return provider_presets + def resolve_path(self, path, root_config, anatomy=None): + if not root_config.get("root"): + root_config = {"root": root_config} + + try: + return path.format(**root_config) + except KeyError: + msg = "Error in resolving remote root, unknown key" + log.error(msg) + def _handle_q(self, q, trashed=False): """ API list call contain trashed and hidden files/folder by default. Usually we dont want those, must be included in query explicitly. diff --git a/pype/modules/sync_server/providers/local_drive.py b/pype/modules/sync_server/providers/local_drive.py index 8d816b008a3..32a5017fe39 100644 --- a/pype/modules/sync_server/providers/local_drive.py +++ b/pype/modules/sync_server/providers/local_drive.py @@ -85,6 +85,25 @@ def create_folder(self, folder_path): def get_tree(self): return + def resolve_path(self, path, root_config, anatomy=None): + if root_config and not root_config.get("root"): + root_config = {"root": root_config} + + try: + if not root_config: + raise KeyError + + path = path.format(**root_config) + except KeyError: + try: + path = anatomy.fill_root(path) + except KeyError: + msg = "Error in resolving local root from anatomy" + log.error(msg) + raise ValueError(msg) + + return path + def _copy(self, source_path, target_path): print("copying {}->{}".format(source_path, target_path)) shutil.copy(source_path, target_path) diff --git a/pype/modules/sync_server/sync_server.py b/pype/modules/sync_server/sync_server.py index 83d7cc9d7f0..b5bd29821af 100644 --- a/pype/modules/sync_server/sync_server.py +++ b/pype/modules/sync_server/sync_server.py @@ -111,8 +111,7 @@ def initialize(self, module_settings): Sets 'enabled' according to global settings for the module. Shouldnt be doing any initialization, thats a job for 'tray_init' """ - sync_server_settings = module_settings[self.name] - self.enabled = sync_server_settings["enabled"] + self.enabled = module_settings[self.name]["enabled"] if asyncio is None: raise AssertionError( "SyncServer module requires Python 3.5 or higher." @@ -404,6 +403,14 @@ def get_remote_site(self, project_name): """ End of Public API """ + def get_local_file_path(self, collection, file_path): + """ + Externalized for app + """ + local_file_path, _ = self._resolve_paths(file_path, collection) + + return local_file_path + def _get_remote_sites_from_settings(self, sync_settings): if not self.enabled or not sync_settings['enabled']: return [] @@ -529,7 +536,7 @@ def set_sync_project_settings(self): For performance """ - sync_project_presets = {} + sync_project_settings = {} if not self.connection: self.connection = AvalonMongoDB() self.connection.install() @@ -537,12 +544,12 @@ def set_sync_project_settings(self): for collection in self.connection.database.collection_names(False): sync_settings = self.get_sync_project_setting(collection) if sync_settings: - sync_project_presets[collection] = sync_settings + sync_project_settings[collection] = sync_settings - if not sync_project_presets: + if not sync_project_settings: log.info("No enabled and configured projects for sync.") - self.sync_project_settings = sync_project_presets + self.sync_project_settings = sync_project_settings def get_sync_project_settings(self, refresh=False): """ @@ -767,7 +774,7 @@ def check_status(self, file, local_site, remote_site, config_preset): return SyncStatus.DO_NOTHING async def upload(self, collection, file, representation, provider_name, - site_name, tree=None, preset=None): + remote_site_name, tree=None, preset=None): """ Upload single 'file' of a 'representation' to 'provider'. Source url is taken from 'file' portion, where {root} placeholder @@ -797,42 +804,40 @@ async def upload(self, collection, file, representation, provider_name, # this part modifies structure on 'remote_site', only single # thread can do that at a time, upload/download to prepared # structure should be run in parallel - handler = lib.factory.get_provider(provider_name, site_name, - tree=tree, presets=preset) - - root_configs = self._get_roots_config(self.sync_project_settings, - collection, - site_name) - remote_file = self._get_remote_file_path(file, root_configs) - - local_file = self.get_local_file_path(collection, - file.get("path", "")) + remote_handler = lib.factory.get_provider(provider_name, + remote_site_name, + tree=tree, + presets=preset) + + file_path = file.get("path", "") + local_file_path, remote_file_path = self._resolve_paths( + file_path, collection, remote_site_name, remote_handler + ) - target_folder = os.path.dirname(remote_file) - folder_id = handler.create_folder(target_folder) + target_folder = os.path.dirname(remote_file_path) + folder_id = remote_handler.create_folder(target_folder) if not folder_id: err = "Folder {} wasn't created. Check permissions.".\ format(target_folder) raise NotADirectoryError(err) - remote_site = self.get_remote_site(collection) loop = asyncio.get_running_loop() file_id = await loop.run_in_executor(None, - handler.upload_file, - local_file, - remote_file, + remote_handler.upload_file, + local_file_path, + remote_file_path, self, collection, file, representation, - remote_site, + remote_site_name, True ) return file_id async def download(self, collection, file, representation, provider_name, - site_name, tree=None, preset=None): + remote_site_name, tree=None, preset=None): """ Downloads file to local folder denoted in representation.Context. @@ -850,16 +855,16 @@ async def download(self, collection, file, representation, provider_name, (string) - 'name' of local file """ with self.lock: - handler = lib.factory.get_provider(provider_name, site_name, - tree=tree, presets=preset) - - root_configs = self._get_roots_config(self.sync_project_settings, - collection, - site_name) - remote_file_path = self._get_remote_file_path(file, root_configs) + remote_handler = lib.factory.get_provider(provider_name, + remote_site_name, + tree=tree, + presets=preset) + + file_path = file.get("path", "") + local_file_path, remote_file_path = self._resolve_paths( + file_path, collection, remote_site_name, remote_handler + ) - local_file_path = self.get_local_file_path(collection, - file.get("path", "")) local_folder = os.path.dirname(local_file_path) os.makedirs(local_folder, exist_ok=True) @@ -867,7 +872,7 @@ async def download(self, collection, file, representation, provider_name, loop = asyncio.get_running_loop() file_id = await loop.run_in_executor(None, - handler.download_file, + remote_handler.download_file, remote_file_path, local_file_path, self, @@ -1184,7 +1189,7 @@ def _remove_local_file(self, collection, representation_id, site_name): Returns: only logs, catches IndexError and OSError """ - my_local_site = self.get_my_local_site() + my_local_site = get_local_site_id() if my_local_site != site_name: self.log.warning("Cannot remove non local file for {}". format(site_name)) @@ -1206,12 +1211,14 @@ def _remove_local_file(self, collection, representation_id, site_name): return representation = representation.pop() + local_file_path = '' for file in representation.get("files"): + local_file_path, _ = self._resolve_paths(file.get("path", ""), + collection + ) try: - self.log.debug("Removing {}".format(file["path"])) - local_file = self.get_local_file_path(collection, - file.get("path", "")) - os.remove(local_file) + self.log.debug("Removing {}".format(local_file_path)) + os.remove(local_file_path) except IndexError: msg = "No file set for {}".format(representation_id) self.log.debug(msg) @@ -1222,22 +1229,13 @@ def _remove_local_file(self, collection, representation_id, site_name): raise ValueError(msg) try: - folder = os.path.dirname(local_file) + folder = os.path.dirname(local_file_path) os.rmdir(folder) except OSError: msg = "folder {} cannot be removed".format(folder) self.log.warning(msg) raise ValueError(msg) - def get_my_local_site(self): - """ TODO remove - Returns name of current user local_site, its Pype wide. - - Returns: - (string) - """ - return get_local_site_id() - def get_loop_delay(self, project_name): """ Return count of seconds before next synchronization loop starts @@ -1320,59 +1318,35 @@ def _get_progress_dict(self, progress): val = {"files.$[f].sites.$[s].progress": progress} return val - def get_local_file_path(self, collection, path): + def _resolve_paths(self, file_path, collection, + remote_site_name=None, remote_handler=None): """ - Auxiliary function for replacing rootless path with real path + Returns tuple of local and remote file paths with {root} + placeholders replaced with proper values from Settings or Anatomy - Works with multi roots. - If root definition is not found in Settings, anatomy is used - - Args: - collection (string): project name - path (dictionary): 'path' to file with {root} - - Returns: - (string) - absolute path on local system + Args: + file_path(string): path with {root} + collection(string): project name + remote_site_name(string): remote site + remote_handler(AbstractProvider): implementation + Returns: + (string, string) - proper absolute paths """ - local_active_site = self.get_active_site(collection) - sites = self.get_sync_project_setting(collection)["sites"] - root_config = sites[local_active_site]["root"] - - if not root_config.get("root"): - root_config = {"root": root_config} - - try: - path = path.format(**root_config) - except KeyError: - try: - anatomy = self.get_anatomy(collection) - path = anatomy.fill_root(path) - except KeyError: - msg = "Error in resolving local root from anatomy" - self.log.error(msg) - raise ValueError(msg) - - return path + remote_file_path = '' + if remote_handler: + root_configs = self._get_roots_config(self.sync_project_settings, + collection, + remote_site_name) - def _get_remote_file_path(self, file, root_config): - """ - Auxiliary function for replacing rootless path with real path - Args: - file (dictionary): file info, get 'path' to file with {root} - root_config (dict): value of {root} for remote location + remote_file_path = remote_handler.resolve_path(file_path, + root_configs) - Returns: - (string) - absolute path on remote location - """ - path = file.get("path", "") - if not root_config.get("root"): - root_config = {"root": root_config} + local_handler = lib.factory.get_provider( + 'local_drive', self.get_active_site(collection)) + local_file_path = local_handler.resolve_path( + file_path, None, self.get_anatomy(collection)) - try: - return path.format(**root_config) - except KeyError: - msg = "Error in resolving remote root, unknown key" - self.log.error(msg) + return local_file_path, remote_file_path def _get_retries_arr(self, project_name): """ diff --git a/pype/modules/sync_server/tray/app.py b/pype/modules/sync_server/tray/app.py index 3d8535f8551..1d45a7d7d3b 100644 --- a/pype/modules/sync_server/tray/app.py +++ b/pype/modules/sync_server/tray/app.py @@ -159,7 +159,8 @@ def refresh(self): model.clear() project_name = None - for project_name in self.sync_server.get_sync_project_settings().keys(): + for project_name in self.sync_server.get_sync_project_settings().\ + keys(): if self.sync_server.is_paused() or \ self.sync_server.is_project_paused(project_name): icon = self._get_icon("paused") @@ -203,7 +204,6 @@ def _on_context_menu(self, point): menu = QtWidgets.QMenu() actions_mapping = {} - action = None if self.sync_server.is_project_paused(self.project_name): action = QtWidgets.QAction("Unpause") actions_mapping[action] = self._unpause @@ -212,7 +212,7 @@ def _on_context_menu(self, point): actions_mapping[action] = self._pause menu.addAction(action) - if self.local_site == self.sync_server.get_my_local_site(): + if self.local_site == get_local_site_id(): action = QtWidgets.QAction("Clear local project") actions_mapping[action] = self._clear_project menu.addAction(action) @@ -241,6 +241,7 @@ def _clear_project(self): self.project_name = None self.refresh() + class ProjectModel(QtCore.QAbstractListModel): def __init__(self, *args, projects=None, **kwargs): super(ProjectModel, self).__init__(*args, **kwargs) @@ -256,6 +257,7 @@ def data(self, index, role): def rowCount(self, index): return len(self.todos) + class SyncRepresentationWidget(QtWidgets.QWidget): """ Summary dialog with list of representations that matches current @@ -478,7 +480,7 @@ def _add_site(self): local_site_name = self.sync_server.get_my_local_site() try: self.sync_server.add_site( - self.table_view.model()._project, + project_name, self.representation_id, local_site_name ) @@ -802,7 +804,6 @@ def _add_page_records(self, local_site, remote_site, representations): self._data.append(item) self._rec_loaded += 1 - def canFetchMore(self, index): """ Check if there are more records than currently loaded @@ -854,6 +855,9 @@ def sort(self, index, order): self.sort = {self.SORT_BY_COLUMN[index]: order, '_id': 1} self.query = self.get_default_query() + # import json + # log.debug(json.dumps(self.query, indent=4).replace('False', 'false').\ + # replace('True', 'true').replace('None', 'null')) representations = self.dbcon.aggregate(self.query) self.refresh(representations) @@ -891,7 +895,6 @@ def get_index(self, id): Returns: (QModelIndex) """ - index = None for i in range(self.rowCount(None)): index = self.index(i, 0) value = self.data(index, Qt.UserRole) @@ -1000,7 +1003,7 @@ def get_default_query(self, limit=0): 0]}, 'failed_remote_tries': { '$cond': [{'$size': '$order_remote.tries'}, - {'$first': '$order_local.tries'}, + {'$first': '$order_remote.tries'}, 0]}, 'paused_remote': { '$cond': [{'$size': "$order_remote.paused"}, @@ -1027,9 +1030,9 @@ def get_default_query(self, limit=0): # select last touch of file 'updated_dt_remote': {'$max': "$updated_dt_remote"}, 'failed_remote': {'$sum': '$failed_remote'}, - 'failed_local': {'$sum': '$paused_remote'}, - 'failed_local_tries': {'$sum': '$failed_local_tries'}, + 'failed_local': {'$sum': '$failed_local'}, 'failed_remote_tries': {'$sum': '$failed_remote_tries'}, + 'failed_local_tries': {'$sum': '$failed_local_tries'}, 'paused_remote': {'$sum': '$paused_remote'}, 'paused_local': {'$sum': '$paused_local'}, 'updated_dt_local': {'$max': "$updated_dt_local"} @@ -1669,7 +1672,6 @@ def get_index(self, id): Returns: (QModelIndex) """ - index = None for i in range(self.rowCount(None)): index = self.index(i, 0) value = self.data(index, Qt.UserRole) @@ -1777,14 +1779,15 @@ def get_default_query(self, limit=0): "$order_local.error", [""]]}}, 'tries': {'$first': { - '$cond': [{'$size': "$order_local.tries"}, - "$order_local.tries", - {'$cond': [ - {'$size': "$order_remote.tries"}, - "$order_remote.tries", - [] - ]} - ]}} + '$cond': [ + {'$size': "$order_local.tries"}, + "$order_local.tries", + {'$cond': [ + {'$size': "$order_remote.tries"}, + "$order_remote.tries", + [] + ]} + ]}} }}, {"$project": self.projection}, {"$sort": self.sort}, @@ -2015,6 +2018,7 @@ def _pretty_size(self, value, suffix='B'): value /= 1024.0 return "%.1f%s%s" % (value, 'Yi', suffix) + def _convert_progress(value): try: progress = float(value) From 25fcec78b04a047a194da5f0bd7f65c8608f1676 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 11 Mar 2021 16:29:38 +0100 Subject: [PATCH 20/25] `get_anatomy_settings` and `get_project_settings` can not apply local settings --- pype/settings/lib.py | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/pype/settings/lib.py b/pype/settings/lib.py index 0c0b9739a65..a8f4a46a686 100644 --- a/pype/settings/lib.py +++ b/pype/settings/lib.py @@ -529,7 +529,7 @@ def get_default_anatomy_settings(clear_metadata=True): return result -def get_applied_anatomy_settings(project_name): +def get_anatomy_settings(project_name, site_name=None, exclude_locals=False): """Project anatomy data with applied studio and project overrides.""" if not project_name: raise ValueError( @@ -546,20 +546,15 @@ def get_applied_anatomy_settings(project_name): clear_metadata_from_settings(result) + if not exclude_locals: + local_settings = get_local_settings() + apply_local_settings_on_anatomy_settings( + result, local_settings, project_name, site_name + ) return result -def get_anatomy_settings(project_name, site_name=None): - result = get_applied_anatomy_settings(project_name) - - local_settings = get_local_settings() - apply_local_settings_on_anatomy_settings( - result, local_settings, project_name, site_name - ) - return result - - -def get_applied_project_settings(project_name): +def get_project_settings(project_name, exclude_locals=False): """Project settings with applied studio and project overrides.""" if not project_name: raise ValueError( @@ -574,16 +569,12 @@ def get_applied_project_settings(project_name): result = apply_overrides(studio_overrides, project_overrides) clear_metadata_from_settings(result) - return result - - -def get_project_settings(project_name): - result = get_applied_project_settings(project_name) - local_settings = get_local_settings() - apply_local_settings_on_project_settings( - result, local_settings, project_name - ) + if not exclude_locals: + local_settings = get_local_settings() + apply_local_settings_on_project_settings( + result, local_settings, project_name + ) return result From bf127b0432797bd62c777b6f6257525fee557b94 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Mar 2021 17:41:22 +0100 Subject: [PATCH 21/25] SyncServer - refresh project settings every loop or project change --- pype/modules/sync_server/sync_server.py | 15 +++++++++++---- pype/modules/sync_server/tray/app.py | 1 + 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pype/modules/sync_server/sync_server.py b/pype/modules/sync_server/sync_server.py index b5bd29821af..c78fbe44c5d 100644 --- a/pype/modules/sync_server/sync_server.py +++ b/pype/modules/sync_server/sync_server.py @@ -542,8 +542,11 @@ def set_sync_project_settings(self): self.connection.install() for collection in self.connection.database.collection_names(False): - sync_settings = self.get_sync_project_setting(collection) + sync_settings = self._parse_sync_settings_from_settings( + get_project_settings(collection)) if sync_settings: + default_sites = self._get_default_site_configs() + sync_settings['sites'].update(default_sites) sync_project_settings[collection] = sync_settings if not sync_project_settings: @@ -632,9 +635,7 @@ def _get_configured_sites_from_setting(self, project_setting): initiated_handlers = {} configured_sites = {} - default_config = {'provider': 'local_drive'} - all_sites = {self.DEFAULT_SITE: default_config, - self.LOCAL_SITE: default_config} + all_sites = self._get_default_site_configs() all_sites.update(project_setting.get("sites")) for site_name, config in all_sites.items(): handler = initiated_handlers. \ @@ -651,6 +652,12 @@ def _get_configured_sites_from_setting(self, project_setting): return configured_sites + def _get_default_site_configs(self): + default_config = {'provider': 'local_drive'} + all_sites = {self.DEFAULT_SITE: default_config, + self.LOCAL_SITE: default_config} + return all_sites + def get_provider_for_site(self, project_name, site): """ Return provider name for site. diff --git a/pype/modules/sync_server/tray/app.py b/pype/modules/sync_server/tray/app.py index 1d45a7d7d3b..bb87ae18eb6 100644 --- a/pype/modules/sync_server/tray/app.py +++ b/pype/modules/sync_server/tray/app.py @@ -880,6 +880,7 @@ def set_project(self, project): project (str): name of project """ self._project = project + self.sync_server.set_sync_project_settings() self.local_site = self.sync_server.get_active_site(self._project) self.remote_site = self.sync_server.get_remote_site(self._project) self.refresh() From 541dd063012320d39cbfaa80613b20612d787f77 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Mar 2021 18:01:43 +0100 Subject: [PATCH 22/25] SyncServer - fix lagging of gui Resolving of path is expensive and not necessary here --- pype/modules/sync_server/tray/app.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pype/modules/sync_server/tray/app.py b/pype/modules/sync_server/tray/app.py index bb87ae18eb6..b28ca0f66e8 100644 --- a/pype/modules/sync_server/tray/app.py +++ b/pype/modules/sync_server/tray/app.py @@ -540,6 +540,9 @@ def _open_in_explorer(self): return fpath = self.item.path + project = self.table_view.model()._project + fpath = self.sync_server.get_local_file_path(project, fpath) + fpath = os.path.normpath(os.path.dirname(fpath)) if os.path.isdir(fpath): if 'win' in sys.platform: # windows @@ -797,8 +800,7 @@ def _add_page_records(self, local_site, remote_site, representations): repre.get("files_size", 0), 1, STATUS[repre.get("status", -1)], - self.sync_server.get_local_file_path(self._project, - files[0].get('path')) + files[0].get('path') ) self._data.append(item) @@ -1390,8 +1392,10 @@ def _open_in_explorer(self): return fpath = self.item.path - fpath = os.path.normpath(os.path.dirname(fpath)) + project = self.table_view.model()._project + fpath = self.sync_server.get_local_file_path(project, fpath) + fpath = os.path.normpath(os.path.dirname(fpath)) if os.path.isdir(fpath): if 'win' in sys.platform: # windows subprocess.Popen('explorer "%s"' % fpath) @@ -1604,8 +1608,7 @@ def _add_page_records(self, local_site, remote_site, representations): STATUS[repre.get("status", -1)], repre.get("tries"), '\n'.join(errors), - self.sync_server.get_local_file_path(self._project, - file.get('path')) + file.get('path') ) self._data.append(item) From fe3de0cfc9825d70f134ab4b3479f528234fd0cc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Mar 2021 18:07:10 +0100 Subject: [PATCH 23/25] Hound --- pype/modules/sync_server/sync_server.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pype/modules/sync_server/sync_server.py b/pype/modules/sync_server/sync_server.py index c78fbe44c5d..a95e618573a 100644 --- a/pype/modules/sync_server/sync_server.py +++ b/pype/modules/sync_server/sync_server.py @@ -362,8 +362,8 @@ def get_active_site(self, project_name): Returns: (string) """ - active_site = self.get_sync_project_setting(project_name)['config']\ - ['active_site'] + active_site = self.get_sync_project_setting( + project_name)['config']['active_site'] if active_site == self.LOCAL_SITE: return get_local_site_id() return active_site @@ -398,8 +398,8 @@ def get_remote_site(self, project_name): """ Returns remote (theirs) site for 'project_name' from settings """ - return self.get_sync_project_setting(project_name)['config']\ - ['remote_site'] + return self.get_sync_project_setting( + project_name)['config']['remote_site'] """ End of Public API """ @@ -460,7 +460,7 @@ def tray_init(self): "There are not set presets for SyncServer OR " "Credentials provided are invalid, " "no syncing possible"). - format(str(self.sync_project_settings)), exc_info=True) + format(str(self.sync_project_settings)), exc_info=True) self.enabled = False def tray_start(self): @@ -662,7 +662,8 @@ def get_provider_for_site(self, project_name, site): """ Return provider name for site. """ - site_preset = self.get_sync_project_setting(project_name)["sites"].get(site) + site_preset = self.get_sync_project_setting(project_name)["sites"].\ + get(site) if site_preset: return site_preset["provider"] From 066256806dd31925b240fcc99782d0feae1e6eb6 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 11 Mar 2021 18:12:41 +0100 Subject: [PATCH 24/25] delete look manager --- repos/maya-look-assigner | 1 - 1 file changed, 1 deletion(-) delete mode 160000 repos/maya-look-assigner diff --git a/repos/maya-look-assigner b/repos/maya-look-assigner deleted file mode 160000 index 7adabe8f0e6..00000000000 --- a/repos/maya-look-assigner +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7adabe8f0e6858bfe5b6bf0b39bd428ed72d0452 From 1daa755ae4015b9d91e3eda5c5012075b91198f0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Mar 2021 18:55:27 +0100 Subject: [PATCH 25/25] SyncServer - use local_site_id properly 'local' should be replaced by local_site_id anytime in real world --- pype/modules/sync_server/sync_server.py | 11 +++++++++-- pype/plugins/publish/integrate_new.py | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pype/modules/sync_server/sync_server.py b/pype/modules/sync_server/sync_server.py index a95e618573a..e9a0b942e7d 100644 --- a/pype/modules/sync_server/sync_server.py +++ b/pype/modules/sync_server/sync_server.py @@ -398,8 +398,12 @@ def get_remote_site(self, project_name): """ Returns remote (theirs) site for 'project_name' from settings """ - return self.get_sync_project_setting( + remote_site = self.get_sync_project_setting( project_name)['config']['remote_site'] + if remote_site == self.LOCAL_SITE: + return get_local_site_id() + + return remote_site """ End of Public API """ @@ -653,9 +657,12 @@ def _get_configured_sites_from_setting(self, project_setting): return configured_sites def _get_default_site_configs(self): + """ + Returns skeleton settings for 'studio' and user's local site + """ default_config = {'provider': 'local_drive'} all_sites = {self.DEFAULT_SITE: default_config, - self.LOCAL_SITE: default_config} + get_local_site_id(): default_config} return all_sites def get_provider_for_site(self, project_name, site): diff --git a/pype/plugins/publish/integrate_new.py b/pype/plugins/publish/integrate_new.py index 63f746da7e6..c7d9ab3964b 100644 --- a/pype/plugins/publish/integrate_new.py +++ b/pype/plugins/publish/integrate_new.py @@ -966,13 +966,16 @@ def prepare_file_info(self, path, size=None, file_hash=None, ["global"] ["sync_server"]) + local_site_id = pype.api.get_local_site_id() if sync_server_presets["enabled"]: local_site = sync_server_presets["config"].\ get("active_site", "studio").strip() if local_site == 'local': - local_site = pype.api.get_local_site_id() + local_site = local_site_id remote_site = sync_server_presets["config"].get("remote_site") + if remote_site == 'local': + remote_site = local_site_id rec = { "_id": io.ObjectId(),