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) diff --git a/pype/modules/sync_server/providers/abstract_provider.py b/pype/modules/sync_server/providers/abstract_provider.py index 9130a06d948..001d4c4d50b 100644 --- a/pype/modules/sync_server/providers/abstract_provider.py +++ b/pype/modules/sync_server/providers/abstract_provider.py @@ -71,3 +71,39 @@ 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 + + @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 4d16b8b9301..32a5017fe39 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,69 @@ 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 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) + + 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) diff --git a/pype/modules/sync_server/sync_server.py b/pype/modules/sync_server/sync_server.py index 22dede66d80..e9a0b942e7d 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" @@ -110,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." @@ -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_settings = None self.sync_server_thread = None # asyncio requires new thread self.action_show_widget = None @@ -128,7 +129,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. @@ -146,7 +147,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: @@ -173,7 +174,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, @@ -322,6 +323,115 @@ 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'], this site is default + 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 + """ + return self.get_active_sites_from_settings( + get_project_settings(project_name)) + + def get_active_sites_from_settings(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 + + # remote 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 + """ + return self.get_remote_sites_from_settings( + get_project_settings(project_name)) + + def get_remote_sites_from_settings(self, settings): + """ + Get remote sites for returning 'default' values for Local Settings + """ + 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 + """ + 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 """ + + 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 [] + + remote_sites = [self.DEFAULT_SITE, self.LOCAL_SITE] + if sync_settings: + remote_sites.extend(sync_settings.get("sites").keys()) + + return list(set(remote_sites)) + + 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 @@ -335,15 +445,14 @@ def tray_init(self): if not self.enabled: return - self.presets = None + self.sync_project_settings = 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.set_sync_project_settings() self.sync_server_thread = SyncServerThread(self) from .tray.app import SyncServerWindow self.widget = SyncServerWindow(self) @@ -355,7 +464,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_settings)), exc_info=True) self.enabled = False def tray_start(self): @@ -369,7 +478,7 @@ def tray_start(self): Returns: None """ - if self.presets and self.active_sites: + if self.sync_project_settings and self.enabled: self.sync_server_thread.start() else: log.info("No presets or active providers. " + @@ -425,31 +534,48 @@ def get_anatomy(self, project_name): """ return self._anatomies.get('project_name') or Anatomy(project_name) - def get_synced_presets(self): + def set_sync_project_settings(self): """ - Collects all projects which have enabled syncing and their settings - Returns: - (dict): of settings, keys are project names - """ - if self.presets: # presets set already, do not call again and again - return self.presets + Set sync_project_settings for all projects (caching) - sync_presets = {} + For performance + """ + sync_project_settings = {} 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) + sync_settings = self._parse_sync_settings_from_settings( + get_project_settings(collection)) if sync_settings: - sync_presets[collection] = sync_settings + default_sites = self._get_default_site_configs() + sync_settings['sites'].update(default_sites) + sync_project_settings[collection] = sync_settings - if not sync_presets: + if not sync_project_settings: log.info("No enabled and configured projects for sync.") - return sync_presets + self.sync_project_settings = sync_project_settings + + 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 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 refresh or not self.sync_project_settings: + self.set_sync_project_settings() + + 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: @@ -460,83 +586,91 @@ 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_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_active_sites(self, settings): + def _get_configured_sites(self, project_name): """ - Sets 'self.active_sites' as a dictionary from provided 'settings' + Loops through settings and looks for configured sites and checks + its handlers for particular 'project_name'. - Format: - { 'project_name' : ('provider_name', 'site_name') } - Args: - settings (dict): all enabled project sync setting (sites labesl, - retries count etc.) + Args: + 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}} """ - self.active_sites = {} - initiated_handlers = {} - for project_name, project_setting in settings.items(): - for site_name, config in project_setting.get("sites").items(): - handler = initiated_handlers.get(config["provider"]) - if not handler: - handler = lib.factory.get_provider(config["provider"], - site_name, - presets=config) - initiated_handlers[config["provider"]] = handler - - if handler.is_active(): - if not self.active_sites.get('project_name'): - self.active_sites[project_name] = [] - - 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") + settings = self.get_sync_project_setting(project_name) + return self._get_configured_sites_from_setting(settings) - def get_active_sites(self, project_name): - """ - Returns active sites (provider configured and able to connect) per - project. + def _get_configured_sites_from_setting(self, project_setting): + if not project_setting.get("enabled"): + return {} - Args: - project_name (str): used as a key in dict + initiated_handlers = {} + configured_sites = {} + 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. \ + 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 - Returns: - (dict): - Format: - { 'project_name' : ('provider_name', 'site_name') } - """ - return self.active_sites[project_name] + if handler.is_active(): + configured_sites[site_name] = True - 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 configured_sites - def get_remote_site(self, project_name): + def _get_default_site_configs(self): """ - Returns remote (theirs) site for 'project_name' from settings + Returns skeleton settings for 'studio' and user's local site """ - return self.get_synced_preset(project_name)['config']['remote_site'] + default_config = {'provider': 'local_drive'} + all_sites = {self.DEFAULT_SITE: default_config, + get_local_site_id(): default_config} + return all_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"] @@ -606,8 +740,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 @@ -654,7 +789,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 @@ -684,40 +819,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) - remote_file = self._get_remote_file_path(file, - handler.get_roots_config() - ) - - 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. @@ -735,21 +870,24 @@ 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) - remote_file_path = self._get_remote_file_path( - file, handler.get_roots_config()) + 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) - 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, - handler.download_file, + remote_handler.download_file, remote_file_path, local_file_path, self, @@ -907,7 +1045,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: @@ -1066,19 +1204,14 @@ 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 = get_local_site_id() if my_local_site != site_name: self.log.warning("Cannot remove non local file for {}". 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 = { @@ -1093,12 +1226,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) @@ -1109,31 +1244,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, project_name=None): - """ - Returns name of current user local_site - - 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") - def get_loop_delay(self, project_name): """ Return count of seconds before next synchronization loop starts @@ -1141,7 +1258,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_settings[project_name]["config"]["loop_delay"] + return int(ld) def show_widget(self): """Show dialog to enter credentials""" @@ -1215,59 +1333,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_local_site(collection) - sites = self.get_synced_preset(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) + remote_file_path = '' + if remote_handler: + root_configs = self._get_roots_config(self.sync_project_settings, + collection, + remote_site_name) - return path + remote_file_path = remote_handler.resolve_path(file_path, + root_configs) - 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 + 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)) - Returns: - (string) - absolute path on remote location - """ - path = file.get("path", "") - 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" - self.log.error(msg) + return local_file_path, remote_file_path def _get_retries_arr(self, project_name): """ @@ -1278,12 +1372,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_settings[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): """ @@ -1334,20 +1436,20 @@ async def sync_loop(self): while self.is_running and not self.module.is_paused(): import time start_time = None - 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 - 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 @@ -1477,3 +1579,24 @@ 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("Some of the sites {} - {} is not ".format(local_site, + remote_site) + + "working properly") + return None, None + + return local_site, remote_site diff --git a/pype/modules/sync_server/tray/app.py b/pype/modules/sync_server/tray/app.py index 2adec8382b8..b28ca0f66e8 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") @@ -29,6 +28,8 @@ -1: 'Not available' } +DUMMY_PROJECT = "No project configured" + class SyncServerWindow(QtWidgets.QDialog): """ @@ -157,7 +158,9 @@ def refresh(self): model = self.project_list.model() model.clear() - for project_name in self.sync_server.get_synced_presets().keys(): + project_name = None + 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") @@ -166,8 +169,8 @@ 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")) + 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( 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_active_site(project_name) def _get_icon(self, status): if not self.icons.get(status): @@ -200,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 @@ -209,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 == get_local_site_id(): action = QtWidgets.QAction("Clear local project") actions_mapping[action] = self._clear_project menu.addAction(action) @@ -239,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) @@ -254,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 @@ -473,10 +477,10 @@ 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, + project_name, self.representation_id, local_site_name ) @@ -498,13 +502,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))) @@ -535,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 @@ -620,11 +628,13 @@ 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 # 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() @@ -790,14 +800,12 @@ 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) self._rec_loaded += 1 - def canFetchMore(self, index): """ Check if there are more records than currently loaded @@ -849,6 +857,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) @@ -871,7 +882,8 @@ 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.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() @@ -886,7 +898,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) @@ -995,7 +1006,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"}, @@ -1022,9 +1033,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"} @@ -1381,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) @@ -1460,7 +1473,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 @@ -1595,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) @@ -1664,7 +1676,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) @@ -1772,14 +1783,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}, @@ -2010,6 +2022,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) diff --git a/pype/plugins/publish/integrate_new.py b/pype/plugins/publish/integrate_new.py index 14b25b9c46b..c7d9ab3964b 100644 --- a/pype/plugins/publish/integrate_new.py +++ b/pype/plugins/publish/integrate_new.py @@ -966,10 +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 = 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(), 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", diff --git a/pype/settings/lib.py b/pype/settings/lib.py index feeeaf3813e..a8f4a46a686 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. @@ -398,36 +398,40 @@ 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") + # Check for roots existence in local settings first + roots_project_locals = ( + local_project_settings + .get(project_name, {}) + .get("roots", {}) + ) + roots_default_locals = ( + local_project_settings + .get(DEFAULT_PROJECT_KEY, {}) + .get("roots", {}) + ) - if not active_site: + # 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 + if site_name is None: project_settings = get_project_settings(project_name) - active_site = ( - project_settings - ["global"] - ["sync_server"] - ["config"] - ["active_site"] + site_name = ( + project_settings["global"]["sync_server"]["config"]["active_site"] ) # QUESTION should raise an exception? - if not active_site: + if not site_name: return - roots_locals = default_locals.get("roots", {}).get(active_site, {}) - if project_name != DEFAULT_PROJECT_KEY: - roots_locals.update( - project_locals.get("roots", {}).get(active_site, {}) - ) - + # Combine roots from local settings + 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: return @@ -442,6 +446,44 @@ 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. + + 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. + 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") + ) + + 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"] = remote_site + + def get_system_settings(clear_metadata=True): """System settings with applied studio overrides.""" default_values = get_default_settings()[SYSTEM_SETTINGS_KEY] @@ -463,6 +505,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 +529,7 @@ def get_default_anatomy_settings(clear_metadata=True): return result -def get_anatomy_settings(project_name, clear_metadata=True): +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( @@ -498,23 +542,19 @@ 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) + result = apply_overrides(studio_overrides, project_overrides) + + 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 + result, local_settings, project_name, site_name ) return result -def get_project_settings(project_name, clear_metadata=True): +def get_project_settings(project_name, exclude_locals=False): """Project settings with applied studio and project overrides.""" if not project_name: raise ValueError( @@ -528,8 +568,14 @@ 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) + + if not exclude_locals: + local_settings = get_local_settings() + apply_local_settings_on_project_settings( + result, local_settings, project_name + ) + return result diff --git a/pype/tools/settings/local_settings/projects_widget.py b/pype/tools/settings/local_settings/projects_widget.py index 28765155c24..482b67882c4 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) @@ -223,9 +217,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 @@ -241,6 +236,15 @@ 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"] + ) + + return sync_server_module.get_active_sites_from_settings( + self.project_settings["project_settings"].value + ) + def refresh(self): self._clear_widgets() @@ -251,7 +255,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) @@ -294,10 +298,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 @@ -547,7 +553,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_settings( + 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: @@ -575,9 +588,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_settings( + 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: @@ -601,17 +619,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 +642,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 +692,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) 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