From 7c302631014f85efa05cf809679f55260a0d9593 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 7 Jul 2021 18:50:53 +0200 Subject: [PATCH 001/450] #1784 - location of existing unittests refactored for futuru use --- .../igniter/test_bootstrap_repos.py | 0 tests/{ => unit}/igniter/test_tools.py | 0 .../openpype/lib/test_user_settings.py | 0 .../sync_server/fixture/openpype/logs.bson | Bin 0 -> 2824 bytes .../fixture/openpype/logs.metadata.json | 1 + .../fixture/openpype/settings.bson | Bin 0 -> 623 bytes .../fixture/openpype/settings.metadata.json | 1 + .../fixture/test_db/test_project.bson | Bin 0 -> 13295 bytes .../test_db/test_project.metadata.json | 1 + .../sync_server/test_site_operations.py | 164 ++++++++++++++++++ 10 files changed, 167 insertions(+) rename tests/{ => unit}/igniter/test_bootstrap_repos.py (100%) rename tests/{ => unit}/igniter/test_tools.py (100%) rename tests/{ => unit}/openpype/lib/test_user_settings.py (100%) create mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/logs.bson create mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json create mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/settings.bson create mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json create mode 100644 tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.bson create mode 100644 tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json create mode 100644 tests/unit/openpype/modules/sync_server/test_site_operations.py diff --git a/tests/igniter/test_bootstrap_repos.py b/tests/unit/igniter/test_bootstrap_repos.py similarity index 100% rename from tests/igniter/test_bootstrap_repos.py rename to tests/unit/igniter/test_bootstrap_repos.py diff --git a/tests/igniter/test_tools.py b/tests/unit/igniter/test_tools.py similarity index 100% rename from tests/igniter/test_tools.py rename to tests/unit/igniter/test_tools.py diff --git a/tests/openpype/lib/test_user_settings.py b/tests/unit/openpype/lib/test_user_settings.py similarity index 100% rename from tests/openpype/lib/test_user_settings.py rename to tests/unit/openpype/lib/test_user_settings.py diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.bson b/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.bson new file mode 100644 index 0000000000000000000000000000000000000000..37efb8a4a8dd94738bd7800df8e7a6d0f4c010c6 GIT binary patch literal 2824 zcmdUw&ui2`6vxM)*7^gAJtf+z9Nn@3+}vNbEQ71~3DWoPofFEjJz^WMwHLxha@EF|~7 zz5P7?bL&O>*~`kPW-%yDGe0;#+Nv}}9ze$b*au1j$r0Q&2}FC_p`a5 z&MQfuIM#=p#67H=kzS=XU4nieav4{FdqDRC+0v>Up#-gb^%J zj*z8wa9dQlm_1TMIE40ek#~;oUDWr*m*G+T*q+5R4>mJZo^YCAd2I*rVH9MC57E7f zwi42PS3tL#qkCZwx=-|9>0(L~o;g zUqpM*u-kcd$b%fg#Rq4}s4(jpl`TGBo|ombo3b%gOE+UxR=` literal 0 HcmV?d00001 diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json b/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json new file mode 100644 index 0000000000..8c7a16261d --- /dev/null +++ b/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json @@ -0,0 +1 @@ +{"options":{"capped":true,"size":{"$numberDouble":"1.073741824E+09"},"max":{"$numberInt":"5000"}},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"f982c4d7baf54d03b88aaa540c9ced8e","collectionName":"logs"} \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.bson b/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.bson new file mode 100644 index 0000000000000000000000000000000000000000..dbfe2e88c6e63f08fccd8a06a280b29dd9bf5bbc GIT binary patch literal 623 zcmbV~Jx&8L5QS&6AXcIY5+#SAqo4%|3JN6p!MmPKO#HL*%p&bg=(!1J0nWidfUr@D zkir^Sp7;6LZ=MT)19TSd-(GHRpW6A!-NoxfhK^tg06efzKqe6vXAGq^Vj!rf49WJM z81Qq`N;b^`&QwxSG_@xSu!oo)DQ9OX;(`W7gk$ZPoI<7Lw+~8jfihz-(ab3CjNu_R z>NV@7V@9#P6-&l?7ikRm$6HuTw8p1MU0-|0bCi_)t~mXtb6x8Zy{{sg`BWRf9?`yv z0IvT?<1*dL!=>~{kGh;5sF97@$}w7MP45K?xY_`kt~~sJG(SmU_#Trl4`a>;Ny@BQ literal 0 HcmV?d00001 diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json b/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json new file mode 100644 index 0000000000..dafcd98d52 --- /dev/null +++ b/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"8329d557adfe48018cd533dc648e3b7f","collectionName":"settings"} \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.bson b/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.bson new file mode 100644 index 0000000000000000000000000000000000000000..c81a0bd315f5c02f4ef86523e34f8b4098ae31cf GIT binary patch literal 13295 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 literal 0 HcmV?d00001 diff --git a/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json b/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json new file mode 100644 index 0000000000..b43f27f459 --- /dev/null +++ b/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"bfe11cd230d041438b288f7d6ad8e70f","collectionName":"test_project"} \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py new file mode 100644 index 0000000000..7e1c994456 --- /dev/null +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -0,0 +1,164 @@ +"""Test file for Sync Server, tests site operations add_site, remove_site""" +import os +import pytest +from bson.objectid import ObjectId + +from tests.lib.DBHandler import DBHandler + +TEST_DB_NAME = "test_db" +TEST_PROJECT_NAME = "test_project" +TEST_OPENPYPE_NAME = "test_openpype" +REPRESENTATION_ID = "60e578d0c987036c6a7b741d" + + +@pytest.fixture(scope='session') +def monkeypatch_session(): + """Monkeypatch couldn't be used with module or session fixtures.""" + from _pytest.monkeypatch import MonkeyPatch + m = MonkeyPatch() + yield m + m.undo() + + +@pytest.fixture(scope="module") +def db_init(monkeypatch_session): + backup_dir = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + 'fixture' + ) + ) + + uri = os.environ.get("OPENPYPE_MONGO") or "mongodb://localhost:27017" + db = DBHandler(uri) + db.setup_from_dump(TEST_DB_NAME, backup_dir, True, + db_name_out=TEST_DB_NAME) + + db.setup_from_dump("openpype", backup_dir, True, + db_name_out=TEST_OPENPYPE_NAME) + + # set needed env vars temporarily for tests + monkeypatch_session.setenv("OPENPYPE_MONGO", uri) + monkeypatch_session.setenv("AVALON_MONGO", uri) + monkeypatch_session.setenv("OPENPYPE_DATABASE_NAME", TEST_OPENPYPE_NAME) + monkeypatch_session.setenv("AVALON_TIMEOUT", '3000') + monkeypatch_session.setenv("AVALON_DB", TEST_DB_NAME) + monkeypatch_session.setenv("AVALON_PROJECT", TEST_PROJECT_NAME) + monkeypatch_session.setenv("PYPE_DEBUG", "3") + + +@pytest.fixture(scope="module") +def setup_avalon_db(db_init): + """Connect to Avalon, only after 'db_init' sets env vars.""" + from avalon.api import AvalonMongoDB + db = AvalonMongoDB() + yield db + + +@pytest.fixture(scope="module") +def setup_sync_server_module(db_init): + """Get sync_server_module from ModulesManager""" + from openpype.modules import ModulesManager + + manager = ModulesManager() + sync_server = manager.modules_by_name["sync_server"] + yield sync_server + + +@pytest.mark.usefixtures("setup_avalon_db") +def test_project_created(setup_avalon_db): + assert ['test_project'] == setup_avalon_db.database.collection_names(False) + + +@pytest.mark.usefixtures("setup_avalon_db") +def test_objects_imported(setup_avalon_db): + count_obj = len(list(setup_avalon_db.database[TEST_PROJECT_NAME].find({}))) + assert 15 == count_obj + + +@pytest.mark.usefixtures("setup_sync_server_module") +def test_add_site(setup_avalon_db, setup_sync_server_module): + """Adds 'test_site', checks that added, checks that doesn't duplicate.""" + query = { + "_id": ObjectId(REPRESENTATION_ID) + } + + ret = setup_avalon_db.database[TEST_PROJECT_NAME].find(query) + + assert 1 == len(list(ret)), \ + "Single {} must be in DB".format(REPRESENTATION_ID) + + setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, + site_name='test_site') + + ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + + assert 1 == len(ret), \ + "Single {} must be in DB".format(REPRESENTATION_ID) + + ret = ret.pop() + site_names = [site["name"] for site in ret["files"][0]["sites"]] + assert 'test_site' in site_names, "Site name wasn't added" + + +@pytest.mark.usefixtures("setup_sync_server_module") +def test_add_site_again(setup_avalon_db, setup_sync_server_module): + """Depends on test_add_site, must throw exception.""" + with pytest.raises(ValueError): + setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, + site_name='test_site') + + +@pytest.mark.usefixtures("setup_sync_server_module") +def test_add_site_again_force(setup_avalon_db, setup_sync_server_module): + """Depends on test_add_site, must not throw exception.""" + setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, + site_name='test_site', force=True) + + query = { + "_id": ObjectId(REPRESENTATION_ID) + } + + ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + + assert 1 == len(ret), \ + "Single {} must be in DB".format(REPRESENTATION_ID) + + +@pytest.mark.usefixtures("setup_sync_server_module") +def test_remove_site(setup_avalon_db, setup_sync_server_module): + """Depends on test_add_site, must remove 'test_site'.""" + setup_sync_server_module.remove_site(TEST_PROJECT_NAME, REPRESENTATION_ID, + site_name='test_site') + + query = { + "_id": ObjectId(REPRESENTATION_ID) + } + + ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + + assert 1 == len(ret), \ + "Single {} must be in DB".format(REPRESENTATION_ID) + + ret = ret.pop() + site_names = [site["name"] for site in ret["files"][0]["sites"]] + + assert 'test_site' not in site_names, "Site name wasn't removed" + + +@pytest.mark.usefixtures("setup_sync_server_module") +def test_remove_site_again(setup_avalon_db, setup_sync_server_module): + """Depends on test_add_site, must trow exception""" + with pytest.raises(ValueError): + setup_sync_server_module.remove_site(TEST_PROJECT_NAME, + REPRESENTATION_ID, + site_name='test_site') + + query = { + "_id": ObjectId(REPRESENTATION_ID) + } + + ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + + assert 1 == len(ret), \ + "Single {} must be in DB".format(REPRESENTATION_ID) From d8b7fca9657fda4e95337cb8ce0eaaa46c9f119e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 7 Jul 2021 18:51:55 +0200 Subject: [PATCH 002/450] #1784 - added base implementation for helper DB class Added example of usage of helper class to test SyncServerModule (WIP) --- tests/README.md | 12 +++ tests/__init__.py | 0 tests/integration/README.md | 6 ++ tests/lib/DBHandler.py | 144 ++++++++++++++++++++++++++++++++++++ tests/lib/README.md | 1 + tests/lib/__init__.py | 0 6 files changed, 163 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/integration/README.md create mode 100644 tests/lib/DBHandler.py create mode 100644 tests/lib/README.md create mode 100644 tests/lib/__init__.py diff --git a/tests/README.md b/tests/README.md index e69de29bb2..727b89a86e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -0,0 +1,12 @@ +Automatic tests for OpenPype +============================ +Structure: +- integration - end to end tests, slow + - openpype/modules/MODULE_NAME - structure follow directory structure in code base + - fixture - sample data `(MongoDB dumps, test files etc.)` + - `tests.py` - single or more pytest files for MODULE_NAME +- unit - quick unit test + - MODULE_NAME + - fixture + - `tests.py` + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/README.md b/tests/integration/README.md new file mode 100644 index 0000000000..00d8a4c10d --- /dev/null +++ b/tests/integration/README.md @@ -0,0 +1,6 @@ +Integration test for OpenPype +============================= +Contains end-to-end tests for automatic testing of OP. + +Should run headless publish on all hosts to check basic publish use cases automatically +to limit regression issues. diff --git a/tests/lib/DBHandler.py b/tests/lib/DBHandler.py new file mode 100644 index 0000000000..258ff67df7 --- /dev/null +++ b/tests/lib/DBHandler.py @@ -0,0 +1,144 @@ +""" + Helper class for automatic testing, provides dump and restore via command + line utilities. + + Expect mongodump and mongorestore present at MONGODB_UTILS_DIR +""" +import os +import pymongo +import subprocess + + +class DBHandler(): + + # vendorize ?? + MONGODB_UTILS_DIR = "c:\\Program Files\\MongoDB\\Server\\4.4\\bin" + + def __init__(self, uri=None, host=None, port=None, + user=None, password=None): + """'uri' or rest of separate credentials""" + if uri: + self.uri = uri + if host: + if all([user, password]): + host = "{}:{}@{}".format(user, password, host) + uri = 'mongodb://{}:{}'.format(host, port or 27017) + + assert uri, "Must have uri to MongoDB" + self.client = pymongo.MongoClient(uri) + self.db = None + + def setup_empty(self, name): + # not much sense + self.db = self.client[name] + + def setup_from_dump(self, db_name, dump_dir, overwrite=False, + collection=None, db_name_out=None): + """ + Restores 'db_name' from 'dump_dir'. + + Works with BSON folders exported by mongodump + + Args: + db_name (str): source DB name + dump_dir (str): folder with dumped subfolders + overwrite (bool): True if overwrite target + collection (str): name of source project + db_name_out (str): name of target DB, if empty restores to + source 'db_name' + """ + db_name_out = db_name_out or db_name + if self._db_exists(db_name) and not overwrite: + raise RuntimeError("DB {} already exists".format(db_name_out) + + "Run with overwrite=True") + + dir_path = os.path.join(dump_dir, db_name) + if not os.path.exists(dir_path): + raise RuntimeError( + "Backup folder {} doesn't exist".format(dir_path)) + + query = self._restore_query(self.uri, dump_dir, + db_name=db_name, db_name_out=db_name_out, + collection=collection) + print("mongorestore query:: {}".format(query)) + subprocess.run(query) + + def teardown(self, db_name): + """Drops 'db_name' if exists.""" + if not self._db_exists(db_name): + print("{} doesn't exist".format(db_name)) + return + + self.client.drop_database(db_name) + + def backup_to_dump(self, db_name, dump_dir, overwrite=False): + """ + Helper class for running mongodump for specific 'db_name' + """ + if not self._db_exists(db_name) and not overwrite: + raise RuntimeError("DB {} doesn't exists".format(db_name)) + + dir_path = os.path.join(dump_dir, db_name) + if os.path.exists(dir_path) and not overwrite: + raise RuntimeError("Backup already exists, " + "run with overwrite=True") + + query = self._dump_query(self.uri, dump_dir, db_name=db_name) + print("Mongodump query:: {}".format(query)) + subprocess.run(query) + + def _db_exists(self, db_name): + return db_name in self.client.list_database_names() + + def _dump_query(self, uri, + output_path, + db_name=None, collection=None): + + utility_path = os.path.join(self.MONGODB_UTILS_DIR, "mongodump") + + db_part = coll_part = "" + if db_name: + db_part = "--db={}".format(db_name) + if collection: + if not db_name: + raise ValueError("db_name must be present") + coll_part = "--nsInclude={}.{}".format(db_name, collection) + query = "\"{}\" --uri=\"{}\" --out={} {} {}".format( + utility_path, uri, output_path, db_part, coll_part + ) + + return query + + def _restore_query(self, uri, dump_dir, + db_name=None, db_name_out=None, + collection=None, drop=True): + + utility_path = os.path.join(self.MONGODB_UTILS_DIR, "mongorestore") + + db_part = coll_part = drop_part = "" + if db_name: + db_part = "--nsInclude={}.* --nsFrom={}.*".format(db_name, db_name) + if collection: + assert db_name, "Must provide db name too" + db_part = "--nsInclude={}.{} --nsFrom={}.{}".format(db_name, + collection, + db_name, + collection) + if drop: + drop_part = "--drop" + + if db_name_out: + db_part += " --nsTo={}.*".format(db_name_out) + + query = "\"{}\" --uri=\"{}\" --dir=\"{}\" {} {} {}".format( + utility_path, uri, dump_dir, db_part, coll_part, drop_part + ) + + return query + +# handler = DBHandler(uri="mongodb://localhost:27017") +# +# backup_dir = "c:\\projects\\dumps" +# +# handler.backup_to_dump("openpype", backup_dir, True) +# handler.setup_from_dump("test_db", backup_dir, True) diff --git a/tests/lib/README.md b/tests/lib/README.md new file mode 100644 index 0000000000..043dd3b8e9 --- /dev/null +++ b/tests/lib/README.md @@ -0,0 +1 @@ +Folder for libs and tooling for automatic testing. \ No newline at end of file diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 42774d337360e53075da3c9131d74e3cd634a17e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 8 Jul 2021 16:34:17 +0200 Subject: [PATCH 003/450] #1784 - added base implementation for helper class to download files from remote url, mostly GDrive --- tests/lib/FileHandler.py | 272 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 tests/lib/FileHandler.py diff --git a/tests/lib/FileHandler.py b/tests/lib/FileHandler.py new file mode 100644 index 0000000000..e90eac34c1 --- /dev/null +++ b/tests/lib/FileHandler.py @@ -0,0 +1,272 @@ +import requests +import hashlib +import enlighten +import os +import re +import urllib +from urllib.parse import urlparse +import urllib.request +import urllib.error +import itertools +import hashlib +import tarfile +import zipfile + + +USER_AGENT = "openpype" + + +class RemoteFileHandler: + """Download file from url, might be GDrive shareable link""" + + IMPLEMENTED_ZIP_FORMATS = ['zip', 'tar', 'tgz', + 'tar.gz', 'tar.xz', 'tar.bz2'] + + @staticmethod + def calculate_md5(fpath, chunk_size): + md5 = hashlib.md5() + with open(fpath, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b''): + md5.update(chunk) + return md5.hexdigest() + + @staticmethod + def check_md5(fpath, md5, **kwargs): + return md5 == RemoteFileHandler.calculate_md5(fpath, **kwargs) + + @staticmethod + def check_integrity(fpath, md5=None): + if not os.path.isfile(fpath): + return False + if md5 is None: + return True + return RemoteFileHandler.check_md5(fpath, md5) + + @staticmethod + def download_url( + url, root, filename=None, + md5=None, max_redirect_hops=3 + ): + """Download a file from a url and place it in root. + Args: + url (str): URL to download file from + root (str): Directory to place downloaded file in + filename (str, optional): Name to save the file under. + If None, use the basename of the URL + md5 (str, optional): MD5 checksum of the download. + If None, do not check + max_redirect_hops (int, optional): Maximum number of redirect + hops allowed + """ + root = os.path.expanduser(root) + if not filename: + filename = os.path.basename(url) + fpath = os.path.join(root, filename) + + os.makedirs(root, exist_ok=True) + + # check if file is already present locally + if RemoteFileHandler.check_integrity(fpath, md5): + print('Using downloaded and verified file: ' + fpath) + return + + # expand redirect chain if needed + url = RemoteFileHandler._get_redirect_url(url, + max_hops=max_redirect_hops) + + # check if file is located on Google Drive + file_id = RemoteFileHandler._get_google_drive_file_id(url) + if file_id is not None: + return RemoteFileHandler.download_file_from_google_drive( + file_id, root, filename, md5) + + # download the file + try: + print('Downloading ' + url + ' to ' + fpath) + RemoteFileHandler._urlretrieve(url, fpath) + except (urllib.error.URLError, IOError) as e: # type: ignore[attr-defined] + if url[:5] == 'https': + url = url.replace('https:', 'http:') + print('Failed download. Trying https -> http instead.' + ' Downloading ' + url + ' to ' + fpath) + RemoteFileHandler._urlretrieve(url, fpath) + else: + raise e + + # check integrity of downloaded file + if not RemoteFileHandler.check_integrity(fpath, md5): + raise RuntimeError("File not found or corrupted.") + + @staticmethod + def download_file_from_google_drive(file_id, root, + filename=None, + md5=None): + """Download a Google Drive file from and place it in root. + Args: + file_id (str): id of file to be downloaded + root (str): Directory to place downloaded file in + filename (str, optional): Name to save the file under. + If None, use the id of the file. + md5 (str, optional): MD5 checksum of the download. + If None, do not check + """ + # Based on https://stackoverflow.com/questions/38511444/python-download-files-from-google-drive-using-url + import requests + url = "https://docs.google.com/uc?export=download" + + root = os.path.expanduser(root) + if not filename: + filename = file_id + fpath = os.path.join(root, filename) + + os.makedirs(root, exist_ok=True) + + if os.path.isfile(fpath) and RemoteFileHandler.check_integrity(fpath, + md5): + print('Using downloaded and verified file: ' + fpath) + else: + session = requests.Session() + + response = session.get(url, params={'id': file_id}, stream=True) + token = RemoteFileHandler._get_confirm_token(response) + + if token: + params = {'id': file_id, 'confirm': token} + response = session.get(url, params=params, stream=True) + + response_content_generator = response.iter_content(32768) + first_chunk = None + while not first_chunk: # filter out keep-alive new chunks + first_chunk = next(response_content_generator) + + if RemoteFileHandler._quota_exceeded(first_chunk): + msg = ( + f"The daily quota of the file {filename} is exceeded and " + f"it can't be downloaded. This is a limitation of " + f"Google Drive and can only be overcome by trying " + f"again later." + ) + raise RuntimeError(msg) + + RemoteFileHandler._save_response_content( + itertools.chain((first_chunk, ), + response_content_generator), + fpath) + response.close() + + @staticmethod + def unzip(path, destination_path=None): + if not destination_path: + destination_path = os.path.dirname(path) + + _, archive_type = os.path.splitext(path) + archive_type = archive_type.lstrip('.') + + if archive_type in ['zip']: + print("Unzipping {}->{}".format(path, destination_path)) + zip_file = zipfile.ZipFile(path) + zip_file.extractall(destination_path) + zip_file.close() + + elif archive_type in [ + 'tar', 'tgz', 'tar.gz', 'tar.xz', 'tar.bz2' + ]: + print("Unzipping {}->{}".format(path, destination_path)) + if archive_type == 'tar': + tar_type = 'r:' + elif archive_type.endswith('xz'): + tar_type = 'r:xz' + elif archive_type.endswith('gz'): + tar_type = 'r:gz' + elif archive_type.endswith('bz2'): + tar_type = 'r:bz2' + else: + tar_type = 'r:*' + try: + tar_file = tarfile.open(path, tar_type) + except tarfile.ReadError: + raise SystemExit("corrupted archive") + tar_file.extractall(destination_path) + tar_file.close() + + @staticmethod + def _urlretrieve(url, filename, chunk_size): + with open(filename, "wb") as fh: + with urllib.request.urlopen( + urllib.request.Request(url, + headers={"User-Agent": USER_AGENT})) \ + as response: + for chunk in iter(lambda: response.read(chunk_size), + ""): + if not chunk: + break + fh.write(chunk) + + @staticmethod + def _get_redirect_url(url, max_hops): + initial_url = url + headers = {"Method": "HEAD", "User-Agent": USER_AGENT} + + for _ in range(max_hops + 1): + with urllib.request.urlopen( + urllib.request.Request(url, headers=headers)) as response: + if response.url == url or response.url is None: + return url + + url = response.url + else: + raise RecursionError( + f"Request to {initial_url} exceeded {max_hops} redirects. " + f"The last redirect points to {url}." + ) + + @staticmethod + def _get_confirm_token(response): # type: ignore[name-defined] + for key, value in response.cookies.items(): + if key.startswith('download_warning'): + return value + + return None + + @staticmethod + def _save_response_content( + response_gen, destination, # type: ignore[name-defined] + ): + with open(destination, "wb") as f: + pbar = enlighten.Counter( + total=None, desc="Save content", units="%", color="green") + progress = 0 + for chunk in response_gen: + if chunk: # filter out keep-alive new chunks + f.write(chunk) + progress += len(chunk) + + pbar.close() + + @staticmethod + def _quota_exceeded(first_chunk): # type: ignore[name-defined] + try: + return "Google Drive - Quota exceeded" in first_chunk.decode() + except UnicodeDecodeError: + return False + + @staticmethod + def _get_google_drive_file_id(url): + parts = urlparse(url) + + if re.match(r"(drive|docs)[.]google[.]com", parts.netloc) is None: + return None + + match = re.match(r"/file/d/(?P[^/]*)", parts.path) + if match is None: + return None + + return match.group("id") + + +url = "https://drive.google.com/file/d/1LOVnao6WLW7FpbQELKawzjd19GKx-HH_/view?usp=sharing" # readme +url = "https://drive.google.com/file/d/1SYTZGRVjJUwMUGgZjmOjhDljMzyGaWcv/view?usp=sharing" + + +RemoteFileHandler.download_url(url, root="c:/projects/", filename="temp.zip") +RemoteFileHandler.unzip("c:/projects/temp.zip") \ No newline at end of file From 046966dee18690766375144b03fcbb40aab77770 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 8 Jul 2021 19:07:02 +0200 Subject: [PATCH 004/450] #1784 - refactored names Removed unneeded test files, are being downloaded from GDrive Slight refactoring of fixtures --- tests/lib/{DBHandler.py => db_handler.py} | 1 + tests/lib/{FileHandler.py => file_handler.py} | 12 +-- .../sync_server/fixture/openpype/logs.bson | Bin 2824 -> 0 bytes .../fixture/openpype/logs.metadata.json | 1 - .../fixture/openpype/settings.bson | Bin 623 -> 0 bytes .../fixture/openpype/settings.metadata.json | 1 - .../fixture/test_db/test_project.bson | Bin 13295 -> 0 bytes .../test_db/test_project.metadata.json | 1 - .../sync_server/test_site_operations.py | 102 ++++++++++++------ 9 files changed, 74 insertions(+), 44 deletions(-) rename tests/lib/{DBHandler.py => db_handler.py} (98%) rename tests/lib/{FileHandler.py => file_handler.py} (96%) delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/logs.bson delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/settings.bson delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.bson delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json diff --git a/tests/lib/DBHandler.py b/tests/lib/db_handler.py similarity index 98% rename from tests/lib/DBHandler.py rename to tests/lib/db_handler.py index 258ff67df7..4f134e4b66 100644 --- a/tests/lib/DBHandler.py +++ b/tests/lib/db_handler.py @@ -69,6 +69,7 @@ class DBHandler(): print("{} doesn't exist".format(db_name)) return + print("Dropping {} database".format(db_name)) self.client.drop_database(db_name) def backup_to_dump(self, db_name, dump_dir, overwrite=False): diff --git a/tests/lib/FileHandler.py b/tests/lib/file_handler.py similarity index 96% rename from tests/lib/FileHandler.py rename to tests/lib/file_handler.py index e90eac34c1..79f86b5cf9 100644 --- a/tests/lib/FileHandler.py +++ b/tests/lib/file_handler.py @@ -264,9 +264,9 @@ class RemoteFileHandler: return match.group("id") -url = "https://drive.google.com/file/d/1LOVnao6WLW7FpbQELKawzjd19GKx-HH_/view?usp=sharing" # readme -url = "https://drive.google.com/file/d/1SYTZGRVjJUwMUGgZjmOjhDljMzyGaWcv/view?usp=sharing" - - -RemoteFileHandler.download_url(url, root="c:/projects/", filename="temp.zip") -RemoteFileHandler.unzip("c:/projects/temp.zip") \ No newline at end of file +# url = "https://drive.google.com/file/d/1LOVnao6WLW7FpbQELKawzjd19GKx-HH_/view?usp=sharing" # readme +# url = "https://drive.google.com/file/d/1SYTZGRVjJUwMUGgZjmOjhDljMzyGaWcv/view?usp=sharing" +# +# +# RemoteFileHandler.download_url(url, root="c:/projects/", filename="temp.zip") +# RemoteFileHandler.unzip("c:/projects/temp.zip") \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.bson b/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.bson deleted file mode 100644 index 37efb8a4a8dd94738bd7800df8e7a6d0f4c010c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2824 zcmdUw&ui2`6vxM)*7^gAJtf+z9Nn@3+}vNbEQ71~3DWoPofFEjJz^WMwHLxha@EF|~7 zz5P7?bL&O>*~`kPW-%yDGe0;#+Nv}}9ze$b*au1j$r0Q&2}FC_p`a5 z&MQfuIM#=p#67H=kzS=XU4nieav4{FdqDRC+0v>Up#-gb^%J zj*z8wa9dQlm_1TMIE40ek#~;oUDWr*m*G+T*q+5R4>mJZo^YCAd2I*rVH9MC57E7f zwi42PS3tL#qkCZwx=-|9>0(L~o;g zUqpM*u-kcd$b%fg#Rq4}s4(jpl`TGBo|ombo3b%gOE+UxR=` diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json b/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json deleted file mode 100644 index 8c7a16261d..0000000000 --- a/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{"capped":true,"size":{"$numberDouble":"1.073741824E+09"},"max":{"$numberInt":"5000"}},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"f982c4d7baf54d03b88aaa540c9ced8e","collectionName":"logs"} \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.bson b/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.bson deleted file mode 100644 index dbfe2e88c6e63f08fccd8a06a280b29dd9bf5bbc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 623 zcmbV~Jx&8L5QS&6AXcIY5+#SAqo4%|3JN6p!MmPKO#HL*%p&bg=(!1J0nWidfUr@D zkir^Sp7;6LZ=MT)19TSd-(GHRpW6A!-NoxfhK^tg06efzKqe6vXAGq^Vj!rf49WJM z81Qq`N;b^`&QwxSG_@xSu!oo)DQ9OX;(`W7gk$ZPoI<7Lw+~8jfihz-(ab3CjNu_R z>NV@7V@9#P6-&l?7ikRm$6HuTw8p1MU0-|0bCi_)t~mXtb6x8Zy{{sg`BWRf9?`yv z0IvT?<1*dL!=>~{kGh;5sF97@$}w7MP45K?xY_`kt~~sJG(SmU_#Trl4`a>;Ny@BQ diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json b/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json deleted file mode 100644 index dafcd98d52..0000000000 --- a/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"8329d557adfe48018cd533dc648e3b7f","collectionName":"settings"} \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.bson b/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.bson deleted file mode 100644 index c81a0bd315f5c02f4ef86523e34f8b4098ae31cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13295 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 diff --git a/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json b/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json deleted file mode 100644 index b43f27f459..0000000000 --- a/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"bfe11cd230d041438b288f7d6ad8e70f","collectionName":"test_project"} \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index 7e1c994456..cea201e0c8 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -1,15 +1,34 @@ -"""Test file for Sync Server, tests site operations add_site, remove_site""" +"""Test file for Sync Server, tests site operations add_site, remove_site. + + File: + creates temporary directory and downloads .zip file from GDrive + unzips .zip file + uses content of .zip file (MongoDB's dumps) to import to new databases + with use of 'monkeypatch_session' modifies required env vars + temporarily + runs battery of tests checking that site operation for Sync Server + module are working + removes temporary folder + removes temporary databases (?) +""" import os import pytest +import tempfile +import shutil from bson.objectid import ObjectId -from tests.lib.DBHandler import DBHandler +from tests.lib.db_handler import DBHandler +from tests.lib.file_handler import RemoteFileHandler TEST_DB_NAME = "test_db" TEST_PROJECT_NAME = "test_project" TEST_OPENPYPE_NAME = "test_openpype" REPRESENTATION_ID = "60e578d0c987036c6a7b741d" +TEST_FILES = [ + ("1eCwPljuJeOI8A3aisfOIBKKjcmIycTEt", "test_site_operations.zip", "") +] + @pytest.fixture(scope='session') def monkeypatch_session(): @@ -21,21 +40,36 @@ def monkeypatch_session(): @pytest.fixture(scope="module") -def db_init(monkeypatch_session): - backup_dir = os.path.abspath( - os.path.join( - os.path.dirname(__file__), - 'fixture' - ) - ) +def download_test_data(): + tmpdir = tempfile.mkdtemp() + for test_file in TEST_FILES: + file_id, file_name, md5 = test_file + + f_name, ext = os.path.splitext(file_name) + + RemoteFileHandler.download_file_from_google_drive(file_id, + str(tmpdir), + file_name) + + if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: + RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) + + + yield tmpdir + shutil.rmtree(tmpdir) + + +@pytest.fixture(scope="module") +def db(monkeypatch_session, download_test_data): + backup_dir = download_test_data uri = os.environ.get("OPENPYPE_MONGO") or "mongodb://localhost:27017" - db = DBHandler(uri) - db.setup_from_dump(TEST_DB_NAME, backup_dir, True, - db_name_out=TEST_DB_NAME) + db_handler = DBHandler(uri) + db_handler.setup_from_dump(TEST_DB_NAME, backup_dir, True, + db_name_out=TEST_DB_NAME) - db.setup_from_dump("openpype", backup_dir, True, - db_name_out=TEST_OPENPYPE_NAME) + db_handler.setup_from_dump("openpype", backup_dir, True, + db_name_out=TEST_OPENPYPE_NAME) # set needed env vars temporarily for tests monkeypatch_session.setenv("OPENPYPE_MONGO", uri) @@ -46,17 +80,15 @@ def db_init(monkeypatch_session): monkeypatch_session.setenv("AVALON_PROJECT", TEST_PROJECT_NAME) monkeypatch_session.setenv("PYPE_DEBUG", "3") - -@pytest.fixture(scope="module") -def setup_avalon_db(db_init): - """Connect to Avalon, only after 'db_init' sets env vars.""" from avalon.api import AvalonMongoDB db = AvalonMongoDB() yield db + db_handler.teardown(TEST_DB_NAME) + db_handler.teardown(TEST_OPENPYPE_NAME) @pytest.fixture(scope="module") -def setup_sync_server_module(db_init): +def setup_sync_server_module(db): """Get sync_server_module from ModulesManager""" from openpype.modules import ModulesManager @@ -65,25 +97,25 @@ def setup_sync_server_module(db_init): yield sync_server -@pytest.mark.usefixtures("setup_avalon_db") -def test_project_created(setup_avalon_db): - assert ['test_project'] == setup_avalon_db.database.collection_names(False) +@pytest.mark.usefixtures("db") +def test_project_created(db): + assert ['test_project'] == db.database.collection_names(False) -@pytest.mark.usefixtures("setup_avalon_db") -def test_objects_imported(setup_avalon_db): - count_obj = len(list(setup_avalon_db.database[TEST_PROJECT_NAME].find({}))) +@pytest.mark.usefixtures("db") +def test_objects_imported(db): + count_obj = len(list(db.database[TEST_PROJECT_NAME].find({}))) assert 15 == count_obj @pytest.mark.usefixtures("setup_sync_server_module") -def test_add_site(setup_avalon_db, setup_sync_server_module): +def test_add_site(db, setup_sync_server_module): """Adds 'test_site', checks that added, checks that doesn't duplicate.""" query = { "_id": ObjectId(REPRESENTATION_ID) } - ret = setup_avalon_db.database[TEST_PROJECT_NAME].find(query) + ret = db.database[TEST_PROJECT_NAME].find(query) assert 1 == len(list(ret)), \ "Single {} must be in DB".format(REPRESENTATION_ID) @@ -91,7 +123,7 @@ def test_add_site(setup_avalon_db, setup_sync_server_module): setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, site_name='test_site') - ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + ret = list(db.database[TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(REPRESENTATION_ID) @@ -102,7 +134,7 @@ def test_add_site(setup_avalon_db, setup_sync_server_module): @pytest.mark.usefixtures("setup_sync_server_module") -def test_add_site_again(setup_avalon_db, setup_sync_server_module): +def test_add_site_again(db, setup_sync_server_module): """Depends on test_add_site, must throw exception.""" with pytest.raises(ValueError): setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, @@ -110,7 +142,7 @@ def test_add_site_again(setup_avalon_db, setup_sync_server_module): @pytest.mark.usefixtures("setup_sync_server_module") -def test_add_site_again_force(setup_avalon_db, setup_sync_server_module): +def test_add_site_again_force(db, setup_sync_server_module): """Depends on test_add_site, must not throw exception.""" setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, site_name='test_site', force=True) @@ -119,14 +151,14 @@ def test_add_site_again_force(setup_avalon_db, setup_sync_server_module): "_id": ObjectId(REPRESENTATION_ID) } - ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + ret = list(db.database[TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(REPRESENTATION_ID) @pytest.mark.usefixtures("setup_sync_server_module") -def test_remove_site(setup_avalon_db, setup_sync_server_module): +def test_remove_site(db, setup_sync_server_module): """Depends on test_add_site, must remove 'test_site'.""" setup_sync_server_module.remove_site(TEST_PROJECT_NAME, REPRESENTATION_ID, site_name='test_site') @@ -135,7 +167,7 @@ def test_remove_site(setup_avalon_db, setup_sync_server_module): "_id": ObjectId(REPRESENTATION_ID) } - ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + ret = list(db.database[TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(REPRESENTATION_ID) @@ -147,7 +179,7 @@ def test_remove_site(setup_avalon_db, setup_sync_server_module): @pytest.mark.usefixtures("setup_sync_server_module") -def test_remove_site_again(setup_avalon_db, setup_sync_server_module): +def test_remove_site_again(db, setup_sync_server_module): """Depends on test_add_site, must trow exception""" with pytest.raises(ValueError): setup_sync_server_module.remove_site(TEST_PROJECT_NAME, @@ -158,7 +190,7 @@ def test_remove_site_again(setup_avalon_db, setup_sync_server_module): "_id": ObjectId(REPRESENTATION_ID) } - ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + ret = list(db.database[TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(REPRESENTATION_ID) From bafba8dae019e13dec3865a72b729c4d44b8d883 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 13 Jul 2021 15:33:51 +0200 Subject: [PATCH 005/450] #1784 - added json import option --- tests/lib/db_handler.py | 103 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 3 deletions(-) diff --git a/tests/lib/db_handler.py b/tests/lib/db_handler.py index 4f134e4b66..af3ff0742d 100644 --- a/tests/lib/db_handler.py +++ b/tests/lib/db_handler.py @@ -32,6 +32,73 @@ class DBHandler(): # not much sense self.db = self.client[name] + def setup_from_sql(self, db_name, sql_dir, collection=None, + drop=True, mode=None): + """ + Restores 'db_name' from 'sql_url'. + + Works with directory with .json files, + if 'collection' arg is empty, name + of .json file is used as name of target collection. + + Args: + db_name (str): source DB name + sql_dir (str): folder with json files + collection (str): if all sql files are meant for single coll. + drop (bool): True if drop whole collection + mode (str): "insert" - fails on duplicates + "upsert" - modifies existing + "merge" - updates existing + "delete" - removes in DB present if file + """ + if not os.path.exists(sql_dir): + raise RuntimeError( + "Backup folder {} doesn't exist".format(sql_dir)) + + for (dirpath, dirnames, filenames) in os.walk(sql_dir): + for file_name in filenames: + sql_url = os.path.join(dirpath, file_name) + query = self._import_query(self.uri, sql_url, + db_name=db_name, + collection=collection, + drop=drop, + mode=mode) + + print("mongoimport query:: {}".format(query)) + subprocess.run(query) + + def setup_from_sql_file(self, db_name, sql_url, + collection=None, drop=True, mode=None): + """ + Restores 'db_name' from 'sql_url'. + + Works with single .json file. + If 'collection' arg is empty, name + of .json file is used as name of target collection. + + Args: + db_name (str): source DB name + sql_file (str): folder with json files + collection (str): name of target collection + drop (bool): True if drop collection + mode (str): "insert" - fails on duplicates + "upsert" - modifies existing + "merge" - updates existing + "delete" - removes in DB present if file + """ + if not os.path.exists(sql_url): + raise RuntimeError( + "Sql file {} doesn't exist".format(sql_url)) + + query = self._import_query(self.uri, sql_url, + db_name=db_name, + collection=collection, + drop=drop, + mode=mode) + + print("mongoimport query:: {}".format(query)) + subprocess.run(query) + def setup_from_dump(self, db_name, dump_dir, overwrite=False, collection=None, db_name_out=None): """ @@ -137,9 +204,39 @@ class DBHandler(): return query + def _import_query(self, uri, sql_url, + db_name=None, + collection=None, drop=True, mode=None): + + utility_path = os.path.join(self.MONGODB_UTILS_DIR, "mongoimport") + + db_part = coll_part = drop_part = mode_part = "" + if db_name: + db_part = "--db {}".format(db_name) + if collection: + assert db_name, "Must provide db name too" + coll_part = "--collection {}".format(collection) + if drop: + drop_part = "--drop" + if mode: + mode_part = "--mode {}".format(mode) + + query = \ + "\"{}\" --legacy --uri=\"{}\" --file=\"{}\" {} {} {} {}".format( + utility_path, uri, sql_url, + db_part, coll_part, drop_part, mode_part) + + return query + # handler = DBHandler(uri="mongodb://localhost:27017") # # backup_dir = "c:\\projects\\dumps" -# -# handler.backup_to_dump("openpype", backup_dir, True) -# handler.setup_from_dump("test_db", backup_dir, True) +# # +# # handler.backup_to_dump("openpype", backup_dir, True) +# # handler.setup_from_dump("test_db", backup_dir, True) +# # handler.setup_from_sql_file("test_db", "c:\\projects\\sql\\item.sql", +# # collection="test_project", +# # drop=False, mode="upsert") +# handler.setup_from_sql("test_db", "c:\\projects\\sql", +# collection="test_project", +# drop=False, mode="upsert") From 7fd3abc91fe764d656a6b5d2f321baf84b47ae9f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 13 Jul 2021 17:13:03 +0200 Subject: [PATCH 006/450] #1784 - implemented fixture for setting environment variables from json file --- .../sync_server/test_site_operations.py | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index cea201e0c8..280e4daafe 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -12,6 +12,9 @@ removes temporary databases (?) """ import os +import sys +import six +import json import pytest import tempfile import shutil @@ -20,6 +23,7 @@ from bson.objectid import ObjectId from tests.lib.db_handler import DBHandler from tests.lib.file_handler import RemoteFileHandler +TEST_OPENPYPE_MONGO = "mongodb://localhost:27017" TEST_DB_NAME = "test_db" TEST_PROJECT_NAME = "test_project" TEST_OPENPYPE_NAME = "test_openpype" @@ -54,14 +58,36 @@ def download_test_data(): if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) - yield tmpdir shutil.rmtree(tmpdir) @pytest.fixture(scope="module") -def db(monkeypatch_session, download_test_data): - backup_dir = download_test_data +def env_var(monkeypatch_session, download_test_data): + """Sets temporary env vars from json file.""" + env_url = os.path.join(download_test_data, "input", + "env_vars", "env_var.json") + if not os.path.exists(env_url): + raise ValueError("Env variable file {} doesn't exist".format(env_url)) + + env_dict = {} + try: + with open(env_url) as json_file: + env_dict = json.load(json_file) + except ValueError: + print("{} doesn't contain valid JSON") + six.reraise(*sys.exc_info()) + + for key, value in env_dict.items(): + value = value.format(**globals()) + print("Setting {}:{}".format(key, value)) + monkeypatch_session.setenv(key, value) + + +@pytest.fixture(scope="module") +def db_setup(download_test_data, env_var, monkeypatch_session): + """Restore prepared MongoDB dumps into selected DB.""" + backup_dir = os.path.join(download_test_data, "input", "dumps") uri = os.environ.get("OPENPYPE_MONGO") or "mongodb://localhost:27017" db_handler = DBHandler(uri) @@ -71,21 +97,22 @@ def db(monkeypatch_session, download_test_data): db_handler.setup_from_dump("openpype", backup_dir, True, db_name_out=TEST_OPENPYPE_NAME) - # set needed env vars temporarily for tests - monkeypatch_session.setenv("OPENPYPE_MONGO", uri) - monkeypatch_session.setenv("AVALON_MONGO", uri) - monkeypatch_session.setenv("OPENPYPE_DATABASE_NAME", TEST_OPENPYPE_NAME) - monkeypatch_session.setenv("AVALON_TIMEOUT", '3000') - monkeypatch_session.setenv("AVALON_DB", TEST_DB_NAME) - monkeypatch_session.setenv("AVALON_PROJECT", TEST_PROJECT_NAME) - monkeypatch_session.setenv("PYPE_DEBUG", "3") + yield db_handler + db_handler.teardown(TEST_DB_NAME) + db_handler.teardown(TEST_OPENPYPE_NAME) + + +@pytest.fixture(scope="module") +def db(db_setup): + """Provide test database connection. + + Database prepared from dumps with 'db_setup' fixture. + """ from avalon.api import AvalonMongoDB db = AvalonMongoDB() yield db - db_handler.teardown(TEST_DB_NAME) - db_handler.teardown(TEST_OPENPYPE_NAME) @pytest.fixture(scope="module") def setup_sync_server_module(db): From f977cba564c96fb6a6ecf99c009d75643688dc27 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 14 Jul 2021 14:58:36 +0200 Subject: [PATCH 007/450] #1784 - added wrapper class Added documentation --- tests/lib/README.md | 43 ++- tests/lib/testing_wrapper.py | 105 ++++++ .../sync_server/test_site_operations.py | 310 ++++++------------ 3 files changed, 256 insertions(+), 202 deletions(-) create mode 100644 tests/lib/testing_wrapper.py diff --git a/tests/lib/README.md b/tests/lib/README.md index 043dd3b8e9..56ff9749a2 100644 --- a/tests/lib/README.md +++ b/tests/lib/README.md @@ -1 +1,42 @@ -Folder for libs and tooling for automatic testing. \ No newline at end of file +Automatic testing +----------------- +Folder for libs and tooling for automatic testing. + +- db_handler.py - class for preparation of test DB + - dumps DB(s) to BSON (mongodump) + - loads dump(s) to new DB (mongorestore) + - loads sql file(s) to DB (mongoimport) + - deletes test DB + +- file_handler.py - class to download test data from GDrive + - downloads data from (list) of files from GDrive + - checks md5 if file ok + - unzips if zip + +- testing_wrapper.py - base class to use for testing + - all env var necessary for running (OPENPYPE_MONGO ...) + - implements reusable fixtures to: + - load test data (uses `file_handler`) + - prepare DB (uses `db_handler`) + - modify temporarily env vars for testing + + Should be used as a skeleton to create new test cases. + + +Test data +--------- +Each class implementing `TestCase` can provide test file(s) by adding them to +TEST_FILES ('GDRIVE_FILE_ID', 'ACTUAL_FILE_NAME', 'MD5HASH') + +GDRIVE_FILE_ID can be pulled from shareable link from Google Drive app. + +Currently it is expected that test file will be zip file with structure: +- expected - expected files (not implemented yet) +- input + - data - test data (workfiles, images etc) + - dumps - folder for BSOn dumps from (`mongodump`) + - env_vars + env_vars.json - dictionary with environment variables {key:value} + + - sql - sql files to load with `mongoimport` (human readable) + \ No newline at end of file diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_wrapper.py new file mode 100644 index 0000000000..fd50abd18e --- /dev/null +++ b/tests/lib/testing_wrapper.py @@ -0,0 +1,105 @@ +import os +import sys +import six +import json +import pytest +import tempfile +import shutil +from bson.objectid import ObjectId + +from tests.lib.db_handler import DBHandler +from tests.lib.file_handler import RemoteFileHandler + + +class TestCase(): + + TEST_OPENPYPE_MONGO = "mongodb://localhost:27017" + TEST_DB_NAME = "test_db" + TEST_PROJECT_NAME = "test_project" + TEST_OPENPYPE_NAME = "test_openpype" + + REPRESENTATION_ID = "60e578d0c987036c6a7b741d" + + TEST_FILES = [ + ("1eCwPljuJeOI8A3aisfOIBKKjcmIycTEt", "test_site_operations.zip", "") + ] + + @pytest.fixture(scope='session') + def monkeypatch_session(self): + """Monkeypatch couldn't be used with module or session fixtures.""" + from _pytest.monkeypatch import MonkeyPatch + m = MonkeyPatch() + yield m + m.undo() + + + @pytest.fixture(scope="module") + def download_test_data(self): + tmpdir = tempfile.mkdtemp() + for test_file in self.TEST_FILES: + file_id, file_name, md5 = test_file + + f_name, ext = os.path.splitext(file_name) + + RemoteFileHandler.download_file_from_google_drive(file_id, + str(tmpdir), + file_name) + + if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: + RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) + + yield tmpdir + shutil.rmtree(tmpdir) + + + @pytest.fixture(scope="module") + def env_var(self, monkeypatch_session, download_test_data): + """Sets temporary env vars from json file.""" + env_url = os.path.join(download_test_data, "input", + "env_vars", "env_var.json") + if not os.path.exists(env_url): + raise ValueError("Env variable file {} doesn't exist".format(env_url)) + + env_dict = {} + try: + with open(env_url) as json_file: + env_dict = json.load(json_file) + except ValueError: + print("{} doesn't contain valid JSON") + six.reraise(*sys.exc_info()) + + for key, value in env_dict.items(): + all_vars = globals() + all_vars.update(vars(TestCase)) # TODO check + value = value.format(**all_vars) + print("Setting {}:{}".format(key, value)) + monkeypatch_session.setenv(key, value) + + @pytest.fixture(scope="module") + def db_setup(self, download_test_data, env_var, monkeypatch_session): + """Restore prepared MongoDB dumps into selected DB.""" + backup_dir = os.path.join(download_test_data, "input", "dumps") + + uri = os.environ.get("OPENPYPE_MONGO") or "mongodb://localhost:27017" + db_handler = DBHandler(uri) + db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir, True, + db_name_out=self.TEST_DB_NAME) + + db_handler.setup_from_dump("openpype", backup_dir, True, + db_name_out=self.TEST_OPENPYPE_NAME) + + yield db_handler + + db_handler.teardown(self.TEST_DB_NAME) + db_handler.teardown(self.TEST_OPENPYPE_NAME) + + + @pytest.fixture(scope="module") + def db(self, db_setup): + """Provide test database connection. + + Database prepared from dumps with 'db_setup' fixture. + """ + from avalon.api import AvalonMongoDB + db = AvalonMongoDB() + yield db diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index 280e4daafe..9c27da21c0 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -11,213 +11,121 @@ removes temporary folder removes temporary databases (?) """ -import os -import sys -import six -import json import pytest -import tempfile -import shutil + +from tests.lib.testing_wrapper import TestCase from bson.objectid import ObjectId -from tests.lib.db_handler import DBHandler -from tests.lib.file_handler import RemoteFileHandler -TEST_OPENPYPE_MONGO = "mongodb://localhost:27017" -TEST_DB_NAME = "test_db" -TEST_PROJECT_NAME = "test_project" -TEST_OPENPYPE_NAME = "test_openpype" -REPRESENTATION_ID = "60e578d0c987036c6a7b741d" +class TestSiteOperation(TestCase): -TEST_FILES = [ - ("1eCwPljuJeOI8A3aisfOIBKKjcmIycTEt", "test_site_operations.zip", "") -] - - -@pytest.fixture(scope='session') -def monkeypatch_session(): - """Monkeypatch couldn't be used with module or session fixtures.""" - from _pytest.monkeypatch import MonkeyPatch - m = MonkeyPatch() - yield m - m.undo() - - -@pytest.fixture(scope="module") -def download_test_data(): - tmpdir = tempfile.mkdtemp() - for test_file in TEST_FILES: - file_id, file_name, md5 = test_file - - f_name, ext = os.path.splitext(file_name) - - RemoteFileHandler.download_file_from_google_drive(file_id, - str(tmpdir), - file_name) - - if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: - RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) - - yield tmpdir - shutil.rmtree(tmpdir) - - -@pytest.fixture(scope="module") -def env_var(monkeypatch_session, download_test_data): - """Sets temporary env vars from json file.""" - env_url = os.path.join(download_test_data, "input", - "env_vars", "env_var.json") - if not os.path.exists(env_url): - raise ValueError("Env variable file {} doesn't exist".format(env_url)) - - env_dict = {} - try: - with open(env_url) as json_file: - env_dict = json.load(json_file) - except ValueError: - print("{} doesn't contain valid JSON") - six.reraise(*sys.exc_info()) - - for key, value in env_dict.items(): - value = value.format(**globals()) - print("Setting {}:{}".format(key, value)) - monkeypatch_session.setenv(key, value) - - -@pytest.fixture(scope="module") -def db_setup(download_test_data, env_var, monkeypatch_session): - """Restore prepared MongoDB dumps into selected DB.""" - backup_dir = os.path.join(download_test_data, "input", "dumps") - - uri = os.environ.get("OPENPYPE_MONGO") or "mongodb://localhost:27017" - db_handler = DBHandler(uri) - db_handler.setup_from_dump(TEST_DB_NAME, backup_dir, True, - db_name_out=TEST_DB_NAME) - - db_handler.setup_from_dump("openpype", backup_dir, True, - db_name_out=TEST_OPENPYPE_NAME) - - yield db_handler - - db_handler.teardown(TEST_DB_NAME) - db_handler.teardown(TEST_OPENPYPE_NAME) - - -@pytest.fixture(scope="module") -def db(db_setup): - """Provide test database connection. - - Database prepared from dumps with 'db_setup' fixture. - """ - from avalon.api import AvalonMongoDB - db = AvalonMongoDB() - yield db - - -@pytest.fixture(scope="module") -def setup_sync_server_module(db): - """Get sync_server_module from ModulesManager""" - from openpype.modules import ModulesManager - - manager = ModulesManager() - sync_server = manager.modules_by_name["sync_server"] - yield sync_server - - -@pytest.mark.usefixtures("db") -def test_project_created(db): - assert ['test_project'] == db.database.collection_names(False) - - -@pytest.mark.usefixtures("db") -def test_objects_imported(db): - count_obj = len(list(db.database[TEST_PROJECT_NAME].find({}))) - assert 15 == count_obj - - -@pytest.mark.usefixtures("setup_sync_server_module") -def test_add_site(db, setup_sync_server_module): - """Adds 'test_site', checks that added, checks that doesn't duplicate.""" - query = { - "_id": ObjectId(REPRESENTATION_ID) - } - - ret = db.database[TEST_PROJECT_NAME].find(query) - - assert 1 == len(list(ret)), \ - "Single {} must be in DB".format(REPRESENTATION_ID) - - setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, - site_name='test_site') - - ret = list(db.database[TEST_PROJECT_NAME].find(query)) - - assert 1 == len(ret), \ - "Single {} must be in DB".format(REPRESENTATION_ID) - - ret = ret.pop() - site_names = [site["name"] for site in ret["files"][0]["sites"]] - assert 'test_site' in site_names, "Site name wasn't added" - - -@pytest.mark.usefixtures("setup_sync_server_module") -def test_add_site_again(db, setup_sync_server_module): - """Depends on test_add_site, must throw exception.""" - with pytest.raises(ValueError): - setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, + @pytest.fixture(scope="module") + def setup_sync_server_module(self, db): + """Get sync_server_module from ModulesManager""" + from openpype.modules import ModulesManager + + manager = ModulesManager() + sync_server = manager.modules_by_name["sync_server"] + yield sync_server + + + @pytest.mark.usefixtures("db") + def test_project_created(self, db): + assert ['test_project'] == db.database.collection_names(False) + + + @pytest.mark.usefixtures("db") + def test_objects_imported(self, db): + count_obj = len(list(db.database[self.TEST_PROJECT_NAME].find({}))) + assert 15 == count_obj + + + @pytest.mark.usefixtures("setup_sync_server_module") + def test_add_site(self, db, setup_sync_server_module): + """Adds 'test_site', checks that added, checks that doesn't duplicate.""" + query = { + "_id": ObjectId(self.REPRESENTATION_ID) + } + + ret = db.database[self.TEST_PROJECT_NAME].find(query) + + assert 1 == len(list(ret)), \ + "Single {} must be in DB".format(self.REPRESENTATION_ID) + + setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, site_name='test_site') - - -@pytest.mark.usefixtures("setup_sync_server_module") -def test_add_site_again_force(db, setup_sync_server_module): - """Depends on test_add_site, must not throw exception.""" - setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, - site_name='test_site', force=True) - - query = { - "_id": ObjectId(REPRESENTATION_ID) - } - - ret = list(db.database[TEST_PROJECT_NAME].find(query)) - - assert 1 == len(ret), \ - "Single {} must be in DB".format(REPRESENTATION_ID) - - -@pytest.mark.usefixtures("setup_sync_server_module") -def test_remove_site(db, setup_sync_server_module): - """Depends on test_add_site, must remove 'test_site'.""" - setup_sync_server_module.remove_site(TEST_PROJECT_NAME, REPRESENTATION_ID, - site_name='test_site') - - query = { - "_id": ObjectId(REPRESENTATION_ID) - } - - ret = list(db.database[TEST_PROJECT_NAME].find(query)) - - assert 1 == len(ret), \ - "Single {} must be in DB".format(REPRESENTATION_ID) - - ret = ret.pop() - site_names = [site["name"] for site in ret["files"][0]["sites"]] - - assert 'test_site' not in site_names, "Site name wasn't removed" - - -@pytest.mark.usefixtures("setup_sync_server_module") -def test_remove_site_again(db, setup_sync_server_module): - """Depends on test_add_site, must trow exception""" - with pytest.raises(ValueError): - setup_sync_server_module.remove_site(TEST_PROJECT_NAME, - REPRESENTATION_ID, + + ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) + + assert 1 == len(ret), \ + "Single {} must be in DB".format(self.REPRESENTATION_ID) + + ret = ret.pop() + site_names = [site["name"] for site in ret["files"][0]["sites"]] + assert 'test_site' in site_names, "Site name wasn't added" + + + @pytest.mark.usefixtures("setup_sync_server_module") + def test_add_site_again(self, db, setup_sync_server_module): + """Depends on test_add_site, must throw exception.""" + with pytest.raises(ValueError): + setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, + site_name='test_site') + + + @pytest.mark.usefixtures("setup_sync_server_module") + def test_add_site_again_force(self, db, setup_sync_server_module): + """Depends on test_add_site, must not throw exception.""" + setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, + site_name='test_site', force=True) + + query = { + "_id": ObjectId(self.REPRESENTATION_ID) + } + + ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) + + assert 1 == len(ret), \ + "Single {} must be in DB".format(self.REPRESENTATION_ID) + + + @pytest.mark.usefixtures("setup_sync_server_module") + def test_remove_site(self, db, setup_sync_server_module): + """Depends on test_add_site, must remove 'test_site'.""" + setup_sync_server_module.remove_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, site_name='test_site') + + query = { + "_id": ObjectId(self.REPRESENTATION_ID) + } + + ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) + + assert 1 == len(ret), \ + "Single {} must be in DB".format(self.REPRESENTATION_ID) + + ret = ret.pop() + site_names = [site["name"] for site in ret["files"][0]["sites"]] + + assert 'test_site' not in site_names, "Site name wasn't removed" + + + @pytest.mark.usefixtures("setup_sync_server_module") + def test_remove_site_again(self, db, setup_sync_server_module): + """Depends on test_add_site, must trow exception""" + with pytest.raises(ValueError): + setup_sync_server_module.remove_site(self.TEST_PROJECT_NAME, + self.REPRESENTATION_ID, + site_name='test_site') + + query = { + "_id": ObjectId(self.REPRESENTATION_ID) + } + + ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) + + assert 1 == len(ret), \ + "Single {} must be in DB".format(self.REPRESENTATION_ID) - query = { - "_id": ObjectId(REPRESENTATION_ID) - } - ret = list(db.database[TEST_PROJECT_NAME].find(query)) - - assert 1 == len(ret), \ - "Single {} must be in DB".format(REPRESENTATION_ID) +test_case = TestSiteOperation() \ No newline at end of file From 1c99861702b82c75a97957ecfa2aa799be4db928 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jul 2021 12:20:54 +0100 Subject: [PATCH 008/450] Stop timer on application exit. --- openpype/hosts/tvpaint/api/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/tvpaint/api/__init__.py b/openpype/hosts/tvpaint/api/__init__.py index 57a03d38b7..27ae5769c3 100644 --- a/openpype/hosts/tvpaint/api/__init__.py +++ b/openpype/hosts/tvpaint/api/__init__.py @@ -1,6 +1,8 @@ import os import logging +import requests + import avalon.api import pyblish.api from avalon.tvpaint import pipeline @@ -51,6 +53,13 @@ def initial_launch(): set_context_settings() +def application_exit(): + # Stop application timer. + webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") + rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url) + requests.post(rest_api_url) + + def install(): log.info("OpenPype - Installing TVPaint integration") localization_file = os.path.join(HOST_DIR, "resources", "avalon.loc") @@ -67,6 +76,7 @@ def install(): pyblish.api.register_callback("instanceToggled", on_instance_toggle) avalon.api.on("application.launched", initial_launch) + avalon.api.on("application.exit", application_exit) def uninstall(): From 0750a2545613adc3a8930ac4667d9d740af11fe9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 4 Aug 2021 10:42:06 +0100 Subject: [PATCH 009/450] Get task time method --- openpype/modules/timers_manager/rest_api.py | 21 +++++++++++++++++++ .../modules/timers_manager/timers_manager.py | 9 ++++++++ 2 files changed, 30 insertions(+) diff --git a/openpype/modules/timers_manager/rest_api.py b/openpype/modules/timers_manager/rest_api.py index ac8d8b7b74..1699179fd6 100644 --- a/openpype/modules/timers_manager/rest_api.py +++ b/openpype/modules/timers_manager/rest_api.py @@ -1,3 +1,5 @@ +import json + from aiohttp.web_response import Response from openpype.api import Logger @@ -28,6 +30,11 @@ class TimersManagerModuleRestApi: self.prefix + "/stop_timer", self.stop_timer ) + self.server_manager.add_route( + "GET", + self.prefix + "/get_task_time", + self.get_task_time + ) async def start_timer(self, request): data = await request.json() @@ -48,3 +55,17 @@ class TimersManagerModuleRestApi: async def stop_timer(self, request): self.module.stop_timers() return Response(status=200) + + async def get_task_time(self, request): + data = await request.json() + try: + project_name = data['project_name'] + asset_name = data['asset_name'] + task_name = data['task_name'] + except KeyError: + log.error("Payload must contain fields 'project_name, " + + "'asset_name', 'task_name'") + return Response(status=400) + + time = self.module.get_task_time(project_name, asset_name, task_name) + return Response(text=json.dumps(time)) diff --git a/openpype/modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py index 92edd5aeaa..dfe5e6fc4b 100644 --- a/openpype/modules/timers_manager/timers_manager.py +++ b/openpype/modules/timers_manager/timers_manager.py @@ -124,6 +124,15 @@ class TimersManager(PypeModule, ITrayService, IIdleManager, IWebServerRoutes): } self.timer_started(None, data) + def get_task_time(self, project_name, asset_name, task_name): + time = {} + for module in self.modules: + time[module.name] = module.get_task_time( + project_name, asset_name, task_name + ) + + return time + def timer_started(self, source_id, data): for module in self.modules: if module.id != source_id: From 8ae107f4a40b8b4b60e3f8c396df721a424a77c7 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 4 Aug 2021 10:42:26 +0100 Subject: [PATCH 010/450] Get Ftrack task time --- openpype/modules/ftrack/ftrack_module.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index ee139a500e..ee58a175f5 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -3,7 +3,7 @@ import json import collections from abc import ABCMeta, abstractmethod import six -import openpype + from openpype.modules import ( PypeModule, ITrayModule, @@ -368,3 +368,14 @@ class FtrackModule( def set_credentials_to_env(self, username, api_key): os.environ["FTRACK_API_USER"] = username or "" os.environ["FTRACK_API_KEY"] = api_key or "" + + def get_task_time(self, project_name, asset_name, task_name): + session = self.create_ftrack_session() + query = ( + 'Task where name is "{}"' + ' and parent.name is "{}"' + ' and project.full_name is "{}"' + ).format(task_name, asset_name, project_name) + task_entity = session.query(query).one() + hours_logged = (task_entity["time_logged"] / 60) / 60 + return hours_logged From 86c2aaff37b336ed3f11498dc46db51dcef4ec24 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 4 Aug 2021 10:47:30 +0100 Subject: [PATCH 011/450] Get data from context with defined keys. --- openpype/plugins/publish/extract_burnin.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index ef52d51325..88ccbdda1c 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -156,6 +156,16 @@ class ExtractBurnin(openpype.api.Extractor): filled_anatomy = anatomy.format_all(burnin_data) burnin_data["anatomy"] = filled_anatomy.get_solved() + # Add context data burnin_data. + burnin_data["context"] = {} + for item in repre_burnin_defs: + for field, setting in repre_burnin_defs[item].items(): + if "context" in setting: + key = setting.split("[")[1].split("]")[0] + burnin_data["context"][key] = ( + setting.format(context=instance.context.data) + ) + # Add source camera name to burnin data camera_name = repre.get("camera_name") if camera_name: From 26174213bd443e5f92471ef634b71ab83c24b944 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 4 Aug 2021 10:52:06 +0100 Subject: [PATCH 012/450] Hound fix. --- openpype/plugins/publish/extract_burnin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 88ccbdda1c..a30f713e8a 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -159,7 +159,7 @@ class ExtractBurnin(openpype.api.Extractor): # Add context data burnin_data. burnin_data["context"] = {} for item in repre_burnin_defs: - for field, setting in repre_burnin_defs[item].items(): + for _, setting in repre_burnin_defs[item].items(): if "context" in setting: key = setting.split("[")[1].split("]")[0] burnin_data["context"][key] = ( From ebcfe422ac612b84096a2599a7fdfecb8f187d7a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 4 Aug 2021 16:51:09 +0100 Subject: [PATCH 013/450] Simplify to predefined data variable. --- openpype/plugins/publish/extract_burnin.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index a30f713e8a..2fab67cdb9 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -1,6 +1,5 @@ import os import re -import subprocess import json import copy import tempfile @@ -157,14 +156,7 @@ class ExtractBurnin(openpype.api.Extractor): burnin_data["anatomy"] = filled_anatomy.get_solved() # Add context data burnin_data. - burnin_data["context"] = {} - for item in repre_burnin_defs: - for _, setting in repre_burnin_defs[item].items(): - if "context" in setting: - key = setting.split("[")[1].split("]")[0] - burnin_data["context"][key] = ( - setting.format(context=instance.context.data) - ) + burnin_data["context"] = instance.context.data["burnin_context"] # Add source camera name to burnin data camera_name = repre.get("camera_name") From 08ceabd441d5762aaa1788ba6e9212a37ea6f5ed Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Aug 2021 12:01:41 +0200 Subject: [PATCH 014/450] #1784 - Mongo command line utilities are expected at PATH --- tests/lib/db_handler.py | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/tests/lib/db_handler.py b/tests/lib/db_handler.py index af3ff0742d..97e69d9bd0 100644 --- a/tests/lib/db_handler.py +++ b/tests/lib/db_handler.py @@ -2,19 +2,16 @@ Helper class for automatic testing, provides dump and restore via command line utilities. - Expect mongodump and mongorestore present at MONGODB_UTILS_DIR + Expect mongodump, mongoimport and mongorestore present at PATH """ import os import pymongo import subprocess -class DBHandler(): +class DBHandler: - # vendorize ?? - MONGODB_UTILS_DIR = "c:\\Program Files\\MongoDB\\Server\\4.4\\bin" - - def __init__(self, uri=None, host=None, port=None, + def __init__(self, uri=None, host=None, port=None, user=None, password=None): """'uri' or rest of separate credentials""" if uri: @@ -141,7 +138,7 @@ class DBHandler(): def backup_to_dump(self, db_name, dump_dir, overwrite=False): """ - Helper class for running mongodump for specific 'db_name' + Helper method for running mongodump for specific 'db_name' """ if not self._db_exists(db_name) and not overwrite: raise RuntimeError("DB {} doesn't exists".format(db_name)) @@ -158,12 +155,8 @@ class DBHandler(): def _db_exists(self, db_name): return db_name in self.client.list_database_names() - def _dump_query(self, uri, - output_path, - db_name=None, collection=None): - - utility_path = os.path.join(self.MONGODB_UTILS_DIR, "mongodump") - + def _dump_query(self, uri, output_path, db_name=None, collection=None): + """Prepares dump query based on 'db_name' or 'collection'.""" db_part = coll_part = "" if db_name: db_part = "--db={}".format(db_name) @@ -172,7 +165,7 @@ class DBHandler(): raise ValueError("db_name must be present") coll_part = "--nsInclude={}.{}".format(db_name, collection) query = "\"{}\" --uri=\"{}\" --out={} {} {}".format( - utility_path, uri, output_path, db_part, coll_part + "mongodump", uri, output_path, db_part, coll_part ) return query @@ -180,9 +173,7 @@ class DBHandler(): def _restore_query(self, uri, dump_dir, db_name=None, db_name_out=None, collection=None, drop=True): - - utility_path = os.path.join(self.MONGODB_UTILS_DIR, "mongorestore") - + """Prepares query for mongorestore base on arguments""" db_part = coll_part = drop_part = "" if db_name: db_part = "--nsInclude={}.* --nsFrom={}.*".format(db_name, db_name) @@ -199,7 +190,7 @@ class DBHandler(): db_part += " --nsTo={}.*".format(db_name_out) query = "\"{}\" --uri=\"{}\" --dir=\"{}\" {} {} {}".format( - utility_path, uri, dump_dir, db_part, coll_part, drop_part + "mongorestore", uri, dump_dir, db_part, coll_part, drop_part ) return query @@ -208,8 +199,6 @@ class DBHandler(): db_name=None, collection=None, drop=True, mode=None): - utility_path = os.path.join(self.MONGODB_UTILS_DIR, "mongoimport") - db_part = coll_part = drop_part = mode_part = "" if db_name: db_part = "--db {}".format(db_name) @@ -223,7 +212,7 @@ class DBHandler(): query = \ "\"{}\" --legacy --uri=\"{}\" --file=\"{}\" {} {} {} {}".format( - utility_path, uri, sql_url, + "mongoimport", uri, sql_url, db_part, coll_part, drop_part, mode_part) return query @@ -232,7 +221,7 @@ class DBHandler(): # # backup_dir = "c:\\projects\\dumps" # # -# # handler.backup_to_dump("openpype", backup_dir, True) +# handler.backup_to_dump("openpype", backup_dir, True) # # handler.setup_from_dump("test_db", backup_dir, True) # # handler.setup_from_sql_file("test_db", "c:\\projects\\sql\\item.sql", # # collection="test_project", From 7d2974f8e9126d176f8ffa61fb417010a51657d8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Aug 2021 12:14:36 +0200 Subject: [PATCH 015/450] Hound --- tests/lib/file_handler.py | 13 +--- tests/lib/testing_wrapper.py | 8 +-- .../sync_server/test_site_operations.py | 60 +++++++++---------- 3 files changed, 34 insertions(+), 47 deletions(-) diff --git a/tests/lib/file_handler.py b/tests/lib/file_handler.py index 79f86b5cf9..5d7e64b9cd 100644 --- a/tests/lib/file_handler.py +++ b/tests/lib/file_handler.py @@ -1,5 +1,3 @@ -import requests -import hashlib import enlighten import os import re @@ -84,7 +82,7 @@ class RemoteFileHandler: try: print('Downloading ' + url + ' to ' + fpath) RemoteFileHandler._urlretrieve(url, fpath) - except (urllib.error.URLError, IOError) as e: # type: ignore[attr-defined] + except (urllib.error.URLError, IOError) as e: #noqa type: ignore[attr-defined] if url[:5] == 'https': url = url.replace('https:', 'http:') print('Failed download. Trying https -> http instead.' @@ -110,7 +108,7 @@ class RemoteFileHandler: md5 (str, optional): MD5 checksum of the download. If None, do not check """ - # Based on https://stackoverflow.com/questions/38511444/python-download-files-from-google-drive-using-url + # Based on https://stackoverflow.com/questions/38511444/python-download-files-from-google-drive-using-url # noqa import requests url = "https://docs.google.com/uc?export=download" @@ -263,10 +261,3 @@ class RemoteFileHandler: return match.group("id") - -# url = "https://drive.google.com/file/d/1LOVnao6WLW7FpbQELKawzjd19GKx-HH_/view?usp=sharing" # readme -# url = "https://drive.google.com/file/d/1SYTZGRVjJUwMUGgZjmOjhDljMzyGaWcv/view?usp=sharing" -# -# -# RemoteFileHandler.download_url(url, root="c:/projects/", filename="temp.zip") -# RemoteFileHandler.unzip("c:/projects/temp.zip") \ No newline at end of file diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_wrapper.py index fd50abd18e..75ac476dfc 100644 --- a/tests/lib/testing_wrapper.py +++ b/tests/lib/testing_wrapper.py @@ -5,13 +5,12 @@ import json import pytest import tempfile import shutil -from bson.objectid import ObjectId from tests.lib.db_handler import DBHandler from tests.lib.file_handler import RemoteFileHandler -class TestCase(): +class TestCase: TEST_OPENPYPE_MONGO = "mongodb://localhost:27017" TEST_DB_NAME = "test_db" @@ -51,14 +50,14 @@ class TestCase(): yield tmpdir shutil.rmtree(tmpdir) - @pytest.fixture(scope="module") def env_var(self, monkeypatch_session, download_test_data): """Sets temporary env vars from json file.""" env_url = os.path.join(download_test_data, "input", "env_vars", "env_var.json") if not os.path.exists(env_url): - raise ValueError("Env variable file {} doesn't exist".format(env_url)) + raise ValueError("Env variable file {} doesn't exist". + format(env_url)) env_dict = {} try: @@ -93,7 +92,6 @@ class TestCase(): db_handler.teardown(self.TEST_DB_NAME) db_handler.teardown(self.TEST_OPENPYPE_NAME) - @pytest.fixture(scope="module") def db(self, db_setup): """Provide test database connection. diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index 9c27da21c0..7dba792965 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -23,93 +23,91 @@ class TestSiteOperation(TestCase): def setup_sync_server_module(self, db): """Get sync_server_module from ModulesManager""" from openpype.modules import ModulesManager - + manager = ModulesManager() sync_server = manager.modules_by_name["sync_server"] yield sync_server - - + @pytest.mark.usefixtures("db") def test_project_created(self, db): assert ['test_project'] == db.database.collection_names(False) - @pytest.mark.usefixtures("db") def test_objects_imported(self, db): count_obj = len(list(db.database[self.TEST_PROJECT_NAME].find({}))) assert 15 == count_obj - @pytest.mark.usefixtures("setup_sync_server_module") def test_add_site(self, db, setup_sync_server_module): - """Adds 'test_site', checks that added, checks that doesn't duplicate.""" + """Adds 'test_site', checks that added, + checks that doesn't duplicate.""" query = { "_id": ObjectId(self.REPRESENTATION_ID) } - + ret = db.database[self.TEST_PROJECT_NAME].find(query) - + assert 1 == len(list(ret)), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) - - setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, + + setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, + self.REPRESENTATION_ID, site_name='test_site') - + ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) - + assert 1 == len(ret), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) - + ret = ret.pop() site_names = [site["name"] for site in ret["files"][0]["sites"]] assert 'test_site' in site_names, "Site name wasn't added" - @pytest.mark.usefixtures("setup_sync_server_module") def test_add_site_again(self, db, setup_sync_server_module): """Depends on test_add_site, must throw exception.""" with pytest.raises(ValueError): - setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, + setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, + self.REPRESENTATION_ID, site_name='test_site') - @pytest.mark.usefixtures("setup_sync_server_module") def test_add_site_again_force(self, db, setup_sync_server_module): """Depends on test_add_site, must not throw exception.""" - setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, + setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, + self.REPRESENTATION_ID, site_name='test_site', force=True) - + query = { "_id": ObjectId(self.REPRESENTATION_ID) } - + ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) - + assert 1 == len(ret), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) - @pytest.mark.usefixtures("setup_sync_server_module") def test_remove_site(self, db, setup_sync_server_module): """Depends on test_add_site, must remove 'test_site'.""" - setup_sync_server_module.remove_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, + setup_sync_server_module.remove_site(self.TEST_PROJECT_NAME, + self.REPRESENTATION_ID, site_name='test_site') query = { "_id": ObjectId(self.REPRESENTATION_ID) } - + ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) - + assert 1 == len(ret), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) - + ret = ret.pop() site_names = [site["name"] for site in ret["files"][0]["sites"]] - + assert 'test_site' not in site_names, "Site name wasn't removed" - @pytest.mark.usefixtures("setup_sync_server_module") def test_remove_site_again(self, db, setup_sync_server_module): """Depends on test_add_site, must trow exception""" @@ -117,15 +115,15 @@ class TestSiteOperation(TestCase): setup_sync_server_module.remove_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, site_name='test_site') - + query = { "_id": ObjectId(self.REPRESENTATION_ID) } - + ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) - + assert 1 == len(ret), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) -test_case = TestSiteOperation() \ No newline at end of file +test_case = TestSiteOperation() From fa542e8f65428ad30f6c9384396b4cbc7e8ab068 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Aug 2021 12:17:34 +0200 Subject: [PATCH 016/450] #1784 - added example link to readme --- tests/lib/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/lib/README.md b/tests/lib/README.md index 56ff9749a2..1c2b188d84 100644 --- a/tests/lib/README.md +++ b/tests/lib/README.md @@ -39,4 +39,8 @@ Currently it is expected that test file will be zip file with structure: env_vars.json - dictionary with environment variables {key:value} - sql - sql files to load with `mongoimport` (human readable) - \ No newline at end of file + + +Example +------- +See `tests\unit\openpype\modules\sync_server\test_site_operations.py` for example usage of implemented classes. \ No newline at end of file From c76889dd552e65f46d5f61cbca7f11dc4a6e4674 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Aug 2021 12:25:19 +0200 Subject: [PATCH 017/450] Hound --- tests/lib/db_handler.py | 2 +- tests/lib/file_handler.py | 4 +--- tests/lib/testing_wrapper.py | 1 - .../modules/sync_server/test_site_operations.py | 12 ++++++------ 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/lib/db_handler.py b/tests/lib/db_handler.py index 97e69d9bd0..44f3e80f98 100644 --- a/tests/lib/db_handler.py +++ b/tests/lib/db_handler.py @@ -52,7 +52,7 @@ class DBHandler: raise RuntimeError( "Backup folder {} doesn't exist".format(sql_dir)) - for (dirpath, dirnames, filenames) in os.walk(sql_dir): + for (_dirpath, _dirnames, filenames) in os.walk(sql_dir): for file_name in filenames: sql_url = os.path.join(dirpath, file_name) query = self._import_query(self.uri, sql_url, diff --git a/tests/lib/file_handler.py b/tests/lib/file_handler.py index 5d7e64b9cd..4c769620a0 100644 --- a/tests/lib/file_handler.py +++ b/tests/lib/file_handler.py @@ -148,8 +148,7 @@ class RemoteFileHandler: RemoteFileHandler._save_response_content( itertools.chain((first_chunk, ), - response_content_generator), - fpath) + response_content_generator), fpath) response.close() @staticmethod @@ -260,4 +259,3 @@ class RemoteFileHandler: return None return match.group("id") - diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_wrapper.py index 75ac476dfc..373bd9af0b 100644 --- a/tests/lib/testing_wrapper.py +++ b/tests/lib/testing_wrapper.py @@ -31,7 +31,6 @@ class TestCase: yield m m.undo() - @pytest.fixture(scope="module") def download_test_data(self): tmpdir = tempfile.mkdtemp() diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index 7dba792965..85e52bf5df 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -31,12 +31,12 @@ class TestSiteOperation(TestCase): @pytest.mark.usefixtures("db") def test_project_created(self, db): assert ['test_project'] == db.database.collection_names(False) - + @pytest.mark.usefixtures("db") def test_objects_imported(self, db): count_obj = len(list(db.database[self.TEST_PROJECT_NAME].find({}))) assert 15 == count_obj - + @pytest.mark.usefixtures("setup_sync_server_module") def test_add_site(self, db, setup_sync_server_module): """Adds 'test_site', checks that added, @@ -62,7 +62,7 @@ class TestSiteOperation(TestCase): ret = ret.pop() site_names = [site["name"] for site in ret["files"][0]["sites"]] assert 'test_site' in site_names, "Site name wasn't added" - + @pytest.mark.usefixtures("setup_sync_server_module") def test_add_site_again(self, db, setup_sync_server_module): """Depends on test_add_site, must throw exception.""" @@ -70,7 +70,7 @@ class TestSiteOperation(TestCase): setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, site_name='test_site') - + @pytest.mark.usefixtures("setup_sync_server_module") def test_add_site_again_force(self, db, setup_sync_server_module): """Depends on test_add_site, must not throw exception.""" @@ -86,14 +86,14 @@ class TestSiteOperation(TestCase): assert 1 == len(ret), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) - + @pytest.mark.usefixtures("setup_sync_server_module") def test_remove_site(self, db, setup_sync_server_module): """Depends on test_add_site, must remove 'test_site'.""" setup_sync_server_module.remove_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, site_name='test_site') - + query = { "_id": ObjectId(self.REPRESENTATION_ID) } From dde84172e4833e238aa70759c053126630967ddd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Aug 2021 12:26:59 +0200 Subject: [PATCH 018/450] Fix wrong hound modification --- tests/lib/db_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/db_handler.py b/tests/lib/db_handler.py index 44f3e80f98..c38f351b76 100644 --- a/tests/lib/db_handler.py +++ b/tests/lib/db_handler.py @@ -52,7 +52,7 @@ class DBHandler: raise RuntimeError( "Backup folder {} doesn't exist".format(sql_dir)) - for (_dirpath, _dirnames, filenames) in os.walk(sql_dir): + for (dirpath, _dirnames, filenames) in os.walk(sql_dir): for file_name in filenames: sql_url = os.path.join(dirpath, file_name) query = self._import_query(self.uri, sql_url, From 65b60d6b5dd6a32e296a7b81e3d2451ee409a5ff Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 9 Aug 2021 16:56:20 +0100 Subject: [PATCH 019/450] Stop timer on application exit setting. Making the feature optional. --- openpype/hosts/tvpaint/api/__init__.py | 7 +++++++ openpype/settings/defaults/project_settings/tvpaint.json | 1 + .../schemas/projects_schema/schema_project_tvpaint.json | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/openpype/hosts/tvpaint/api/__init__.py b/openpype/hosts/tvpaint/api/__init__.py index 27ae5769c3..1c50987d6d 100644 --- a/openpype/hosts/tvpaint/api/__init__.py +++ b/openpype/hosts/tvpaint/api/__init__.py @@ -10,6 +10,7 @@ from avalon.tvpaint.communication_server import register_localization_file from .lib import set_context_settings from openpype.hosts import tvpaint +from openpype.api import get_current_project_settings log = logging.getLogger(__name__) @@ -54,6 +55,12 @@ def initial_launch(): def application_exit(): + data = get_current_project_settings() + stop_timer = data["tvpaint"]["stop_timer_on_application_exit"] + + if not stop_timer: + return + # Stop application timer. webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 47f486aa98..528bf6de8e 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -1,4 +1,5 @@ { + "stop_timer_on_application_exit": false, "publish": { "ExtractSequence": { "review_bg": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 368141813f..8286ed1193 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -5,6 +5,11 @@ "label": "TVPaint", "is_file": true, "children": [ + { + "type": "boolean", + "key": "stop_timer_on_application_exit", + "label": "Stop timer on application exit" + }, { "type": "dict", "collapsible": true, From db622cf9898c3ea87b96703b9f39162020dfd7f9 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 19 Aug 2021 22:00:40 +0200 Subject: [PATCH 020/450] Nuke Quick Start / Tutorial First attempt to make a hands on doc for artists. --- website/docs/artist_hosts_nuke_tut.md | 166 ++++++++++++++++++ .../nuke_tut/nuke_AnatomyAppsVersions.png | Bin 0 -> 29588 bytes .../docs/assets/nuke_tut/nuke_AssetLoader.png | Bin 0 -> 49946 bytes website/docs/assets/nuke_tut/nuke_Context.png | Bin 0 -> 5480 bytes website/docs/assets/nuke_tut/nuke_Create.png | Bin 0 -> 9903 bytes website/docs/assets/nuke_tut/nuke_Creator.png | Bin 0 -> 15493 bytes website/docs/assets/nuke_tut/nuke_Load.png | Bin 0 -> 9928 bytes .../docs/assets/nuke_tut/nuke_NukeColor.png | Bin 0 -> 42347 bytes website/docs/assets/nuke_tut/nuke_Publish.png | Bin 0 -> 9940 bytes .../nuke_tut/nuke_PyblishDialogNuke.png | Bin 0 -> 52744 bytes .../nuke_tut/nuke_RunNukeFtrackAction.png | Bin 0 -> 150648 bytes .../nuke_tut/nuke_RunNukeFtrackAction_p2.png | Bin 0 -> 87912 bytes .../assets/nuke_tut/nuke_RunNukeLauncher.png | Bin 0 -> 38738 bytes .../nuke_tut/nuke_RunNukeLauncher_p2.png | Bin 0 -> 31166 bytes .../nuke_tut/nuke_WorkFileNamingAnatomy.png | Bin 0 -> 18795 bytes .../assets/nuke_tut/nuke_WorkFileSaveAs.png | Bin 0 -> 26227 bytes .../nuke_tut/nuke_WorkfileOnStartup.png | Bin 0 -> 22326 bytes .../docs/assets/nuke_tut/nuke_WriteNode.png | Bin 0 -> 19061 bytes .../assets/nuke_tut/nuke_WriteSettings.png | Bin 0 -> 41188 bytes .../docs/assets/nuke_tut/nuke_versionless.png | Bin 0 -> 5034 bytes website/sidebars.js | 1 + 21 files changed, 167 insertions(+) create mode 100644 website/docs/artist_hosts_nuke_tut.md create mode 100644 website/docs/assets/nuke_tut/nuke_AnatomyAppsVersions.png create mode 100644 website/docs/assets/nuke_tut/nuke_AssetLoader.png create mode 100644 website/docs/assets/nuke_tut/nuke_Context.png create mode 100644 website/docs/assets/nuke_tut/nuke_Create.png create mode 100644 website/docs/assets/nuke_tut/nuke_Creator.png create mode 100644 website/docs/assets/nuke_tut/nuke_Load.png create mode 100644 website/docs/assets/nuke_tut/nuke_NukeColor.png create mode 100644 website/docs/assets/nuke_tut/nuke_Publish.png create mode 100644 website/docs/assets/nuke_tut/nuke_PyblishDialogNuke.png create mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction.png create mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png create mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeLauncher.png create mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeLauncher_p2.png create mode 100644 website/docs/assets/nuke_tut/nuke_WorkFileNamingAnatomy.png create mode 100644 website/docs/assets/nuke_tut/nuke_WorkFileSaveAs.png create mode 100644 website/docs/assets/nuke_tut/nuke_WorkfileOnStartup.png create mode 100644 website/docs/assets/nuke_tut/nuke_WriteNode.png create mode 100644 website/docs/assets/nuke_tut/nuke_WriteSettings.png create mode 100644 website/docs/assets/nuke_tut/nuke_versionless.png diff --git a/website/docs/artist_hosts_nuke_tut.md b/website/docs/artist_hosts_nuke_tut.md new file mode 100644 index 0000000000..5743c8c756 --- /dev/null +++ b/website/docs/artist_hosts_nuke_tut.md @@ -0,0 +1,166 @@ +--- +id: artist_hosts_nuke_tut +title: Nuke QuickStart +sidebar_label: Nuke QuickStart +--- + +# Nuke QuickStart +- [Nuke QuickStart](#nuke-quickstart) + - [Run Nuke - Shot and Task Context](#run-nuke---shot-and-task-context) + - [Save Nuke script – the Work File](#save-nuke-script--the-work-file) + - [Load plate – Asset Loader](#load-plate--asset-loader) + - [Create Write Node – Instance Creator](#create-write-node--instance-creator) + - [What Nuke Publish Does](#what-nuke-publish-does) + - [Publish steps](#publish-steps) + - [Pyblish Note and Intent](#pyblish-note-and-intent) + - [Pyblish Checkbox](#pyblish-checkbox) + - [Pyblish Dialog](#pyblish-dialog) + - [Review](#review) + - [Render and Publish](#render-and-publish) + - [Version-less Render](#version-less-render) + - [Fixing Validate Containers](#fixing-validate-containers) + - [Fixing Validate Version](#fixing-validate-version) + +This QuickStart is just a small introduction to what OpenPype can do for you. It attempts to make an overview for compositing artists, and simplifies processes that are better described in specific parts of the documentation. + +## Run Nuke - Shot and Task Context +OpenPype has to know what shot and task you are working on. You need to run Nuke in context of the task, using Ftrack Action or OpenPype Launcher to select the task and run Nuke. + +![Run Nuke From Ftrack](assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png) +![Run Nuke From Launcher](assets/nuke_tut/nuke_RunNukeLauncher_p2.png) + + +OpenPype menu shows the current context - shot and task. + +If you use Ftrack, executing Nuke with context stops your timer, and starts the Ftrack clock on the shot and task you picked. + +![Context](assets/nuke_tut/nuke_Context.png) + +:::tip Admin Tip - Nuke version +You can [configure](admin_settings_project_anatomy.md#Attributes) which version(s) will be available for current project in **Studio Settings → Project → Anatomy → Attributes → Applications** +::: + +## Save Nuke script – the Work File +Use OpenPype - Work files menu to create a new Nuke script. Openpype offers you the preconfigured naming. +![Context](assets/nuke_tut/nuke_AssetLoader.png) + +Openpype makes initial setup for your Nuke script. It is the same as running [Apply All Settings](artist_hosts_nuke.md#apply-all-settings) from the OpenPype menu. +Reads frame range and resolution from Avalon database, sets it in Nuke Project Settings +Creates Viewer node, sets it’s range and indicates handles by In and Out points +Reads Color settings from the project configuration, and sets it in Nuke Project Settings and Viewer +Sets project directory in the Nuke Project Settings to the Nuke Script Directory + +:::tip Admin Tip - Workfile Naming +The [workfile naming](admin_settings_project_anatomy#templates) is configured in anatomy, see **Studio Settings → Project → Anatomy → Templates → Work** +::: + +:::tip Admin Tip - Open Workfile +You can [configure](project_settings/settings_project_nuke#create-first-workfile) Nuke to automatically open the last version, or create a file on startup. See **Studio Settings → Project → Global → Tools → Workfiles** +::: + +:::tip Admin Tip - Nuke Color Settings +[Color setting](project_settings/settings_project_nuke) for Nuke can be found in **Studio Settings → Project → Anatomy → Color Management and Output Formats → Nuke** +::: + +## Load plate – Asset Loader +If your IO or editorial prepared plates and references, or your CG team has a render to be composited, we need to load it. + +![OpenPype Load](assets/nuke_tut/nuke_Load.png) +![Asset Load](assets/nuke_tut/nuke_AssetLoader.png) + +Pick the plate asset, right click and choose Load Image Sequence to create a Read node in Nuke. Note that the Read node created by OpenPype is green and has an OpenPype Tab. Green color indicates the highest version available. + +## Create Write Node – Instance Creator +To create OpenPype managed Write node, select the Read node you just created, from OpenPype menu, pick Create. +In the Instance Creator, pick Create Write Render, and Create. + +![OpenPype Create](assets/nuke_tut/nuke_Create.png) +![OpenPype Create](assets/nuke_tut/nuke_Creator.png) + +This will create a Group with a Write node inside. + +:::tip Admin Tip - Configuring write node +You can configure write node parameters in **Studio Settings → Project → Anatomy → Color Management and Output Formats → Nuke → Nodes** +::: + +## What Nuke Publish Does +From Artist perspective, Nuke publish gathers all the stuff found in the Nuke script with Publish checkbox set to on, exports stuff and raises the Nuke script (workfile) version. +The Pyblish dialog shows the progress of the process. + +![OpenPype Publish](assets/nuke_tut/nuke_Publish.png) + +![OpenPype Publish](assets/nuke_tut/nuke_PyblishDialogNuke.png) + +#### Publish steps +- gathers all the stuff found in the Nuke script with Publish checkbox set to on (1) +- collects all the info (from the script, database…) (2) +- validates components to be published (checks render range and resolution...) (3) +- extracts data from the script (generates thumbnail, creates reviews like h264 ...) (4) +- copies and renames components (render, reviews, thumbnail, Nuke script...) to publish folder +- integrates components (writes to database, sends preview of the render to Ftrack ...) (5) +- increments Nuke script version, cleans up the render directory (6) + +#### Pyblish Note and Intent +Artist can add Note and Intent before firing the publish (7) button. The Note and Intent is ment for easy communication between artist and supervisor. After publish, Note and Intent can be seen in Ftrack notes. + +#### Pyblish Checkbox +Some may say Pyblish Dialog looks unnecessarily complex; it just tries to put a lot of info in a small area. One of the more tricky parts is that it uses non-standard checkboxes. Some squares can be turned on and off by the artist, some are mandatory. + +#### Pyblish Dialog +The left column of the dialog shows what will be published. If you run the publish and decide to not publish the Nuke script, you can turn it off right in the Pyblish dialog by clicking on the checkbox. If you decide to render and publish the shot in lower resolution to speed up the turnaround, you have to turn off the Write Resolution validator. If you want to use an older version of the asset (older version of the plate...), you have to turn off the Validate containers, and so on. + +Time wise, gathering all the info and validating usually takes just a few seconds, but creating reviews for long, high resolution shots can take too much time on the artist machine. + +More info about [Using Pyblish](artist_tools#using-pyblish) + +:::tip Admin Tip - Configuring validators +You can configure Nuke validators like Output Resolution in **Studio Settings → Project → Nuke → Publish plugins** +::: + +## Review +When you turn the review checkbox on in your OpenPype write node, here is what happens: +- OpenPype uses the current Nuke script to + - Load the render + - Optionally apply LUT + - Render Prores 4444 with the same resolution as your render +- Use Ffmpeg to convert the Prores to whatever review(s) you defined +- Use Ffmpeg to add (optional) burnin to the review(s) from previous step + +Creating reviews is a part of the publishing process. If you choose to do a local publish or to use existing frames, review will be processed also on the artist's machine. +If you choose to publish on the farm, you will render and do reviews on the farm. + +So far there is no option for using existing frames (from your local / check render) and just do the review on the farm. + +More info about [configuring reviews](pype2/admin_presets_plugins#extractreview). + +:::tip Admin Tip - Configuring Reviews +You can configure reviewsin **Studio Settings → Project → Global → Publish plugins → ExtractReview / ExtractBurnin** +Reviews can be configured separately for each host, task, or family. For example Maya can produce different review to Nuke, animation task can have different burnin then modelling, and plate can have different review then model. +::: + +## Render and Publish +Let’s say you want to render and publish the shot right now, with only a Read and Write node. You need to decide if you want to render, check the render and then publish it, or you want to execute the render and publish in one go. + +If you wish to check your render before publishing, you can use your local machine or your farm to render the write node as you would do without OpenPype, load and check your render (OpenPype Write has a convenience button for that), and if happy, use publish with Use existing frames option selected in the write node to generate the review on your local machine. + +If you want to render and publish on the farm in one go, run publish with On farm option selected in the write node to render and make the review on farm. + +![OpenPype Create](assets/nuke_tut/nuke_WriteNode.png) + + +## Version-less Render +OpenPype is configured so your render file names have no version number. The main advantage is that you can keep the render from the previous version and re-render only part of the shot. With care, this is handy. + +Main disadvantage of this approach is that you can render only one version of your shot at one time. Otherwise you risk to partially overwrite your shot render before publishing copies and renames the rendered files to the properly versioned publish folder. + +When making quick farm publishes, like making two versions with different color correction, care must be taken to let the first job (first version) completely finish before the second version starts rendering. + +![Versionless](assets/nuke_tut/nuke_versionless.png) + +## Fixing Validate Containers +If your Pyblish dialog fails on Validate Containers, you might have an old asset loaded. Use OpenPype - Manage... to switch the asset(s) to the latest greatest version. + +## Fixing Validate Version +If your Pyblish dialog fails on Validate Version, you might be trying to publish already published version. Rise your version in the OpenPype WorkFiles SaveAs. + +Or maybe you accidentaly copied write node from different shot to your current one. Check the write publishes on the left side of the Pyblish dialog. Typically you publish only one write. Locate and delete the stray write from other shot. \ No newline at end of file diff --git a/website/docs/assets/nuke_tut/nuke_AnatomyAppsVersions.png b/website/docs/assets/nuke_tut/nuke_AnatomyAppsVersions.png new file mode 100644 index 0000000000000000000000000000000000000000..92e1b4dad7371ffac83828170d260fcf4de1d95b GIT binary patch literal 29588 zcmd43by!qwyElw_E4LzIqqKySbdQuGF?4rGOAatIbSWYt(v5^l*C5@Xq_lK{G{X=Q zLk%$BTB!GPKi_`#^X~oa_jup$4-eK_SDn{+o%uVjF;H1in)njgB?1BhVyMg$RRV%P zo)Qq8z4PZ;;7fiN@iPK~^M+RH+6Zk0d59^@j@{S{W@66nYUcp76A%cCxjGn|+L$9~ zOw28<>_uods_SWKtjt7cwfGb`6&xhYpIOPcJDIDwE2^8i+n5TO(Ta&)5_W|E6WEy} zjA>l$Z0+F?R}tFNc_F|v_?d&2=CljKMub*dL77GZ=44L8$Ii#jNh^AZM%c;B0;2jv z>epc4O@#It0^tDR;BawqVRzwShdEhta0v+sad2{TaC5T(J=oxG_6TEFHhVZ7IK|I7 zo|wZ;ova)XRxo=Sa86?r7!n~uOAEBq{F>a%^sl)ckWRLzGn<)mnA@7$ncE}a99-;N z9Dna`?rQZ9YV6^^*aa|-18f1hbN*9zgq6jA(j9F158WM}!4NR`Gnm6)ME%nee>MMy zfdISz^$=HMhkw;sLE+zbx3l}FZNL$d&H&?n9l}4+^m_rgx|@SJhpIUohIBGDmvjcW zM|a9NkS`ErD_3({?I%_MQ{ljAh|qFzadZB^nsxrx%*Vt3k5&noEzC&`kVA7|-05eK zc@U_bv8B0|l^NpM-=_OVi@dqLCD`c{8F`f25+C*rD_yJJ_ z#pHC9&cAH>>og#z1S-JH0Qz`NNByG-Dj}il1hcTR1=`@M(vNANk`nw}Lj3$}-0VO1 z0vsI#7;9zW_QV(g^y22^=3?U%WaAQ4=jMd)@IW~ESvk2OoImL~?G0#_Y56PxP+=F3-$?$^LVma{PTKzbO;u_z$%I-1y%N3NY^HH^5K<7LVgElLx%~Wi-w0 z0ZZuwm_$}4<3<94Gk#WpIQ$1OARu50M}~LEJ$`gEK&({R{k?k7Z8rKGjaOtccDi@W zAC)Tm8Iso?{OPH`u0cC?!Os6hmO_E#@%d6>UBX8yfAq3HGZ@I2b+*4CyL=hm-3nWz zu<$-N^h~O4h1l@x;w4?dn~0t+j*7J&!Do%l1Z8m_bNjS5>qmTUdKD7T));>!e;i-s`yct)tsO@uPC1=M_icKAFlie! ztgGK@&DDn5=oq^RiHypt98`xMM8rI?=cldI(1*dFi$nahmWa?YhYRfDJE{5+jLzlhNXOuaJ#qJxYMoPgj;6Zr2; zH0u>|0s4FR4C%l~-^KHi=`re^!d?A)dWE{E2MTleP$G&<3f~Bhe+Ho^1- znXZT8B*+4KxKmqDSFP{+6 zyUMf3VR;hPDRW308NOrrO0V5>zW{%dO^@!!3^UxdPgO6j+o)QzNN;8{!Z#w@con*a z4JJ)@{zjgwwh9frO`Q% z67yRDym{=DU2HFl6^%a0CLew=5;f<=z1@rKn0%IyVRolw_PPYOd480~3P*z1G_oFM zacL=AHosMG`A`~v*LB{i+`=*vn2F$X+4Ia1wyF6@=*hk9s2T5-%|$cxXfmddgr`dh z6jP--VC?OBK*Z|tXxx{6rQ`36N^Owl_1a?h#UDmT3p0%LT`4A7isD6mG2>G?r&Wt&t&x>uBdfS z0eLgJ!mx70)m*`Jc?7;zp{0ddcV{TgIJFJ~X~gl54i7jK^RYwuu-fX}wOQ0TYFKnp zYf9~CQSuzf2;YS~{N)Jq{B4=Cw$<{qyLRua!tbZg9;5>U#*c>VFv!5Im?2DSZNxy~ z(pVFf4v)$hyo)6&P2F@{V>qD|-0;)lvCkqhdz_(ZAuW0I>177tC2NZZUTT!(<h(tAU=AYmu03Pmt~W z?}#tU*d#nk5ZYQT4fNW*LTi(pna(v%tB|1|8Bv0jwa(@3?-eXoKbV!$E9;W6-n7u8 zpPcgcMbm-9({0tobuoukz%Fd;#8stj(8My4qcEuT=q@sTAp+G@uGF63SjAtsF?KL? z{T`T>5%AN~Om?i(K_eY|^vcui1VUqFx!F(!3<^^xn!p${j>ck=QkzYS4RhDi?zvbJ znZ39WdZ^xXRh5^jwc8H3lezJNPPVXq`2ZKGF?VxojOs$feHbiyV76#mqcs^LT?U1# zLCedhw*LeTxb0(dn57MyJJ3ngWB8DJoZI76s-x7)F@qSdp&vmr!>Ad;s0^- zAxN$3r>p;U`Ts3%|59`Q8_My^?{ztH2-FV?8P_hK9P?nf?tt`U(Odj)qUSfUr8mQB z)NbHV3af3J?iAn}IZ>vQdPji#9|P_OwTqg=p^y()YMhY6tl7xB>N=2J{fFzd(@CHn zWKm(L!%8yV?zA@>MCj@kr1&7DvRle>QNedk>3v`-aW-NI5~|)%_HGGKACua$7m+U3 zCQ6#j!?$2%5`AlHgs7DYjaEg?(K%cOqy)LOE8oEnO*pDJ5cjk}^IH~_l^BYxN2i5| z4By|CajJDH+-#qnvSdt9*Z&hB;OUJKK#9cg&DkDy4egA;+CkMBe-eS1jwu@0^)lUr@+*-yS$e2R;%1#w)^Uc84v%|0a3N7#Q zqmh7FDB_4-nQT{yq!uU#I_$3Rwt09x2HB)xcJ5qXZ4V8yWXd)iC0|LeU{TMIZ9YD{ zTRKq2+Bd(JA8}s|rTS<=FS4A$t~O$fCA*oA$7f-8_uOttC`MK`LZnw+af5B#g1*$X zm`!pGoOI+ezjff`VO~KbZ~dY|M2TI7xpjOM58HU!xTGYgiNIr>=9f*RWXHT_KW=_- zEpqA|+qix5w2N90=?179u3>gE;<~Mfv}Vguw^%uupwf!E7+yD=9-KiFQdHFS25PnW z!I_ZVKE)=j`Q5EkgFfLqkN)|wq>tQB~;tN(7K7+LgGWa{twiMd&=TH8b6wmTMHIa5dodZCdgpl>T% zd^e3r{A7s0pTr}{da6E!95{n=Ae$Zl%Ha1iMSxocxBl5Ly()GjKO7SsRQ<~jkx>1Q z6-E;@60)J-0t7i{dHxHr>HiV$Hf}*8b9(T0v!n0WqN%`h@LkceJHzbZIStr;KwSVo z7v;QK37Tui4JQVFKtYvOtN9BVt6yDvFwG4PQ0N3=@tfOI$z778R|-&RLnEDLHI;1a z&@ujE_ET4i9Eke!aL;!Ej2b#749^U(TrR(7|AoREz+ zmcbEzq=!l9;ySyvl zWnTqOEm$kBT~iJ;AndsfM-TdK?}0QDjzI|1Qt-0_vkmpfn5nJxZfDo@aTF_atS*dd zb4vKSOD$3_a&u9u%;4erV03P^VSJL>q3xqU7ukriN#Tv8@Z09mc1B8dfGgiUUDS=& z%Pi+0pFv9bBL^#GI@vg?>ZkEqIRd;$l;}F!SKa&t5$aQxmUVlC-M0Kt)E};^3v-r< zWFAf{GlbejTR_OPs>Qppfa$X%B`!mvzav zlq*^9zOL~a?k+FtKh=xN{AMJ5(OG*wc#KCHQX46ZP4p-Yd(#Mo^|-!AcFsPl4i{*Xj(SMBDlRA$zJmo?8`}vikd>Aks@Y8)m z)!TAF{b z_&cSvNNNG2bb?#ODsrpf!}sSB|AkofxBL5-zWR3~n7rohoI0}iES!F)(e}U2QM&*w zdMY&S#)Kc+>%1~O{8Js3Z~Q5vu~sT#)l8sDcP)jU+Fm1VLGPmZ zT3}M~uFT~)f863Lt`%JpYg#`tpn=_DG$Eek3V3Tv9~IiNzH?zK0wA}eZpT+C zVs79Q-%KP)K!!(fLj(goCbfN?cYaSn`i*<|I-dIW2t?VYGJ{a7;H0E#X1*)Jth!ufPV2<6?aPaV{W>$3bMEfjDD zIku5)!Xkr=;&FA_p>I|1V`nbh;wZ$hJlmZ!e`oOCv1h7fKwCG0k8~%LkUf2uicMz_ zbG)tUpS%SDrO~>}LL}{;z_-W{J^Xoigjv;v$Yu?BON4!23j*6r5n%?o4(C&lejMSH zB)aq-$)`ij=akeEF)4Xj3-cmTZA5O9&74rA zbM$MDjo9l8JF1$+L8a6p!y5vm z^&Cf)8P#%NLj-&l#TU}j54giW$JBjzm4(ckcq1?Uxcdutw}DJkomabn$W;gDRtnZV zRp;XKW1-2I;kM%f-SGg#T%R0{qsz!TMcb3gfCW2ktve#m5^eZ1NZV#F)Qgy|NHz0P zj|j)G*l4`lcH$;Y7IFN*8i&>`$}n|kO8=byNu`LimC<5KY})joOLrB4aJOB!wjt^~ zvr3L5XJC$2T(cWzg>Lclg#;^3lwKMqA`6PPMycE#qIGQ<<|4|3(G43JxLs(xCN_X*fS z$2sX913?e@K0v2ZUGf`Z6>r`~jeu#6GTmvGjplj^-AqROLvi20>|x(KmsUzA<9+r& zhdxTLvp!PJR_CCyoW!W#}0AC5#@2cI*Rj`{bKq+hDM6Tlu7PBc8IZfK}uwFyj6Ft6@1~HXe2RTZ)t2JnPlb zRG_0!CIT!_{#kHi1du!7qzWD$6r+Jw~D{U?O9a3ymN2 z$gJ-0c2xRkL^FV5B)WMNxLJLkVPj_l70K&<_&sp*!1}nF!D?rxp?&Q}Pmcy@hLz3W z=jowNfFP#sWfx3{GrhaBiC|>NW{umFBadx$rml8C6r}YA9u#FPO)U?LQ3(n=YkA;$ zsBEakgse%ECwyGx$Hws;^@W$@ra)G_|GG#d(+t>^tgQGotPuX5^U}syw_L$%Hp?MG zW@dCBJb9fn1;S!)Qf}(+rW1NbP-MQ!*GcjelZ1tX1DIgX)_1fFZuzwsj27a$DpsQF)=V+)f5Wq#e=oHHn@6mZ93qmn^Do>+hLcWWLJd zr@W{ZF?WM!BgM`qU2&})nd?IVRb@%ChK;K+LZ>oX3VSyz8)}P)3PPN+1VCak$PymO zHtF{!%hxcfl~6v@=?#CC^a1TE9ub8oXRk=iQ`+e?WD{IuCxiP-pet5kQo40&xm&He z2RExn;Z>!$BqFgRBtDlECL_RJi+D(PAD)juZASN}NZ^)F9(S4|15CLRuk zAsX(NwN+~vC!Jah&?>-Jg3{S{&}B}^F( zNI6*{x0y->Irx+>E+Nah9#J*H1x+e-DT`cDiWbx+M;V z>nxr3>rgx5z`VW*;^H~h1#1Vh9S@NYdnz_G-9h^z&PGF{Q5qs`q1#=-Dgv6d_nMzh z6D`2vq_I3T^SRM}__KlFofl{pZd#E1j6cfP=@};T>AJC)_n?{tE-Ag8W@B>8^l?c_ z>+L60dfTIgnP!8gW|C^!r?`5f$UrJ+--*ybBZ^^PDZPGFk9(~)^wvg5F)nr_BzvS9 z;a$J~7%^A_qLw_bqxmcYS10@omlX`pfp1}X;0A8om$vk3*qghfGRi7E;~4mf#A^5S z`7KRHz*AGgzXIXW!LwCG7!3zAWVUiuj%1%+NRe3Jazez`%r``VingkEXtT>LMS*zK zt#^0DDt(U3aJ}%g^X7)3E{NF`1E}m|9Au~tH@{q=A$Y&~2W-s62I0BB>oO)+N(?e9 zr}_V@g6k0NF1jbyvr=ENxIve^o@NFxR=?lRwKkcX2=on6A8wyjwSUhj@U&LGO3yR` zO(uU^iz`WmM*ufAND7yU_$R%VxB`_OlJ*-_Y>d_x3{?M?1lYQr`12xTYcf)(4hMFM zmRgzgKI=VK(%UEH8gaVyAt3L%a)C8AGiZJ*c<$<73ZBp-#sJePA#BtBm|gj)guTWv zRP_dPo1Xgq(k~?)cr31)z;WZEEt=f=H4F8XN!`jiw9Hqb3U*7*JUNIgupCP+Xh+2F zS|56Z9koz0U{Z^2*F?huemeHyQ^yXU&Y&}%hY&-za85SI*VEJ?gHvUhiV-ym!KSHm zaDNYTQbWoGWf(BgdwD()zo^%hA4TF_m83Fwbi3UUs)#gW%V!X(Fi`Xua&E3YDsmBt z{=Q0nz(OUvCmWcy9m!plERv~LJze>u(P*#Uee)F~+`RVD1$7^)WJy5X&AKSBpk6xdrAuww&fu-va?u0$2>AE^E4u4p z3b?|&`o2_Bx?O(1}Z!I*ONg_+`;O{BS2%Cl~1JIhK@$3eYs52ol_C1Ty2W*L(;YNM9r%b&}G; z7tMpL?~KYQX%<~Mab~aa+4Ts!;GQ<4;j>lT>S?q0kkor0bE3sDrG0z@oIzXhX9?t0 z*T>F+AcJ66rK*Lu9>-&#RsTd0Gon%IbHK4g6xyxseiQg4o=-!QPu)LQi|Ut-5Z$j4 zJ=)QlR-UXXJ2lO`yMVF&Enn`redoXvvOfENMZTf|CNNa^K0IUwaayQK7l$yr&>Sj*i&RB0kkd%pK)5dvIfJ6$-|n z`~+vRUyE^4{tzcZpUfP^Uzp<$!(Sf3oC`9#J!|Q(Pug=-7*~!Nc>MII+ zWCv5qpezOyb&%K}0r@T9vI-haL28(S@1~uVnkrz6jWt@cFgxZ_!n{6uj+Wu{aQpJ{ zA>FXKAYtTrhPK13qK9%%n4uwGW-jF02ZB+6FLBdl)iZwyrsfIsR4#QO2M@ybVl_^^ zi2pUfg&hzJe(DZ?`U1r0T(4dYy9A`cRmDwC6_TgYuM_4eZ#mG3JiH|9X@u<37!TV4 zP14gzpr;U_=?bxV)oOX=^Yr>NZ5tg2k=S1U=8z7kwAQEh{!iJj!Mikz`?a*5qIAII z3CJ`isgy9nzyv?<0)SHUdCXBpA{;m>z5ejz9bbCDV?wj`9fat}(QUFrU#7bw*h&)O z_ic%`r~S;T4oxa$m?l{zO;Js&!WyR;6SW+-L4G63yoKMRv)s8JJ`+&C?t`5T_HYBP z1BsZeQImzjCo_VV6#&p2HJl z;rkfj{WEP}rQEp<?ciuj-D&2Wn8{IS!O`)e+}*Y-S*m8-NO(3cqDesWh#Z3Yt2 zELjtfn)iE;!Fb0+J|e1$3z_G&*Venq=LJH)1TjTc=Tw1MiY(G>Aw_S-tnocP9VXOG zSDN1FK2@h7l`}&W38AIsb0GQB9+1&;sA3)cMh3q*!0{Avz5UfW)HB<4Q^e&f0!)CG z!>!s&Cpe>&ZfY1N3}bXKr>NF@`FC_Q3m4p-&TYezBuNzj5$~#V9=#!&(HH^&LYKId z^Sa+`Gkb&t=fl#>ym;f*c=ZE6($B4ucS}F+-}D6yuvhxHj{6;(NFak)-;~Yvcm}%O z&$PhALbz8GDwuR2bzNn!I}e68F;#0{jz(7a zo!nWhdsG4qC3v0~JNO@Is-E#OPvxeSCDZ{}Z75)JvORH^yzhmt z`XZ-h6tH~B?rHQ8HSDN1MLu?rVS4 z_mBOJUh$Q18ngMT%hC-|^ZQMmI7<(Gt2@pMRngQ;(Tb(zYti&>@5ovOPhkvgpLfm| zn6JSlJ-9cM-vTI7CEe9TFFK=UAa7+3N3h2!yb0l5O`J4IO9N^#`E7?!dR7Ntn67{1 z(h`gc0Tgtyk@a$8&QYPJaPxXgdaNRah|A2+FvzI|Op!jF{2L5ofB7F6#z98XGN`&a z?HQAPB|@IRA|I7*DhA*y!QR{Kz?|J`C5)d!Ws}mz%>HSXVs1+E^Y)Im3FvMA7ngZL z4p7$rJK)li2Xy2Y&5!*#_%mXsZT08CmD|hi#*P4K1CyBGd`OIE^^5_jbP~X`rb@+p zFyN`zi;z;H7Urr`3py^3}}P;l_S zFpqWD#Fl_N)r0|P-GsQ3{nHb>O?I08yL|BC0w6%b`2W-}hcW-4gtZ+Z!T^d0AmP4H zob-yd(BXP^7H0W%W{qilp78Vbka1ai7fdKU}uN)XAyJ=4QoXtyuSOo-+#x>9veu@S0hb&qjF zXheCo#PlG*{FtN+Lf;9rE5{wZ^?=a@Q)LX0y?JQ(j_ zWF^7>k95P0_!qvXE&s%Y%g^)>Lmvca0mhV z7lGUgdf>o$il*`zS+7@9D$O}-AtwgQ}$E3t*7E+R5}C>7c_I@*q$2N17< zYv#k&_ZKL5(<|cyhULv6ET8bNa2UJ3W%I-|Y-KoA;ir+~hvJ5SatvE&B zBrU@dy(HG3JcZ15;L=~_v58cok|#c|jwnK9s}#>?(23~iSPC(1q)>0iyo}!?@3RgS z;T#%~X_%NLnLpx^&YYp(Ww3)XjayZiO$6R2s>}cS6{i*J);)asXpIb!1Qo9-gA z3TX+#!PXNVv#Um<&DGOFGj7@Vh;qE8hqRGbTe~En4F-ZN-B!Rk65P)DalQJ<>UgGS z|G6`kE}i%K!^7YXUn$vgX75n!UY0#|9+VV?0%sZM(IL|g^q2g)j?&HJJNS#<0aO)4l=U`X&n> zz5r{8HfH*`EZL&=M@O#Tv@yh&BTg^BM!ZE z>fh*bQU;Eidz>enjwWkk89`^cjxnA_G1-yyFtxdnWmmKZ9}`Nc^`k?{Fj1DvUAn^mX&kyI+Z)Hq zMJ@@%HzPT)sw(L`?$(IlJbGiuk*MU-Y#3T*zvx6;!jY)EF1%p&1EL^G2C~E+O$Hs! zyRi~tFyht8e&^H>16(rk2a4r5ziyHq@~aTT$*1!Fj|4{KE10l6*6~S~RzVTkfDwbZLRG!o>Klp2h^GG7iTBAsxWX39 zdpV)__yPzI<$YiP!l?zqfQjA+7^#+_fpqhn{?uufk{>(AxR*i|3EE-CEX@zz2T6;gdmM!QWZmr`-xj<8GBnzE7 z7W0^Ef@hz|F+<73+`jK<_ujh_j8iY4Il&b09^g?Om6qA)bE-Ro3)#H&?^zK zI3P~S=s+zB@=b6uL02wk&}~z8O^H729x@(|FaW~v=tBkBh$ymmGL%1D)79H@*2Io- z2he_r=HHm_BIdzp znW;m?qh`6)v~+T=)8cemVoXrfoiZ;5>MQ0)g|CLnWFcPNU+x9a1EA~|+f0=^Dv5qvZC{<+%wgIrDw)DkCGCkdf7RpBSx!`}W$}6B%9=F-nY}AI+ z72`6@sQWm_$37bOAWLHYEXk`q78`#E@tAlF0QRIIIfA=y4Jt}DUbaF$-9ysZ#fB*A z*Wwym?xR}{uYgKA62(&7#C{F8%RId!{&^Ir4It>}^%3lerVb-KOb6%s!^NE)n!-6P z&PA{96H9;O9O*RDjj6Py%hhliX_-hz4vj2pSR>P7U%Pc48F+60s_(Sik_j94Jdnov3HUvG-4x{z2B{pX?$YT zN=EgcO=|x2#^klH;haUrxpZ{(t1i@|b9s8%#o4*3Y94z%$;*>dWm@QL@*$n6q1s^F z?uKF#zAKKGAA;JYUbl&$-#A$p$qvkwrv~NGJbQxKIU|=NCg>%1%H_&bI^u^CV9Lyl zwvRSrF-cmxZ#f;N*7yB2>vh@IwTKtQGR_ZyG?EO?U?HIbKstI@1+Bf;8jIRR;e5JvGHA=|m3<*5D&paa2!fV*iqx3`u;M6k_x@9}-=$^0E{;Ja^EDA4d% z;tA7mrvbi-D?GgbB1Z(a(U3)Q@FC~Qzq_SQ-s-7Ux*;6pv*uwDuz~pvFacL`cE^%A z>T1-=x8J@i2m}GR^>b%cH88Gb84GW#e?sfl=YPSkyW;2RPczS-)dSzI710a82Q;*G z4bN==_s4{}ZOte{>QqJni=Yq?tg7`UnMve3Yk|Lq`#k8@U544<*oIL!X?HSay7uus zU^(CG>)U_yOq*_$z{kx8r3oA!Wv^XXy{*4@p< zYksnoonn&J3h{9Vdsz8citloOyjxs z6^NXGkPyFw?~m_0iZ02V=C%5egMBX{hI)}_@&R54U*e9R7(DWj zWdy44ii@<5B)e=2V2^AVs^eIrI`P<$I?t)F$7n;H(Uxc?q1V7=kI|%8M?m3-1}0_! znCtu1rIoj(c(1BYe`4hli!Nz6r`w{mPAxVH1YNl6!?jc!fd0`lBTZMdaqE$(b6dcz zAAf&4ydXsJF(^4P%EXx?UUL(laN{EJXHtrLI$wDKx0_g*6BujthX*V^Tf&PH|+^bxVuhGiv6k+stb$mMeA+$bEA>8A-ZR}Mq`fD?)kt0Im zC-|ycRq5fk&b}23TjNfe%(xC3=tm5qs{hRB=+;}qVyRz*f|Y;QV|Xj2so ztL6OY*+^Fa=zHIyz8LljB|emr$Dtj(xJvHme4hIpn9A#>$6$7Z*3j=yEA%lF=jm;{ zlD3POkO{mhH+yBdK~dv2A>37JK+-c^@%Hl9E3ThI^=2a2^xf67aH zy49PVr}uekA3Yh-7!$tNY5Pe)N%-DOrFvS`E~#3UcIBVCJsO*wX4qm&7K#3XVx?YY7v&xBhyUK zY(!42T>*XD_<$fk#GZ7W?Lp$xv!DUe*5#w<`yoas5syl%9*mNAG2#}v>O}v_;O9kz z3S`sczDeB`MWE&+K;&{F$ic{CY~=k-y$Qmn2#9g!+q}1VwU-N?B8{@y0Y(vDQfT$y zcAk07^Yg+&j7W1gaH-5GTzN6_o>mFtjg7XO>yKEhjhk5DmqRLOH9sUCOR%PVA}a3iCY8K2z<7P3@wo8e4dZu3^m9WJ%0QXGPF|yDX$p z)O02NxWh-t$Q6$u1q-jr(@oekw7_a zS3Np>gLw4T5nO-_s9NewBF__%>tDeSH(RrvH1Y3QY7_NTTO~X83(E-ck2cyTw2tzU z6nz|X>qCBNWe|Od+!FVB_Kt&Q;i1=?`;@(La&?J5sFAOSk;y~%lw9O%zvTJEH=H&2 zf;Q}*gv?t}Zk1fAo(kgYtkwLkIpP4+0us3?GheMWj7B4Wq7@}qfO&~V?XW2&pEp~RA%q&fx9Z0h~l28`L`=WM=THf z`QoC|o^;+KHhV~{Ws=?CR;ic*ST>!Hq@Y@NgnSu{jXI5v{U_TH5605wDc7A}qTB)H1qfj|Cs-V9;Ek0- zv>=l4HtL4W17Hott|6BCZ9evHaE(z_&HLleJFB{at=S9qi5P@jLnn?nd)_`P4Ws^a zjLZ(vWNMkiz2B-KYo}hDeojfUvA^o4@kiYG+xT(ik?$b^F7ne7a~)%a z62-lk8B3g@ci|jXpx{X5A;ie@5v;xs|KV6~7{4nG^w7FJTyi2EoffKjgU2#uFOXD` z!hNRgO`3lniJ|s~xdjR0*lEv&ElW**(v$tI0#WU`ZOz;*{Lb)VF>676r6?WSW?{h* zqxK#nmfv}14B>H z3;q)BTi@^pn>UcT-hB(KQ5JYT<(cVkd`uflyMTOFRW~uhL-B|@x8;NOqkf&BcDapR zqs52G`h|;I&WIY%4HU_%tyR5g^|8d!G1}Voyd65pdY8-)Rg$c%pvNmuDuHG$yYn5@J5AoyG_&EM` zVc2(SF{OeBX4S9X|C zy_>rIlcJ5#O0lCGUP}eoBJO2>8?G+@`nY*V6lSiWL&)bTO1^+GA=S(6@UzGX#xmp` zPrNMicQqww-G{Q_Htq4G0ZZ19cXYL$Dv72H1w}g^l-=pZ@B=<}DP+Tze2Z{>?z=gX zF^(2_wg3f4ijSQHHhR%$s89F)fPoc^$PJ$h4A|>A)Jx*% z4yE$>;2@EU87o2N6X(&=SJy1XDrKdrq_4j%ya&YnKkou0#V)UW&8FCWV#(?F0y`wR z=1Y_$_Gb7^Oj`lv$AsmWydl3dRh?SV&_(~ohV!0@B1%e8FE->Cy-vIW6y~>Y#LK(; zJnepx)+fkZbaMsk$1(}_)p`ua? z+`Vs>ic^>L)-)jdUOygU6#g0e;d67O7>gZpA*lo|Vb@DIBrtFiHEsV!K~~ARw58^I z$A({@UjCSQn!5YOwoRw-t{3AYuf2t}$WlGE1p6fKdY`W3Oh#(yoJ_sTjtl~%(L}3o zAFnD*J#=a~wp|db^uQ@X0E+ zPsrg;dfx+6uLt*NxrdVZ580=_E9UZD#+I2qDDN@C2dr%`ytwd?Rs7A{0^mRAQC7K< z$jYwI?46kI`du1%Po(YWN88cDoDuFv2xmLWwa4drifvE(-rl!>Cr^5g4%)O9-dy>} z7*8!4&%trj5)c}A2|tHZ;x-Do=6;L%q~>T-z&+(#BcsnFr6DByPWt^VVng-a77LCP&m3RMisv>+$zMaVFa<-3v{Wd*(@6lc)hc`mF2bn>V!-4RZeAe zvMuwHeT_HQQFJ3C{yv*uc9q7)iaoL`ys+khMKdU2?4PfN8y1Ad%O*RS?$bPRN6I+l z(Gp=77}qA0^~#MZhMtXinhy42_R{Bhm|^xuTbQG0MHo!vIMoTiz@=Jp$&}f^yEeK& zj+FSK^!Hh4^@hoNUVSffg>fTlT_O$&auR*+yKU4oA5|lJ#ZH2>hm`?IyqM1DbA7Ky zraMFV3)TR0Bl!z)xwg(lj%L<-DMhv8_4n=KXWIes3!dg{DIM6FCQVpfm|xK(7u9kv z+P;1*-*SE>QB2vrFL}uYt$_195zmGyvFayYp;Udv}!%IFkpe!DKemkJoN*znGp{i`AcCH@rWZueru; zl7rHiz|Br846rM^nX~0Pj{D_JuFMWTFfZ@s_CR~mEJ~kMLk6D+MCi6wd3tRs%{|wQ zR9Pa)yB3}B7VdpA_i}6jBJV6in%ez%!2J0vWgBrF+Kk?E_v6^4(SmsPlz-`(Jod() zy`*@==2m^PeGH3$jbvG7u2q)ZTb`l&WS6d5$=#2%@=gJMrWW%$;`L5h8YLf8!f{5n zaCI}ev+#$$^K6MzIT4T1?Q>LE|7$%vkUi|`fqq=&`hI((hYs~^Z|{2h-IL9NhNQ>T zQyna#?A<1$Q$Io~IM;hC{ZUl z(HUv|iM3a z1E*zUSt6UqG&sT|=ssVY{iW}Ph^E=mjC7_=+Ty8{>y|O@9RijD*hAZDYtN~ z)_SFmah9qdj%4#yx_P>iCX_#(J??nxn3$?pq?y>$bM3B8RW*AOZ_c1UdG*MyyUjQz z->!h<{7X?gg}d9sTyw^?X_T_VTSRlb^on+kf0|YrtbK2-`DB}R?fSLWz1n9o*&Je% zsw*TuabX4`{MFY~b6BpY<~x~6poC`d*f)-k(=QJB_%X2+FC82=G%I0WD9tvrX{KM- zmufP-&I4DqV9!V-cWL(MXw50GCQ@=(zJ48+4#0*BUCdjIFb8 z!UjpC5>k{jy4YsQ^Sy_)b%=}>?f!M1lZq}mq^l*|(h>UBs3U!QJxU(v=wI6G`| zdekNh-7}=2R+yETi!-5Fj9t`4x#q-P8?7x^bAP*+H{?=oePB8$Uh;c$@>C9{Y>+Yb!t|g98MopzEXW?9+q8IYJ1|2F{c)L>0}@nf~T?% zdHu{NIr&|X7uQa3b-U77M_|h>n>ai_~)o ztA5-u!?sA-WiPY<;ZvdJ?hR-W_k6U*_MF{f#R1;}xn6}sM?GOe%3Kq3{?4Iry_j&9 zi+X!-FS(rgFi}^ltke9qe9Mm~g&3g$KqIZIdQn-5=(go+CP#jkPy>cd+dH_|VxKl! zIGa8Lru2pxcXSIi)#uO;w1d23JttCste-c0?P54T$ko+Ky>>O`$K?!EbBplz$4sia zyCR!b27@t`D*sPuUm4YAv#w1IC`}7NLn%_+-MtWsLxDn}6e;fRUNlhLoi=#U1S?)B z8terM1TRoLIK_&bgm<64zx}QA%Lvi!1A6t4U)7>9F=THtCzI6Rd8YZbB)FTb?u6d~?oBja3MEujoy8 z-0bJG*@2(O*sU+-B*FWPr}2?{CdHButBL42kv$2|WbWMQ_VcM{4=V8=93oKLCDyOy z%mhdX3*PTqFFh`yOKiKd+VHk+0CK!Qb|h;L9k9;rFPXfny)7ms#AVwMfWA@4mfBz$ z_M9x`ig|KVXHqoa!Bs%}sG(^l8BR3A*LQ5Z>%`Q8%Z8*%18q!FiQ9qWira)oKT0&k zUS0Ti<>mig2wO2&UlAC%FSOnD<8993o)J-5QA64zS%wCoV;XxYZ$?uprk#L?82rgP zHie(%`mIzRjvJ&YY!Lo_Y%naw;gNOW*ixAYt6fjKp)5$xZ~Wo1(!*(3G|rLM)Ey>! zM~}RxG&@f#?Cz94$>M3cQsjo&RH4GP{nXVQvYVoA{!+%7D5<}IE{EOxkNeGAFPW^d zUCtqOobYXm+#d{TJFn5k$>7Y{WnGsN>k$*?!6n&Ce)BGqmY3&~`r(j~=5K8tq1{9G zH%41(!WViZLV~|+Bq$|c9N_djxjs8jo(~7t=GL_Hw0#at&P^NFU{KMeO!=O>59qOi zDNaaNgl5}1%^`iz+NDouHE}iKrpfDpcVsCD7xb>kD~2V5azS|5%9_dqS#vip zy%piZ!I)-0sgDlX@bKhMcY3YgN^Q5eB^HpS^7G3$*op5;oiP~WKiKn2zPqr=Vd!R}(nwD3|`FjyWOvTacJin+-A<*vO^`y2W)1$l8I`}?C zXZsy7k~G|IG~rTJsu=UlyRbS2O;NWRfRslXo48)qN!#yua9AK>=9NNwUP(;($gWoQ zj#3l0zHze^%zxKIUZEre2^inx>)n zp%KX^mlGd`I0ikA%`Qg9q&zQq<~n~Hj5q>&>cY0aN{SWHzAdPPv)C$7z)XkdV@nle zX5mFF*9qJ1$kYh72`2(g8D~&K!aPrxgFLb=#;yiT4a@NdG`RR5Ay9=ZAjXEm?XR4- z)l|bvZT?kFb8!*NhwpO2xGeAr{HNi_JJK^lF-DWz4tHqo@Y5sZgx{QY4{_f6<`DMW zLqF3EVgS1ECQ>>p?0zB^-u$^utSA;!DXJQ=o4$ogkD{;jwaFwDeis@NPdnSR2Z_M4 zR@QoadvOdFjq3IGg8zAz0XD?iaqW$_(M{loz-|T1DQc{s;yX$FcGhiyS8tQfrLG@s z3O3VV-eWc24M*Br2c9>yW*K1O##liH_a$!sg1(IP{=X7pM!m;z-*58ruEjP9DJ$M{ zOq%?^kG&a3?u!|O;5A_Ee6!tmyXx9g2@kW;6svmW{q=_Z+M;9jSR!hB;>{Tu1apcB z3#(Q(sP@WBoGOQNp=*&;x!}z!eSFQ~W{z$y!qJe3-j`{bufT|NYvlvaFaXO$#%*N< zkM0&wajyv>E$SNhoRV2{-$K&+{NVYQb;#Pem^n5G(L;7CaVnjNPos)DPWuNv8Cnfa*Vf=& zcHSz<_T%ACO1cdFPOJHW{eBvL{`TBX_B^aYv{9&FE9Zdp6A>3W^Abw4M{IU`lIq~& z4g3#pt!Wzi+UQPrB`($r3zT%VU5b3RQnHgsI-5z2sa&TL?_=E!oMDh%xV9Y}NP z`)2rqMXCbCECfyi`7Cdi$4qGvL+sJM(!?x*Y5TXA0)KM|UVJe_?<><#CeyvS7-!-) zzxdZ!jvTbRP%PP1Txe-hq&Ls#=}$3xDt7;5HV1g$6LCm^cSH9H>@03?OHbValYdcK z)K8IG+5HSBUF$SwFitX3T$@4nowo|w5dDRiB@i@^E1{sMf$zwxkKJ~#WI@dI#2SC> z+HnmvGSsrrxgRKbVX^kjWUe^Gh3^r|`aJ1ubLS%4)N4ZP+y!ZY1~8@MaS-KMeFE2! zeEzN{6}Y(1yP@#wKK_T94eiE^u-e1sJ~Mm~shF(8*q%}r%#1KzmkCNvX42!rHv=9d zjJ;{q=i>{x5eXahXVLt~7H4fO4ZNYBA4)WHV4K38pE~O&X&VBF zzakc1FXrgx--_)LN0(yJ;>gSPtSkC^IFDpen)d1&9`h(P8vb!VQuSTtq^E`MQLm?s zEJ$8zi$=KyT%w9;v|Z`Gq8J-)Vs5_v(qaljoJ#;>cWMkLa#k_)evMt{V_Q>EsZB z3B@fnpJy=p8@{OpQ5F2S__FD>v6OR}MomN#5OCvVHJI`HmH%@f5dF(reRANQ9=>(q zrqf$IdJ3b4KFQc}y{4m2Uw5-IqVgy%Hi+sgdrq9gxIfOz=YQyYy3)J&oED0MA6JTc zA!-wgaqE2HWvlfr_T16}J#qS-SzL+(ul_uoIN+NZ9TQ7ijboL_JE8?@BMr-O*Y^U} zI}mjp5qtGW$x0e%VcubumBRV3`QQQOm=9I~n|B9%+8jAdEQ>k`b)YULeSWRi_>RnH zV{S?rMz-*0>txB&@JX+-#`U42FNyN?RS!#CZ~pk^GBLsg=GS_Gck}jK>~AC`pzw*; zv>AERogjNbhM(uB-4X%$K`M#ve6OBF#Jb~D7-ZwS*f~d>z)qJVUv8k?$>*B9Aa;}b z=w{oB2t0L2$ zG}RPwyK^!~HJqw|iyiIKW z1XFjs2X4H_!y;&YO=9>+tH3kjKhQzdp>U(t>5c5Asj-w34INX9*c!?wA#Rby>#wW+ z=&KH4Tl(fFG0?h>|K7J9Vz8Uk!R7(b(YlfG2#+*H90Ah!5SXnkU`a)6o5vrF~g8l z`r_%Or`n4-1Vb-V^coYBU%4*Glfy9a9=S z!laR}OcG!){{VJ{AYWtaVR8qnB!joF?bCi8DP!fsPnbFMZ5ZubDOUc(qC4)i)EPE7 zIEZ{mUX#LDRc$!p)Xh@XAwoodcv2q+4zFd2XvoMCjq{qJ7^yb1-xXWk&n16CsL@8Z zs5g#O?h`okgJyEs-5~u#{P<8Wqd7Qa;@h4 z`}$URh*?=lln7whxNdc%DFocb9j+VMm5e<}8J&f>{s9YX+|QB1)YO!#^?2;~nE%F7 zSafT5H#ag;Qu{^{RcZV(C>|U z%Upl}-N=wf*VgV7zjuf)|Jo}FeRGl(chQvh_ygFHJlvx!bR$L>K*TDM!5^!4Mj>Id z4UR(m{C@i#IHEaf`inr|qIV~^CC;a?y{NVg(f zZ&OQjS5e)yFh%2XE`Cbn-82tUz7ycGu;3vpZ)rpV1bX{C)#go&&0Gn@VOgt|aCB^5 zy7cwe7FBdWZDe?CZJ;O!JV&=vQ#uq=E~7nu?FMRJmn2SDFhYwu+7zw@uBbeYRn)Jr z+W(q2T3^!&P?i_fL(Bw>WC{-nWmG-J&e>TX@N%htA>rW~|tWf`P$L^x;fIDxv_TUzU`s8n+_N-fH$ ztpp!c&0kfWc;zd??jhpHxmKmDfi%%c@(*ERJbWI;zkXV&HP6pfvqR zXn-`TtG!5a{swUcAZ!k~&lfGni$W82YKJ#&`@p<~fa99TeDHC$?fE0$dgNzf^3Q3b zPQi6rJwFa@H0tf*;E9lfEI3GCvV>39{+#Ih7!}BM+*e+FC&p42?H_VnycVB6Aq1}U zc~0`5Bv$VaI>o0)X~j0O)!hu#^h8K6ujMQaRpGKQWc9N2lB!=q;tNi`=vAG_Rniq&R#E=hcn+|bR^D#I z94m~ON)%ye1&KmO61T1c0?cx@9U?I$Cyfkp`e|szh-jhEa;``#8%DO4NKD@3b=kmc zw+)RqA3&5t+^PBuReqT-9-xLo$_}DntH>=JG+<(n=mqZ2jNgGwP%d1?oeT2OEsDQ<&sqkmLT~)&y|?|lT1F$w z&d|~fEBEtOYuks8i~TmtsIbNuW0Lp_KH*q<@3DEFztO8Chm)0)nrmsCumVX)LZfyq zRSq8FrA9)m>XxiZAiR|es?RD_<`=g`(RWFwqAzsfVAW+nIKjxlMufo88m$3v@=|8M zm2mCb0`lZ^(svEdW=fT(`t zv@ZECKI~NOaQ+#gxatXfev0g!btqF*wP0RdQ#VeM-S2xLzdOYr6T7udZ_t}Ov&6}K zI6{M2g3_XeY|=_E+`!^?!&ADQy;)zl=!Ql0(^t<%gt|29XQ`$`ON0K5B+7j~IyHvgQ&Cd_&!oB0{#Z?;@^ zo2RI4Cy*QQ@z)R?`LOtm9Y{(ryKTuFvqXpkoDpe9bS={OZzI*a_UW zcY)J!lB`YbH(PLIN^LfeT-hb-ltEERP#qaN|J;b?F|I0yB|!zLc?DtQl{lYo6%|0h zjA${7nBj+$0?23G9f{9LJcP=jg`&c;aQG_6f}x<&3U{S2oX)WIOd=gG-$i~m zw-aq3FagaTqjw_bu3GEa&?M^8&V_iP#h}UCG@Dk}-hviAgpYP?_^HSx*H#syUgJWy zo$}egfD2$rj$4-?{d7agu$sR0z;S@-jEw8%x|xL~#wByFZ_c@M(514Of%7HrMr>Z( zi$hsh*SfTxeFG?seQzQhF8^32*_Q4BItm0}!F(ftp`4^=Pmb}^BljF*2C3d;XLrAL z0;~S@@qF^47+rH7O&8o>ZMYAe7hK70iHL{}i1gW0xpdsEnK3(~vA#LL>h2a4f5eZV z*NCsC7yUZJKZd2K<@1kV5Jh~C9;&`6*jxemza7#(C#2+*ZOJ}}kN65#) z(U@zebn9NQjM)@c@Voz}`F32mf5)n@?*B)L<}=Uu`jmCv$y)mFd;Pa{+1>w_66l5s zaP>H!5dY32f5NB{8}m0GudcIamcH24JtI0l?D=@txmYi0-+QcmqbKS7}^SML;B{8rdIq#!!U*kn#UWV*}>kuZp!RI zKZd6bnVEx;l zq-}-mV>Xmx(HL-dIS6H#huB8@0kuX)_4;2GjE6gCOUNbF#454%`?z2Lg!257P7PHZ zKD0_HiE?WHiw^uE&i4c-`h$X~!iaGlx%l_Xw&39fpzS)4&09i6awRugmk7_XE~7t2 z?N%=-P#@Fm&|;Ty1j>*ar)MpQEn7R5msz9C+0;bi%E3e z7kzU-$QN3claVa+c2usy*&Q|mdFDitoo2=$6Og(5?OiC+qR*vg9%I1D|6YhC8i8l& z3(dehr{m!Oldq4wQr9-W5vr*;Kc{%bE=469qzSf zMRf*^k$p~9!|!WqwbKre1X3fa?c(R?qF_#64EVp$E!R47Vt`r8ai|e=rThXd1NpMY z1UH}43W9{>G5rhr(Mixe9}3IJn4VUUkMniue5=O*rdcamOQ)OD6=Z($4ehR%U0>`f z#;K04ftNN)jHoCxvatV%c3I|iY3gJ<@(M?^g25P4YcdK1uJU2yTUg!Je2l4?Mx20C zd@8%TbM{Zs=lq(gC1UjYBS*exCf zRd8}hDD-zLoaE=ti`OdsO-(So;&Y{&Cjklj4@)AT#)*vV3m?zA7bE87*5?h=-S;sT zMK0tjYPJ10(~NzLhpfCyi?C_Vk^_Uu-}Gic^DmoTtv{?+QAk5$gW^usnp} znfY|*hgEj6Fw``Bl{He4T{={k+xmjM$3^HPp6P-;MhgwO zcfPd4#K^czE%ULPQ0m3~6=C0~ixB`$okKkIzv(x>WJqmgKmmqYK`h`5?^SeW=h+d4 zU%i7l(7re7uZr54S0^RU_2K3xom}eGC%O;eWC7&O6o_t%9WGoFjglpTMkHWb^fTQ@ zXCMh>W{TO_>W}&T!w0_QU7Z0nGohMt2-h0B&sO2E1y=9XaR95aBh@<+9g_HM9mQEd zJ4fAoRP4Q^PT}DvfRo6Zt%|0utb(u#NzG`B8XK1KA(P-?yq7Thn)6NKTZHgv;Kx?j z-g?1`Fw{004vm5oS-^`R5Z-ruPd-B$JYt+>Y+8YBKk>vu=(!XlJuamFLzjAXz zOIHC=ujP|AH#gG^eXQ;5*gXh60}WJ7(GMn`qnbrz%?%KlrP#K4x@|Kol5uhLB!u_F ziX9Q3OSpQq&h&FIc&(^I5`4SF_iGDmA@dhKQx89U@QJ2ZCiO|Rs$>4695u>Opi-R; z6dLPmy0GpUl6^j9U=c-Hz=o2S+8#rHTExvnav=-Pyh};S`Sm58ZyjW!Dc@>*d>k#a zXFlA+iKP@v75>dm@y_(u$MmS!VWn^gGJ$U=znnYR0i!)q*y%A58E<*X#iQ>}Y>{&q z@AG&sf^wIvjQk(+qafrMpp(_XH@L^Pfue+YP{9(XWEU1PU!o}{a?;lfGwrbw<5Q^$ ze!rBb_b7hV0XHKQNl|=ymf)o|Fw%oydVhB{U~jKs|K2S9S&-${_|;^I_ULP(Bgz+Y zdRx!)Fr+>8%+asvWSBM#ZO`!Va3~atju)6&STL$MR?i$V%^CI+{zHnm3&gbAt+GUX zhzkVCw>2}u;~|hz;nnnJ%~{o{i zI|qKW?CK{3de0g5SgaL?8e?R}5_dR^^8H*AQIy^xCbF}?7C6|DL4Ll*gQum}?Ukb@ z7es8P2cO|izJ)NsH%;M4h7R|JUn#3-WeFqdbgC<_KVYY|Y3_E*HFnJX8SyP_J@}Dh z+C;#|(Qf7H>%_T5G3{z&sf+2Z(dbezA43UK?)!lB-y3V6!?Ku2A-qMMtLW*E5T0k( zKU<9qb5e>a6!kzMcV-(Xo<5a4`*|-%tyf-t<|6h#agM`80(6bV>)AC&cJxArqC(uq zn9KdJXjt@_rD+4c&0cME>+Lis&5#UOfRxSl^>t<|2v=Xi?;`-(V_bXwtCNN)Q$XK9 zkY2j8d&{pcU*B=S3OF(9R)<9#KU8T=Z!)8#0#X^9Mj*Fb^Zy5yVq@ub8`3HeBTUP( zc?K%|`c*h%0|EhMWn|>UxE+6GVk%G3mQgWvh?3BI_D}f` zH2g35Fz}tqb==q};tZ9fi-#a?$Rm&cr^M~g#CbQH!Gob|g*R6qA^)f-myEN{EH z{C z5@|3&9v+|+tlQ+=gapWj>7evs{Uo0PK}2_=-`{Z|thY4MGgN;G+&Z{@V&lZWJvY{! ge+Jk87m1D=oTAV;SN>=Gi>*$IZat;^IQ@!a{HB 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 S+fal&TS{DBtVmSf?|%WTv)8%+ literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_Context.png b/website/docs/assets/nuke_tut/nuke_Context.png new file mode 100644 index 0000000000000000000000000000000000000000..65bb288764912c14afe73ebfd1aeb401df0d67be GIT binary patch literal 5480 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 B?4JMt literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_Creator.png b/website/docs/assets/nuke_tut/nuke_Creator.png new file mode 100644 index 0000000000000000000000000000000000000000..3cb7f01666ac8bfebca6c19c0f69c087e3f24f95 GIT binary patch literal 15493 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_Load.png b/website/docs/assets/nuke_tut/nuke_Load.png new file mode 100644 index 0000000000000000000000000000000000000000..2a345dc69f9b79dd48f4c52225e53ebcfebee4d7 GIT binary patch literal 9928 zcmaiabzIb2^Y9`FQc8)W!h#?Y%hD{}EfNCKwZIZfEi5HSNQu&2(k0D`fJh2bOLwDm zN-6zY^*;Bx_r1^aynlQ@pL5Q{nKLtI&dltFJ$|G>Oh7{b004-U6lFC504yrZM{?sD zX60+s`8xpMx*AMd522@~3bAl>;4-sxG>39|I5=V80D!23hm)Cw9TdS}4z-5C#elmF zO+W^ir5I3`UyWPMNd{^IQ}l9yYI!}jD2*kh{{$Ort@h5L5Hy8U~oGmRtPi|?)rzi zFp2~HgkaFQ|Avl$S^XFEpOAl`JJ~oQ99?Z3o&FT+Z(IBc{s$pO-T#@y!_4VFA*-qV zZ*&KTza7IBA?J=!+#g2xn@s=Oz*XDR2@2ALx;nbKSU~06G1_DLrMRDZfjowJK<)Kp zVHl;lVyqzsTmORm!RZ&|r*!siKi%O89ELg1?@fMz^!^PK0}2aZ9Py`<`~p4wkDxzo0r@4L znxiE~Jl0Rc`PuHQ$&7!HD=g;{yZnjtV) zJls5BPHrJiu&_3m8^X&6;TC4+=7Dhkmgg5X=KffkAFBH1W;Vn(acg5^z+)-!~UTDHTxI2DClpz zMKQ9esX-KBt_VjL&%bP^1$F-WcE4^P1H&($f|yzSG=~__$;Hvq%>ruqOMw`{e?wgz ztq@2v7pRmq#+}4~QdU+ljNv>P9{;>23}7x{uHPp7lK^Q0{pno)Rf(X#p5$MWiGuzi z`|pGQC4OM&ey?HT5+=Zb{)}*#$)7P23daOS7fb|1g6M)UQ8o~UvHw5p4gfrec8l&& zk(Qzekw886dZYc4l9RdkNidC~gFY2h3iUY9n6~lchL7RS6W|oC!;64IwQ{+O>nLJ< zJSh#FK`tAkk^CihIIi*afQyx7kkV`zH5Wn`11;Zz7PeXzYp|&yI;tr?GxZ# zQg+F3^tT6Yot^ZQyz?Ht#z+05bbUSeMp!RpJY+Rc0eeAvBeqifo(At_lsXe$sJ}Sl zuY0psTGZKdZthatYri3Ha2`+3^ zWq(30_>e>HjU-W?#oJfC?PC6rA-O49VcT#-n3IN<}|M_9Xv)`lz0Pe*o$x3N^q;030M9_7n&-*p-o`uX9ap{h5 zJt1RDzQeWrx*FOi#>Iaj6q3Imn{rcTJk&xSUJ&WNY)icQ#ouY@C>&fdGO^ss1 z4fFlj;I~C(#p`8`Vzzzj^EyhbU zoKMf69beT`D{Ql09i*0@uiolnJHF+2)Nr+4-tA05C^=L^HnjDEkWgj*>@@S^ljKwh zKlg}2#PZR1uB*0!<)$;w6%5}yHZU_Y|4^Hw4ZCt91Gk&s6lg%sjH0ZhgFj}@C+olu zpH(3?ingp)?B-8{+hc)GYGaoW&!JNv~;P`~k+Zn@9VP@YQSOWQZ-hZvJG<@0=b8)Nedxov(NC^EzHJ(5-Gd>s>>AU;9M(@-T4MuuekE z+c$SRyx0YgK}jEfeP?4s?(!OEVzCk-Az{hAE96zvljvEaGbgbIh24t~mH88i7M^_d z@fA9nyR;?R4=D_pqItVg^GEF*ON}l&)lJSmwYCn?mVZ=mJuv>Z{RD^6-*-S+v1GjL z;&7jRe&kZTx%JaajM2Q^U_SQZN&Qd8_10HaOfII_n<{;v(3DUs&N*9UiV96cmI}av6)CwKZ>btKIB>b~Sdk z**UYz&MpWaT(5SuxC}{|vIc!P_ii{AG;R>yT-hxICT@JDQxjg=Gagp93~trQfnCE1M9&xZJH85uh9io%K>qm5YVvgI^cogjbHdk=VcXyK2R(UP7|3wJUL?%`=k z-$2yXw+(;#K_pW3)ay`%zsbWVn0|7^F0?|O>4mhJwKZ#Omu2;&YN4q+5*c|{j7G~% z>j1Qu)#U5C9_x+l9$uC3Hoq9#ekGQYVshTsgxqPLaCg3FS?LHYZ7gh?tUFm!znbrh ze^QDaQV^ByQuKk>eW|EZfqm5EgBDjj@^qGGfIc-f)xf|&Mn4u;^ zEa*sK_v7JnxPFR zuB-C9mus_mqoY8ve z5u*~T?rUc5ZtL@K!rq+EUn1=m&pOG_$oUd*SodmowyS7+bd(3oB~-*Nk)7dg z@p9eF3i07X)yiCNj~#6TUu;*x%lo^>9~}yhvUDr&8GB=`vT*~Y9k|v2AyOy zmX}K$*Cr=fS6?jbq(!8|F4!Am?upOL^;z+{_2FXoT{`;RakM+_C66Q_B~6u3sNIFT z>j1u(JKLTRAFY2KIo>E9x6mDf1&`H?@rlb!8~BcO5HD+13L#VIk6fH|VtM!_`or^X zrpmTcu_p+?te-Vj7Uoz}p4h{lTx}`rcQ@3FwxN3?@NTB32@O3W&NFx38Kho482M^h zK#bEB(;TX8s3Vx6=ku)=O{VcJuvUWR?i|-H4&_k4?>euw`1@0<_IJl4=b}u&OM?vY zniyqvH|6)FVfuF6LbpnIJHneh67{zE%HR7Ol@8ATI4TeAj~)(4-YPW>zbp~l-JzeH zgRN@!iuJ8#xTiWV3#d8Fo4`42wKl7pxPlDM@EnVV6vqfPIJiLbl;nQnSV|Cjzyt<4RKD|-7m4GK-|iaz+W z(QC5_iQ|Vfo?$H;Rk78)HIz1AdZ{lb0y8;g4O>joaKogt_7++K?FB>x1QOz+^vO@H zFJB+~P@9Mz*Q$IM7|W)ZWQur+VcXG(f3&i&QmdmYp1sgZNdyRFW6o}-B7fsG?^qHv zY43c`+o1Ip5T_IM8jYBVklG&45`@-?EOvR~r>ewfTZlR0OVPxNI-?^+7<1kMYK-qgNTz z8%oXtPuQyYgFDX!CjA&;rNXPZ6r|veP-k@R$m>Mu@>@kZ8txo1IB(d=NMbanOd}M@ zS5>ECT;+4aoF8?M@wbH);IgO&#!&{4vx%^lc;GEE`}m+*G7NIQ76V59U(-FA-E<^p};Q$s$O>`SC0rNilOm zZ{m2~Pp6{Z9`G#$-+$nBIc96yDa?u%t0av!N7oEja6nh>^XwaHGx2ZoJ}udiM+d>e z{Vs=8oOQC}c!YFi^+}`0nBNZ=vZWTz67#bp>3jvftGAQS8M72;O&{!S<{><6-n$>^ z=M8nRcuC8mdv%)GVf1n=g%(#!IhJ?y%gn6JfWGS@mthi#sO_1Q3T#?9wSKNwN7=be zA_0GRVW&y98@;?ggd$oAULGesqJ9pkBIo5Dy#wOyI27@_9Iy2Dvdya@$xy5rivV|y z%J%qVs1Cd6NOhz6jds{YtZ5UVZq=YSTOat;{NM#!bg=xS%xtK5j9A z<>f`qTXjVC*;5qTF;6&SEntgqVG=TJoMSBRwLe|;Zv)BTP-S9Ep5z>b$m0eFQ%Q`K z3Ih{QI^67gT&{(wCi$I%d`+A*brWN~x)xVfRxZxIv&Spf8KNPB_HGme&X_dZQEl9S z?;&}kRISxc5G* z%8JdYe?hA=x)}7C|DG9vv|>h~-Qc7VXKPH9YngpN{nx4hrb*0vx0eQ1tXTbW~ zmP+>?oS=5%z)`sx+vI+7{t5Wx!*+1VO8%l14~~VUersc3(22c2cXruza;v?#jj7F{ zvd==R%9G*RfiW~(Pbnh6^jls0sIwOQxhL++Qo$`@@1fBjPgywMJ<+I!PZFPja!<9i zuH2g4*?Yk%cAan9)p&_Bs`EHK-7-kLM#QfQf&RZy;-Y&*V;Nd{41*oCCXGv)L!tZS9e419w%RR)ZCeeQhg!^+t9s z%??;Y!9T;MLWZe0mEL1@raE(7^upMc=|1^u? zjhK@(b>kQkw}QI#{Y{G+;niY`+o&U%shx!F=+NQf-qkp6Y$Q7ny2__vlp{k|9crKI z&604*Hmdosr-H+pgFu8Ji2NC6Ktauy2EJ9pF2lZhqlYgKHdH?x$H-CU=2BRhW|Tq2 z=aXMgy)B$9S_@{p`!0rqf*>^N`WxYv8&5@nnCROshN8`$Et+kNZcKW4JtS#ydr=iE zbL(vObw1IHeDR|Zt@KCR@9s_)rxkDAoqqLz?D<^}#pI*jiB5~0S_K2^N8!?0P6sP$ zs|Qge+2GEz-KP1EQO6y}cSR3OSEJJ>Xl_X2OnOh|NQgiTsYCB8Wj#(xN}{@VFEcG| z%T0EQcAEWJWsTtLIy%MB#r?gnM_p0_nohFa^4n9Wr(=QEcO~T*l#V;T|75q{8G8Z0 zK{{BZRbW1Wn)b3Mp;_LekF_N+z2t1c#_yu7YF+gDlugjL3Xwf5cWyUm!{i$&kvu*e8H3GkDr_k>3q zqudLm-Rl)}_B z#|LXk#7cC+UZ-G#ag zk$B+8WIL{~vfVFn`&>>>ujX^7fj*XefT@T_Lg_lDo=R3_N$tD2$$ebwQp$g4hd2zs zrICPE?NQCxbXkM=oNC+sxGEh^E@M7feZ}%7bf9?@g}`UyPuSati5((nLDf=tVDx&4Ekn zj$Vq%uf!!PX>vpOhh)0nTDuo)-mVs_rQff{3XSje3O>E+wT;if?W~OKT(ZRj(1YFW zDo%~nrD+<)QW`@FjwFXHccFNkrUm26Q-hiG^jx;Y-p`KVW=81N{-EX3w|qvZ|cu z`AJuEY%|=TbX@*>)h+IPdr2 zvkRqAAiwo+Cczpc_O;}@W<=IXt^(Z+A5AM?vOPvB1Q{h6c>p_%Tuv=jn61+#kT!u) zI|=xnG(t&m=3tBda}bPxON)n5F)1!N^GDdEFYqJb;`ZJe=&{M3|>&}4(8Ce zgX<+!xxUkUXM}vCqfE;|kF1o+k6dBh{+@sZT<6!c4uPqSkgV=Ptex7bBKSBJ@SY=* zGIL$vbDi6c{e(=1>Hh7)w~%ILt$aeHY$Az0Dzohx*mv28o6jwq*2dfNm6iJgaymwH zZG9F~viaV`o98Dg_4tU}k6a_oZ8>L-KRo5J0^H>MmXrwq`24d~_diz#$dWQ^Sg;tK z+luiG3a=y-saRCmLtBalN%KVBo5>l@xc8{rIVxGn4y9b5``CMY=FrbT1J`5G<&T!R zM6DObRiL+%G7I-^RKM#{f;uWal-7x1d|z|>i=m3r&SXiQny3H6nF=A#J8dr$i2A6b z=@eC=70co=1P8H&b36mgw8QlU6A!n?|c&dWvBKBL`Pt!CFT8aW5#Kz6uJUh)7gHF%v;~wYV zt<4ff)#R<8zOgBt#ZRj`@O_A}VJ*l(osC;!lJ#cz3BDjlR;3JH46}`I%m%Uvcl@o! zjuC*lMT$Baph$u+{kbhJbd>e{Kmm@TM&%qR5Ccv-x!nS zkBW@rY^85Jt1cuTX3?>uzWHTJnf)n>MV4ck6M*mGqFNZ4bUf z**4hh@B6US9MtQ;uQPqiE+mOWa*B=^$R`peOby0TAxcwC}Qe<|_&{ev5 z9JtU&!fv#%dm8}XS8pVx0pjST(Z%}h+eAAVbZ*WJhn?mnx~!XsPjGl@wSNf z^;`H3GB@;B9O!!otlZK)g8dJxu9#wCG?VNEihsqhjnZHvL0)qv;q^4m%s+R~f7e zPSF^biT|A4ZEpkyStwxGq#`j~y83yoDDc0DIgc>e9 zI?W*)Uot*Md^TC$Pv;bvN>PpZz>{Y-+03H3Hb(RP{OKbW$V80kbZQ*-P0$T~yf-SQ zzSks0MX$~e$jQk&BkxJ{M&7_0VBh_4fKF^WU*7e*I!l_u8o2L7dX2&Ae+DUl`NA6l zeE0hd8bjv3WI8vEZHRD#50f&T+KPic$?Kw*RT*Qjhya+luo&F3VJ}zHyV?lKd8H=Z zgeevkVT?kaD=a80wk%&3er$78w*2>aZV5`K3Z<|UrAC@S6MlYyu=50b7S~6g?+U(! zRL8x^#CLAv!Bm;0=_Ky8dczCC2BQ(R=oXHzNGWB!bc|_aNnnaY=LVd4f-d|idGC`; z6!0-MGRlDJ^`4L}&uYBmwCqgiD$-v&n(TnQl;2gv^?VJtIxhHn+Su#xYfOo^8`(nD zqS_Snd(|=NaILk%47VJ+MXbP_rPI##%1{0UAC5eR4@5@#r*Y~LZMN__ifCHBr${Q z3|8DgjH0Qc0io1CTfEPs{^<4o=YhZWjUpr~c~j5@ublsEcNR;f2XjH@ee$AVPovq% z>JdE@j`Ui~A`=^CtQetCJ2Nvgx=!;Q@b=nl7A{j(7?)}iOYEgy z3$e-?iCuI>eacXtSdWP!WsNN(Xdk%58uYYa&uchdK%JFJh-@E4t~wPS8pk8sEL}2a z%KR|;^RvPCNFj3ILc)N&(ICR1zqJEfhm&8Uc}Lx}mG62Y@m;or24h}nRb9!4O6Jx6 z&X1IZ3Gvy&T>Zdgd{kz}UN+gOOZU((-V{Gm5^>{~nH0wBQtheYuufY@6Y=FtfIiJP zU6k6(TCS6jVqXp_b45Q1Tnw_SU+So13g;eW<_3b%)E53!&h$Ix*(f+dqDW;#(^kc) z$YafE#N9{(;n+WP-65&$c}>*wlQ0tI1L4dM%#0h~!?1CgILYEQU$5GJb{a7@dZln= z+CQ}NAP;qXq4_~ZKe_BVU*#?nS@}JYnz-(UMM0{F$J*w%??Xqphil4g>T9j>ec}2q z65g4_j7>R@5rU1dV0n>QD;S-Y8Y!flZ52E(oRyCf2*_Fqqmk_ZkOwowe@sO>9f%;_ zTwljsVFe)Yd)5D9ZU3=UVp>{S{2J`%+T36;Y08|wqK3jXfDbnc`#=4>_wzcM4g`(N z2-;yeRGp=N99dK6akby}JUSyW@$%g^VGae9JC zD<~m~ItEAIi80-k+v*7A8dCj+W#otfZm4DJgKGsjbA!3xtI`*=@;>8>LuZK3EF$=E zC*Iw%P%YszY(+b@zATxucwIucIpD*wba(UxslKA{*`&eoEAe(F*mxSV2bt$UPw{2% zSno|5t|&~#6+sGHN>eg$L5J2@1e$NAv^5&s$20_#30uv^tx;@ITgK4NFaR-i>*B_tPuvghI@ljTQH+xfCH#q|z@kpBk7ZkF$ zR?H^dgvLLPeJ;i=rJznzQi_0;)(8l(=F!uG&Bo~9;U89Gx-HSfB-X8tR-=ABI#}r; zHN}HPc6%U?#=y+TvPx%PKU;?JC$(rW7Z`3E#5xks{P(xw6b2Qg(H0ssI2 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_NukeColor.png b/website/docs/assets/nuke_tut/nuke_NukeColor.png new file mode 100644 index 0000000000000000000000000000000000000000..5c4f9a15e0253d59de517113cd366193f5f07a76 GIT binary patch literal 42347 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 HtGE6OI0r}; literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_Publish.png b/website/docs/assets/nuke_tut/nuke_Publish.png new file mode 100644 index 0000000000000000000000000000000000000000..b53b6cc06c14c7871fcf5dea12834778c453aa42 GIT binary patch literal 9940 zcma)C1z6PEwjU5=K!gE7MG%HzkZzEW9J;$pVt}Dz=yVv65&=PJq(ccwX+%J}lrBX= z1Vl<{-yb~poO|Al_xQfq-|W3&uf5i9tsO(8hMN3UqFY2D5a_C+f{YdjgmV-4K)|@b z$WrS`EeM3KfzmNR8>p&?Sh+ZHT3EYSA~}7WTmd==Br4(KYGLJoL_;l+wkT&Y*j8gJ z42rTAgXs&Xa;v&ZBkfQMe(p$ZKQ$dIKL;yeYnX&Mk*JRdpuh= zCMOK;%>{uBP=Y;#m&pb%gX_9aCrDSqb+X+N4cU=F3!+% zofeiZo@g-`44^~*P;PDYr*2nIcgG9O)>d3dN2C+d8STNv!^y+-&-_Rq)L+&(d;H-p zz;RsX6o8%kZ|rE4&3|A&r~C`MtDOtl#lz0U^-rVzHpQRhe-Q-S{qIG5EL{H`v#RQU zXLoY?+cG@RvR;7W{s_X~Z2HFp9y-3RNG>g;hl{7X6;jp<@E*g33IvU8J z0H=BY(GY|23UU80s^LFV1$qCXO1n6^xN8G2M2f+9E{4y2C!*+NVT;s9S)=X#()JfY z1?g;iLHR?|1?Aj2N6&LO=sKf-_57aXf@JU)O$;V{7XacpNG?c*|CaP;EJQBsQ+2Th z%;UR|`irC}Ev@11VuNx7XdYVf2&kg0^j#j|yLUNwIe#w&2%HEYi?Z>Ru|NY{yxhDz z9Na=2JiC7i&)|r1gal0o#95JzQ+i z-WKjiDO&(hVlXKi8x#;bU#P};sz7--g*ksm@lOG7JLEaO{-Ync{&^+;uuPQeUu^%q z@P8^HfZXpfpfmwBj_c1N2Xy`{lSpTvHo5~v@Ugu?15ju~Q2+$~CG;Q=bG&DKpE5#< z;-N&1hTn6Y=<6Kt65a4y3Qk5hky14pp=Q+0hu{E{Z@RGQ%T8gKBGq!)Q~a8%Mg&rt zcq5#4rXLCxy__#AeIfQ3eCM)4V-s{~+)l3N>s7Pv6(S=c3iQwb*Xzw8A-lJopxWe| z7w_Y@nLXdVyM3K+!KdGBe`t(mppVcLCtBJ^MzaR<9sB)*y_A5j@oQ0Z)re#c?tJF|gl`!^lRcP|VS9Skjeg~g^+G!GjeA0|ANalQ+yH`co$ zVu8SOf(@Vafcaw$v`dG!&cRvybRm%U~K6P!s_y}Q4{ z=IGagzL@YAE^ik?#OEg#P5YR@D=o_5+rD-;7v%5zW1~&-#e+f8TRG2IjfTX91V3(6 z=5%_047esj_aXWEagt-@%N}@BrsTS4wYl_r%c-i?z}PMN22OKN{6_y_rqv8(2_tRW zk3xf|>A-$Q=&E4o0RoY9oPThV*hr{BAi6X~87Uo~^vw)&NBFzI)!-#!VUsgM&Kj6L zBL}04b3LyhTPLS+Y{IP8>P@Ldwo4D$Ni=>iK6p}lzdxEBZ@aJI65Q&oB(4NASmeQ| zWL#+cl56Bgf96@1hhowueGoN&r0mlH0~5pfmpRkR8|*cF((X0~w!6c@1&xLN1-1o+ zg=r^&v=J7nr|aL2_RC8Of){A(_tJh*Lh#lek63tvq)0B=DS*jw{$n$h)w27<;wO%j z7O5W`FEZq%%h&O7z5dgPboY7{FqIcqTrQ@S9;dC|V|8&ue#xXYC~(qLG8Q6KBOM2p zlt%o~fK3>LQ(6z&udwZ|NH~-SoF>&Ur+SUuGOYpg;)zg??&PyQqcuC-z0taJBzF~* z8MwVwb~0Vpa@Oi1H*(bV>*#CruT3%TlGDsK^PKJWgeSeH5Aq|Yp%r1kN%UVu$YH~C?e(>upRtBqqLL$TR ztF8za1-t?%+*Q^OMEpzGqy&-Z)5D z!A0E9-bj4V&&-@DYD`OM^>Ooca@uVEb*L_3a(2k{b87}wezxiD?6~3e9)cl$0CC7W zZJyrPHuRpJv@Sh!0e!Fa`7F+E_PWC0)OB;ON||6`%G@k`d08T8Eg@;Qbjo<4{xs5N zaLQj|>frI?SeFwDwM*;)XAZ%qzCb}JJ3E{xo6pKpQO6!Yol``|zSkhA1E3&TyhM;ba^yZ3> z$B!HiF0W>qHgT^X9KAda4Lyu*ZNUYiU~2GPhq{s;j|$g9ObetYaqaf`koBrZ!8>)i6X5hp#^^MvUU z>6BdVh&TZT+LEZmI1_+-l~5`!c5)^3LEt|!J6v@v@r+bZ#2p$a%XGO42=wh1uua#e7!wAn-@6kL)C5wUJ@xs$q`!U*JjzLoU1YH? z*DKvzRukX;;?wNg;a_9#Vr^j85iAcZ$!jtc}c zj7Dk$pH^NjVtH{#10>6LzI{FFCt{3+aZstWE^6z(xWSl6)-zEk;p=R3EAy!TU1kF2 z%JUcnPHT;USm%erO7I{O5)xWb9}pjLXJ=e`X zKFSz2_s!WmqTZTo_Ld*Fp-U?jpgr0vq-{71Ut3vOd++gczO{-^dN6~!^|-liJgSpO zbZ7gDcAeSjyx(E&dFNeMKD$6cDpk}|51La_wxP^DJx`3dU{%mQ!A@=t|E<|s8ZqA= z9id<4=}sI@KOj!KNXK1tEo>YeZQNF*JZ&3OGrR?y_`Q3lHVJvfv`Y1c0~sSTGBW1o z=0-+F9Pg%On%+a`M+BH|&`{6J;DL7bJO;x2c=!Yb$*M=8*zT;B;EhRVO}(FaiPt=A zvr_8=9h^iPts3h2)YDV5T1Y|E-hxc`<$G13t5wD|hgK`AvdJkaMIw9(akk=TlezDy z`NVYt4T>R0!j<#~Xx9Cm|pp!N>D?<1Qq* zoo?`niU={v289gk9{c6(?PpVG@De9G2J`wzEIAF7d{0^(olI-f#bi&NwldIETVF%! zk>XgYRpY#M{AeTD_m2I&0%7q<@4(tu*RqY~#qQq4&E?P?8`n2a+TDH3#UnQ$OeOsO zkhwIKPIPi^$c8V|nIY2V`^-Yp47vD^Nof#B^6B84&Bl9kx{}PsDTcwi0sLZB#_UE6 zUb(~`6)gGKNa0DRNke17#1~1u?XZ+;ca24*6hk9elKnGNTqstjPlH-7mUOWv1z)!Kr&&NQXpBTqOtH*@Z3EH_1 z`N>~i*>ZGr!1Zf%b&{#=%Nvs)Px)leb{)J)V>iEr2~%EC9}Qy~a5iba%GdaF!uZ8> z{LTg!k4^2_xAh#6r!@!)w)OQos?&ZC+x(9?PTFJpTMIsp%QqyvFN0W(;-H#?f{*4m)?z4={tWZ^Xy4As9 z`P$$LFON&XZc60&S}1@2duanJ?)9&Ui}_k4cD-v>mA~|E2OBrm)lgZ-2O^m7nO+wZ zk3jL?6Ku_YeIzbqTdRPpP(Y6(6c7`-5-6c_b$6;4ByZeL3KKdt(qF}iyi`cfXg^U_ z%W{0u^Qu%E}&-qa)x@l6^Z{lqbTWN>(>Ae@CUp8^X4 zA;1tN!qw4XtuH_6$L=5=cfiT=m@K*B^mzzy2%Zw*gm6BdByKhZmecfGN9E;q9ek{H z3@%t~D6NL9poIAK@(0}QU9VWGoN9Yle$IOnQBa%?h#Y+wD2#mY0fZ$iktiRUXCaR* zCi)cCBv7Jj6Km5B9$NJTfuJ^%cP&bkeCEAJ@O;zuSTMZY~c1;P}d-DYuEz#UiKlVIIsgv&1>qDsmOK}c_9)43> zOljcJWfn%{N_!Z$TxcyFe6=BSB71efkseQXqY_xKz%Rg0SzNB#yveMob8^GBc-wIq zsc8-^J}D5`!pXznNR#*4MD$cuF)~VuU>+S+I2VPau7DLKd@o>Txn+2_AqQo|b&QMP za@DO_^+F7V!ZDL92{(vdBM^SpOOFKSxy7ljQ6Jq)-C^-LJ6zpH_Jc<=wTi1-TU|sQ z1l?Cuv{k$V?49jtZW*T~P2=1m$5g*VDSRuw4Fbie4@i~rrAXyR}Z zqT0mq6h;2y)qx6BR2$c0eTRwZ;hZ3xUPIPuxU(-Hmv!b9tvt-)Yu3rU3pbIar&!rD znr|B$v#r6cX5|z1H805qRYFtMMhr3fgLQ*;+>$$_ooGFK7 zLYx*W`gY=Q6sUAxL$*exy%&ymg; zBDoJo-f<%NkS8Ifk@QUllN%r?wtd#yOa1hX!eG7)GimkgHI|3~GeMWqM8Q&-FAWv9 zNs802yw`YWF(vRyWpnyd+}w_S|Bj4e&8!thi9T=y_VDt@l-mZYvk6(1coH~xt13ME z=s=YI{lb2OUrY;aT4 zE3G@@8_zv^q?j482CR2M(3$G@1(|okWy4J1o^zjq)We?L43Z~L5B=#$uCwjijLouq zn&SM_y`N`et}8^VsAO4w#^F=r18VZ#o8J-h1#J&_hEOEht%7oCTC5GVIT{4yeg@$T z*YkDFx~~+7eA5FVhLl0-g@F`RAasTi?CB4dr1UGQBm8e~93~-72wScXYESQ*rfRYJ z%-1-tB66#RuX&qSwnYIe^z45R1hAnhH|}c9J<4Wo#Hn6Fz7`>aHCdk6AlNWK1}p^w zii;HNeuv_u?iwCNE)kbx2)BMMYgbR~`{Z z>*kyH+_cfB5eqF|+FN`7+TkHIo@dW?69Vs?Z8`n)=8b}vBN3q4&(?9HqbJ4BcY_~D zyuCrr4JYSuFFD4G&g84lg?IS8`x;rM9owD>UX2bKJfUy3Z}e{*w)?7+X|-kz4#L$C;2*t%t zb*(;b;z;fqU+ui8f=WzlcO*AgQ^qbtz1E8UaFMAP$s8^dQDL{Ts$|7Ng*pw=>$%h*tdLfdY8|q3w1J4QcUrchQ(;Uc%v78jV1qd zY8%Z#l(we?Ogbu8Z!$kS%YHq2TV=Co^g|WI&y0(r?Ro~pFH;$`#^s(f({N`lx(8Yc zH$B07?pd9;*kC0JxFv#*mPJyu?>im@;!tW73?lIpnffWInel(4RoVPq*60zt_ zFNtaL6nX(#)Ccv6pO3c)*c%~vq_mO+={TjLZ>SqhGP*!=KENmd!{5_z51mx;LAAs0Fo z__v}s>HaqzZ+;wZTKsHRHV^;cllPzn9GL|$-zvQI_FGtV3vqE!1FfzEB#-TDbk8mS zK*zf=ZD-r3<4dpZS$(Gd{Bidx2CaS>;BLIqm}%`1EX3o#*i1co7Bi&wbFypl!#(Z5 zeX!JQj8lx0Sj}=xKG|f}{E!Z>^w-2^^Mzh5tqrjYYqn_%`pj?D92s)mx+(954}9-b z4|q5B#s}k7pA%I1##U~1eEQ3&+dQXbX1o{N=mdVh8XlAnQNI~h?TNe5y<;#fYozet zK#^5mc{Y1R&sx|mU20;gx)67iVf0h?N;2nbH9twIjyuRjCX(BjylYkTWL-rcMI?u- zQ!w0c!J-@%l>JCeYYpP+t>W7^Fn+8h_S(|-mlMZ3K`I2 z+nGN#F-9jQu%_PGYvRO~`4cGt=Z8_Z8_K-W6cU6IM&vTH4td|A6bXmPS6x_bHDuo> zQgp(Y9-t$#^4y)pI|}VMIEWX#FXQHaFb@mk*3AY%_X1klkkqnQKPaGZI@K?CR=}h2 zSD)fNR36J?39yLDsfLC@OOh0K5O0PY=yCF-PT)yk7J*tVYmR zrPvdMCoi)EB_uN4je9G*_)d2#od4@j=lnde*;pD9z^!%+Qf`&a z^u`4w-0KnqejM@UZO?q3@;?}qfICyR?OoCDnZ!VHOfmdc^M;Jb_IUc)vc;PN+k5ae?7Fri%jeo5Qtym3D|zJbS;!# zQ`RGl!{?X0WjMF4M~;S_uFTri5<8q5f{v%C+Z@|IaF)o=n|;>9F8*?u?5F~*Pf;|L z<*NEM2Une?Ph<|{lf%lGl5!c#z=kA5xh6NTT0O$awnLt2mAJY^Ihqxm;glR}X?)PB z_~hREeK-k3+fSa4B-kw;oyr4eG$P63kN6ciy&G{>R(ot8Mk(zzdAA1?DU*n_4Wn37 z-5QE>$erzTAqAJ2i)d6wVL^)kVpJU^@`wgga(E#P=*!g&0gZvVJKDUcfhqE*s5`j% z13&vhCFv8*Zsr^i;>AA2yS8wRFu|jz=wocRYl)p2l7r)*>xMYfb6O?tHHZX{dv(lH z>uak&>J~tC_|zht8pa+pE35{>G1t z7t#~fO$0nO@)B3@)V|=~qXc0y=@b^UiK4QIBj!8R6YzEU@3=iAiM&>LpjeVM?Nw$N z`k@g5Cn1Rjc8f`>JenIA8pYu_2pspK5plWe=;WJkNw(C@E3o{lY48%)~&|Qcf?h%=}%}X;S<6xOS>y2hLD+Vj3g7jtAMpT z?$WF1Qu;E#9gPd3YNGc|a-i8!Gn-@oB3B*xnlV?1FngmjlFD~}kHgf?TX9Nf5zGY- zPqmQ8yrE85GO?GltMczSShK0Q=9CjfDKSqIquRq9GT3XzIwrEb(S?drWxmZlI2qhIZIonmjE@iJ?8Tf%9 ziX{6$e2={2mc6qPW>E%!c#%q`OxdMu??H`#y>#d`vHnHH=xpx56yFNE>@$ zr|pB}Qh&u+-T1N}xC&&a0k-DiL}hh=?Za<#l02f(XO#r3#{r(S*T~Kfvk@1Y&|mM> z|M8npZ}?Imr&v^aHyI4jQp2UUS1SgH!HU65{Oa8wa*<3A=*@R#Nv&;&+)~G zuSNX9x1iJ(*P9bnz|!?olNJwi5GS&ItqvS%&Zwu07+SqzLXMCGHuU;9U_)my=CAA5 z%TT&gDz;`q>Mrw#?SnVTucOP!%J}%FdjBR4s0n3w(9ADF50B-=rQZEfCJVZxCBhE(DC0venF+t zlSb5&!}ZQ_Blx{9RZ9dQvg|KoRr+1K(^-N;l4Yq08Rc zKg-_9{K=%Dj+^*xuFm%+-PW>88jPu*5n8fJ9qM_B9W?zT48|Q=M%P`nZs{$3H!=EN z0CmRDhylxi2@wos-qw&sW?)qj-AM-#`L9Vsrt=-SkPdl5>jWjt{NG>4Nvqm-sKdFI z@Tv&MY!Y;0+;&Nn`}inJZlUbixYar zeu=h+@@nZ?9QS3-@MthuXrle7GOTEZ!eWHtL@Zg^6%YhrK|)xn_aOec3}{#Ovz}Xf zvNQ+1jPaX2_94U_8ZC}8=KH%4u(qz8Z?s&LP1ytm-3TQ-spHj@Br`sJOc>s(WO4IN zH=-B_+nnHt%ByJYuIDV~kiOarQ(GDz$)jkq&RmILjBT1!h2J zXt5gMF{zAD@(te31#7Dk(?)72f!#VHl!Ga+u^?X@MKWo+a|+1SKX3WQj5e0q;~W~j zL@%QsR}Be0Ahc?`;+uv`(A9L@LGmD;BoYCZ_85D2JIvuljSk%oJAHE^@JoX7t0*+N zsnlpkn;7~25lK&dh4TRiE(0WSQpS~b$!5BiQe|t0eQ?MrEzYFQAwla_ub+4hn}QLQ zWl?>3n}Y3F{11_17N}hvnIiR|#;yTAj3Iw;&1Jt#+yqt8Ew1>wwIy4DWg$UjzlGN< zH@kqv%A@Uav@v1B?c)n$c8ZW~tfr9KL;aDwUk#5wYSXuY#Th>kA2VR9FHeR)gurBP zaawra{M4zs$s;`$hGr4J?)vzv6nLO8Q!4i910Jjo92NCMNFC;BZ|FPp(!p!k{Xrf? ziynBC%Ezv!YvDq#Qc;*TL>bp9{eGLeH6k@I4M62`Q(Ghgr>KDH)r*%k`eylMOXd#T z9Z{kgNlhA{cAdiNWOP`;%a_l+CTz<{p<1)aq~$s2;3%h){F*8Lu_%ZZnvyiZcv5LW3sECxY+Bf zo97~ZIwWeU3N1(Iu^P;jAT3ZxVjJ@l9q`=Hvjgdy@wj$3v^r6VLkU{s;~A1EjZO5> zmAcwBC2*;#*nIjxq<>{(@NpWijSH>;xemY2&PR$$qW(Mic~OX%wQ34nW`UVFEv+v0 zQ?`}dE#!Dxz4I56htn^wYJF7_!FYWCwy)iJ(n=>)!HuwM{;);E5oyEh0RMa)E@WF0 z&xx4}(e7sDPNP8Z+GM!0Fw$eG^*XQMYm|=P>xl)R!j@X#dHvmv?pGz8gd01U;@CbM z=w1`LFUC{M_1=|iMcdO&vHC4LMUiuKi(vqulk@zqr=|yuUin%qgF#R*hB(SL)7J+0 zbYC@+D8fQtYEfsmvm#gsuHs51}_ow4=92CXU=??HRU>yxBW`0-K>-9lR~~?XH5g(*>}qXgn;75GoSJTS;`6%#M8Q7Tele*FygG~&atRSQUif)Eh=8k4!<_1=_rt(gfU_Qp@Gh}7{ z4^n?x|D?so__vAp07m8HcqGjoA+`=~|1q};*#7Tx|2c}p#D5fw$H3@$Z}>^=9Bhr9 zjKIc!qzSCzFQ}uf3B<*~0W53^=oUYzu!)H|u#0ZQiqD6Jn3;i_;jjJt*8mqYFhJ~o zmnh?ZuH>Jb@iG1v?|&`)Ka2@5?yo+;NCDQ4@n7Z+X#C4yf^7gR=>V8Ry0YbP!0ZN? z1IqAUN&o^;#W=+b$%qJ}2Mg9Kx@W6~VbYVAsC~kfwAR7~3)d?K=;5{9z5_R zk8_h~w8jLuIM)tpa(i6XJkfZFrjs%660B;<2FSl=3hndN>5EPo&ewK$MIMv1Fz7qI zZ1otY+|G~@)KW2B;~ITP2Rs|RousBC2!#Co`4=jI78wulhaf32VO7`kgEkKc*__w* zO=%aNr%Tv!tTs|&b}*RR++!n=Gxk$ZZC~I4d2QV3dqWCVT}TZA)1>{^D-B9ybt{N` zpbcYRBsy;e@P3nva&gF#)k7JmNX@H z;RV46qvXeBmyyBwU!YU%1Ks>D2YCRXf?rJ}G2Enzh9y4U{o&^sZlPGASQsf(AxUCC zaz846DrtYJP;sizSdDlXrNO5i&0oKMMfOAx!}%vGTQ4eVD%@4Q%+b2V?sr#v4Iu_p`0O#PlGp}lJc#QP=!%o%;UAfm||gKVTgqp z>TkAuZaP*Un3EQ@VEwCpq{9e<44QY3qE=2Vp-MfB`?|z|0a;j?UKPC)tM}7nyqE)4 zJHtMAN2~O0b!9Tw{7jFfM0ejIu7yE7mIFK>$yjnI`$s{a^Db`Z`;aHD+sCNr55ZJY zB1)1vo{l#PFJ{|+F8Z)jNf9TQp3qzio*rxyDL%H>oosAB-PV|sJ@=3u??fe{8l2JU z-zOx=4W=fF06cuQSkTozhvzv6?9fA>x)URvfPVu^$QR$JfmC?qOT&+Fox(bN9y%l&oCKqcSZ65^P^ zb%Fw6Ii153+L*w(wnTglpP{mAi@_4GeIoQZFQl zLqLHSBwN#rdb;hmw{Jjevv*IMhaH6I;!^QK=$ixX^pW0YF|Z6z-P;WMEf>Fr7?)h0 zrUX54K!i=lk4bu-8>mZf>>y|Q58c}Wm;D7~ar!sEI-bgjQp=NlE@B3T#_qS`7%eT; zr2nvd8w1;ZJ}gsy0rDs2mAGCiJuuiSE0-abD%hTdqPrG+d*k!e=mA#Q^nf zGbo5iz~SjFD6dT4dwa4-@F~GX@TvLu%l>(R@}H4{4%2n${=~~L2#wSW9{IhzUq+N| zt#$ol%g{i%cjMO5%-2xD(d6)AQn6xR1hhE?K<8&&VNrai19?enF?p5u#XphvCh{ae zdd#J}XI!C=V_#1WCXycSV~Tu!tihAQ`9s)yJ@Tz-Jj&~xlt4=HNptUnQC>m? z=E8xFl%lNHvRTxygwZXBXXTnMx_x}_2vfek(R<7(*^@Ya+ED`el3lO*Y-}c#%3Kv3 zdphZY4s87Cy*?k$FT2WDZM~H1(KAKwVBn1-_efOWPV|Okga60z5JO@`= z3#qMJscDHBLPSi;@_3PXGjc}vI9YK0aEF!2j{stPxFOq;*&uqjN6cy`0)h0rZ&i{& zHr6$2Fv8I+y!W2&?B16~SxhTr3HpzFeF75;ff>J-SAFVIcQZ~HAEj5W#pi{x1IeL$ z$)7;=bi20Ri?>%kHdQmQ0uM_Mm1`mhfbrTce()ueB>fyqE*blUu0e8-Us=k+bGaav zEG|wWQ(H(*0tIINm4yxA2^yx=V0-u_#yysxMRs#u$7PQxMU;+~>)E;7(-Qi#J@N(Y z5nvYkGohDbo=qr1yc57_<_js=;|riXv{VQoqCQ zr^uld)qa~0X$&!3mqmTcvQ6xFS|*rX213u1PAd+N$NL+@>L2p_HjQ^{odHC5Kl;|Nye&Dp>uF>=rANr?JgbKp};7exbn?gT^%;A+H{macrxURUJ5r|&T6l^ zZD&}_+nLN+8ndM6Ff=YI0p`SR(Ymp`qQbeV%W|$_*?G~Loh7^Mns^tYh2FCq`9c^4 z1WFs3-C78X^b|O@o@0-St5^v>OwXGP}zslvF+B)MC}IZd@u1F#UF<#z+MtG$jrXM=Ni`% zP8yn-muz?w)hCMb-A2L>a5kMB0`TXTl|IWCTgO@V16k8tbzY|s%+}?{jW~shvNF%} zwAJrwWH7=lu%6$l#m{c;i&cs;-S?LZ=G7 zHut^h;`P2L!P}j@CfopGNmk=owA@mPB>lTe#=eR=MfO2=?d%#dxJlfU>lGchhxhe) zw(VSZ_fc_PUcuU5aKLT4it}$_%#lbvCRFEFJ?*c0IDeinKUIHFB>=afm6eyb0n+Y^ zB0ApRUp&+R7Ab?B&?YAb8-IC40{3e|f~ajhl2<;1Cqb6Ng%=-i4cKB=ziuWU8Rpub zeCmsLeQQ=IY6^)2z3-=sRn#l>mY;WZK8}lh?&hI!bv3dOdeEIWyAa?oD7&l?&H~WK zW%x(@VsXh>d^|i>3m$Q$dQSDBO$WX)TU$Fg{{th%S#2?tKt(EJ*{5Ms0Tqw^@5U@Q z-_aDTv~>V5KWgTwf zv>HF$tv)^GMG0sf?yN@%>K%@^9=Eshza;_e7b~!k$*1deOS|8r9JC7znonsRH)-m6 zZas(2oO)vDJ=ZH9jApr)B;rCos|1=+jT>+Lp}pXnsuY75Q?$O zH&}cR6o2B>7I_hr{3IdGV__WzEU90-VZ^$^EGY*EY(t06b1OwKg74Xns29~Ym%&#Qx(=eBG36Q=#cN_;avGr-n#aSfT~r?Q;Xg5!HTwJ2X3iIi`VR|8gen6&8F*)jO+5L}XHl1EJOs zvD}y7zIrEPAHUQ{wy+({xN8bOnCq8~Ml(v-H2Ftzhsn+jOosN)D^m{Zns@)Za9lzReD+b6+(rJG_fZW8e*o0!zzJY66D)}7#bpmVIj7x zH&eh`v6`48NnP%41);K_nyeqsxpGf25n)rKR2T_~n%gg5{On@rY+xX6V&I%B4P|R* z04CX)x%WE&_tQ^4G-%I4Zr*_;oz44Rm^pW~KVYHq9m6|mp}V|t#~=Ou;N7uGlzh!( z(k029(|f5LmttsA_{*SU_4rL+kqLH*UqiUKFS59)Z^m{hCszac98RPHv+fh7oq^64YN zZq3tNay{y9alM5*#h+94iArvHaC2!{J*bjRxtk;0viU9-S*Ty={2=@x?`d9B*&9nW47@V2rbdL`LX%>^-y$`#^4;ttS9K1jX3yF9z^d{(ue^`- zJ=974Vs0-Vg*a}HsNdv`&W~eQZk<|}Ktoaz>uHFjmlSi1q?uY|L?OxA^|=spO#f^B z+gjC}YFoEB_g}K$CkPA!zON!1s-IAHb>}M(S}mA5BUx7y%C~2CeJbozxLzd|<~C$^ zW$oZV%*x%tfq4r6C+ot=%a7vVoxU;+VB&Q1WZptzpc{ULMTtde60p)UuimDUo*Y6VoZd}B^!l*YN6Ez2ytD`GYyLc*u* zo5p6wnNq(zb#JR~ZtlSbeaGD1GF*YLed%)iJrHX{v#nI&Xx8)hfl z(BU{Q!9B7w-cyLt(7|+uP9}JL0>`9&bR#^p)_b-nCF%Tp>zpApq<>JA9vb9Ih42B& zSNoU9Yf<}BfmCe-^^p&GG?5vm*a{gp)j3>5S9hBpy)8Ig9k{{OXG4SRSmzBEn8i>% zVkT>{zO-#ra;el5EK?dt);U+@I6C!@<~TPZj3`^bDG++L0I9}H-Tk`AA7E>XXT}VG zuf)n`$B7&<-~P4oQ6_=7HIPq%(lP(8?}%Xz@?&6owz+Dnm!Jc;hzhLfXEMktkr@Sz z^EVOEuv)Y3Y&Qc1O)@z?3aJ0t}XU+m9=-$P85+Q*@?u?zV&D?+Mm zEvNGiv#!jfrqW0nny}AbnVM35)dQWlx9Kqo(P5sy3P1x|@-P7fI8Ejg*(@>F?rnf+ z&t|J-v_oI&)%()t96>733Yxe3F@a9fV6jLz<_?Q5P98@ zt=>TQUtxuvwAp&^bGGAp9kGH=CL7z|UK`(qiL;*zK28kxwVy8$nF`))cHA#BK6Scm zSfM=4nhM^{Ic?CTg+HnPo^~7y0%G^uGs>qkTp_`4^~a9{H#0tRUi-Ckp*@c@^Fph$ zL)Si+rB5y73#+#Wrj-~u`g2Ryk9RUzHvv12S#G;Sy^8ET`T&JhnyZe2f%`VW+cSxc z;Zs|$E5=F;{-{}78FjINHF;aD+MdVUYDT_}%bBt9ZXv;I#ueQz5C;>U8^ zwmNv5W==MpyCK2bS#_D?p(KFw)@~NuEULJ? z?p-tJs+$L-k~1=NH5B#a6-Mbt<>b3X0H1*gG7OuVa%2YO@$_vK7PZyZBGMMPZ($Zu zZewC%e((o{O~p-B_w@AqI5-##y=f7sN!vK;hv(?5-l8I8@ENDmPOR|4{`7p)xu{R_ zSsHC%iB%gU=--q$^Or$6%>9VweB4-i&3FG}XOc5<_F^}$y^rwe8=gKRL2)9IdDf@9 z4=^&FB3n{2y@nkIF68)P_QBo}ZP4Y!ZO~A7Pv@WDav7SB4YM9{y-Y>Sq_faM#P$IN zq1bxw*c{M&uSnQ(-(q}``39e7%qve0r(^r4%9~XcZ@0&rqD;Y6y$AwpOVdyYGAc|k zF;`%h5QzsBBI*y$gUEg4zSICZ$SyhDe3T^9#kFYRuC5Gi4TEOE^n~iI*KS)?-td>> z>OQaFwfl30dU}oRET6q%`M^3k>qFTX>j2+-I|0G;)fVfahlR%8idryToHADyM1(#=ucKlE?9+#+K>cs|6i+eI}i7DIRm0 z7ndZM)$OFuUrlN^Xd`I~9Gwk6FsH)_yZkicHXv2lYSyygU&z@Ar*gn( zyfb-e{8##v=69?Yz0vIRIe2$4`FzFG6X+;wFR(!5aRZ~%;RjwYNyPYX&m_)9!{jNC zmoC{8GpZ)~BTYDu7nTr986=O1xq|SBT9|*I3vX++3hu?hnXhL?-1~jysP52z`uf)Y z8zZ!RAU2O+ew#A-ZQ%XL&9GT-Y%NzM4?z+^EA~fS+}KO>1Ze`fnUVLHk}`va)EfQx zQd^c4Y9x2q1Ee9&N|4X~awxYj6CWHmV*^K8IfDdpqK++#CeRZm&3XJ)$$W^jaAhvf z3v9h!aQK){r8XdcWhcHs)SgZhlOB~>ZuJrJ(p+uwAmdC`(bsEs5Kr=xjZ37(9Kr|# z>vmkr@y@;N_x|<*(_E4!91|fg2sdnbASS=<mXa`0kCsbIO# zP~b^rG-@zGAkkmxb(%QzbQe*i=?4&D2ljQxFz$9`$swS4pEW@??JN zHQk9-@1owlY#gZLfAC{Vp(szg#CkI`7O+Men_PHb$mQFLIYp9_!1I=llkTc%`Js?kFd7T_UuKbijf*?k@wTo6uTZ>jBGkgGpjYQbi8I&;pQStPX zYM(20ngujG1sExT%!5FL_*Q?PGVE4~CPsOyVHg=!C1)S3#;BdLmm$ku>r@L>M!PdJ zpMu+_5VK5r_h_{zO6|185DrI55}R_=HeV4XYX_H-CMUpj#d(mhzuR$D-+1YHIEQO) zZ^!`$gy3;gVI`gGr|WTHD<91-rB+E8r~nP)mRxlO9b2l4Dhf)2LzgyD;5tu@|r{JQ35pwKg;YDcpbv(+DnJr zes%>RVqHifu{U#Z>eg6go%pL)V^V2{^k8CGO7<8H^D_U8pavU;g;i%ay3adoN{)T> z8Oyvh4r&jz)Pv8zr+3@#xjObtbxrzUj}C~BUU-bwp7BnFuVXS+_V*MHLgY#?OVPsvg zfOwKv;d@(HT;H}K6LafiprHAK|LxC+1B!+w>zxdeY6Wr!vFp1PHM%Z1nBI=#fTgGkLDob1MZL61A3CPw2KXa=xJ%0dKWYc5ikKKcc z8jL*P>0QQrC&IG4UwuUhl|V30Ti425X&JTy~%OORWjk=euf zn;Dysup#Wqq;Wwgnjnvr12=bh!+n-Zu*;mcM%y?;vqoQ#81@a)!rcDxHyebNNeR`n zZZ{^Ko&o)**0=K7(S<&VQ-Mdz*PwaZPw1J*;y40)V|S^DUoAeDqNId` zXTVp@DC8mi1|yRKci05)>=f!Xizq*nRrEtVEj)&UH?5%Xcs4GW% zWh!TUDeD65I9tAyWm6MFEEie|^)-o_JGgB8qBfxJjZ4c&%+~qWdnagJD9+w=$}onrJvqG)7hrZqX?lP6|;L#g=({S zO2<=20|a2XwAzRhWNse(+!V~-Kg20j#L@mrF`znkFS$g;LUz3O6k2sLDblQ9ABMtW zKuVYaSD+*IFjc84HKU4gFW_F5O7!ya5y67I+2xf8xvkR+`HbS3dk!8f@fh$wuB)hF zN-R`*h2(h+x}HoF7$}xj8p{T!0$ACkTJ@{|{)h#%O$g$R*&5d& zp5C8DXde1Iz2_yM(WN~H)Xj3%@bVXsJ4wRYp}y2AV?n*-n~9Obqh%AM&R3~pjdhvh zOGCVS5DLznP$VlU=-9Ajs4Y6Jx&RlSo~_zv7S+p4W^{@w%`h6K?Z2Bskd8$FB(DwC zh#nQLGDKY3ayGy)(#^Gr@U@_Jt!uCZpCB~6XGDbunIQBNF+QB*eS<3FK0)`m5gVXP zz8tqDHl3+?x|}Goz0PfK5XWBK56K#T%K61)^f^C&f9*WJ{W}t(P5mgcc^rp?0~X&} zf6#+L#jg^b7XNohOrmr4xWNOc0#kR`QH1gDcd!M2-MAcSh8`0F@xA>_05 z90UZ4q-M)&`{!GL4A?=5PS57Asgj%k)8OTn}H zQiFnZl@vwmJ`@CP6l2DwN;5egpSta;JRwM1~?cJUd1`TNT4Ik($g~jDn4Ra5uM~svW3$7|tkqzc-gqORMuj7b( zUdXqy_F!7zo@F~Fw^>;X=`vxSiIw(?muMQ!VZC-4T?LN(p00s+m|e`7=<5iExAhAe#8qDPK$!#Np+0-fqvP6l5oHSf zc$k(0^yxx6OY0O$nfZ|K*^+{iSURP>{VqzXZ3174EG7 zYArHeDwC$Gp$Yt?ASFhof<>=LB4mHyacmtDGO83L9@@Lnm-zIG6G{a515jF0Mrb ztz{@~Ez{B>o{{*5%qPslbGOB-O6#X&oX8I|z_b;(TaIpGnb_Y(supbV>H3+@pV|Db zNirWOR23E+Q7bx3l}#yQ*keEsz1MC_bNvC}@&%lBH)ecrd3cW)yeyV=G? zarXVOWK6aSf}4a}UIQ<1b1q~0SAx$M+jkk14Q<_000BA8zr&16GB#e0>@i&VXxD`Z zS_%26nAw#!Q^m!}&kq|xSNt6lYla8vtDa7P@InFNSJ_3EsTqwm4pXhg&*CM;y9|@B zTX(?=xM5*owY6wrj$thw!2N?82@$wpX#z`@SC~sS0y0=|x%s&z)3Ughaqw4JZEpxcJHzW@q z^#4S(i?ZiK-IWaz?{LpaS?-pH(1qP!P21#Oo0(Y?Bo)N)!|G9!-X*@bP06hRZpqUS zW5KEKq~T=;x^?2O{jr6FC8B|YXulA7fO+$i?_wVdVgcVcDWetFe!=vS^E4Wq2cR2`KVns8FPhW#JOj zRxT?}`3-gyjl86a&6s$eNvOV3QZ--xtvX3WL!;RN-X*yMrEN1mw2fIl)>#=kDK3TO;y)BIU!nauiaVv&^KQK~q=ts|+8wP$}dMbAYPP z+EFwMzm}C#0zwgJJ~X9A)zKE`Rg3-N&*CZ9ZZz%~(s9D)7PR5}NSTNfw)~a{y%x)+ z0=Pr#iHNYo=qVvW=PT_k+^xlB=R^4Yr%FbdTthEd<*WJL5F&6T!iI)0eRC@;{3w8; z0hVnU+*@6=~Kk`^b6ZrBjpGhW8+z+b0s1XWn z2!)SXE1}ts5Sa*tLVOl}E&j)O*Sd{0c~%ws*)eGASR~{1?@X29JcnEyPa1YVnTjF- z)0$3TBrQkPJTr{mZMI*ms&n4(g*i$p=Dkur2weh)dkN>$uZ!c{-fyo!szcK90_gDN zgVhskF!$bsfbC5ADdUq!!*)e)tE`Y-e30cJXzq@|wyKFS!@`EQHCR8H^Tk$5kiu7G)m}u(kcR)@0GpgL`XWG&(R*&iFhw{;`Q&H-^$p&G#!W* zj5CDMGnjkQ7_)faW?qRpm3&Q5+hcC;pm)NXL;!-&P#m6=8HFE+0WSv@R``o+$VmG} zp*JUZn$5z!Bj)+g#pT4M-){}Rt8^%C*$A6$m1IWW*2EMUNzUb5`QWH1n@{KVuJP(@ z(&A+%0vVxo@BjmMC|TqlWi#&Fy(SI#K)cpG^(f0-AcZ2i{W5|?v!LsvaFXQK1zFEbAJ~G ziFAJN(38n@h2&wMtzn<;yX2RCUQ&Cx!}eTG%GrREvMWLOIy}mhWyKd;8YEF$UC7u?iePJ<103STcaW46%i~_b8i!8r?A^ zucxFKRr;KQnI(=sRurVlDDI62FG$eNhT!wQx!_W{HA^!s1*e-ir%7`wk0B}V*ME7g z6J0Zzn@FoWCNx-J6eAF%PLwCjFvm@2h^r(fY_c+4SVy?awo$PkC9&stW#0ID<~4_U z+y99hv7=aOt()-V`f{AV+cXvilx?V}6pY`1M_JilvKQ?{%*Q4E?Aps%Z-H*Q`naaP zZZU-mtrpip%~D1^p8>d@O`wlY zP}eWC$S3o8m7uD3w-N&l%Lxgr3 z%1xr4gljth?`b$axWf!!j!K%q>h576O$A?Vfn%2>la6h!MQehq=}Yzs7z2b5cpz`4%Hu$FRq|NRCYCQY%Bpj} z&0jZuW7=rLsp$;`f6ItVC-)YH+JIp{v97?jery8-m3F);yOOxHk{#`(FqyXW)pF|h z9T{jx|FALtGHQ_&E?3#9TlV9`I*)C21N>ryMG++i~pvD73;B;55Vo z)Lo`T;rzcOmo~g6LG7-5v-R?>W2HlmVv*n=0Uo{yi z5hiUM@hkpVan!@y9h8@8iU+h?wu6X~Ja>ZHQ?of)$SFK+z>~PVNA$(m+HioL+qKD& zP&t1GOmH-}f0Q6;MxT8*^Y1O9C-ziL z@{=Vu8}2>@qCy7h7~QVkh26qA;B_c`1Y_{N&EWfpDJG&5T7f;n%`xl3Vn_26G2Ajg-iPs(dF)~| zyFcly`aM@zIpA3}PzZ#5+!bHLCVZHPJOh)?yN;{E=QSK$gWim!r>T|&tBdSlz%*v-5qEjAg==DWl(jb}d0@qxb z!k2g~0-N(fplSN3A%STK)pUMV`2;Pth4w^AQQkGzOzWD1R^RliTUcM-hv=s|im~0P zB@Nch(xRc$@GK-p552j?OBUjf-;w^9JA5vf zB1t3(?&t2cyq~FO)$v}bOAgLe?IVfI*iN~9$&W5RvAN9TR?-gTjv=^Mt&E5(sQu{_ z(*-;f9!ZaE81<*MMqy!$2JLgi$rOcCLwPJtJj-A`oKlm<`=rEZ5^ zv-i&zFJM}UdBi3d5S)eU!oHk+<|d7Hn~ZIYc8eX3N&E^PJRMrFxtAG6Hd za{+Uv&3|8O;zM2uq7B)DD058Q_uUN0#i92C#O`3p_A`0a2HksGKA2sS=cJP-dc2$~ z?ULC@0n_L?#9S%Uq>u_uomNsQ6w5t5IC|^R>P}ImU=K(Ep3{M_)1u4Gv>#Z0c0Jlr z<=Vq1?ia5Lf=bz2a&>&%kYulPz#h{Dt=)Qsa5aMFOJpJXTYM^YEh58nPI)gudh61I zj%UF$WgpLbiz23c)tQDkKZ;v^>U^obkE+w*_;ubxLc9e&s2Sbwfp=tPZ-!70$h z4Agb&jgu*3CL^A9U!mBo1;c*Mbto$3(phRUHTq52?XUf##cO{}1~x6Cj7h6Xx3=ES zjiKfHcR`ugZ34#2RfWg0i1G$&l+dwiEvFNB0k+5sHmV0x<@ryZ7wm6Rch`in^ee+P$^aA5`m-wnJor@q2^DKix%f{BcY z>OzV9u%?JuBmX(jYly+8Q!<}C1{L@=$X}dkd8c0M)>$P_x4w^#JIsP)QG`E&tRvuX zwgz^WYg+lCX!ylplS_fv-($;WM}nmv}7@pvLd*RNu$d4hjs_X zZ13R*Qi<31`4Yo72gmb=$A{GeFKRfMwI+`8iru3o1{YeamlIpq3)+XHHV7<{fiJAM z)?aY~jNEj3K3U9ny@!P2D7?2roQ46v~DD6($M9u{d6Yx>ds|%UUpTTf>>lPPc|7lQzvpiR2cr~8#Jl|`?rnTaGdONFitBr~R ziHnSUjm)T;X|pbk3N`~!?(J(u-Y?ayTeDJ5;dI6fCH?bK(zY9(L+S6d*sN)>p z!4BU~II{I-V}NwGX|Jk0!2?i{euBXej)~NomR}5{Mc)}UfwJ1oy<{d8YK4mqk~4Y@ z=g%&`R}QCE-=bJA^s*B)tDU-OG+-v^U-%j)IUJ?a8`M&iy_67!go21K;9pQ{%ngy& zKmK$ARar08z}Dh*d?jHFhw~EowGz07ia$p%8_({+)OwnOMC^+K$0G5<_nnav);9i0(9Huuiu57!`L4juJ zEq@&a?qJeO$r#iLVfOoFz3#7-ml>?F@B6faN-q_*o=WDWvVh|vQV_ae^MhQ31{uSe z)2b`_BefPxDe-HBStm>Td`ZbbdIOb&(h$}`&EQ9SIC48Z2Z|#Z<6ahMK^&CZz>S!V zwI1P{_qnu3(o0GGk>F)4*#17QKH-}=whi5lt&B9-N`kW1S72(5PwJmGo#&2q z6;=}o@2i{T0-1=>t$ThFy@}IA1@+@oHw}qcjcCH^6TrZy+nfGWM+l1XKWveMglIR~ zAbp432!rl>*Vik-!J=AtJks9fCt|_Ies=rW6UV?S%W3^8!h{z|IQl5RfZYlnKP~c? z(A$lmOD9-K5GvO!!I@_*ld7GSDxXRYgR}-`8Hkv+GR`IP-VTO5T2cxd#kdxE6WJ8w zGv7~1?$B;&+AE}1vR$gzSR0b*c(;{)uc}w6mS07Gi?Q2ygA6#zQKCdlPWl^dF#4mKb`utEaOyiU&)n>VWLr)4+Hk|IS~|$KW~2x+6X@53AS`odI=Jm3=Ds0h1y`m+dP&lb*-hEf7eMONv!*H;$%H);gRrzW3Y2bW+PX- zE`RTyt2B2yQSHUR^k)R=6`|rq<$afOG}NC6&Gtz|nWw-SoNV;mM z)bI?QnKh;Qe#VJ)n@aO<^CirbX3Ozr`4X^hgZ$gFo2! zjsjIUCQC3&h*3zu(|We&ap%R*Co0rHF?`R>Y@1pK5puvM@L4my^D`6gwujk$tK}Iz z-U6=woOlpZMsKP@#8sV7H|wt^H9TF(?V&R3HFFrwTb}kIT4)C?+u?2Rg_OlmVvkmp z2_KG3r=gTK9Uf*#Jc|*I#9&TF9Y@eoovcCrM}cIN&KlJE2-?| z!lki6KVLw>q>w`SqDz*?W9edWiq{`*1)pD)wJne;+czcOe^e%YCo}kKBLiBH2t`Us zd+GB#aEy;^c5fDM?2voS8A&G%+=KM#ua8Q^r-{`57jWeQL=1jj-I;`Pi*vM@CC+8M zlX>P6xj96%gEBL@GVzkdb}A%N?`A%4Y{Q)gpB1X8OLWBu!~=LBz>@YQl6esyHci|z zlB+c4BfuiY>JGZHHb18G9=!rBvZwJI0q;@}@?H+mY_23Nwm1#7fW3tk>oBHV3dM_( zTi+#dr|)(wF^-*DOKAgp%%y{_Yj`^fnNT9x-xb~MG83+m!l8-7Q(a*1iLKlC1MWT2_OH@}_aCsA2y$0)8!+*+h*HSn>TNWpu0#kVa_hU0gX(+0IO%On6C7u%mfK4YR>(z zgdPk7LBav567et*TA8NlO+kaml-UgA1x%8ViPXAui*W*ZnyQhe`T7ddV&TX`qBbN% zERt!Km`B_V=Q}y!l(XD=(o>y`kne3N>AB+^MloT>4hnoHiiYjPLMGEExtD{H7&EBS zX5oQsX=P@i_099rK|wJFagO!gQ?q!*U$u%Y@P@{7sJdNTu?*adwAdpve&iH-Tm|N& z#8r8=Y(C%tFDgAea??tDnUzW|f$(}W%SIYQBE%Czu2LP~SM|mvqJ+FEpT5vVB7}6J zzx)M=n3ZA`0R-^B>7v%_D!a%Yp^r?VplZgGoY7h#dHH)FUGw8-_~&w(>^x11Nuh74 zwS(gF#t42WJo^sYNYDeq44$I4?N==`0h4U(S4~{}^7So@N(m>fD`*S)qti4?zy}7_ zDUo?IFa6nI#%k~nY2lF$^SB!}DvPt#NqF+TIc8*26-xi3%40|N%S5?j(tXBarVL2r zFw2U`Q}yXQe63SlXFEAiPA4QHD@jkRNF6+%3|9Zdi}ot73a^ycE!KdvfIKuLy)-;U zU|x*V8GdOOgjckv=)7dx2HoVwsrikuKeGt$t1NgY+BpltRH+ZIrESg5tEhD%FEItz z6b=!iZSgy^?A{KyR0l9r@m?$9J^5S3n`8lrN1k!t?0M=R@+Y5n)*iP|)) zgxT1iK2Y9JObaqDNF*IE-*(Hahqqu42N5AKy7j(hlpB*l8hg!vuLm9!-;*eKHwryv z$LWl;0}dtTm3DDWDY4u?a|oAuz^$9%uz+3o=BD&|Jw;GOpf}_HarV|xaeQ6YaAQFN zA-GF$cMG22?(XjHP7)+I1a}LKHtrq>?ry<@yE}Zv@A+oEYu3E;%&eEcs_E+LTldyE z_nfo$K5dqodIoQ}9mClU7Ef2ffKg+@p>q{7HEj zUHSGk`)b(D+KG#^2LrXDr7;Mg86wqt^PhC#{|0vX_isIDN)u|#lfvS_BEyXeeQ)<(M* z2ZYjBa(K`Y)<4K{6@TM0SF6+>nS}$Ib(B2T+#J6WII#gKGL{pgVWCjU(hq5RLbXPQrp@zcHIkCV0UJRzx zG=}?l`-|)Qf0!O46%_+HQE$F-NGE%yrUe-dS2)qAp@?0{FoS2ogrJHvp`ImY7vV+F z>rf?tp{j`BQr#6twrWZK2N%G}4r)P>10Q9*`abKQ)B65(T9jwDGoT8he~9?(G7IjH z2J%CM>U`JGI7P|l8uTf^(ST(*kX|}PgSxd>9I%*;8_JksGR>d z5$bBF>a_{HM>f;wBd2tkp}p!4E~bi_H*H0g)wrFu)onWfXn@@CZ## zG?a_*u*_^C6U@1Mf3)&1pXxe>nZ>g$*GYOnRFqF!ZdTbWV>sTye-4fo3rLsypMR9rJ zoMH9m5?MFO(-$4J!Fi{W^}p*5?mjEV3;DWFl~>|aRBS-gfv;gF!+8ugbDf#BgWLNs z4nUOdFieEkV&u7M`Fp$PjiWpX+!t@R=Dn!Mk~e^wvb2HFSXOFG0$Yk^I==QNZ}?YF z!Fw4gL0JvC9OijlnoqI%F#n@G_%;6(h`bSy7s{~#J0Sc!nb$C~q z0^~$dKN-GST4ti~`y^r{mq~e!`Z253Axtx(g_D%QhT=a{xt9IxB&YDV&S@4)8zpaX z%FSX#S#!^}9BWny7nLzxoPFpu(!-nGGt0k_m#+ zz=Ny+T@4dOWrDWIaUn+`>+Kza-4=EJhit$Bf8-X!a4u#E0$Zk>4{!?8u=T#t(r2?O zC9|Y-OZWM_JUZ7h<3EyO&Z5p%Z{>cagz_B6b-5I~UIS4B* zso-YJd0~wLgjHyJRMQghW#xwa3v-XhJ^73;os5P^K}D z)=H#T+2Hp}BT$hEPTuYveUn&GOlQXm0ByU92wkXmAEoq55i2`Ig_qt>GQ4`&7Q~9? z=w7T7m~U4!!Uf#4Admn?!A3}G*~-sp6Plv?%(TAFste>zCFHxyCQWr-52j|_a$Owi ziN(8FI|M7@*GJ@tdh`O|S(T&ZPU^(qqZ}0R99Z~GyoYEAmdnwrqKf^puKV_9FZ8jH zGZ7$L8TQy#7U;m~@*GBFML>g2FT$Tx#6*gY5So7Vut!Oi2naXeX3A+} ziC$&)vF>XN?7BZ6`A2+7D{sL209gN7Vmw|dKGb!7RL-K?AnCgj~5#l7HE_X z3U{d2W=>XVh|0p~-5BGA^2`(i(U!(0rG>gb=R1?_BWrnIp?~&Gaz`26??$_r(1H>b6bb=#)I&R+Zj0Z4=iOccChdJ9K|2dUX7IKo5{|XbEO>l z?`A5>ijU;OO05tmP@p+jdO;ScWF;3xJEhkvHd$-mYzo%#Gh&Xr*5(AySWJ3`nBM-4 z!jbz8$v*)ZQpd0t*s`1>W?PGkI7+k-bbEJa-EdPo3+AT|&>TUs_7jb=FH*A+^k96zBTnJozm^%;dj-3W#+m;ONO9G!R_&+_AN>a3}ghCu&Y+0^wbO0zMn~ z>9<_4KFlk(WY9ksDrrtDp6QM*QVPYt5X>34(ovh1X|+IECoN!IKWH9hl)?Y8o)=t& zm6@K0z<#0pEt^inWjz$RTE#vv8}-dX#E-pHx^hT+wN2@4Yl1PE3+98Oru)kGaoxaz zZLnqD;rZglzXHKS2?|SxUx9g8Vq{J>7hcP~M|qaT&Jm zJlVl&nx!cT&1)<$^TV0^!08{Jn$Fim7i!Bt<>+Z;e`p)K^@)=i-NbY{o<17kJe?Y(ZJZ+l})=EDcC`Z(X^V`AP+gn?}d))#`j0dzM!}iFn z-v4^a(gA%UqlAJzQu-)H=0-*pMk@W2BJpLMib>$=j1pJqZZ%(lDf{_$Z_Al@|F;3Z z6hG&g)4M?4Gn^Or*dL;4Gs54YKO_=R{%%-UeekfjbAB+rlBBT93n!e6B%EBR>hS#G zIr!ov&xVpe-3&{L_Ei#qIod~_S4_ZAtr$M(#4kHZl=4d=iDxoFr08m+m~@MYI&pN` zANO-%q4*`F_Kd}T*#UKxJ)KElz?&bYe$~L#?!AWnmprCfwP1*thf;EuV46IMTn7Sk zxj^GJS!Px$ck;^HrdY?EGb=5}ZOa_JjooG|6q%TUwr zqGFF3gy^W!xId^OPi_*s8h!5m&~#j{rNN%|{&JZUY^{H@8l6>y-I<-mAF#h}A#UKQ z@1mITuyHGpk6NAyAE>mt3$%p73FxT5&apCRJM*h^dFTH?$nK8uGUyd$zIt&gFf_LM zpyhcxojoXL$?F1N1%{aV;}Anhye0xM2h}zQqY!*NplL}lS32&Mz;)vZ&~YUr;JKJH zu`lr?11Nk{1{p(jpX(-oEd_T z0nbsZ3~r;7+*tuUm~@Hv_uQka7xNBXZ`UtCr;ism8N64g7wbD?HoP@)<9y$MG50M_ zzD6^X?*V86=^Ir{&Ft{+6=nyFrHslU`m84Du}c62t*@7LPc8wcOp5Ys87_(qx0MxFF>XSh6CE-`UF*AN8)eOztXCsTe+%rxMldDqn%AV4CQnro~_}F{=k&|q>)-{m;g79m!Akj<6b<^gtu|XA&B4!5k;n5iLBq9X> zGtDxVGMXKftWwk=0}U=tNGZCKY8QUgFTaERR1c@C^H1v^U^_cBKo9$E9~wxyU3)S9 z@xQjhAdi!j#Flam^X$M>dR4Oqjl02+#YxuY90H9p1V7h?ci`TNtsj3~L*L&Kzs5_7 z>~ZheZv9Jvr1$fLk%kYrpg})LKTxwv9WnFZd;e1gP4}xX;vKoy&4K+=l)z&SCU6RK zKT*HkpPD#X%B7;s`7q{89(C`t#RcXJKM2g156dBdGLS_6s%_Sy#XRpYI6pX&o4p_9 z_%Fusi2S_LY}|@OZCJX8ox3SzTUHv$wU86^s31|>)BQ41X5wa{76%i!x5qC{ujPKI?)SmcKaZ3!Wt{!V_Z z$b9P3;yEiYsmO$T2{Q>b;Cl$J(TL9EaoU(S0yUm5E?A7VQ$Z14XY;HJx#O-$y9#=H zN;5u$S{NLJ8Sot&iI%1mRF`LK9FC;7QOj&gO}zG}YmsxW?e+HVnGBbWqP?raFt|qt zbtdjkDM;j7H+`vma%ZUVr$Gz)iaiK7~-FXWMq#Ks? z)U`kl>XY{!5OTUu$ea9)__98i_5AsAaJ}2Hpqioj;jhD}jhVp7Km^xB)4uG=%&78; zeBi6w`KrQ_S;D80nG^{}dh$IcTAkSWQN+ODkLJcU-}=gMz-<)sld%879Q;6Xm(T1l(Ou2rh{PNb7g(P>IxMlok|oV0LGPE1d(-z`3jM1pmJ0V@ zhsF;cV{2Y<{6twvdmhJnt==EVf~t99+5lRlp#L2>$cOK_-nV z|DmtFPtbbhTQ_DNRb9vTCfd>m#7=43W&A%(sO61^3d?-j10wAQ8Wwg{Z{{n#w%zm7@@xVK$)h+es(g(v*TYPjGer@~+3 zqu?lUW_hR?_K;_*0*jgDSL_7-U?<610YchF9mR>pq$^C&$t&9wFj8zEas{h?Y+&-3 zvaijk6uz0rUpD+`WEII|Ypd&;O)e1)0tI%j2lkRn;BWaQ)iE#fgg43d!~#hc!Q&6< z$=2Kf=VW!1A>PmQj(>j#9{!G#R0?QaTHp2O*Pd6%;@{(OHa1-Q9hX2vv^*Pc=w%MP zK$B>I5r*cuyPdGF)^NXF`tk2FuKm8)&g`Qm|GC6-q;Rd}Qq}QB9`%F_|s3n1r+xpdv=tIys zQj**Md1DPO+ES-x=?E`5T^4*9jBK%!(r?n0?pZC_Eo)H`SIg8qkhUn{6=B4e+Y#}- z)By^ZY(Q2?cuFA`-5KWre?}DCE+<|1^+Yl8dwxg(VojH)|jv zf#@t*c|{0z;p)(U3?q^E$U)v|;3de!ujUnDhn1zCyhPx??>}@eS>ZX_6Xjx|!POLJ z*SS}6IF)-^2*`D#`wSd;cvRND<;|-(;;=aaBHc*|9+S6XokcPJn%}u<%w}pTEme$p z_~2Vp4Ys^g2qEOP#@=afK#pPKqf7!vpZd6rBO_^={{}}yE`D;z*@BOQ>|s+?gT5Qu z81j8oC%MyKL;s>DPBK3G%k{dv01Oj7i>6JLRwrKg8HC_~E($tp9I;Mhj-&7`t?bMq zgPOJ9LBEFiEJomQaPq;;K(kZJG7sXn_n*Ds zfJX;m3oAoM!cqinCF6i8$L@{90m6p_jmk3eAVvLO)zSawEQbH?jmMAZi?i5+Y{6!~ zQ^UC;v~U-5q8WYXe`lzK)DS#qZy}&2P_qnS=GF~rwzDVs+(j2xH7bE?=0@EZGbrS0 zbY+A(CewWxYc)PI#*ieRmJSH&>Gdfl0r&9vpfii%LY!l%_woW!EW_0eEyQfkvWy06 zd32B!Bx~Wphw+xdjJG8oU5-{Vj4JB5M+@V6Fw#}a)|)FWd~hNlI|qF zlH%;W>PAWbXc$D`-m>>nwKjJ)!z?`CEhtGsVd}|mz{CVt^L*mYL?f5U88PH4opUx} z_HjzoOT5vQiEEvRSgY-zQ~oM^*a%W4t_m_zyQYr=$T$XfMGSG;&)`abb7$~I1 zgALMJI+AwF2;+&~p~2GKqdsBl;ZazVNF4@Y&B!!^_0l3ZK-b2I3FAUqAL(3xsB6xk zTsqZ8E2{sbNaHH=d6>XeYx_XC%ptOEjojTIR|Ep(jf-eU$~xQpjA1!v3o%j_njP=` z#h?PH$s{7Y2?n0Ukys%-#gzAGKgve&D{63c{xPFK^r+ z1(db6DQ4k{>fb-3TCJmlTT@3z`xP}@n~fKKA`?WE%OEkxNpTR#FO)J~=;MvsdQgi5 z!F8m!WE3No?xmJiiM&TAija#OeM?t_R`A z5h|ctWB~WWMT=HeEw{NNfxeGWO`dlNc8S5rOrgt#lKF_`E5_gK%Y}c01KkaqFFLWK z!MU_1=ZzSng#`T`R>L4U(WF=Ktoen%muy+1$u?a5{Ry`0$UD6!Y);7>#ypj*HmHZ4 zPQ!co-kLRYIfQr!yA2#`4p9$DN#==Z>Df|AqK%%NsIM=t<5QSmM3#ef5uCf^{|0#6 z+V^R7+Dm(C&-;j5ufDfB&{fNMIe6whT#eN_z5O^i_}|WqbFK%MZeJVF=QUr~YZO=t4yOcjh&z1?H7ym#2ZMwp zmx6q&ytpTvejL5?M#juD+3EgY80<IRhKUFrf_(7c5-J_BNO%#+CMx3>NsIES$B8{YLrsECl0P9w7~2pF}tCMoSJ`|v4%_kxantO5|$%{agm1tx(tE45W}Hf4D!EIET_!F&Xd5& zEDs9(@nKe;UnkLQtMlNt%X1SMhhQ|KUdpd%ckYy7cQ;<0%Gl3r& ztYssS)Lc)zdz*}ucC;cbusZqba59W|fNd!ZKH7wAuAd=RbRSa>8SUA6$bpb@xlN927om!EmDoL1t;)KB@(h?b2sxMHfS2Gf5EY? ztVw3SrmpLDbeDI~QIi(U((!pU1vG8Ju5>bN8dr`X2vdn|E|H1EUn4jz7n_RTh+~+I z!TGmIPrhQGFMEU3ar6DZZW#fTW;} zGQZ8B!5wOiEZSfmp?NBsc<&70w`{o&5_HUX`1(?Ai7gnW5{#BYgVoWr$@mKF7ZAl#@T5{e0t1FJOi zvg_-9G|@D4gmoZZfR2p&;HQU3B(*fZu!>5=SPNo|EIn!L7bk}K$kfF~v6TkS{GsW~ z_#Ig5`th#_jEvoW(1EPg>2Z4}2jO-ihrQV0vF-Em#v7##Xv1t`G#XsIL#pN5$QBkT zu77a>k06EL6&8n%AnwP|!CTMdWRAT={D2eW@1g(>||!fESk)CJW^2|Was z4Z`>Zw)hx56aV~|83m6En6{83;9FpY4^NU)-l;2K<6%ygC7OIEo%3To(^4#69DVJ!`FkT>r>l+cD7W!^YfoEBb`=CE;8(sr@$j3MGB1O%UJU|71jnfb$PhZ2#YdC;x5S z(mI2)I15<19oOr$aoJ@vs3freGlRDAz@gpiA-3HZaOn1WSZ{j+9CD_ZHP_A9d{z0> zqf6sJ{ks;aSQe{!e{6_*$-SRVY#~GotTQ7gCU{J>gSvWoj=FLw)ZsZ4;CEBcev>fP zGy8m)g+pxL*vE2L5#QXzefuUkBF_koY~ zOe)Tbs28I=Ou6e1+R-~wkA_mJbd{R07ac1JdxFJ%Gdbu2j*=qDwbC$P7)k)^jV3HB zGQ|-OXgcqgbx8%=MePHik17rF(&S( z8k?O(R1Xy@17pY+_bQ)zh6w- zdRE}#(PzyILiTz4Y_=;5=`BTHq7l9y3bAo0A-oAq>>#MPKeJS`-SjxlxbNOS zz_LCuiz&BgeEvawKAI0&(cyNI2-Yb?YGqNnI$CTgDyEzF zijQZ?!pmg#_ilWC%n;WvSCc$KF&Wvi$HHpo(76_{SoRc_g7Wn~F)R4&y`e0k2@SGb z_4BK+Kc>gIVjtLfBWUQje5$6MSr@j-GdgC~&h5Hp@}S=gnLd3O*zizQSk`5;=up3F zU)I>Kb0NQI4#4(kYK1$v;w7&YSf`MEM@W+zFMM_=Dm8eXty}yE*4zo3?emyHWt}mx zC>)vz9}mSh@XB%jtp9A(J)^L4nWv5By`bk-d3ZmEib^=`wWUT3cVR2ai80 z4?i8fFe&thR?zK%er7)hmW4;}{5tL8q@n`~1Q<-+L}5=;UP4-VP9PI8Nk-W<7-b;a zBN^2B1sxTH8{R5=W%j(b_t|XsYA<|gutL!3VDBo;`?KXzS9KQ5gS0u7@79IP$Y)u2 zBoYC)Qvwk;5+OH3Iz@Mh9tsc#=9_2RIkAVCbRj42mxdl*_qFl^!DWL9CQzf;PFuiD zs)cTY|9M?{mh)^Dk4H|i>CXZ-fAHlul(}cqxw{I3M#zo0m2Iv|`S~|y?f{jtht5tt zBx!3KT)4mo8>;JMegE@EqHHg}lVm1N-^(qZ!RaXyHeIMd0|VxV<)}8)edgndd!Y%} z^+(qyRHnjFOioK^IHVJ`3aTe5y|ZF-=z<4V_VerlxTzN>v6uN&KLa0=xenx`ib+|H zWKWH=A7nwsm1R4^nt2JTo&zKL=_P3>K;La>9wEKDnsFGAN74v3jcq_n2=rV@=Op=j==oC!XS)`pShCnmt%|mY>1Zzi|GQ~}k3LjdZGF#hvd=n> z4H;A=m4X=Xs2j!oGeO44E*BN_e7%jMyjV5^i_&wErFgcm&^6!n`}(BZx%mm}=BSYl zS>vJL7AIJPYSZP@PN58HQWOnM5#uHD$k8ced+|wtDM#vpLu>+s;G1zF_|7v?( zR%LzE>q<7KRzKb74+Q)Z>B{wFrhmAdxlDjt1J^Ga38c72f&AI#o0$MS(+Cz+=Q_GV z2jAXg0_tAHv(i&RpZv0)cl~=%SFBM|FNQL7r$LAFYKl;Cmi(K>c!ux`mPr5QiYu~zcv|u$@TmJ9lKG=&+QwB z8px)08?zr$2vg9TxiKd0(JMdSD+!8j3h&G4$8es1)EQd#kzt-qIhqkn{%Z)j)!&E$sXQ~SV66gBu-&K6+Eu`>^u0{ zj+ucRy^x^Rj?eo|z0E%DK6nVcwBMigM-v|nO)b9QFL}HiU&x8IYku=_Bff)zbF4h< z{a%O+${^Nj^4=G|lr2m9Vb9dYbr|N0@Z`2xDZcmew|J?~kFqa~$&7#Cu9UuxGx)f} zwlS%(vV`p_G*d%q`mxZ$;+zeDCnt zd!ed1PMqn-OKG)@V+E8MpRbD@R?fde#(~co(pjKrzr3t4Ct=G9xOm}!9zB09642e7 zP7yeirZuVUJcYu%!@{~BE9HY)vr}Uq9*t0lc+gw$zAmplbOQrnbVb{4$ya;_Maz8A3&Qt#g z57O8!?eM%FO|VLJMep1hI}SLc>VqdRxHBY|y&sa-x4U1r!~)$??q5`_p3gr=WUsvB z!G^z`x>{{@CEY^r9>_U=FrG5tMj?c(e&kU@26YfFJlu{{VAHgIam>AnS_(Lrh3e$& z*eB8NIC4lE4cP8Hfm&;>*gwE|_c9A9RSXtvzWH;vxBTK=fDZEK*81ZC*=s}D$xf$< z&^sl*xa^P4aeeUPD}Ol}si32MkcTO#p1z~6cL%Kj>QlnmOWesqcuoMVld{o?7 z^E0lzW}Y5KYrfhVY6(B{NZl2iOsf3Lljkeo6{Nl?e2UUtXxONLp9twPNX>J@_))}O zgGV)`LRV&xN7^*s^iA4>A?J}JllRh=E89B@pN4P=k+R7cGb`8)`iacRxX@_0btCt z$Y4RD5qF(EVhrX`6HPp0wB2L(H;YF{!nf+b#z&wh8XCTh*kWH+<@z7ye_1DMSf}Ji z)wS*Wi~26DZgESeR$>;Y9>Dv4IPc&G>Qsfn)Nf|-1(JF;Y!A4gShEah@vp1uQM!_@c?hFT(>Bd*li6;qH3@T}1_P-t7>20{)zIA7GXP|Pb z7+tBC9JK;-ZPhU)$sHmM3cO}II@MWbbiG_;U!%;fK;^;pvi;-VPZDWAklR6M5DXiZ z5LX8=9w$zys*)H~Uh9WoSPc_fbaRwTjy6wyB4PfaX`q;Fw~-dp^!!@*XDoj#YOT>9!*5y))R@c2kyG!MaD7W)n~r(KT;48zg2Ia!k6h1|5$y^pz^v9 z@n|CDEtOHX^iT11@gPTJGP5VzqPulBk&PJHB(G==BYPsCYKy!~*pHKP+B&f^bVij0 zWI8oVUrr&2HEVgKJU)8~HeL5*lbj$ALT!fYJgL*VwRQpA*WMc&@ht0e582bdv&&Qaw|I*4@#+zx26*DGYv z7JnFXqkgxtvl~&fc)v*MIf!Y8v&Tu4+Ztp>WQU_UHNo}ezv2r2Q$c|nRd{Lf^G%S} zZV1a6F3{3W5xbw;I%*j<(i~DY)tZ4xo_1uOnN-jq+qjfWh(=c4USnxDoAs{>s!Y2{ z39U4XugAnv{1ijqB)woiv`<)G)>4sAE;~?Rdhc@S1t|ef+3wI~F$@Osd^xHt-VW@_*;!vEt z)_A2wqc37}E7^4&T4!#%{*qHN%UT7QYlWQB)@7JO2#2)uA>bnd9a3f^9-9`{+@&gg zBf37iSf-ja<5|UpsxtCgexRquiA>fAsh{aDLkghYE1JGw8ZMhTj*M__nc`9BQF}Dt z%Wc?oAzGWNBZv1jFU)YNX5giioe*9Ra*mtM0;D=kWD*@J2;*c93N`BL34NQz+3&p5 zsQmEEt#IG8k6W|7H3s`h{RNpf$pq~@gybnC0TXFyHR8%pD;vc9w%q-Ij-GE6LY{y{ zTl^E{ZQ&25KS7Qentb#AG6AMgCZ>`G$(UNS?_1)aphG-UslKxOh6e5at41LyLe}OA z`$O`6%Xe1xfhxn~y&Wyz;L=U`Urm}3?P?%71n!VY$(EH(82wHCYZ-$d)K;Bz$$>Je zcJ&DpHn}Z-q$x7Kz^aAGm28?aTz2q6+t}dlLAW$bQn+R|I&w)TNH%rZ(rXsTe1R5U zj4?Equ}Qx_!hvhzSUZ{ROs6nSzqhos*p#`RSq1{O!g`yZk4B}ir zYw4|-kErQjZ2z$0^W=?<8a68py_LZ>m#K*YfG%FqKWMK0V!w@%NdMxkEGMONjtLb# ztMp8d&vdSF>#`U;7jUjB9}}ZqZrMmPr>99t2+4me%u=?IsB1cgTa>QN*S?xofBF#$ zSp8ed=pS8GUAi~Wb|}Fn!N4os8{;cS^fz{>=n48XGmV@zm$pSL2?r}Va?*Qh`=XiK z%~W$lS{Q(LCz#Nkvn;oPZWdO=LADZ^VxMX{Mq4DLQ1rws(MmMdUWbZ-=WjZGU$&J8 z>{6CXv?+`!?W~N&6r8p*11h!0MWh8OtD^Cf_6-E;I)AFl^6e-T@lOx0Ws9gNC|4?B z3-Ugzerd^3WKflNNE(`1w!fjHd!V{^{cI^Ra-8Z$;vQquTLsRKq$v2ix}M!`dmZ}(}vqYGpq0bGxO!QUAR1s(u&-@$v<F4h5jxxd_bXi~mPP^L2T%2x2Sur$YxwdeWc#oNrq1OBSjbR1Pwg3j$*=Sxy zIEU>XKUvFW-%mGVKjBZYK4K0~G9N_=yu*f{A>Z$+39fpfd7&aIfsyaZX{Dz9DePsq zca&7o>w2g;lGD~}S^^cw`k>Ka9lBj_B4Pc7_d3X`a;GZm7rx4p2zXmNu zwJfTDDX%QlG^lglP(cATCCifd5K2YIWyEl7^(u0Cadvi=Q0t*Ct4e1nI@KB!EntLy zc-NBkD-&GLrQ#yj`l)sudJ9Ss6!f7CB{bnf&u|hREP#%BF=-OY8c0edT<0Yrseo<- zuZQ}M<@6s8lb;X6ET%k4?7oTwi}dv52*I8Rsb2n0e!iq_ys2_{Uhb0**bKAaQy6_& zdI%-)`zygjGy5`w82=m^%_)@XZyn)q&|LbqEC^n7gKx5A0+cw*A=_!oYM#a+!TprD9u|8GL7m~@UfMXQP`p8%LO zfn51h$bo+N7$%M`JwQHiiV=}s=(_l!Es)l(qQ!Wa)d5!Tj$S6seC)&N6Q4Q#A@P;G z0jI>kRdg=;^JdFRpD_7F6qgrwUjTOLa-GlDtPQ2QX2(AJQw5<4brIkKUYkoNerH}F z#nN2hN7(Lzwj}erp`zAy+rdgPNi$vc3O|3r1_{ox`bA7O z9uG8#x`jjtZ@GW{JHK_U@5H3%`L5pbJAfL|`J?JrzC;TVo_da3^(hf$Lj2$cclb=V zbyGCXH0Cg2hSFT|TXCf7*%qt?wi+=6J8vI+0w@q6wE{0b<2Jbu6ey>v3N7cXe~BRA z;#N=Mr1@ZXGXi@Dpld`^JYw0Maz~3tY)P7Id`{q$Zcsi_?d#nxIiM5MM59}7^}y}| z_Ty%ryY%>Ia_Tp41G|6Bxw$`Z!6zW1Zi(4Y z^pjDFDh}uDx$xN$Xb}MAWGNlIan));Hpfz-)dXP0{HeWazyA0bAy4N$$T+yby?7>h zW<8H(tFZd+LAjNc`SXyPzJB?;QVk~u{#w7_Pps)}S~igr!j_z;(omS2%r_#74Lo<7 zLO9D>82o18{|lhKwG++Grmd|_70hRmDhsYP`-cy5PVshG8!x1$CT{`gjJQMv?@O=n zceNX+=(}m>1>{EkuFM=?6$8j1fdJG!3pe6keaTzj%hpiT`=L! z7a1YruH$oz7)(+iLD`IHVRyx1MBQV1%Vds{NCc=6v_Gant(cOLDg?4XD|oh7ERCC> ziN;zmh|172OLqeCJ?mMgo3b7i29zOBOVtXx(#n-X@F8LBiyL)`rF0L8I{eMA8I`AmB=V!N?5%bTj6wG+naU55Ge zHR{OF3f&;N(Z_ zA%_V9VEFkX_pYHWp)G}R0?Wme`NfSf6(0FABHNB8As35`+IfYX{9x>y0<*@Gw`BA} z=7EG`w;2=8tm1DFNPI%e5CDjEHrtY22bOjHk!-c+a`7mkm2T@N{fKtHiC0>kJHxa# zc#}o^4K8?&_<;Zcsw-UqHsbBk2Lh4s)CcHj#%2QAP9|eN(4-W=rU9Qz|yJ-ao1K$%Zqi^g- zjp_l^94*Ot6-?N|R^{3SvV7x7iAGJEq;mO|W#Uzhgoa!#bq7A;j_p_kuKN5f#__W| zcWwR7bWxZkJG)hk2NmdvVVAR625ci5|FYR(4Hon)?!_iagLrrDbU3ABdW0wRez&%^ z<-|)lI0!P3nDrpL_zZ;tVcuAQY3ktMSVAdvwMVzwE72v76N4FQHG!-9!Bp>+yP_pb zaGA$H1U-Pkh+qes5%kG2Kdj4Xa(Q`)xNW+}wl|k|Q5dkYMbA7#E3BjyHK{uS~A%@i8!w&7+FJQI6z3XPg9RGs-8eiL?FO0Ea zZRtFv*@@(h5Vq``P&G0_t;=~**UbC}Pz+c;J{0a$Mk`|#5L8689i#QW|&?04-{&ze8d!>w6Sl0Rv-hr=eZPtpHS23w%Tidli zVNnI>m4ng*z^xoNma~ImZ%$MB0CehS>C7}%jK+vHY6ID89^umOpR~soCa+P(31-~e zNEI8e+Ucx3dLIEbFz)NU!ydj}%ye?Pg!6haDN~^#S2#Zm+cGu#FY~r!ZLOKMWJ~lC zDd;P;kcDOlY8K@RS?m?sE(hRdL|5&MWIVXP@eSFwmafql38|s+wwdc(_ z=2>dIJ?2CD?2#~XwL+tJ6RqVSr9`9lJGE24uCqoHT1Zq}%Y}q?LN9r0(>OIyHOV1DekOy;5H{ zaRQwy;UicTKRqd_Z}C_!tB!JqDmyFT3qs`tr4z#jSJ+RsKd%e+^5&TWR0Xb*O2Vv1 z9_i!N+UDqf-%X!DBo2B}lNFvin{4Jw-+=1K9*f(^;iZ?keHXK1rrTJstfZx+PD)8S zTAW)ou^dMe=R*!MF04V7xV`RH0tW4tDF7z3reH&LQk-!coxfgPVJKGNHTufXBg+UH zNL|&f;MwBhpLhIZfGcV6;MiSf@?cP==wpyJKcTt-09q$8hbeCYSu3b;)ki{+#CPh@ zDP~;Z;9jO5?@0~`okp-wWmV@ey%BAL|v6blX=X&ysF$oC3LhrZfc0%{)LE}YN0iGwR3x>aviuVxA zaFVVv;y~^GVM!ptDh1i5In_3*+m(>FWBRK%8qBNrj8)NoXYZ0N`a-XDm6epo`lF>f z+%4-bb&T}}4@c}MFWeT39rT*-r25JM3kY~vWJIS~Rb|ce-&Nq!!_8#2fVcM<)OskM zv&X+ltiD(BQo}7$K;e4H&wrk`k#iWHB`Hq<1sa>B6fnP&Kj!LiKBcudg4gHuH7|Mo z=0k`n+(c)(;?sPhJr!!uRfn!$Z;%^gPJO~A6!*tYeDo1lYln$Dz~>>nui z3&M!h(h$FKbvFubZ&@9+WVJLnd@jr}45&J3AH;Jgs#HV+w0DKbbJl(E)TMtBGjc@z zE)%ymama)iU5C~6&&0sn`q^}=xv6l0I<6~|U-vYYu>j-;OlFWj8{pntEAP{$j=`UE zL08=Q79(y*TimbYrtNf=BuafCU`1WE9z8${ljbvb;A zJZ`LLerDc#J0|xBPp47l048u%Vc%FrKa2mMGjw#YR@|9(bk|Dn1qHAie5rCpo1f0E+l&f+Ai;E&e
  • 8jh8c%*RhB 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 Q6b&HpuQI}Ag1UbH3#et^U;qFB literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction.png b/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction.png new file mode 100644 index 0000000000000000000000000000000000000000..75faaec5725287b33426edff95ff09c024a3d760 GIT binary patch literal 150648 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

    9R{w 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

    `7$lRiZg)#TM$ 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png b/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png new file mode 100644 index 0000000000000000000000000000000000000000..47c8b61824e0428424c6b6fb4f8ab97fd74899db GIT binary patch literal 87912 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�`ul7l497JN6CRceF>W#qXx`GjE-u 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 E8}T?SegFUf literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_RunNukeLauncher.png b/website/docs/assets/nuke_tut/nuke_RunNukeLauncher.png new file mode 100644 index 0000000000000000000000000000000000000000..a42ee6d7b97eb323825b652cc37bdb772a3c7b20 GIT binary patch literal 38738 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�BH;NNh)p1_6z{*NF@h0&dj-T=Bmm zG0u|?sZHLbc&}yxurlx4+I;AQ!LVdVO>qF_7D~Uu7eX&}DgYt=gtL$SOxhRxcH#gP zccrAB;6Wd$K09s!=SGJV!>#6ry^fbQz`WIj%u!wVh71jH><@eW-5jJp8P+nbN28m4t8GnA-36Am z`}ZslFy5+hFz0zzTNI)K!OqoJiM=1uh|3M_(0YK-|S(NAus=~MNDBM0Q90BGJyLuYGmF@hko`-Zm;Nx8! zDG2SmXb=`SC2_~{hBGZ2QO@2ZTChv?v*LrWK-!mfNP^{G3j7a^EF-{@lC&oyjkmOm zFz(KQ_R+oRWlWifviAbfRXej0;oa%;7IJ0{%UJ#rs0u5vhBDyBI&7Pg~#D-GfX?ZVY(3INUD zMa_}7@Yba8PG|<-ox#4V+7U8y{I%=+X_|Bb9~N&Yk<1k>RGlNUYDTRHX@6IsW((F+ zZWY=l1h}dtcb&au<#{AD4E&y=E@pTCH9_-o_M6)hJaO=Apf#Ww zQ~)>|2=0Mi1QaZ}Z~+!U5b3{zSpPrN5%2$1IttbeN9KLAL5G<=kmStoD-(lcZt{^Ur<4>hC4smFLFCOtbPQiSpO!aS`&JOx2N|^lL%5Pz z=;@%$Z^pxiNn8!m8*4=@LNV?}@j=l*r2B!rd_^MgH(M$t1QFZrZD@`iK% z^~Q9d71A6d;wt~UenGr9`>R5+82GK~+JnfvYVwvvV<02zm#VqucQMV*A0ECfzXqz= z6$`Kp1-Yl8kc6bMlnGmvt<5TOY_KMp@8GOi3L7t5JU_>pXpgTh&g&ii9&15Z-r z)M$({FWSZp?@UngY)bnFryddwcRv$~>ZF1(_s8@$<=J!_{P?54)0a8TB{}*iEy~~* z`NNS43YDM@b&@r*w>QrCFdLrrQkqCZXO7LocfL;>X*<8Oo(PltVL9K#?+{qPs?M_G z|1|#yF=EZr(}7%4zkb}?JtkL2kF#pp`?!v?5|1|SZHtEfd0aG1v~$!%lZ4&F!EcRl zn4HJ|})==jogcU)}|bgdhujprbe-Dlr_N`>=EE-JKyl6d13g&NaEYYm=D%_Pu)L4#pfz9jXyA$BABw$@`2D2rs0UT>E3N08m;s7%V zubF(2+@!GZ$Zk}(_?MA?muDooUmOqS zYZ6`_9CZj>zf0T8$x`!UP|%h4)R!2Tmeg>X$W1E?Xs22b0Tur~N;x`i#TUjKf~TLrbfM3Cfeq`c>n^y= z9Dxp`7T+AFbTqs}i^Rm6aDB9r93}ZY@@D4e*>3AEJQd5mDm)+C*4d-(cK}fNJw<%) z^81{^&82k8l}PL5Zfg|j&64ZMeC~OzRQKMM{JX?hwMU`hqAdp_F$;oZ2 zVezRc`L)Bg#AAi~{w6=5*Bims&vG-b@y}G5y=BmTeo2aJ`gzaH+!!LO0cP4vUcS37 z3)g?w^)UD!wE&-2MSMkPNP7)5u93}!Hg}Z(Ow*eb=QmMOC!bL|rDO|!FL1EDPH(7j z^$FcuRBlm$Z!4aDiWN)uE^S!SXkvz6?e1TQZzxYHe+hqk8wKgiLUg9dDmYkUg6VMK9UgK@d=KQtRHm@ReHbj7Y|&> zD7{i%wD=Rc`%6Qui3WSSKdQR>nyuGfirme3v4*lH$G5UC;q(94XnuJ z#7PA6i#_}x$2Uai6{pQbYBpXtQgzeXym zaE!!}9PwR~(48TFV7aJJzA;O?@}$X;-NS|C2MK0Px#J0~!FYp_;NMZ(*lV>*OXiug zlr_-bFbx|^*ICSBZZ^OREf?xXt5-urR5+H|T72)Imh7|fa)!jp0PNcw(z46Ak^g#6 zNQ|@4TJzp{GdOv7tw0R49*qtVGI5VYI!_Lyh2OR%QQq8nG0r(lJ`ES2$HsbsUeqQp zc#mxi9lNgImBWy*O_DBP)&&0=e*0MLeeoD+Q`PlSP_Pbuuq>k$o_fot@wi>-7i8LN zTYx`U`SjBA%@J|>%*%E=-+MxyX>Qe*vFV}IJdM`7T^9yDn^iR$X^ubXGLDjj&Ta(3 z=ghwKn2HX>WBWuuq6HKsZq0y02>lB)?Lt8UFe}+`77x-*9G=48<%8egbPA|*;3ZUi zh$XHACTA&MC)S(`ebza#%$cq}Z!EMpR2(eA8xIq(DL#-7++J3j9>kWDojNm9M$Th~ z!K<5HzEsZ92zM4+Je17Tw7?hVGwT`w0cf$(RTd`iM6UOA7k;N*-<$o8eAbk>pny)g z)@G1Hr;E(R$qVwtzY;aNFq*AEf|-BvC`AH}!7h`clP>&TKfn%4J!Txr)Dm{B<4F#= z*&EX?m*M42F*$3>wY#%Owr6b<}$5M(hKE(;@0 zTeZa@e`U1gt8v9BVwD?QMUQA@8CBqqNE7S>J4Z;=G=q^3! z?giQ;*Jz5^eOh&=UFE~-)-J0|FBkC4a5e?`qX!WPBc888Z)_m68!MQMmO4z*B4wBC z3SAqF5~74}EPC96yQaGRQ!yE-PN)>=+qUXDCk=5slk-b64!+_m5zr=;;8Iv^l8g{t z6vLP0{^hTx^YA|vG}VFY>vKzO6XIohdlMnWW5{wSq(uS@HnNNYXNWokBlDH98FbMf z2mI!@Usj~LPxfOkM~XKd^Ix*4Vhg0S-OLQevS@Oxj=A|?<{i#>H^{^7p4Z(%;nRJ+ z(L6y{{kGagoQx#pM_TSaG?CCi{|O`w5L!bi+;9!r+aNqUriUsLT6xolu(J*E_<&w% zj&_0=$fb#Fe+F&!3I_@il7E$mH#blp!iMReEqQuuV!!Avll~8H#=$OnF zTv=M8mB9p*IrLroiEcln!jpLaUQ$lUVZ`!7N`FV|K@mdq!3@xwYGj{3r3{_V)OQX4 zq8ga>1~&2-s+FfqMM56Qv|1ko^LTV!TBPVq3_&zWI`zzc1ZxABkFmAO=wBb_M`Wc= zOzBj}is1Iq3IJV9PuT~bfvHFL zoXd)ce$k_JF_^c#NkRj!!KWyPlbhQSbZ4`pUboNlelV!->@3G|4ncAt z*!`%UI5fd5Qa@<0AKG&}l&Qy?HczAG(-UgB2Q;93(_Oy$MyS<7w^Gc-$U%7c6F>6_ zSDzIuw=s>K`n2ICkWDTXb9?upzE!8HxxKx;wKXlj)Jb#A^RVne7A4S?NkPSsVqigN zJaX9Fvm{38bqCDDq+4zID>SFuQLq5AX0!8&JkESdi~hOk8yoWc1p57aA)0eEs{`pTiTi`PuKm z-*=;{c#~hrJ)4JqIP%Nz<>aIQuEx==PMX>M886$P$q7O`lH*;B$F0=ORm5Bx8iqi1 zFpTDg*xo0&i+mpMfDNQy!r_Thn^OjlOAAR-VA-Qv0Q;_pFgbQTa3P1NY+uUFNqdN- z(|!Acgywo)O}lIESCyo`a5Vlb#TKRaTX~1%VIDG)n9sbk+$LA0JHo$Bo2AVaH+~t0*{rH z=w~3d1*54acDd{)+@r(@NWg3M-^^1O?)x!pKmoO{K@OW)KAMr~AMv}dMGK51{FJmv zr^25Fhu_z!vs2h-o^=&6`~|w**Pb50XT#jezV>_l;E$e)s}_s~IhFGCsSlr3ijL}j zL1G-ehIzNxG)QCJ(C1G?>U_M;xO6q>-PeqJ*EPLZEj=4x1o(HTnpe!l9LQ|KUVU|W z9L&i*sa$4^t*WP)nk>GS4^44681GEa=LASSGYKFmRIYAryjc@MHLeFX@DkxGxp~Bf zd~e(JL9-=FSYkl#ZqOw6**YpxQU)@htX`;&P5G{{K$mzeZ@~oW*XHEET*vjpQi|?# zHaBpN8By~{(}IA}Ia>oq9>vzd|;#3Z@ zAPFbs_k7w2A$bd%>F+=bX2YRbH9r*i0I?;Q!Qa~UQOiqVfzm%tBb+VbJ33A28Cvo2 za6oPo*89mq`k!xrqkAS_<6D{2Tx!B7ByO!(f(5+0$*9 z;$Tg5S&z~L7>ZnlCk!2r1XZz3#CB>#N)ez6OE&rgD_Ykn!A=vn7P`x;%j+ZE&8?puX^Dkb{oS0s*eF)o=~&4EwsF_0nGIRCUXRY2$2eg8^>rj8 zp@468VeA%Z9e3l1B42LT2Xa-Sm_ox1MT;j8neyW=y2E~5{D~);nnx(fMr;u%j-`tm z7CV1oU-iam(Pus?ibmqJr=k=MLV~>dXWDBD3UNE(xP{a zuz=9=en(hSQ3;;VtFJleVEwiu<$^BR3JV?ZX%tj$_ys2|C;7|HsbXoIh=zKcrRV;Z zyQVRdbIoU1n+p&DYH-aA|1QjNz*#pe!5$5l7W2Nx>kX!>@M7g1l8D-oCU5kDS7A2n zPGub*br;_#o$Must$={y33B#{puQF3m2k&?eXno=g>=yYg|O#xo+M#RI|FDSmv+t{ zRM6SGFuhz!;!s1oU}$P;N*YpZPWiTYaV=;{5frbko`c{Jmu;^^w?S z&$|?Cz+91|3%ufl3(l{uO9pzzWT+Z2h^it$0@0iWDOFMal%-G-fa1+K6Z7fE>Xr_}#oqs0ql@2PPy({7P z29J@eVM)MjBq=g7(oB^tMPYbqa%5s+elHOHTfaw;U60&jhAbPZkkhirjFqsgWJ$=SjJ4hO z(*fJ@OAN-K9`@q2(QLdQOEt&3YrCCK8XzR6M#`Zyb~e9$Cwkp_@L->ncYLh-;H$&|EEbSdixIn znVFv}=5%y*Pp&-VsZ&tQTOb8(()hfN+MKfbNL}rxyfvT9NyB zU82$7fwd8rs%o#q3L*P~@WiUHtooy{JsB8V8P&vg&8vQP#@Mxl_ZtxBMy-I=jj?J6 z^*d;q)e0NvIj<3UM@5Gg+k6M*M>&u^spek60s*}Ur*na(Aw6~Cfw*}dSeEZ*1)~)h zm_D~;YGkPXmtuU`sq8iwKwPE7h_6Dxw!xil^>l@g4ue zz(j(4cIYjNzz@Dnw46U1Xt|>o=gWq7y>hva08pGwEFhh3z4f(|L>lp6&#r0>mb4T- zeWRn~He%eryTW?*xfx5UlkpBh??>J2-}n}q9g1Ao@!8e6}Lv-yzopPXA}I zaWv|S2NY6;CO7A6v|W`YqHV-ATZZj5gB89teSM0M94!5vvVt{Clh(p;nO~exio}*4 zHw}9;`B6d4nrEy5me_Im_Z|y9fkaz<|V$EC0`Hcmf#kfL>=VwXryQ6RW zslqLSZo1EIqc8X)u>8v*Ju%K;r0)0a20M^J)_rQMt!~U;e-*x@EyBBnuk6;#_>KfH zfU-VnjUZg1H^10%Y;S?4s5cfMB1@I&Z?f51-0SVHKw~%c)TcSFKE9@T8f5Y_a94S) z7^y=rq5uUj(%^8pv3J?guu<)5S~t6UU?-@hDIYI^1v9wbqMLlz)#M2BdyjPmnd^W} z_oE{Q1V4@Zc_B~)o$O8&bP3`7*ll$l(7*e5UcK>{!qy3f5WB%^ z9fMV$`$+|wnV_SBHHn#Q0~zVm{&cp^iu1d;8@FN7>%yNgZf@-?5{i7?Dc0{v%$u?Z zWUvqKhzwjJ2=njRuh-#Ub)=4Cj;DxhKHzkI%Fax|3^h+Idr z6T05?SDbfqbwGzNMOUytYg0HgU<_=80xHYX|7qFrYul-F6i6n#B!*Lf(vMYx{5Af2 zU6LJZ=VxYw@f)}wz+-mA!HpYU<8ALNOm*i~YO$GkrJKyZO*|j2qBG@h1Sc(s%Za@v zlL{?IXxHY3T>sb};%zI10zUPFncpeV4DG&8)}HemFJ!^F_gMy^wRm4VK(on-=qDhzC*7)T? z9xf@{)ZYEY`{S;`Y_ZUdn|BvHWl|0$P+K%15-Fu(-CKz-U?Tc|W4uvSi) zp{4+a@$UjX_54hBul#=0AKNvD?d=H!gmLu^^4pzS$@a?Llwry9$3ADQyjUw;v!N)h zrazB$m7&<}cYxEKF8|h=Gv==+Km$;5sA)em)2j`jg+W|LtFsJ}Fzc9U-OyB)d4xDYCd3Ux#3vSPa?Pd8RR@*x^MsCp{~F5C79#skNlY*}A^u&n}fHm%Pv}2$ddHCP3X)0M?MD0vtY(d2Gefe_1tWq*IFal13r|)0BH^{?0mi&9z z0)2`=x4Dt{snmi3dBjLY=GQB_W%{q^Oo78_xmgYN^W-Rj#Cmn$-xlSsDgvk>dDcQI zGT_^+t(QYalUm~(hr|J5Yg%)m=s=Ox$Kp~|m$_S0h@I=Mred+EZDng2HO%ZHPr&J8 zujt_8qX*fm5{JRRhfV0P#z~kv8-3*QLmouEOW}dq6SeR`(y?yI;#wD=T}v|O1bT+u z-MrPkB@*j9B&{w3dIK>3`b&b+DtDZz^_=tT8$lru>SloDH>Nr69SwEht|&Z|d`@n5 zpf8W+sLUK7Dqhj6?y$PUM#N`)Y=`7#87(iz_a|DWAZ(H$>fy8Qfte`6eh$2cz3@RJ zSHPqDMs*>#cLLforX^|>bx0g~OXM@h%%O59BruDTTO(CWTbGCC`BTY83_#T+MAM}H z>%(L=<~U`6VlxO@Myz!K4vP9Kzmmg>6__VY5hCQZ@{Z-*iUmzzD$`&iQFbeP=0Xl+ zVI>mz`9BbER2BdORYju9RPP6o>$+Qc@^pKCM79VQa$>+cl1Z@Q76 zaa=8686)Qu^8QB9#1LzBS28TZq2f;0jgYzTosRwB)u^VUq0!UcjMEnSvw%#YaIyKC zpH})Ah;C){#Zr}nq6ioN{`5tkR4dy;LMl-CHz}GrT1@wei<*r$ZoRUeR;#mF9`KoD z^yO)eFc8>mqmMAxloabdWxhGhS@H0!8ndC^3;^}YpLA>Ja64+s{52k z)bQ45=I<<{H~iU}0X22V!c!Ik|TUxzPvMUey2h{7^jE7H6a%QKb&%Vtib@ z*Qv&?GUX7K|2q0u5EUc;6pbZz>~`_h);=&?Z1gxFAlT0opyx$`P>K>^?!z>!q>Ufo zr*1l8f6V`!c9#-K6!SeC$ZRmpCDKVb(AGYE(Zo-I^CTY*E|R*TLxR4|^yp3Nzpq}% zv++sW$`VMq#bM7MR%t1!c%4Uhzy6{_5U42Er)n}5qPu(-fNAbwF>S<MJ!+$l0PpUtT64VJOIG0fXp z0w0+Uo0&yH*ZqS28ol+5^mN~!>q~1$_Lm{lf{Spso~mN9$&<;nE?;4cb9MqyCe zaAUO$!J;XFhz!xgL8K$kwZHdxXc*lO9VEgFu&>*o#&V1%hF2t{jBB!P5Ui9743+NN}Vj7iSSZ+LZGRNVU=dcV3fu0=7D)8+iBn!^aYDRyTFvykrKQ;?S3}> zbD$dE&nRAw-I*YIa*ck`Ii@idE$pC((4RX*+6ffeV8fUrcQ$PrCI$wrHtQmB^CuR> zSl2z3T-$tu=3n(hJL`~4R zk{MWx5y63t-{V<*dYdWDI*vwTU0fu1zlo<5{W&7o%W&b!_FdGNguOd{hV&De|AV{g zxat=HQ@W4+MLj5AO91gh*efFi;&MhP^it2YED=&+F_?6{nJ60U)IY~_b+yA!$hVzPSyJk^}E}*6C#Gz}y&~{kAs8Yd|?Ij zsF|GO6ZHy@+LW_C90m*r6(l*qom4YKC~q^E+Kyv)58mBB1e3!y;`slk;TQAS%CproW!UW_eDqaT4MGo`M8NIx^5cnaRX@vE!L zx@X7k+#e|g>w*y};2ejq&WyfSmxe$^vkv7KXvN8B=7_ptFXqr|Pqsc+m~`oIZ0ZnT zeGLz$-5Q;D>Fb;bwt}G5L@;G}sp+w2BZ@?^yR=iT;*MX1f;xixfGf0jiV z=LKPObV`(Il6tRwwY?E?$yS<8#YeS%>R4K`E;m!6jZMxd<1u2;!cCOY+15mNyP9@Pn-m<`I)no)7R99c8RQu-I+gPdCt2?0_ZFcdNS|WnmG@ldvYFc zfqc=T*sdV&wQ5P|8Si8hj}rU1cbVucxNmG@xdMnbH;{xJOcoO6Nc3SNOeaCNRk z&#PUG!D@s0kn&?+lUJh(L*}ykc5!s_THw5sIbbOtbB)tsaZXyqtI9czeoK3)?wkMT zPcD>{5}B(d8Emo~PuU7gp^*0?V_29z%BbR*ZVM|y$Cj#_*e(6*Oy{=e%QyLyKeH?< zQPLPDrrK~Ff`7h#Xr{h5aSD8yIXe1EldNs7-I6}aWMayCs3ncV9rm0ft@q!b89Pc3 zp=;CN!0mS=ViI+y7KAIt1u?+e?UQTRWtQ(g&FN7(e-3R8dL7cTV4F)vxf_UAt`C$b z)S6}dMGw`r{2~5>D8rVAB#vI%PA$!CbRl3gh!?PvwTQZj85;8OBl=OdT7MOXN>K}4 z_;0htCe~Z7Atu~fJQ&{?W79{2(O}eX@pS{Hm8?(zE9Q6dzp}aE@Gb&uDHa1KSev9cLY5 z>%_#Apki1{1(d%8YG)sueK(EW4wg(1_7ZsaWdhi~HA(q$f zjt=im7Mw;%!>KoxPvIKTTht2FAD9YoARl`8VU<2Z-xnhtul3*f^!DT;_rV2a9bdqJ zohGXeI`R=5SL&(XT#(S5X$2t!-P9jh|mo54W& zek*w3z3UP&gUbMBBG*4c55}Ms`-vXEy>>3@C-&tV1%hZ4>8=$JEqdy8o;Ugh7{mE= zB@AeXef3KqN7klcz^0Qa{RFAc5iZy`2vv=QA{{dX46X_amR#B zkeV$_%>fY?RhLkPi`6#oljbu&knsA4LS@XD*5TlB^DKUgiAF0`Hg+~3x2&x#U+mp} zLnwBTu;$C1oVSg+J_=PIN|#7`DNvX^A9W*3*+8^wyz3)Z0&x+uX@l53= zd@q*@+oF;U4L&wSU>5SSlkcRyiZAH-7EN0^cC4+mIBYan#4?O3C@7>C7ZnvLWD9t% zcY4aDFvut}My_jFkPBu3+^>=K-{9!W-^M7ZX{Kwu&Bk`d@TV>2Ic6`>lY*qZ; z_pCxLsY67@5tB0Quyb}_=y`})wOHQIP^i7_JVaNeow62R;b2#xb=9wY&~5u_en_rL`)C*WqvFzL*FTPZ(Vw}~5#mkgTCgvF08k#ADSNHu5g0C~0)9E<45 z$>M*wB}4n4lWkoV{_-WCchY5a{7LEcJi_f4r@wG*M}C#WbYr#^cD{OZlaEj3Oiudk z&{+!>==v;1UxlI1kw=^VH@eBc26c5+Ne%sf4QHvl9sVn|Tbz^Y4*4ks+fp*LzYh%0 z{4e%O_juJN4Bvx6pv%uum{q1DFN={Br zw|+OXLPNjqZG~tshA6fL1B0cA!eYb8#(Oy=#tkg$?M_H=99JGt3_UHPDykk|9=Gr1 zzvuTu-E-M$tkue8CH9Ou-p>X*GN%7~l*g)d5Jp0HxSu^}Qm9G1P2PZv3j z-HfztZIf&ryW#fq>HhSy)y~2+-GHNIX%5f7ro(R1zj4W`Sa-HB7I%{w8@{3u$6w=m z)014cP?q_@g#7~HJ%1W-I&c3NR%(pe)O6kd`N-V5cCM_hj7^!Z=+U*#40^4vu0Dr+ z{W^`t{3N@orHc!{bPi)<@4nvEGUwO1_At(L`GNt6A)rH>ia;R)073w|vJcVV=qC`@ zTjr=v`F$CZzIVyjVl+>@h@vB1f1laV1^uVP=3i%9Zb_Cpd)-~|Ew)r4V*=h+|7Isy z@Y>Dq2|Kd~o9%o~%STU6P7J-CNXV5Z-hXy;{bgH$nlfSJqvq$QrysTD_!6>ea8XxI zXBdmvBSiZC=s-U-GRE+9!_Jj)*R)`u+u?3^NRd!pj@+lFsd=&1v0%vo7YkpKfi3ZL zAPg?ES|*Pb*!DU9J*ugn!E=J1GWIXxhk@?QE& z`u;!l0&F0Sh>^;wRS#_w)4oH}}PrPXPN(4iB<_Dx+~ zzt*TX7)_|vW}%uo+vATd7kwnA+6Q&@hWh&Iw*B2Q%o9nHvSfT3*8;8ya%ZR2_M1YNr?E(an_U|$ooCm$`Ej^}K zERq72c2@S*wqrv>(vxHD=L4^0vx;3i)Gbzs&G8IhT+AoipGT6<&dw%wK-BJNIl8eDk$@<^4~r95$|&N({T+YO$I(2yNNH(#V?rZvYC0cVM*TdloZXL9W5HqQ z^@o`*so5PI>!AebEgSbH!M&IBx(;&-3-pos%gI?L%cz;Uxj z2w=$=x5#Ru`Ch-edZk*Dot~U5m>){Whk%bvNA^c}!LP*v7s&LS*9L$P2JOzrt#D$$ z(<|x)?CRfwHX4~x_%Uv~WfN7GGbfG^ zw&R~$yWbMVn>~iNaY65D{Zj}c{wRQ6Rh-QG))Nl8if_xFp~ z?lh1<^mw)}0pRm|1n=p&16ifG%m(dFCW|J0?B-*Tm3ooJV*Ca2Rv}QX+vAGKg?Wv^ zlE1;QWNL=vkO1M1?VuR`#%lezI}5)U)9#tl|#6C zc>}%Q)lBkVbWU+V?mIL*tb+Z4>=rVG)_iWppRca3ok1Hl7dA6iR#s1!eP}fhQPF^# zeRo%UXa^<|z{1`4-CC7G5JflwLt6rHBI)hm!&~}JKgSkPuljs*Z$Ylh&w}oF0T9ei zKC>`@PP%JUby82uK4P=Svn+@%gBvO@N5l4LxBB%iqTPBp)v0o# zzcs2&lXuyl_z+++7<;h4yXy4N6%u!l?fYzjhW$SGbO50n$83AD7!yZ0TdWgT!3J&k z_kYsVbDLAYyMvU#ICB8MUHHQ%erRb4bLYIp5SOC1Z_(X2KmtDQ=Mcl2@Uf;LBdE>Q zpyxxO1HS^`U=gN)+&jxAGjMmqY!myp;Z{0W`M=iY z@&YLkXv#q_DdatTkmFxSz1gZeZZ0P!jH^67 zwepZ%f4ph|@5WqVQF#ewD+xV4CPgGdpc~%0CpI;2eP>rbx6@Ktf=geGuXkktWfhe? z8rg5(W*{XK8?LXfA&a6dFK?@|VWR!{xZ=(WK@yg&5Bnfr@Rw>|uupFc}cuaTv_JzT)!eEUE7hLT|g>4VQ0 z=)RCtBDtUQ)5lLIJIuSyniT3p9_Rh3uaWsA)Pb<=l7|j5;WgyrpAU+#M`*qj6Y3V! ze`2fc?|{tYrfwkue97?8kCW3R+Zlr1>$}5vK&L^?cnXK_^VsQWW>{EaBK680T$0(O z#nO%VWRniLaB&)GV@uO>=X&wLKo);DBctzI$J+~r*mzYH$p3g4{BXF_U_SZwQc*;> zt_waah;0?Ta+f9!l7wdLm^|n>%#S(;f!~jht-G|5li@)d8`IN}_g^XiDT;aeQ4B7) z*N6oV0uNQuiK>u7N|X^V$JFxxNa0`@x>ypmBVE`P&n%mhHUHUfs5l*TfzKW)N=z-P z1T1=QRR>BumHXl&ndwn|HB=S&m_L%imLYwDAL)t66X4)(K?YR>1;v8X#s+o5_|-p2 z+zQG8wT@fcQ#%NG5>Y9Bk6YeUiQ2->*QUNECOyeNdjrPIPJhzy^FWYj= z0lny}$mJydH&xtdL;{yL;g1bCjNPY^>XF;vxf=gZ3khM<|Ng-OkO}1n@?UATBjmP+ zaKt@Nk+~8NUp#cI+57r_@tCsZBIIKw_(hdCY{{XR$%6q95D<9#E&SX%xjZPuJXB)_ z?y|$F^t?S4BWe3>rCacdGp3w_zrU_E)U0sah_^UPlTA(K`TdZ$x;FTgVAw**A45z5 zbw+U{64r(z{bYb4?pa!z_*73t z+#T^H9-`=cpXGtempAL7i*f5FvXBX0i=%Lo?u{vQypyC<63?yBgKgC*vOsjP0fY&7 z3^D#67u|RB|EX}71HsVL_rplTx8u|}7FGG=WHvxPM@V0NP8j?&8G@cOS>hm$L_||s zethSZSJC?cfQU+*sD4Whf?p6O&ei#O4G6%^VO;HJ2WC?121Ic=%eY>N;0 zbeJ3<5Ms*O6!`m8cP~PuoXh@`NW}wIY@|;vO?sq!c}v@JgInzAPvy6F1#O7GTpxy7 zf3r%;o~3L+0^y8;V48!J0*fP5`JyT*5L4Xq@bGZ4+wCRaX@`5>_2E}0I};kZd}atg z-|~sIA|P_#Dj;CTM>;e-UQvGPY!U$U1nisdeHL_WEg|%ZyTk2}sbQg_lZMYbu($tqR-~Cu zKzO1wZz2Fu_fOO$HWW^9XtgE>)y4b!kg}{M(GRCf1;RMEuiR<2!wHlSDb-*;{=7p1 z#uI3@o}^L&)k_3iHfl|NqxDzz z=eT2j`+KZ{j`uZ`Ge3zNm`d<#24S%UemdXOFx*!T2tCKpnG2QzO#ZlnY_;_-T}!bM zON!=~T=3mIf9j5^H}NEXY80eUYw=`eVoG*=LOPet`O!1QOlv2iu?&V7Fsk(X^@=)Y zu(1@zVzqU(OjYSaKo)^fZ3tUT zWJ1hmjc&T;+gLKF%yDXZkVm4x0pH`hB-cC)S(&JY{=^C48TaS7?> zO^dhoCA~&z><&<5MjQlpf|X@fM`cI&2C?t}vLuX{rNKRqoXueIxbE{)eSCgV(bKe4 z6p&j~UJS{$ zSwLgfk5A3WNKeqwG+bC+PFG&ey*ajVaIA1>Ju1$~_T2xi-vQm_CnoLZ=Vw_T9gY0v zp{lB?p+O40+3t3d?e}a|F&BvrLDIetEBWQqjBFTs+Jz__M)*wm&y8oRQV}S9zFC;h zEy-bfdnPkcEFU-GYHoc4zscI@yKK^qV%hE1Dcpf{vl9hqQJM&K%GX|-cjh940CO!n zE58S)b|wXUdg;`?%>mQWNJZK!83|*&@8f{1cq#}W|h_C^1{og|;hNzyS8YUpj zx<)Ld0cufq%T^6JP6Abw(H$B&lbWh7`7KJ`d}9z_+cW!IL~OT2ajcRJL~|Y=llp86 zIq^6*zK*wSX#4iLLA)VseJlnw3q~xE3=308p&2E?JM2Lxa~C6ie6rmoi=4MX$ToXB zikV5T_3{Z~%Lyi@=mNqxvuc+xF8+6m>8_Hnzy6D=t#zRY&S15j*aw^66JCDvGG7Vu_`=r@*5 zb90F1dpn-wS|zVAgmuL;PB#bJnV z)C8E?yiS*j#-01!&CKsr|A?Z#XBT%4mmTO6ZCE<#w%CEJ=RQH4qM1Uz&tWKpkis(k z-&NG_3SxyYVe^WM!_X75vki1~p2yfH(Z%MgbRQu7PEJnNuF~)|Iaz~9r=4Hi?d36_ z*1bja8a*3=fLFg=KKnQ&yiTD)VVZD0k|da49%{~w3iKDszFB0Y z{_LC&H^l>jLVS?SmfToNC-SY}y_!i-qg&q~NV}DT+d_{n8@V)`BE$*?x-gcI;YUvM z=Fq$YM3R7Bz%lWpl6|RY0XSTjP@$gEGiucw&--opylY$S8EiJf}o-9sc{ZKuF zVA1aTm_Ho}(bD3%WK-xW7%C#FBB~L^mKvfuoFSpY&>-m{w-+uyt$)kNdHR%KTO8vhDYp+ z01AtIv?L`tJu2%y1Fpe=P3isl22Q_#YTBqObyoT7!5qy#FKl^*&And>ct%cxL95oN zKk^g*k8@iao7vq`s&wvB|+{VDXz zSFplD@%fU{@`^eF>YqT@chOBqb#fymB|0JB)%o=O7D7z3Q4&h1Ba>Sev1i5H`!+^Sc-(MsKh~7 z)IMmd*6D0y{pv7wL>TVs7y-lhGC#&g+wFtOc18M%Qw~=RNB&nVsi4` ziJ_3sTsb{591NN1Mz=4dv@;_!vxBWIY{2R1X&J;P0+FjFIezWEM#}1E5Xtl8do{}# zvD-oU1tGrZ21Qu!dlNzB1`%a4XJHYMh`6=}2j_S#>B_(PV|xC@D>tG~aI)aSLP>YN z-{$1xjA)`Wvoq%M!kpaP)SR53$o{Rb`@Mc0k0+N}!KB?(;$GHeKe2Xf#D*rbTlL`O zWVBbJ!=KcPK?0XHb2jWk$@5BVb#e0h$HPHQBAnUzhf! zWPC7Hh$U%-YO2bNgI2Q2%LaNFE^^7>3euDQrTo^ns~cF_fmD1h--_gC_&BLKQy-QY zQPwow^5$~Zl{yp=S(?nStW`DNAB8g^yyI%y@_%upqSNIzn}>P36)e+m%uu%L{|j$D z$H5cm#r=OGUxzSbkV=Sk(@~t!_%R2bs6V>!5LpN%u(sob&4xC!!+!$++P2JBQMD;u zaeP$P1o#wYdU`~gH!ort3Kq?88Uqb$F`urqaP+UyuT38BPDV%gz?rMqut3_5Cqq<% z>4Hm2{^?}+*62E;OA1dGgA? zbUPWVgWcY@!SuKl+&+hVWjK-OT__Xnq+B@?ys+fIj~#O-A5uo)hsmTF7Hm2aO8?9g zCXkLWG}t2eK2Mt;rF{kYPUDFv(}+`>qjs6e#5rLEe}Dj>oGNK}0=7=^MBt{Vb@2Yf zr(rs}r;`5cfdI-JUERy@V+NT-(f58v6f9ks<4fXv&`#?AqR7kw7};}5BHVQ9V^Z)3 zL$!WcXX1gy2iODMV6>h8J7Vc$-GojtIjdJb{Yep$w!>mHc7=C}7ntqR6PJyTiaS=T zh0{T7IPiJ0!5xsHLH|2J@wtS#vK2PGpn((_Zu5~z&bt^2Nb!y_!59kaBRd+rJcin< zF{Bv|y|Zs+z~;{v4k3U!Y?=Jzv(VsGtA{u!w!nC}r+9P_{#MV@=d`$o{@1{{SeY*( zLcoY0F3wa2miqr`?cC#;Z2JJdIWB})V>vX3)o8>LmUx`cdMIftirFX>2_ed1Xj!Z| zCdH=ZE#*v36Jh3%Q%XpRG{-!TMn#_9dwSl_`~LmD@BPpH`P|q2`Q6v`yMMpya~;0F z-_;3`R%j!*(TI)I3Kes)UhG}!A}S)xaMM*Af^e#w=+b9e34wYqen&jcWaPBFq0yZc z^zx%(G~_Lr`9$>NEUbr|2tJ#H?)r|yDGg}vO&7IeSc&znG-X_ZLNMOR)1Dnk(&BMw z;WuCbe}{nFoW;%1h!(UjSiGF(XrF$3{;E(W^g|5;4wG1sEr_nEDv6Sc=p~>fnJ;*_!4Dz!pol*FRCSq_RQ7=e^4W--3*ic(*id;2w)uK1%EOfY&apStW_?N6 z4U&l@#QH|5O74@?d_0+Ox~%rd`y3=zQz#CuyPKTFo>9J{p;eTU>#RGfpG|@j9q9P% z4k^h5M8aa|T!~DsPdUoi8qfqjENsot0w$ub_bd?(OQnmj_ol&s(jfoHd%P&W8`LlL zG@~V+fipBqp+w~T9B@`-D9ta;9OSDpFz`K30zJBn~B>%I!D-Y_r0RxveZsh?a z3)}{RQu)_2s=k;vR@(8W>)WW~DlA%mQm=Iv22@^x1z0^Y%SQ<5U$349*4GOI;pzAP z_;JNV0ic&|AO-acHP-CaO&zT?^fT`JEGGc~km_w;q=Y~%L1z5|Tf@ncTJkgek#;^7 z77VNBP0a!E*y4=%nht5ZiuY4>G08RQ&U_7grU={@rGuV@KH{#rWRtR|O@hHas~2m) zhpypEih-Q6Ggc1~t9b{KhHTJ)S4!#TJRWgOo;Df3<(+u_;(oU}O^dQobdF3ysX#+V z@PMwE2nVJng^svBq419@`Lc#a`<)UH+p9zdUko5q0^FTxNeNjU38^zZj>(lVyGrXq zorO=67nokb(^lle1o?+w?Qi(L)|jhm(Pgx#@h*Geg3{eh#bPBj8JXohe>wTUl&7WroxVDI)wme_Q)pbDEBubC>~=?=Q>koMRz+`9tBCPdsH@Yrj9q9DJ>wJ9 z$%J058Ksp>PypAem-FWhFrMaXWy%sN*5dS~Jq+AXHRG((IVdL${4Y4IQtCauE!v;% z+Kt6C0>ht{wYMdCakzvxF(qgS0p7Xo=r=cq%oAUbNUH0stcvKGoIhM66@=!pI@p;g z;*teSVk7sAf=bd3OnZJ6d8eexA0gWsD+a(#85t@i>blV~qJwVU*Gh?|JUWS5fj%T* zJ#i zfm5z%VgA*cMw*C)U&X-j3h|Q}T9c=y=IAmDPS6SI&^|ddKS*3-kE)wq@_>*v+x%jM z-ioPW9BX1o%HUcrHYx-vfq{FA34;zC?!RnSwkI{MZ^AWqpH{+D%Z*6h*D>r~8Af1v z?!Jqktja0FydU_#u=nn&FPO^RZe7Wx0rgXLOo?8(~z5Vh<%uDI%cZfn~#u8N~1lsD2N6+Qsp)0 z#Kll>s=mDy7P&6EoboxTI%OwjIIwW>(foL+h|eP40Ak(wsISzRLyLPiYpHR_{Hw7t z+t$1lJ#qrQeb@VV41~bGM45^5_#8c;L%)>#navdFy*y@6=DFnPlcKRQFAC>P6$T~!vhywHvU0nHKjk-hg^|%a`9Xw+ja%>^S{gEfNAn)s3t;ei%|3>r z3G71*Zyg9Q03r=~_#l~yT`x=Vnx>f3kv(^e$#!87RgqZ}p&OdU2^9v0W25B|X@eGF z60IGbi}H)Q_cT?_t%Pq+ygLY_0mmhS>jOcD$;WK(^;wqAdDxqnll*sOCa0qg>jVduYfMrWsXDD;=td z9m~?Anx;1BJD#v>unB`~{-X?nstYu42`wqEdoB_u7Fj4oSEQ=quCkjo$iE2TLKB~z zDoGK|Ts)@-(T@8l%jz_I}s>esI;9DejF+x*=BN7psJAePUNJlCo)}Pxx0u-OC*Hhq2KF) z5%FL_$)eOdR~&+?Hi*#Up}WNKPze^S~{}-h8H{&+zq)G#?{;4GZ-)s*;R*~S?SU=23 zrkA1siPMzWlTlyx`^L9kLXA)y^V_yTrYA~J&@Mxk0yZgvKVB(X4l(BvYVeP1XJM&= z$pb~kEAPQpbj^Q>+wnJX%7W_1NfXV_ox2S?MK5NbFXuf1vJOPK+bkI&w{Sf;iAdt* zy?}x29h`PQpCg+GIwdw16)h9PLMei_$a~yz8A(=4lm*t?m0UQ@8E%4@pUgq)$qOFh z5KHe0{`BwZTfiT6kF51xx%5GEyDc0Let4_XBsu%k;5fV)oKD+Kp6z8_Hx#scmJ?9n z8M9+!aPQRO?Ux>&EouEdgXu;GpT_WSSBGzWW{{k=)M{(wQ9}BwY1BX8aNhw96;NK@ zsgIB3BUiL;!{aeM8*M#nA~#mFh^Qu~!Icj}DnKW+T; zJ!8e|ij0(@Rl}M9MD!IYc@^3>`sE3%r$a$c;Y`c$@at#I5qC!xd~P?BUmOkWxP5xv>Sy!o70C3L zP(a$xV_iH$zUtA(E-!Q%p8-Ls&)#Kub!vQ?{u&i)792cDI0!yj8-!0a z37FY3OJ@KI+aUptp~bD(qTKZsCF9>U!AQ|gxacAvF$`{CAQ_iDIV zeruwfIY=Iv{&Pc|r&_d^9M{jAGui&PJsKDe*PD#FyC{tbtnpIOBmRNt!v3v)f&va0 zW7TiVyL0rII6KMVm3u+|%QC}X+$o&RuQ(9?ZJ-PJzd)#Q+dxAZQ(h8VUJHY`I~7)&hz(GC@`dByVY-BeI!v)c~rk2xt!f7!;6nvZy+lyFz$fV8U}$V@>H;=0HMg|mhn%)|L%^0M{16Rx zc_w)WQBw;`DK95e6)y!|I^>A&^%(_+P?JjQ=g$!PUv;Z^uW?F#qReB})%e8%;6GSFD|1H;Es@!p`x3P__Omm5u9f>c3(8N3OiR$*Wk_ z|AQngDyrmUZ)R!pN^@406ah<%i*hh?b8yhJF#Kb}Ur6#u+Zvji%9+}kyI8!c|CfUQ zq-a>0{7v|mU4Ik)TBMEZUtXnVXZZ>X%io&+CTadBjUU3z@#@ci`P$zkt^bwux^X=6 zuMuHl=wc{l=<;eS3lj@7Jrfr_Gq)-;HxD~I4?71f6AKU1KcfETw>Pmg^Zftf{woT= zd?v;`E|xAfrhkwAi3(vGmw!h83~em`3I!e;LpyVRhzGrishOdxjSECT+0{v2Dpvk4m%*DgT!o$w?FEkndS5yA>^#7|V|4{#bnDWnr|HG93 zv9bR$bzHSIF~JMX>@{++o^?OxNh(`#N?C%0?PEw?&fC0JzC?azcb zG$uuayR8MpITd^m@~I$jB=BbE$Y)c3c`JKMASm(9z3!*TImXeWOs8qEbC308$o z;j@wl{k>jPRd?^R0!uQ=W_PWML{xksOF9 zhhVxJWd=Dy$d8?ZCF46*cM^02>v`9%MjLM5S$JT|-Iq_8?#>Xa0SHYXHm~BbTST^p zJ`Meyy$jPWpVJ-Lr(MqO`)GR~XdfRU{;umP``XyY_dY%X3Jv7`vJee)5LD1m+|~~# zH30!)v`82ssOSFV_!*57zHidz@jN$WS=Wp%4Zb=!dt1&mT4X?We#O=o+64+WT~QTh zmV?U7usOUR`N#kuRK2xdTsy88m~;k%J20&<@WUM0W|<;?m;>z%6lj2>xD*E=REJ?< zY6=9Tn99q`ySlnoxtð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,rP?O!LRzSSzCFOl+h%&D)V!wzR2hJ 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_WorkFileSaveAs.png b/website/docs/assets/nuke_tut/nuke_WorkFileSaveAs.png new file mode 100644 index 0000000000000000000000000000000000000000..661f44632a97348db0735343b22c6471630a3511 GIT binary patch literal 26227 zcma%i1zc5K)9)53K|o0<36U-d>F$#565-Hrh(mWM(%s$C(p@6mQqm&b-E}uUZ+`Fl z-FwfEID5scwPt3`TJxXTfeP;=&`=0a002Ofk`z?}05}%_cw~SK2bTCD(xn0bg1VWC z224XvmfH|&#b{sz{QzNfwz2{10f1M)*~Y-o0s9O=2bu;SpbjuT za&oYq^l!wC4F5sf#=*|=4`m}mCWs}(3StejXL`f8q3N3-r4+CM8~ zA=V}jHGik{r{-aHmJScW?IT_w?s=VRiX5->!Wv63i;b#78K7U$+`^U%tX7K-N|F8>4d5sLYVP-H($e*Xb zc7>27?60T49xcrtwgR`Mfwc)AxifW5=PFi4z;ro2WPHq;0O`2=Vf}>K$d2I zqx0wK?}T`n{%sUqaF%j%+>&PYFsPl&zYMGlvHkaf|Lj9j(m&e8ZD9By7(Q|vJE)O^ zA;jp90Kg^uRcjA5hB+D7L4-^|#o{9uGB!2?Md(7R@UUY@-!O79{w2(RbZ|0-fW-cH zb~63zO8!1GUZ($;`(F$H7fS;6`>PDJP@t(}`iHFpAO2x2A=aRYv;%En-Nbx5Xm9<@ zKu!1$4FCY@NQcON8DSyp0D)=+*9?_lTn36l)lUSHR$4D0Le&a>dW6lF$nH9Os^l|> zRv&%8$d!rRB2=SkAqgqMk20F-j_3V$v__QPcxpe?4P7HL_Ix~PD&D(=rq{EEqQ!{~ z8y;k;+a2aK?dSw*60(ZMA`ht@y1R*SSr?rL^iGE-hz9$g=spS-aTZmq_C4@F7&jMU zchRmBk?M~7DF5-Ay*}fhq@cV1=7U|~fIg+g=mE8c?zcS^obDnGDGN;l7cRbOS;fo7 z@XM$;QR}zl_1bE9+y=t%wyeD`vf&Tj(7*9UYUNzP+Bcta^V&A)ktrcpgrJ4uDCN+M zFqtbhP*G5c$@dI=HNLuEROjQ`U~+;!5nhfqXgc-vKy8uvbm(IGVo~C~n}4uQF25Hb zvY!=Ct2NBe$?hv~{zkFb5R21b1c zghsa!s`XSE0WD>dUz|g?Dd4g8#70un9stn4Km0z5r9&qK08&6oR7k}+WoO<^gK#R9 z^QMdfab6Jf`_GEJQcZILOrmFw6dZTeQZ?ThVau)?jngN(QwR;M+@-3&`5Q~81b;My zqAh%%=Lnt!|fEt^J!f=A{&F(^DxG&;y7U*8UaPOc<4z>WJ4U-^#sk z-$*<7td>c)er{V@e%-!a$7ZJGda$*A{j9eFZISAzu-@#OOA(_mGFjCx(Jc|jnXY{P z+u@2`o4(mgOK*7S^~CAoU@4gc z`YCU*$>sQ9siRoaxzTCg*rT2MMswCn;A}z_M|zsZCd>~NeseDLoUy@TweViKLM82t zjB`i7A$00))FzA~HN8MGp8j}v!Q<)xK_fMO)x*)1x=ex0$c3S)O{bly8FuBX&9Q=m zBH+H+*($q+L+k(C?tw%0)$W&e^M}j&cSrGJ=;u9_Zg14z}+r&Jk%z1koh*c(6Z~Zvn@lKdmllsXKPd4L~v%*5Hv;iLmF&$qX)on7B-Pc3iZ_x5Rp0;(a3dSW|XYb>p)EVB}JI35>#mo5XGVIH7 zKf1H)Y|53zO%b4@voJM9CPjV!rIE+t)V$Rs;v5hcdyq^l$D9%&2b|!FAnNlEyWR7T z)sm0p@diE3!tB#^?uWajwLIBLHNJZtqs`u@8Llz&T<}WR+!Z(Ra?bNrxjIZLIB^*9 zMIbOaIEm=Je2Rt!slN^nU~D@1fsj|7eBKs^bksKAnu0VxuZdyVdmJ+X3SrZF__+Zr zo!^~oJF_jXq|BYt{=gobi>YJN5-vcFr1klkg>D!PdH`0;#pS6F$<8uyk$&Wx%dVsw zHqV)ui6I2weZ4I;xTY^|$fOpg$VX}>t7xmbF9_g3#t!wn0YJv}i40!F%b!OIFR`%o z8-9hwh=^!nSWQPEw4DfH=sIt*@qUHK*q9PE9Q;^91)jaNaZL9t;Mvb)D5^|QMq7pG z9;9G^I*=q_V7RL;S>A7k%9oKp1v{HbLc$qx=d#@h@UD%o`)=6N@7GjZyf?NG5eDw@!6c^;+_fJ8^jb}Y8~srIx8+TuIjC(2zwA7Ai#B;k zsyx_K6Dali>~z%BCPx&`M-=q*UNWcl;Wab{7_({gR|U@tIj{L@H(^+5^1+sJI|2f} zV&Iecj}#-weL4!8|Abm=uujH4?bvgnvKx^x5e$AlOvmRBni+0aBkefyb&d#~G@gsfjJmS*Bz@vTE@;%a(& zlOL_2@iyQhV167qYkUVITk(1y_O1uD2501VxU4sFY1@%dzTumwQgFgya5@Z@YA5O%;&-xDoouNs*dRjIieF)rsIRuj{h#9mhr4 z<#n$XJ(`<-dn$=|_fsed{Sxwc=qCA7U69bTMzgb*2^H=JTEPjD!;3aaQVj?0S!` z*D`}Znj^lrs>KgGdZbuvW*zB|VF~!lQZt2AgXIeD27l^*qZ%@RHWpj|g~fAWFobchyQvi{Qr&hf9;z4x&h9Vz|71{lLbFA?d5S)zGcl) zCRyV=KXmVVjO(e*dbRW|e8={L+h;1VNYe6%`fEybFWL99VXYF@-+AClv9qz|C{W)huN~`9tLEnB&ez$#8m$(jj{Ag&goDyKP9&YM>TIQ@MQ~>w8j#uE z&i0aNwE8n1R03J(*-x4%mV=5c?e98wgCzS0h+*S-Q5pR~OSrj(_3nO4yhfl{ z9uDBFOTW}mnkP(j4Px&}ShQYz#o&5vK*I9p39ejY?&cfnuDIbbD%0oA-*sslktN#N zvokZHU5?r1)g_@>)a4rVxWQAUx`eIASs!J{BYSu$Faz&pSQ0JwR#{)8!u$Tj4OV#C zS*)wArInPJ*vtEpn3&IgE#&-lP!^0^xA>jEMxZEpy$(8HSFb~XsYxH_obsBAYQ1)t z|2nqJy)&6SzTd0d?fjer3iec`#$H^kqWPBk9$x5z!{e(%GJ!869bM)04vf259>Suj z&{zBf-naM9=V9b8;9@%~FfIoYS+ldV7xJ;i5OOmRIW?{H+u7n9iYVh(Gk$l#9IlNaOXj+cksStHWwhe2@ z5G+zET6SvKcrC?_o3amG&{0ttpPtsd^dn*R!37!w?&=yFdCGA_P~Rz#lu6RRqe6ya zu*UbFo^j>7sXc|fL^>6gp@@R{J_{6;{h)y4P?vRcT{X7+m7GR6)3{zdTasc6Rp1r}E+T4DO#2}kIQAd6-6!uRVTi5H`*H#)G)r-N=4)QxdQPt1pg@uIzuXr}IiSO%K=Pq?bb?;^0ufQP=;>sP`}H z%f`3!Pz(=zrKX}9Rh1Nan`~tXgE4yaREAXRyp*TMJVfxFhDazja4|=0&{E(r#wVwx z4Xu9g3P~Q@OsUXHWmM0i^Ns4PE>g$R{fJ#9%7|xTlprB)tox1LutbBtMo#YZ>}=@j zJ2e4IB0V?)<0kx)9e4MkCu=y2KmFJ7AtLX}>90>{fu+JIuBYDvaV{^PX!QFIub+FK zP466$vsvi8#DGqmb3m$tM6*EkdmR~!$N0bux%ERSA{*yY-_!ann26cMgvW}QglTfCukf}ofZ;I({RGP^J+$ z@5&o~?aSGlNl_JqOqI^n!vx&jc*!G~{5E?$b=VR$?y2j(mEPcIvY7Xt)#1RzJ0-;Z>&KU#oND}QP%Fk8>+_>rrB zzcRMFl#}fhq`Den*SdaGq3iPmRjq-OP9`O7LyMjwZ#!6KKlaiq|7SOYn#ogXMn=6K z20HHRL4k>m-8j&NR>Rcm;Zb%-SVa2iM2{xWHa`8t2}iOCYA2=m9F}tL9JfEyT4QQ( zyf2bw#$gvi81@FO_E;i!^b4kLK+XqCCmcNKW_<=b$P*nlo#a)g(26ZVJ9zUnQdXHZ z2Z{y)R9UatqCB+wDn_@O(Q4D(p7cr;?B+Kbh|eywSKE%_tk3v7`gln%Itmb6){f`_xZlSG=@Tz%l*~e zEmi!^Po1XLog07u|q&l%+_fVv1h=0&)mYBi>8$u&W!bnBhTa^$X@ zIAp>HbNTF*IG~^=pj+afu~TgqUC<=^1bA<5H8*7p=Q3S7gK21JI1AI~2}pNiq@EK; z7JC~!jmTVi`Yrm&@jI;N)~R^<&We}&i?q(m8xk$we-HA+rQED1IdvDKD_H)#9X|KE z+c%Nl^Xj`}O5PK=-_C5Eeaduqav`B+eXNV5o;G_lhXDLKWNj9xT@mZNh3?!ANFmFo zHk9ADXK{q-y-DSsxF6?$dYxjZU+7S4)wbM+CWSMQa@XJMzta#eLHC{rC%T(0OXIwH zPBM4fhN*9LHI1SC%_VfrC3&#sDzf9=dGYcViY8uskGG&c-+s1|KA_J?>l?NXs@Zc6 zow(nFPIq_tvl!v>KLt;h_nr7JH<-%gTpQ$(Giz(~ub0+dQxUS?Uk9RL6FTnSd7%#7 ztK&T))or7`CP(ONsiQ}G3Yy$(w+kXy`!MUAFIXk9Y-3fcWoFH5?py}80K4V1)ZBCd zmo-XIdTRXn(~hR@-w-W4m8KUCmgCD1J*PNJ)}AD-rh&!+vfE1pGM>Ed&1@#?M- z(4hoYR=FBV1o(|ob((C4(~wZYT1P-Gr&MSF-^bL(1?Gm_KyUL-fClrqHP5?ro!ace zgM6qfW>9cYM%~6DhV>am6;p)<(szzK*omK4!8LnRx!2s=g{z^`)7PPVq)ogHe{4Mqj}emHHWRa#c=aaRlm z0s}ui+YyBWRh&;d(8ZRluqKno#uo|Ku6TI3lGPYompih^nfVt{&4d}7hra+i{rHW_&i$a*WYP+A`ZG2xW~#{SBBOF>QI0#N52WO7|HI^ zn+e$YkJ;k*E`QW>JTerjvvS9ma13AA-6pQCq$vps*#`&S)0GqzjX#oNXXA*J{rXhu z5zvs!uk-?qfnsl;lFOwb@_uVuP%VyHsDSsvGIl*7m0=ds*GRyFO?fnYL!bLoBa%I>_2c}chX7#*)z{0xIQ!@BpjG7JD z050U6Eo@+*wA8eBV-Db5;>R1xc8rJjLF9L14Tv1S+*odwFrXYGe0^t3uemQ${ z{LagBf?V_b;w~SX?D^Dc)$>jAVVXL#g~`a;D595Bo3P);`BJo+Yh)ca@LGuT-S9$D zS#bI6qYVvxSGWoVl5yD0?Sg6w=Ey=(Mk~VBe3XVr>wAr{Vx2al2l&qhnj*HTUKX8k zIS*a0u3nd#&8iF zK>IIzpM0*B@Q%botZfNSP{JhtX8-M%i$u)8@#FN*-dTgmOP#w(>%SuJi%!0? z*XumjP%7t%37W$%YjKIXd8ZkMmT^2T)#>DU;pZU3&FH?ja{L5fKGmzvhe%WC-(H6n z`QX*qws|I07*$`#baqlk-xt0#*xJ|sU_!OtPU0~;;km?WtTle3`9i0~d1u$_e!WXQ zl@59?XZ$H_=-p~92R(#8>U2ApCj>A1@~0wZVn6SSMvX-_dR3Dj^iU0+-sE}E>=o{F zM+emAcM|Mu>=uBx_2`x<4&bddmUAeo*pS;2W-RqcOFRGP?8sb>EnZH^IDuf9MdsOC zHf5MhbJ!eV%4!^hg@CKEhTr+NTHgcFjGWQCd|pGh*>=!^NiU2yu`;Ip}+>{d4W z-lYwngr{xi+t~EY1{YyFp7>RhBJC1&aB#i=?f0w{qPf?$FDabm`CO)3Cvnr=iti?p z@$qGyW6KsbubtA=g38UVpT7c+MeOt<&zflbh~wm2_`J!kc_y!?7G6iiKRmqk9hZ-_ zy^h18nd;XiT6T^qW@xW7yY;bd1X{n)O$wYH3xpzaSc#F35}jX?eb?NqA?qL4>62t! z;*SovUm{t!JWjv9x)Rn$$x@(hIXo>RxlX?~a2qwNmh~Rth%ifL4(H*kOi7t6e^caS zsz>qso4*T@zY^IJKjy>VDlPD6Ypqvohp?C#Vv%AV*Z->0>Y{AF|MkZBBE1_A(~(D= zo}%robqyBgJ(2->C8vwdc2202$z4dZrp6Y@oUDUZch`>pJ+^Bcy(a>kx7CO5Zr7#P z43+7T&sYXO-0xVX4?JCJkv|-nnyMui@G^3Cp7iK>VG*tM`@BqU+zCws?4CK_V;^2! zKs9GlAzq@F5^bN^>~YnMq*l^kyNp;GkV3#b+4UI90*d~0puwL)GyC~#Iz|DJZ|Rf> z$dTX6_ym!2{h4 zP&bz{zq=2q%^M2Pp`+;6!X52zopNofjYF_T4bNhSRAajHFHHPeg>k=H{NeXZ8u3YB zby$+OSa!)#Jv zV|iTEaYX7vCoJ73;Irx8hso00>7mrjz~kE8Vc9Qi1S20%Uw{zYQwlMXBV~Dhd~EAw zd{e2yOrN72CzBC1^HTsYmt(Dz!kAZ5-=ChFY@EQbPYB`?FF^$N{ZPgSx(DD6g(aSU z<;8x%I$G_a)o3A2Y)dJvGocgv3)-S8fTFtA=i!#9V8NDCTB;<$8b}bUXE9=qWKAQ9b!^&RrAb6L)4OsdQYlK+x)JdNN^i= zWn8UgF(R*xF0F_EV|Gu+0eXgMcO~*%%lngc68}j0OAZXG2J+8hN+)D)gZ(cU{&{fP z2ga1sA`M1=n>bID2zXaRBn1|qkp^8`oB< zlI$?Lqp)+&tX+ho9&zQ-A-xVQ?hHDAS547?i`?1Yn6KBXS1bu*P}q6Ba;LS>Uh7N; z7gvegY2^`&+}QAOF@!2Zdu!Vim}D-PnxuI!i~#U{M2!FdP8x8{p(3newd318&F!lX zT0{@x+XITYc*DPAg6-@|b(^_^LrleuSTwVWB6Rv%&ZF-=9mCHzlcCM0wdeEkZC9k& z_KUMu8na&a#*@>nccIYlUs*mMZUth|9UcE&;WEh#Rl>tDV;mHo6b z#vkl-oVqV@fea5DXbmj-pUC zufA^w!7e^z51MHOY%xU@At9la?>Qv`!k>M1XU1PM&8niw&DJ!rFWo-r!ZP}Nxzk#f zqIT>rN1mctE2$Eriqt0;8fumI6EBq}@&ZYH&8xAY0U%{%wI8=c1cI`LQn*;Txe2bv zx-~({>gssMjv82ad)gwVWYVS8^a_~ucP{7fpDCg^MMpO9lWPdO29=w>8hD;bMx7w4 zM^S)i)wi*M*HTgS>00??ofpOS*-AxdS6?RQhhikJHhx`?++aoTmSj>G)WgOPri1{X zXNX}ohEC4LBWnEf!ieJ#9Pl;kVCg93ATUlw;t>m(%cO{SZ>JHP@a|+#@b!@UQhZ@? zVQg(ZVPaq5l=6%`bI@v%^s zS5=L-M%XxnwG@IiY;vrdAaa$6*&D)-6@x8)LK0S7f4_tS0L>*Pi(~6C1)YJVD&wWP z+>Y;vZmzxAZNm`V7I>(%w6nDkM`m4XZf*Rt>b`G@b8Lcvb z=Do1x!T3uZg}C@+y+zl-3N7WvsuJVvbkLO}<=C6nyt<4wv4-=mFkiJ#vI^*;vi!P6 z6}yWj;CZ=~)8WA)yusJcCr}ty%U8atQH$0S>UQRSA7X+nF}86!$~HEaLmia2IXXJ}_ksTkH|u()CM zR%u5n$rDxS+{15Vbmta@mz-gHD^p|~04kgq1uXg(6)E(6*+kJpRQGex;wFa5U7JIS|^ z@@hQTySuwcMObcmh}e@sy>8|!@?ZmTV#Uft{Nm6w4%bl+4@?l&j}-is@+wdi6I$mnDr#@r5QT)87~;t|kk#9B)APQBRe5G(gBr{Al}c=2fh;4G**Hy{ zn&fpsWcwlHav@AXzMJKyv0B;mxTpN{G+yN87sfwc-`ic zDWSEmbCKEcuD8L>js;c<^juS8gwBj{S%_{kVP_%U`oF7Xx=R^7>xdTkoD1-N{S=BQ zRg8t46E!wmfX0OR^>r&d;0;+lFFOq%^y9NCu?f%DBYr^Yk?ee(lFG z0JOzzOeM5TTtyHM^YXt;VCn8E)f(HGhA=Vy*1cmx203=MR^Vu!JN_Yb{O9pJbJr3V z5qkEhMzi?k+-rltrql0SghB^EX}WXq9?m_fAoBb@lbw7K3%J*~tRp?Qjykm9t+Q`}_#v#o?)#NHM!=GTsW#4>=FFAqg_cyt7JQj>H&@5PIjIauC z+V3jOPNY;2#Evtq&bQB1B~bo zvQkwLxH3qbb8eaHUB~KjT>gPZICa4K8na+s;CX))9iu+4>sfD-{_Uvk6tPm#ShS@j zN9oMhNUrLpVp%iG)@OMYGX0KX4I(tan8IWE+2Bk4K$EX;T^HN}BtL$$d^^ULnwW@R zC52#tYYa>bh`J6-X(os0Exa*ClpD(tZl_?$9keE5OX7AD)0{Bt$7q;9c^%rGR+Akk zy!ax2^~Y#sH+mYM~FG`=;98S!@I3Yb_JYy>lDl-KmhU=_VU{`nRFv{e%} z?_tuv^2fT$LSA}Z7Yq}$ulmb(> z!qdrq`{s){TVh@$%3^%pT#06<>gp<9NR%fJEH`U3Ao70CM-!e-vu8Tvx!@uA-cgvE ztWBV?prpLr{L!A3x>s7j`-Gkt+zZX*FWC zlfP=o;Y%qcH$pbrW-8s>l0u`S%Soy%2M1N|VV7i@SVn`6Dgf|Tkp5alRPSES=FA-P zEbx&x$&ZnhqfVd4`Sh;}{Cn=v_2Qzhh?(Ua8TAL;$wYbdf}f#)^D=?|HScG__vFuE zQ_|ZEJK1>y0Ec}?Iy4rIj$#JW8JWB9F_;6alyDGpR*FjkiH2{2eHZ*p79HZF>ChPG zbYm7RX^hchY7w4%C3jlo5}9p(>EsGO&DD>H){TC((AkkGGAe7sH}yEp(-OSs(zU`u zE@LX}`};bVFx`evLj+5ol1{9X4JZDAf2Z-#(FU%BU3K}!*2W2+5~$|vxTeN&MNMv= z44PL@W*`C0YW+WU4xm$TqP514z+{ZRilb{1bUBEt1s_VsobW5vqL*s&(U>+!VfV{B zb1aKhiJjm47>-|&G$p@Two`#ySe0(j2{<$W$|NyM^0w! zWC0&380lK?4K`fv!@WIv#ta*LZ( zS51yI@1s-_)_w_-MqQ9^3+}T=FgC+>Oe>nJwMi$p&=5mEDQ&}PWwOaxsL+-y0J0r; z!UP!2|%ALQ(KNG z&Lzk@H=alDe|Us)QJTvIvXs%U8&G;zEw=AG$9YfUIrQHB@n*})?%fpgM7`ExHRDO? z>Nq}~3WcPd-`9}~+^+%GslB6;ca99g{Hg2j@Cu|`YfsBX)!{SiUJz|u3ku}N)+cE4 zQomGc1e2oDZI2JGO};7yKa=*ZKOtkPV<&jXzd=C!la9m2r}L0F^(=@q5((IY*kW>SQ z^SeX-l^eGbq&@qU#`Hm3ZRe^sTTW4}8QJ?jDDj_U8ZOah*R5CBP}jHd9bE_ftbESP0np za)_V>*u*WG(0L`3c*au4^!<49?JYYya{y1DvbLJz+sHJ2NHt{xIW<>AP;T07ME1pp zUWu97W>MAG!P@do!&48!5Km*BPcxsKh9rTW!4)_G>!oFGYnpK42`AO@YBd zH5MjVZiG-!RQRI-7*8@rgM1kqwG=COKzo81wvenWx6adeUq_EGAWG+s5lEURJ9Xnl z&4`@x;Wj-cEEyY>OJhG&RS`=sjY33|8_T>EU?VO-eGejLIpXei4;)UvWA=a6AQqt_ z^0=UA%moewjCQbda6J43OpBD)wm3VF+_&@ z)Uv|8NRDJ1BeBN*293${U9Q|r!Gn3k=vbz79%xMz^Lx>g6RKNaRQRRYl~Q_XX;`(P z+YAf(CgIsz^}6)&smj>Qscm-9Gy=3@!9xJRCZ~*ofMkjXL{LbLaRV7<(n1XZwNgr> zc;&G?tbt}zpFT}EBWhLW`6#(~j_u}3#^z05bDFk!Z^o1QLC>>&HJsBW+$Cw9KEF zl(*Rmror#GgOl;{;G1uxJXe=#8b@GcAn##&xBM4$+W^kH$?~M{vkdu@p z2+iON9GGfMn(29Bxym%+(gSiLr`Cb+9M|gmD8hZIyHYM4D6>cj&P^B%n@{I(C?2@eUp2q z243b3cRa(T3VpJ+s}S|G&L55a%=PdfxbwmS+HNj=^Q0gLvd+g$T*fx?&7|a!U&@${ z1e&x{g)?7}kfh&n#$eMbzSAhf z5H&E|QKOiXDWCg;unK5V^ao8pX4xN*JQA)BeCel%?FZ&*Rhi zkzWho2*SM1%;nlB3YZ7mKW3B-pLZ536?orijLkg@ul?kc-0%KHlIyT(vm&M4h)lTM{%JoUfpFia$p7^!RoDZHx4(WcS3@_KEcdO~0krscK zAZrYfW7nQCe6TLk-Z+I-ezLSJtNq3jB*^NAmPS`NrDl#+RjOBH_Mz*^2?MvQ5W`1A zULi0A!qz&aqJbccDA7K6>SxtFvvt~UIVWyxw3LqUsD&GGTP=0{g5=h*K{elQi)bV% zcFV`RM6Q>3BhDpF#Wj7N8;4V$+}q{#)EbL&JoF8dp9pGtWbC*a&ieUq0eD9gWHYO~C=t_%&_Li^^V>4Bz;1gmj{{Sr)s8uCcKzEup>spSv9niQHC^J}t#=vY2T*Jw@ibbjsCUYj@FA0B|6;q*#w zf6UE|RgBUioZw=T7uunXTb|mU&7o$iSIYk#-T8|1;U5uX#TH_}_O?gyV-rOjBLtoC zbt9-i5D2`TY|>(+uKUtWs4bg(V?fmM$3+RL9*9}4*S5hz6YjQHmB9fbUM2oE4icf1 z))hF^n5VmI6-UmzzMrp!*>mcYj%~92FPH|^DiFVZ>+nJa8Hi6OTN>WDy^v5g-em89 zVnn)mW)&X2wO2*~M_j`%4G8%lsHux4QDemgKl_sA5C$+gFfo{0axl(+wM7$9CXaNz zLluA~_Tz=8c~q>ad%@-$0>-yek^l1P6(0eA0+r>r!U=G3al72lY}7GPqh@#pA}EIR>i@jNLa~SnFp9g_c?y4w02qf>%B9>i*QShIHE7UuaeFwNp~^i z^m#IF+xI=MeE+!3X&r>6wR^Kulj9dNLo+7nhbyKtFO5Czq)x{KV8fnp$`;Fe{Ddp6 z!(-!h&g*+31K-|st=G1gyt?>i!yaPY6xScxWLz(EpX^$Zxk*)8xJX~DhXi^nQXgHw zNSeuzS#^j@KGP$BEasWxF(-1aFdSfswb%Ww2r|9}ycAF#Sm!P=G*sJ9p^6lh^tPI_ zq@oc1rn?4@-09x^y~lqYk0g5Zp1D<+m*cbo2M&xBx_zSHoJ1TFS+l7lvfVhY{#WsPR9j5B+}Bk#Jj@T4#pmE>)dU7Y^cd1T}e>p!$Bu1X1^bs z<&*Sh3jcjb`3u0CE(Y`4*8nk1WKmYVZM5`9w&Pf^TTEqZYiwcDJ22%qeXgl>+$)!3 z>Q?sZtlJ`eBTl@}8%%BN)C}sJSs2v!!ISqG9y^@5zG!G&n7#G-J&plpK5k6N_ zY+sL_C3?5tsj7R{~)D z9oH6*Lv;wf+3RKd9j23X>bRNV0A`y_^LNvEbIAQKq(A_eZZ152bv?x0hgE2~ebdHh z(NR0gBC@HXqLSFrEz813!ThMW{xQ6eGl>LE^=vh($=x^d-t#Jn%Fd{ELSuktrFcH( z@w45x?XUcn`;QWbOj^Sr&&uX?-iKUYe`7>LE9U+l(zfD` z-S_gdiZTXrnI<1%yc%)ecmC#52j3b|5dOux@-uRd@-2qzBqC8&LS^TuHT&Zh!R^#9 zbF=0fgX(+(vQZ6=n;I3xRUg4{Wgh7FgxF`pNvW^2)xR{ZtY5I_i%7@XE#V>ai6iSZ z7WZO^9}?r41Eok^oukp{I2}ylrPjLFN)iqWIoS=L`lLTk4#G7zKgvP(Iy}1>nt!e{ zjNH~JD-L+4RI`74z{|N^&p_RBhPaesKSaW+6b5rK@XudLn%mC0f%_rJX5WWXGcw)F z{{2~VG|gmbD#sKfvBl~2xSv`Am~HIu&OA9Z6!a~?0#g?WjNTn1{gpR+`0*jR7ddk! z7G-w^^uH1|Xuk)`QYN3<_w}1w^snqu{R#RU>wUarFW%cAOALU!Oq^X^<4h6xv8ZaE z1P?^4*3id`dFj=J$nK>D$qVVH$W*4=RQ!^jB90!z4$}}=(9|ymI{0v2Rd~yM!fgV5 zwBr@K%M*!@&jLIB{QSm90@lPypAB4dv8uTnicLj_ziqL(xp4V;z;sW_dmk@vTp#HR zUJt70F4ip!%S#MorLN>QjoQ;&DfXlKB^aiHN-E@8KHQ?hew>`jXJ*$4GR237!3rKq ze`IkpX^R69Cr%#MsEyNUj>?RL5HIB^$<032`a^f+L)JOv!%d^btf$*mf%f084u`Hj zAmH)42dIv`PQLeQi>~`@<_>D{?9VUPi(rBX;!^8%hX^2i&_cm|Glh>{%DvKYv%jr5uRM0Zpozh)vx!Ifvmn!@o1dZY z?wThbpK~3}57(7Ia|N#6-lZixMo+kMv#Npe9_`RBKmSsw=3C$Z-bSPzt`RAlm~UQA zFBrwdPpEZ7cD=;{!-xHjsv^qS1v`R)wqU{rh<)%<7hzD()^gw{#!H8Qu}(?=Oj5KP zYf-Cbdi~QzzCe}PoFx(feEVzhU_J#W%FNC!pI_hV()RFma3GLS+O&c%!@+AxBJCIn z-7_~+aGb=*-Wzl>C#)iv#35^-B(lZoy>W>rN?uSz1wdg_^q;;6$!g;aLowF*^7cOP2ZP10!I2{UGMj1rQ!rIC%54$ z>og1&iucFWKW#FnztvK(d07#ky~fXZ;s0gg_$0C0Y885{{c26fptrv-CJ&YSmmL?- zR~S2NRzpmX{`C=bH~T*_38yDrZ#NyBh@dS4>%J9Db0=4m7I71dHd=#NZ+rO!P`K`X zIE+P!B`(*D@SP}2M_}%?OK_>nhbb*6(Lc8F>I`3CHdA(qIjsRPI92q|0q>mFB5(Z+3|AAldx5AWxaf=`4Pa*q@)nP- zN_t_q^`y^8-Bu{Uz@h5vuQb=^oM|q+reAFvx)`N)t-g>x0!~11p*?l-YrNEBEp_#L z!|V9$A8Z!m&n^=XK5Y86Ud{UfKwizKd4}|(&rko#80>zMsX`8)TL>TX8Y#Wx$fz#C zuw0Lgph(q;c<)B?ixO=p9=c}zrWw&11QLiD)7Q4F4}}KMR^L>&(#pv0Hlm3MsIOLe zWYGJZb)@^4z1JTMEPvDUe(^$xGP@arpLhs9BbVj0hq=dy*|I@|6#5*HtssiyVtLxp zfztxD!XGnw!$f1M2?vmF?iXIDymaA5?Nvi=7FBcYbe8z3$r>FcdA!#XX;4|*MwB4Tq?k016E{ZOv0{gh2C#6ADX<1+cB+{RKq!72>iHga~lGoNOLsGCvBKUH}O)RH0E2*oR`w{?CpnoWQ8j+M%GH)m? zj0%Gd_}bo1q)#;uShfKV{?Yy&MEJA6|9S*g_(b#Xd&DQ1qo&1r+3tH$Uqu&Of2V|! z`rnCF5G}TAmT3$yyw&Bk=3rQ}R$+L{;1f*6|5&&O;URM=yx|+mQAH zs151Z6QVQKdkvOsVfZ7~PP+uQSjQKy$qWotNDn-&<=+{jzzd0#EDiBkbxHF2OlnhJ zn#I>1B0U@+OUFa`pGkCM24w6u>2z@@<$Y=^bw4DA+`$rb82uu9`(=!t?JcM7c7Axp zN}qmm2A}Z}3cf_Z8HfK_r65Br9|=qbc!y+;^W*{QbUlCM>gPj&lZkJZI!UPAL0lPe z0FRGZAW9zV{J4a~u+et&Q>X=)0=f1gP=);Y^Zyx08zX|UoR%%rIZT`eyAqyvA?@0n zPqxz4FPwAbJP)eSMRGqU{`lghal;FWTgo4|s?-}=(>%?*l^+;te$3;74@Q$NEsAPD9}m2Z-ifrdhM>DP{;H5*A;Wl;egr}4_LijYYKZ80}FaMPH@I(@P<3PRDX13u`h+JsdZn4K1{=Tv? zTukrJ_5ud`)StHn(a-Gi`4)a!7Z=tLdrb|=;U$NNQ!@$Ncb{9$!JZ4|9BM^8nl?~* z%v@0I%9nttJvTRusL}Xy|pq&o{0@L2s1u)_guBf(~2gfyp zdYv7=Eq+6m(k$}(F&sJkBmtt*) zXB^s8wyr|3sqx`qqy~Q9FNpbTJB3jHfJ&a6p)E})Z||vB4o*P9QkQtDL^WG zj$5=SsZ8OQRlVEAnyJKs5HH^y#um(4sfh4b472=9nPo!TBzF?9Ui!SMv#Mq(X8NFe zcz#A$Af{~D64AVxW0;}@K&M)2ht*OGdxpW(3|-zmywdT{IQ16U|7J4B!*El9%=n9L zy4o7OZw2Et<80(b#&1;gxQyv&R`11y+=E3rJWuSWyvR8R1o%Rv*pzh+=BfAt2T=*l z-{Gs?EaVrS(_9J^cK9UsVteqs@i3it^PZf4a=<4#?26o+WkuUY!H?q346fUNMzM;w zCNU`>CV9ENFp4B!II2NlY0Y{?lXAPZa=gpU&or~SV}8PM%IA_o{5XgIc2?I@zxvp? zq!~pp#WlA^!c6?WD#$wc4h^(QwE#z5lWb$lCj9XTz@{7fg3Y0zVLOw7nSqaupT+2Y z)MhvHxM*bmhq8IvF^@p{`pMzR!iQjW@j(j)*U0E8>>}Du+nLVqK)yKo!4J>Rh48V} zI=3W~k+6>WH)fZ+C26yBxp$+d7(*!7=t`m!08*DQ4z^{Q#Du9sBmkHsLIAS3@OA4( z0YtMEhYLPGc4gquK~bTW;ox%2j{RE-GY}|1Wd|%6_=;(POMn;rm#b^-qddiX_~SxE z5&7;ta9f4&;vaYXpJEUQhGWRZ-C_&gopSP9w5FD`ggLkTpuh9}{WvSa^q$$aTQ=Sy zq|?*^+RYyui<7N9g+v$SZy0611yv1^y+dW!i-4)L3Ok{3^2V}hhiDeNF9fpn(%EiGP#8k z8=rLYjB+rmBJsqZs%TWkxZ~U*&k`Ci(f0xb$z%KXt`VYy$X0OwG4zK?HV#&lBv|lF z-wLFTl{Ms$4hC8(U|f7Ki-;+m=E!>ybUlLCmIWWn5E|E>Gz*ah&Ta^9e`V@*=FZEF zlsvv(;kngC;a3^JZ~C;=c9=hJ^=H0|Nny0oQ-@4J!@M5?w8Hw4xbA8$V$divg(>@P z1++jFYldQ~XS@dy27*L-AcKspSTy$g&NvyCkJCv>WIo7$eqbioC-|9k-gCnc$4#$_ zdCMBkG}q^J1abT(iBBs!X;B9Q;>$j;KUmkAvd~s%PF!vPHo1+%01kXS%6rSKYcFwp zw)MLbu@0P~U6hF_0{F|xEjB9}e)Qk7HOoj=p8%AcDArJf7>oV%vA5WxNYQsan?#~H zt3swcDqadbRdQk*D~tx!YeMqb!7IP^!@N3BcWam7njz{;bQUguILbNudh0yUwgYy- zZ+pzkr!6Xb9agYbipefm-R)M8&&HV*mFO9WzpQ7SPuwkESI#0NY4cbT z*CJt_+uwHvV3`P=Ra7h^lpn70dOq81o}8w#B;T8kco8T@{D8`{(1Xn zAalS5mLU4Dmg^M;CurYs|Cg$BWEv2oE0Q&$SSFO`M1u^wSqBoaNkd8FVij5tLoFRA zlJ0ZsAkePJwVAxNs&o|dj~I6O&N?t4x}(yF3c^qgS){dkTbJhDR#PXS6z zq1pYB5A!jaGzrAVkVnF^qpW*oWxDpBF!``B5f9Za*|M?i)DW8)Y3Q^FQ%sq*nXBH8dtN2m|)}8T=%C^0img^qtsEKe8{K5Lu0gd6{00C7N_)@lk0wVy! zJ(71@N0F5vqWwaIlXK9t4D_^1mUs9)raLal@IZg3f)ohCbDZvn1qDCmo-$N zBnpMl6y;Wo+*l%o4G*C92wjQWrztz;h%c>T)bURL8gc3y;%ox>utx&C_*1m-Q(bPxtu>F8 zwvPP!R`r^G4EmM#n6WNBKf!dTVC(RG5^k?TO5V27NzHx-p$HXcZ4gMcEa7Z8F(zS| z2tk_q?tPov(lQ!%5s&+;``LHgSKC}sfnQI}i2w8sG}9AFT{|=IN%aI{GJb;!q=-dG z(A{IkyJf3J;d-@;&=O=K+a=Jz76(*DkOZt`T=K*&TQYyF$}I*}87TQt2vawm3+VXt z1ChDW+wU>qQ*8Rf{XOI_qfRZB$^}lPDDb|@I_U3%HcoTr2l6^Ne-p}SF<~rfXv|yN z`K44LL<9>u&028NVtYo5uYYrrKjFJY5}O{^8he&ee%r!gIsI;A5Risgd40dK6x!dH ziQexAPml2dh$!=;?S$da3Z zNWEpMF+6tB)^>>aS%Qn`zUfl4O|G7>zdHOhaR=bGi77n^Jiz1n6>Itc1^K2QwpoAQ zAn2;owT|vb6S932-`C?a-0sUo|EEWW@j+&EXrf3wz)RETrVfziYs!;R?*A z^#iwoN!}NT0e&}-U&~h0COaZ)!_ry*s8o}fbYn-d0;qy{DP@NX8v2{_)7Cwn`L68A zFyrRAomF44_jPkI>w)^^nirT0YR0DzT_$AW0K}8Sjo%@EEExGqftTUTCIKiMQFz?5 zcZ({MhhOYIXi11GyM>DxODOSXRSE6d@e2tyS63WQTQ%bRN}GgE{ttA;zw!1*IJ1rw z`ig%2gH~1+_yqV7vz;<*js>VnK7MNu#%_OQ^yi%rnddw>B4b&sF2-3WeSA|N6^@Mb zLZyCAO(pl74Pm6ZsH?pR1fWp#hmT%CkyWCLfw{CmR#STj5rtk$@K0kd{7p#c^FyvWkldu4F zC`H$bcGmW^zeNqastYenriG9_antL^Fmw3*E4oaXxopug{RWYNEH5*Zne2}yQdFg~ z)5Pf>T8hRoR@bkdhqO|RV61--+16_tn(34n>i>hrRdGT%nG`A z=D5@eBFyY3bgf}qp>p>+$$?Xrx$vz`&4XVppqDi7^SejGM#H&BYt0OtO?;u7E(Sw* zIXgnW)=^2%4Ajz5G?0*WKqy5>^Ycg8WA@V=#v{jtrN(pCCJ^kg7YCADA@OuTu>#39 zC8-#C(2rebbFaOar?|%1y7o1*q8>y>4{Fsc`z-tW`V_T_)a@;-EL2ofnCmIR=>bax zE>2|sO`@k0qus6`{e8A!3U50n+b4y=*pj0M|eDC;J z9gSH9~?@;MG~T9ex* zo|#~UsI@#Nhj7Pe+wg}y_hTTSozq zT`Y1TK%A%)Q?I-5wyY%h}2I#M&+4#oBiC)x#M0LP$Hc-c0n|J zABW*=JnB_MI=Q5|5NQWpB`oZGina&t82X7P@oITTDrt8(8Y z6b6iW9Q!}lpz-&Ulau-$yW*lEA}|<2&wABCB{+$Ffuy(b+pNRqqm!#ot>teo3ym_P|e*Ia6~nkNKAvtN>@qqW%(#(9t@Ga zMmK-%*BlmxUDGeM)I-o*oS5US|19NZp5nUy%Kp(`4{4igQuy=}(i{^+e&R z;(PwLuUc9ti@Y7i`}YHsN*XK}|Ddfcs|Y2&EL`j485j+9b8}l+S+UZP#(p#7ZfP!) zw6-%_gM)FWWa{%(kuy_yaaq|5O#Yf_=h{)}TsbATouhFrDA~FB^g?dc)8f#BXW(*H zLtkAhD@)1Ct36_eBq{C+3(3G>1(@X-ClT=)4a#?C>YU0ZP17m%T+GZi{FcYe<-p!{*_0mT8x3e@$ zCMz1aYmg;xA~j7JFNKwPJ9__~?2uBaw5MP|WyU}JOMO6ExGLILK~Fah))WcvW?0{C zF*ck{ak^EABgp3$s=DnvA7x5TCx!`-~P&D`i+$ne`^L@h_4=cban%* zClH9dGydl*rn-r_yJduf{D5TJR$eL$)F|uH*Qjzfcr+7RYM?<%*h%NGKyQr(mG?h}j z=?NEqPT;UMo)Nm_^iCzo+AVjha(~A}w$>Kx8m}bRLaMEwG<&b7N?o#cGl&)FQRxqR znZqA*`j7Gn6p;>5EW+7pQrt5WdW!Z+xiRF|Gr7UhceOErEurszL8qzB(V5rb0*$)H z#AY6=O8?co?Rmu5aviP5qh{tW@S(E8{>@d6Y!xV?SpHGmw9`;M%bdMm=BGm6=(^9# z*e$NKi8jLcAgz#Ll%~6XTjNQ&wY~k(nO{Oef@r_W6BoGn2~wqO^)ewPRW-+wieFPR zkL^IvMT7BXR5rHX(pDyyhqVFYE} z_Sc7KnE7toJg}B+UZ!h_A}z8&R4D5sz3|gB1h&K0$I4l#BIKq7h2SEphnuzuzdv~p z#pUN8tL@mYA}k{faMywX{Q@TGIT{rq_>H$1!Xj)Jk0RP)kSKqIsN^t!7H-0b^8&Qf zo~zx^kaA`U19h?=WO*ND;Ti_7D^$vK47wISLyfL^qna6Sbw{8OLDB|yiuW6}?%Ba! zX5lpG5RVWFjdH$Rl|oOoxyYCcP*J?g8Zo4=qv&I4K!*E#vw2vEmIE-%r4G|%%*$ak^OX3q8^B{&u0;% zfE6@SYO9!azX17D;Y!|E?3>;kL+ljk^{;8;9#20frLVced&P`EXDev0#z*Oj9l3F} zXMy5Ul=3U9`(BNkfwIb#GNuhYewWd$Nez3}T+2xV#SqH+f|XhdP=4&gdk4K!!!Lr_ z4{5#Z4Vr~(uhujl)hyN+f9QsZ$$Z_ODIY0o5i@j2@&QXeCXTBu(EqT;Y^TFtX84i2 z=?fF1%hZxbUb4pyg~IL=B>zJzazjm0GEXqYJD_>i`T|#0U#V30wd&ccl2k*4suNMB z+0C_TP%r!iYdMp~$rmsoz2AxA@iMa&e(pD4Vf0XPvZj2y1m?k4-6yUbHL4u1Uq2Y_ zr{sFfeBMC6wHRPE*|k3YyZ?#*t)>B6Sss%K`K8ps*H*pLP&0bHq&!h{2zh2jp3^X< zJc*mFv(w(2&S=}2h0_|2RNV|XC}#K@^55EdK@M2A7?-nSz?)xPB^m)_QTQWHW^@!o_(h@suRAdD!bZX zc|?$BJk#P8Ohy2>gRi*CIW7X7?S2(44w_Tkf6L>yd{^WAtQ1%NCtl7Lw#LTk6N%$3 zCB41YS;~@L9u7MyIrr+k0Z;7E#Xzf5=h<`ydO8J86n#nyKAz!PQOx)+YEx0|ricCm zeosaQ7B~peHm>}$o&Z{i=UE=hBeTOSv9r4bA~5SHM{8V%8=*|6F@-y{_N z9_tVqvpWxzCt*1nXW2$lD|7D>t*Jv5q}$_fhKgtHcSDUnzhw{6ezvh;o!s4(I7bsC zz_C+X!OqhX1I15gV$@@3$g&%b85sd;O>3TEcVdE!PDPv-ap9Xu`X@&3g+3&uAK7A?+J0 z?F&0!D}LFlr}3`2Id2I3^!tOU!_Yp?u;j;uhoLpoOGXs`_-Q1e+6*GdZ#Ub2PDVNCDAlOlxi{pGOgcks*}T?2VCSWrmlD;zb-@-$u}7Lab4MNBHNqD?XOtXC(q!F zo0Pr*ZZRu{qFc|qeGp*(<(~P&t;>?W*=;6#rIT%)i)(qaB2~Tk+zPk-=vb{JPjllY zwv!Wa7rm@JGl{|2-M}TlB;&K!Mo<_UD81MV_D6~AD&G8vt@kyCFvef_UU1iBu~(NL z@s7>MiC=e!2G>-`^mo8FHi*8z@?kvEj#T$`6S| zl10D{v6kEhQ^-$VYD>62mrsxPDY7d{Sw%jlO$khQXKtXq}M$sEW|KbKivEu~q? zqBZX{DV}7TRYF+S2> zN{h*LR}$j2iw=woLTXz~+kd;epmD-Y^_{(9;=L_fRRO88RsZh=yAyp+;`v6Ja9KwA{doUtk+38USJTf#P&A+j89wIQee~yE#>Jx<^XW+puAkEzSq1qnaJY}+x!CUs7zgb zj=EKutEc+4lMXi z8hYYeWMgWnp>ft9Ej|t0`H-KFjIuRcD1~!Sqz_wF!PzD1Uuck7*!sYy zTw{ML-#WCOXqey(g%T%9cXIPJoFkj_qb(w7X2PLeC?J*lXc-Q7zxCo^ly*_9Qxh7( zsqXJRQ(X&ps{{>R;DFvda832tPso-?8f;aA)8)KOv5p8Qy=$^|n_F;Y>SfcX4xOMC zBn8*(Ri;?#)!Y991Gy+4 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_WorkfileOnStartup.png b/website/docs/assets/nuke_tut/nuke_WorkfileOnStartup.png new file mode 100644 index 0000000000000000000000000000000000000000..450589ee3a0ef5ab20f117c2044ddb9e7be103d1 GIT binary patch literal 22326 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 VUFi#6R|5T8Ohj5Z=Y^){{{j!)=o0_{ literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_versionless.png b/website/docs/assets/nuke_tut/nuke_versionless.png new file mode 100644 index 0000000000000000000000000000000000000000..fbb98c55e211fce76af63564a8ff9cf709b10c8c GIT binary patch literal 5034 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 Date: Sun, 22 Aug 2021 04:36:51 +0800 Subject: [PATCH 021/450] allow publishing model with render sets --- .../hosts/maya/plugins/load/load_reference.py | 12 + .../maya/plugins/publish/collect_look.py | 71 ++++ .../maya/plugins/publish/extract_look.py | 340 +++++++++++++----- 3 files changed, 326 insertions(+), 97 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 96269f2771..77c9f28d10 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -152,3 +152,15 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): options={"useSelection": True}, data={"dependencies": dependency} ) + + +class AugmentedModelLoader(ReferenceLoader): + """Load augmented model via Maya referencing""" + + families = ["model"] + representations = ["fried.ma", "fried.mb"] + + label = "Fried Model" + order = -9 + icon = "code-fork" + color = "yellow" diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 0dde52447d..fa48874f0e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -357,6 +357,17 @@ class CollectLook(pyblish.api.InstancePlugin): for vray_node in vray_plugin_nodes: history.extend(cmds.listHistory(vray_node)) + # handling render attribute sets + render_set_types = [ + "VRayDisplacement", + ] + render_sets = cmds.ls(look_sets, type=render_set_types) + if render_sets: + history.extend( + cmds.listHistory(render_sets, future=False, pruneDagObjects=True) + or [] + ) + files = cmds.ls(history, type="file", long=True) files.extend(cmds.ls(history, type="aiImage", long=True)) files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True)) @@ -550,3 +561,63 @@ class CollectLook(pyblish.api.InstancePlugin): "source": source, # required for resources "files": files, "color_space": color_space} # required for resources + + +class CollectModelRenderSets(CollectLook): + """Collect render attribute sets for model instance. + + This enables additional render attributes be published with model. + + """ + + order = pyblish.api.CollectorOrder + 0.21 + families = ["model"] + label = "Collect Model Render Sets" + hosts = ["maya"] + maketx = True + + def process(self, instance): + """Collect the Look in the instance with the correct layer settings""" + model_nodes = instance[:] + + with lib.renderlayer(instance.data.get("renderlayer", "defaultRenderLayer")): + self.collect(instance) + + set_nodes = [m for m in instance if m not in model_nodes] + instance[:] = model_nodes + + if set_nodes: + instance.data["modelRenderSets"] = set_nodes + instance.data["modelRenderSetsHistory"] = \ + cmds.listHistory(set_nodes, future=False, pruneDagObjects=True) + + self.log.info("Model render sets collected.") + else: + self.log.info("No model render sets.") + + def collect_sets(self, instance): + """Collect all related objectSets except shadingEngines + + Args: + instance (list): all nodes to be published + + Returns: + dict + """ + + sets = {} + for node in instance: + related_sets = lib.get_related_sets(node) + if not related_sets: + continue + + for objset in related_sets: + if objset in sets: + continue + + if "shadingEngine" in cmds.nodeType(objset, inherited=True): + continue + + sets[objset] = {"uuid": lib.get_id(objset), "members": list()} + + return sets diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index f09d50d714..8ede62d84f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -201,103 +201,11 @@ class ExtractLook(openpype.api.Extractor): relationships = lookdata["relationships"] sets = relationships.keys() - # Extract the textures to transfer, possibly convert with maketx and - # remap the node paths to the destination path. Note that a source - # might be included more than once amongst the resources as they could - # be the input file to multiple nodes. - resources = instance.data["resources"] - do_maketx = instance.data.get("maketx", False) - - # Collect all unique files used in the resources - files = set() - files_metadata = {} - for resource in resources: - # Preserve color space values (force value after filepath change) - # This will also trigger in the same order at end of context to - # ensure after context it's still the original value. - color_space = resource.get("color_space") - - for f in resource["files"]: - - files_metadata[os.path.normpath(f)] = { - "color_space": color_space} - # files.update(os.path.normpath(f)) - - # Process the resource files - transfers = [] - hardlinks = [] - hashes = {} - force_copy = instance.data.get("forceCopy", False) - - self.log.info(files) - for filepath in files_metadata: - - linearize = False - if do_maketx and files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501 - linearize = True - # set its file node to 'raw' as tx will be linearized - files_metadata[filepath]["color_space"] = "raw" - - if do_maketx: - color_space = "raw" - - source, mode, texture_hash = self._process_texture( - filepath, - do_maketx, - staging=dir_path, - linearize=linearize, - force=force_copy - ) - destination = self.resource_destination(instance, - source, - do_maketx) - - # Force copy is specified. - if force_copy: - mode = COPY - - if mode == COPY: - transfers.append((source, destination)) - self.log.info('copying') - elif mode == HARDLINK: - hardlinks.append((source, destination)) - self.log.info('hardlinking') - - # Store the hashes from hash to destination to include in the - # database - hashes[texture_hash] = destination - - # Remap the resources to the destination path (change node attributes) - destinations = {} - remap = OrderedDict() # needs to be ordered, see color space values - for resource in resources: - source = os.path.normpath(resource["source"]) - if source not in destinations: - # Cache destination as source resource might be included - # multiple times - destinations[source] = self.resource_destination( - instance, source, do_maketx - ) - - # Preserve color space values (force value after filepath change) - # This will also trigger in the same order at end of context to - # ensure after context it's still the original value. - color_space_attr = resource["node"] + ".colorSpace" - try: - color_space = cmds.getAttr(color_space_attr) - except ValueError: - # node doesn't have color space attribute - color_space = "raw" - else: - if files_metadata[source]["color_space"] == "raw": - # set color space to raw if we linearized it - color_space = "raw" - # Remap file node filename to destination - remap[color_space_attr] = color_space - attr = resource["attribute"] - remap[attr] = destinations[source] - - self.log.info("Finished remapping destinations ...") + results = self.process_resources(instance, staging_dir=dir_path) + transfers = results["fileTransfers"] + hardlinks = results["fileHardlinks"] + hashes = results["fileHashes"] + remap = results["attrRemap"] # Extract in correct render layer layer = instance.data.get("renderlayer", "defaultRenderLayer") @@ -378,6 +286,112 @@ class ExtractLook(openpype.api.Extractor): self.log.info("Extracted instance '%s' to: %s" % (instance.name, maya_path)) + def process_resources(self, instance, staging_dir): + + # Extract the textures to transfer, possibly convert with maketx and + # remap the node paths to the destination path. Note that a source + # might be included more than once amongst the resources as they could + # be the input file to multiple nodes. + resources = instance.data["resources"] + do_maketx = instance.data.get("maketx", False) + + # Collect all unique files used in the resources + files = set() + files_metadata = {} + for resource in resources: + # Preserve color space values (force value after filepath change) + # This will also trigger in the same order at end of context to + # ensure after context it's still the original value. + color_space = resource.get("color_space") + + for f in resource["files"]: + files_metadata[os.path.normpath(f)] = { + "color_space": color_space} + # files.update(os.path.normpath(f)) + + # Process the resource files + transfers = [] + hardlinks = [] + hashes = {} + force_copy = instance.data.get("forceCopy", False) + + self.log.info(files) + for filepath in files_metadata: + + linearize = False + if do_maketx and files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501 + linearize = True + # set its file node to 'raw' as tx will be linearized + files_metadata[filepath]["color_space"] = "raw" + + if do_maketx: + color_space = "raw" + + source, mode, texture_hash = self._process_texture( + filepath, + do_maketx, + staging=staging_dir, + linearize=linearize, + force=force_copy + ) + destination = self.resource_destination(instance, + source, + do_maketx) + + # Force copy is specified. + if force_copy: + mode = COPY + + if mode == COPY: + transfers.append((source, destination)) + self.log.info('copying') + elif mode == HARDLINK: + hardlinks.append((source, destination)) + self.log.info('hardlinking') + + # Store the hashes from hash to destination to include in the + # database + hashes[texture_hash] = destination + + # Remap the resources to the destination path (change node attributes) + destinations = {} + remap = OrderedDict() # needs to be ordered, see color space values + for resource in resources: + source = os.path.normpath(resource["source"]) + if source not in destinations: + # Cache destination as source resource might be included + # multiple times + destinations[source] = self.resource_destination( + instance, source, do_maketx + ) + + # Preserve color space values (force value after filepath change) + # This will also trigger in the same order at end of context to + # ensure after context it's still the original value. + color_space_attr = resource["node"] + ".colorSpace" + try: + color_space = cmds.getAttr(color_space_attr) + except ValueError: + # node doesn't have color space attribute + color_space = "raw" + else: + if files_metadata[source]["color_space"] == "raw": + # set color space to raw if we linearized it + color_space = "raw" + # Remap file node filename to destination + remap[color_space_attr] = color_space + attr = resource["attribute"] + remap[attr] = destinations[source] + + self.log.info("Finished remapping destinations ...") + + return { + "fileTransfers": transfers, + "fileHardlinks": hardlinks, + "fileHashes": hashes, + "attrRemap": remap, + } + def resource_destination(self, instance, filepath, do_maketx): """Get resource destination path. @@ -467,3 +481,135 @@ class ExtractLook(openpype.api.Extractor): return converted, COPY, texture_hash return filepath, COPY, texture_hash + + +class ExtractAugmentedModel(ExtractLook): + """Extract as Augmented Model (Maya Scene). + + Rendering attrs augmented model. + + Only extracts contents based on the original "setMembers" data to ensure + publishing the least amount of required shapes. From that it only takes + the shapes that are not intermediateObjects + + During export it sets a temporary context to perform a clean extraction. + The context ensures: + - Smooth preview is turned off for the geometry + - Default shader is assigned (no materials are exported) + - Remove display layers + + """ + + label = "Augmented Model (Maya Scene)" + hosts = ["maya"] + families = ["model"] + scene_type = "ma" + augmented = "fried" + + def process(self, instance): + """Plugin entry point. + + Args: + instance: Instance to process. + + """ + render_sets = instance.data.get("modelRenderSetsHistory") + if not render_sets: + self.log.info("Model is not render augmented, skip extraction.") + return + + ext_mapping = ( + instance.context.data["project_settings"]["maya"]["ext_mapping"] + ) + if ext_mapping: + self.log.info("Looking in settings for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except KeyError: + # no preset found + pass + + if "representations" not in instance.data: + instance.data["representations"] = [] + + # Define extract output file path + stagingdir = self.staging_dir(instance) + ext = "{0}.{1}".format(self.augmented, self.scene_type) + filename = "{0}.{1}".format(instance.name, ext) + path = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction ...") + + results = self.process_resources(instance, staging_dir=stagingdir) + transfers = results["fileTransfers"] + hardlinks = results["fileHardlinks"] + hashes = results["fileHashes"] + remap = results["attrRemap"] + + self.log.info(remap) + + # Get only the shape contents we need in such a way that we avoid + # taking along intermediateObjects + members = instance.data("setMembers") + members = cmds.ls(members, + dag=True, + shapes=True, + type=("mesh", "nurbsCurve"), + noIntermediate=True, + long=True) + members += instance.data.get("modelRenderSetsHistory") + + with lib.no_display_layers(instance): + with lib.displaySmoothness(members, + divisionsU=0, + divisionsV=0, + pointsWire=4, + pointsShaded=1, + polygonObject=1): + with lib.shader(members, + shadingEngine="initialShadingGroup"): + # To avoid Maya trying to automatically remap the file + # textures relative to the `workspace -directory` we force + # it to a fake temporary workspace. This fixes textures + # getting incorrectly remapped. (LKD-17, PLN-101) + with no_workspace_dir(): + with lib.attribute_values(remap): + with avalon.maya.maintained_selection(): + + cmds.select(members, noExpand=True) + cmds.file(path, + force=True, + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 + exportSelected=True, + preserveReferences=False, + channels=False, + constraints=False, + expressions=False, + constructionHistory=False) + + if "hardlinks" not in instance.data: + instance.data["hardlinks"] = [] + if "transfers" not in instance.data: + instance.data["transfers"] = [] + + # Set up the resources transfers/links for the integrator + instance.data["transfers"].extend(transfers) + instance.data["hardlinks"].extend(hardlinks) + + # Source hash for the textures + instance.data["sourceHashes"] = hashes + + instance.data["representations"].append({ + 'name': ext, + 'ext': ext, + 'files': filename, + "stagingDir": stagingdir, + }) + + self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) From d9b5242dc177554527043f91e1d018d7ae5fcf7e Mon Sep 17 00:00:00 2001 From: David Lai Date: Tue, 24 Aug 2021 01:49:46 +0800 Subject: [PATCH 022/450] fix linter --- openpype/hosts/maya/plugins/publish/collect_look.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index fa48874f0e..d0c5ff203c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -364,7 +364,9 @@ class CollectLook(pyblish.api.InstancePlugin): render_sets = cmds.ls(look_sets, type=render_set_types) if render_sets: history.extend( - cmds.listHistory(render_sets, future=False, pruneDagObjects=True) + cmds.listHistory(render_sets, + future=False, + pruneDagObjects=True) or [] ) @@ -579,8 +581,9 @@ class CollectModelRenderSets(CollectLook): def process(self, instance): """Collect the Look in the instance with the correct layer settings""" model_nodes = instance[:] + renderlayer = instance.data.get("renderlayer", "defaultRenderLayer") - with lib.renderlayer(instance.data.get("renderlayer", "defaultRenderLayer")): + with lib.renderlayer(renderlayer): self.collect(instance) set_nodes = [m for m in instance if m not in model_nodes] From 6b276015e8af8a7c5cb15458d9ea689d66d35bd3 Mon Sep 17 00:00:00 2001 From: David Lai Date: Tue, 24 Aug 2021 01:57:34 +0800 Subject: [PATCH 023/450] update doc string --- openpype/hosts/maya/plugins/publish/collect_look.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index d0c5ff203c..ecc89e9032 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -568,7 +568,8 @@ class CollectLook(pyblish.api.InstancePlugin): class CollectModelRenderSets(CollectLook): """Collect render attribute sets for model instance. - This enables additional render attributes be published with model. + Collects additional render attribute sets so they can be + published with model. """ From 9f11165e87153d61612e824d50175bfdd2b0c732 Mon Sep 17 00:00:00 2001 From: David Lai Date: Tue, 24 Aug 2021 02:00:28 +0800 Subject: [PATCH 024/450] remove duplicated code --- .../hosts/maya/plugins/publish/extract_look.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 8ede62d84f..121c99fc47 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -518,21 +518,7 @@ class ExtractAugmentedModel(ExtractLook): self.log.info("Model is not render augmented, skip extraction.") return - ext_mapping = ( - instance.context.data["project_settings"]["maya"]["ext_mapping"] - ) - if ext_mapping: - self.log.info("Looking in settings for scene type ...") - # use extension mapping for first family found - for family in self.families: - try: - self.scene_type = ext_mapping[family] - self.log.info( - "Using {} as scene type".format(self.scene_type)) - break - except KeyError: - # no preset found - pass + self.get_maya_scene_type(instance) if "representations" not in instance.data: instance.data["representations"] = [] From 900e3aac7ae1e9a1bee97cf12fa4a8544df3208b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 25 Aug 2021 16:40:13 +0200 Subject: [PATCH 025/450] Hound --- tests/lib/file_handler.py | 2 +- tests/lib/testing_wrapper.py | 6 ++-- .../sync_server/test_site_operations.py | 34 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/lib/file_handler.py b/tests/lib/file_handler.py index 4c769620a0..98e14b0541 100644 --- a/tests/lib/file_handler.py +++ b/tests/lib/file_handler.py @@ -82,7 +82,7 @@ class RemoteFileHandler: try: print('Downloading ' + url + ' to ' + fpath) RemoteFileHandler._urlretrieve(url, fpath) - except (urllib.error.URLError, IOError) as e: #noqa type: ignore[attr-defined] + except (urllib.error.URLError, IOError) as e: if url[:5] == 'https': url = url.replace('https:', 'http:') print('Failed download. Trying https -> http instead.' diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_wrapper.py index 373bd9af0b..b72e4284d0 100644 --- a/tests/lib/testing_wrapper.py +++ b/tests/lib/testing_wrapper.py @@ -92,11 +92,11 @@ class TestCase: db_handler.teardown(self.TEST_OPENPYPE_NAME) @pytest.fixture(scope="module") - def db(self, db_setup): + def dbcon(self, db_setup): """Provide test database connection. Database prepared from dumps with 'db_setup' fixture. """ from avalon.api import AvalonMongoDB - db = AvalonMongoDB() - yield db + dbcon = AvalonMongoDB() + yield dbcon diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index 85e52bf5df..029f9a9f05 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -20,7 +20,7 @@ from bson.objectid import ObjectId class TestSiteOperation(TestCase): @pytest.fixture(scope="module") - def setup_sync_server_module(self, db): + def setup_sync_server_module(self, dbcon): """Get sync_server_module from ModulesManager""" from openpype.modules import ModulesManager @@ -28,24 +28,24 @@ class TestSiteOperation(TestCase): sync_server = manager.modules_by_name["sync_server"] yield sync_server - @pytest.mark.usefixtures("db") - def test_project_created(self, db): - assert ['test_project'] == db.database.collection_names(False) + @pytest.mark.usefixtures("dbcon") + def test_project_created(self, dbcon): + assert ['test_project'] == dbcon.database.collection_names(False) - @pytest.mark.usefixtures("db") - def test_objects_imported(self, db): - count_obj = len(list(db.database[self.TEST_PROJECT_NAME].find({}))) + @pytest.mark.usefixtures("dbcon") + def test_objects_imported(self, dbcon): + count_obj = len(list(dbcon.database[self.TEST_PROJECT_NAME].find({}))) assert 15 == count_obj @pytest.mark.usefixtures("setup_sync_server_module") - def test_add_site(self, db, setup_sync_server_module): + def test_add_site(self, dbcon, setup_sync_server_module): """Adds 'test_site', checks that added, checks that doesn't duplicate.""" query = { "_id": ObjectId(self.REPRESENTATION_ID) } - ret = db.database[self.TEST_PROJECT_NAME].find(query) + ret = dbcon.database[self.TEST_PROJECT_NAME].find(query) assert 1 == len(list(ret)), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) @@ -54,7 +54,7 @@ class TestSiteOperation(TestCase): self.REPRESENTATION_ID, site_name='test_site') - ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) + ret = list(dbcon.database[self.TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) @@ -64,7 +64,7 @@ class TestSiteOperation(TestCase): assert 'test_site' in site_names, "Site name wasn't added" @pytest.mark.usefixtures("setup_sync_server_module") - def test_add_site_again(self, db, setup_sync_server_module): + def test_add_site_again(self, dbcon, setup_sync_server_module): """Depends on test_add_site, must throw exception.""" with pytest.raises(ValueError): setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, @@ -72,7 +72,7 @@ class TestSiteOperation(TestCase): site_name='test_site') @pytest.mark.usefixtures("setup_sync_server_module") - def test_add_site_again_force(self, db, setup_sync_server_module): + def test_add_site_again_force(self, dbcon, setup_sync_server_module): """Depends on test_add_site, must not throw exception.""" setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, @@ -82,13 +82,13 @@ class TestSiteOperation(TestCase): "_id": ObjectId(self.REPRESENTATION_ID) } - ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) + ret = list(dbcon.database[self.TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) @pytest.mark.usefixtures("setup_sync_server_module") - def test_remove_site(self, db, setup_sync_server_module): + def test_remove_site(self, dbcon, setup_sync_server_module): """Depends on test_add_site, must remove 'test_site'.""" setup_sync_server_module.remove_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, @@ -98,7 +98,7 @@ class TestSiteOperation(TestCase): "_id": ObjectId(self.REPRESENTATION_ID) } - ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) + ret = list(dbcon.database[self.TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) @@ -109,7 +109,7 @@ class TestSiteOperation(TestCase): assert 'test_site' not in site_names, "Site name wasn't removed" @pytest.mark.usefixtures("setup_sync_server_module") - def test_remove_site_again(self, db, setup_sync_server_module): + def test_remove_site_again(self, dbcon, setup_sync_server_module): """Depends on test_add_site, must trow exception""" with pytest.raises(ValueError): setup_sync_server_module.remove_site(self.TEST_PROJECT_NAME, @@ -120,7 +120,7 @@ class TestSiteOperation(TestCase): "_id": ObjectId(self.REPRESENTATION_ID) } - ret = list(db.database[self.TEST_PROJECT_NAME].find(query)) + ret = list(dbcon.database[self.TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(self.REPRESENTATION_ID) From 4bb1b5c4a0dfc7c50bd7e154adebabe7a6a4fc2f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 25 Aug 2021 16:44:53 +0200 Subject: [PATCH 026/450] Removed default value --- tests/lib/testing_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_wrapper.py index b72e4284d0..df406f0ab9 100644 --- a/tests/lib/testing_wrapper.py +++ b/tests/lib/testing_wrapper.py @@ -78,7 +78,7 @@ class TestCase: """Restore prepared MongoDB dumps into selected DB.""" backup_dir = os.path.join(download_test_data, "input", "dumps") - uri = os.environ.get("OPENPYPE_MONGO") or "mongodb://localhost:27017" + uri = os.environ.get("OPENPYPE_MONGO") db_handler = DBHandler(uri) db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir, True, db_name_out=self.TEST_DB_NAME) From 08b42c9427dae0afd513716362cd715960aeecf0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Aug 2021 11:45:55 +0200 Subject: [PATCH 027/450] Added setup of _ROOT env vars --- tests/lib/testing_wrapper.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_wrapper.py index df406f0ab9..1ff42158db 100644 --- a/tests/lib/testing_wrapper.py +++ b/tests/lib/testing_wrapper.py @@ -72,6 +72,12 @@ class TestCase: value = value.format(**all_vars) print("Setting {}:{}".format(key, value)) monkeypatch_session.setenv(key, value) + import openpype + + openpype_root = os.path.dirname(os.path.dirname(openpype.__file__)) + # ?? why 2 of those + monkeypatch_session.setenv("OPENPYPE_ROOT", openpype_root) + monkeypatch_session.setenv("OPENPYPE_REPOS_ROOT", openpype_root) @pytest.fixture(scope="module") def db_setup(self, download_test_data, env_var, monkeypatch_session): From c130b72f126a31142e71f6d749d5b5ee65e1a3b3 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 27 Aug 2021 11:02:05 +0100 Subject: [PATCH 028/450] Update openpype/plugins/publish/extract_burnin.py Co-authored-by: Milan Kolar --- openpype/plugins/publish/extract_burnin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 2fab67cdb9..b35f514509 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -156,7 +156,7 @@ class ExtractBurnin(openpype.api.Extractor): burnin_data["anatomy"] = filled_anatomy.get_solved() # Add context data burnin_data. - burnin_data["context"] = instance.context.data["burnin_context"] + burnin_data["context"] = instance.context.data.get("burnin_context") or {} # Add source camera name to burnin data camera_name = repre.get("camera_name") From 1693be8778cac688de8c8606c7b19a495ed7e7b5 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 27 Aug 2021 11:04:49 +0100 Subject: [PATCH 029/450] Update openpype/plugins/publish/extract_burnin.py --- openpype/plugins/publish/extract_burnin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index b35f514509..fae79d6334 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -156,7 +156,9 @@ class ExtractBurnin(openpype.api.Extractor): burnin_data["anatomy"] = filled_anatomy.get_solved() # Add context data burnin_data. - burnin_data["context"] = instance.context.data.get("burnin_context") or {} + burnin_data["context"] = ( + instance.context.data.get("burnin_context") or {} + ) # Add source camera name to burnin data camera_name = repre.get("camera_name") From d1e8032fcdfb5f1c27872e8c99e07084856e8e55 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 1 Sep 2021 09:06:04 +0200 Subject: [PATCH 030/450] Failsafe for not finding the task. Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/modules/default_modules/ftrack/ftrack_module.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 828a7f1cab..0258b0ea1e 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -364,6 +364,8 @@ class FtrackModule( ' and parent.name is "{}"' ' and project.full_name is "{}"' ).format(task_name, asset_name, project_name) - task_entity = session.query(query).one() + task_entity = session.query(query).first() + if not task_entity: + return 0 hours_logged = (task_entity["time_logged"] / 60) / 60 return hours_logged From 19c058eafd3f6dbda94cef6230612ddb3489cf7c Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 1 Sep 2021 09:06:51 +0200 Subject: [PATCH 031/450] Respond with error message Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/modules/default_modules/timers_manager/rest_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/timers_manager/rest_api.py b/openpype/modules/default_modules/timers_manager/rest_api.py index 1699179fd6..942db60ebc 100644 --- a/openpype/modules/default_modules/timers_manager/rest_api.py +++ b/openpype/modules/default_modules/timers_manager/rest_api.py @@ -63,7 +63,9 @@ class TimersManagerModuleRestApi: asset_name = data['asset_name'] task_name = data['task_name'] except KeyError: - log.error("Payload must contain fields 'project_name, " + + message = "Payload must contain fields 'project_name, 'asset_name', 'task_name'" + log.warning(message) + return Response(text=message, status=404) "'asset_name', 'task_name'") return Response(status=400) From 348274de5050feb94b3e48821baf1720e373e5a6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Sep 2021 08:22:06 +0100 Subject: [PATCH 032/450] Hound fix --- .../modules/default_modules/timers_manager/rest_api.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/rest_api.py b/openpype/modules/default_modules/timers_manager/rest_api.py index 942db60ebc..19b72d688b 100644 --- a/openpype/modules/default_modules/timers_manager/rest_api.py +++ b/openpype/modules/default_modules/timers_manager/rest_api.py @@ -63,11 +63,12 @@ class TimersManagerModuleRestApi: asset_name = data['asset_name'] task_name = data['task_name'] except KeyError: - message = "Payload must contain fields 'project_name, 'asset_name', 'task_name'" + message = ( + "Payload must contain fields 'project_name, 'asset_name'," + " 'task_name'" + ) log.warning(message) return Response(text=message, status=404) - "'asset_name', 'task_name'") - return Response(status=400) time = self.module.get_task_time(project_name, asset_name, task_name) return Response(text=json.dumps(time)) From b6a4a071f1375b205a7792a3fd5e2cc46e115aad Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Sep 2021 08:26:50 +0100 Subject: [PATCH 033/450] Reset submodule. --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 91867aeb4b..f48fce09c0 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 91867aeb4bfde115c0595c683282cc0b8265e694 +Subproject commit f48fce09c0986c1fd7f6731de33907be46b436c5 From 537b9e7bab559419c37460c1c67d709bb482169a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Sep 2021 08:31:18 +0100 Subject: [PATCH 034/450] Stop timer was within validator order range. --- openpype/plugins/publish/stop_timer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/stop_timer.py b/openpype/plugins/publish/stop_timer.py index 81afd16378..5c939b5f1b 100644 --- a/openpype/plugins/publish/stop_timer.py +++ b/openpype/plugins/publish/stop_timer.py @@ -8,7 +8,7 @@ from openpype.api import get_system_settings class StopTimer(pyblish.api.ContextPlugin): label = "Stop Timer" - order = pyblish.api.ExtractorOrder - 0.5 + order = pyblish.api.ExtractorOrder - 0.49 hosts = ["*"] def process(self, context): From 9887e00859850cf0044452b0413db8406624f051 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 1 Sep 2021 09:36:20 +0200 Subject: [PATCH 035/450] Update openpype/plugins/publish/extract_burnin.py Co-authored-by: Milan Kolar --- openpype/plugins/publish/extract_burnin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index fae79d6334..a9076c3e3c 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -156,8 +156,8 @@ class ExtractBurnin(openpype.api.Extractor): burnin_data["anatomy"] = filled_anatomy.get_solved() # Add context data burnin_data. - burnin_data["context"] = ( - instance.context.data.get("burnin_context") or {} + burnin_data["custom"] = ( + instance.data.get("custom_burnin_data") or {} ) # Add source camera name to burnin data From 38d6e4805e0fc537427ed76e261bc5ffe8ede78b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Sep 2021 11:25:26 +0200 Subject: [PATCH 036/450] Added possibility to inject 'last_workfile_path' through data --- openpype/lib/applications.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 71ab2eac61..a62bd99b8c 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1328,23 +1328,27 @@ def _prepare_last_workfile(data, workdir): # Last workfile path last_workfile_path = "" - extensions = avalon.api.HOST_WORKFILE_EXTENSIONS.get( - app.host_name - ) - if extensions: - anatomy = data["anatomy"] - # Find last workfile - file_template = anatomy.templates["work"]["file"] - workdir_data.update({ - "version": 1, - "user": get_openpype_username(), - "ext": extensions[0] - }) - - last_workfile_path = avalon.api.last_workfile( - workdir, file_template, workdir_data, extensions, True + if data.get("last_workfile_path"): # to inject explicitly + last_workfile_path = data.get("last_workfile_path") + else: + extensions = avalon.api.HOST_WORKFILE_EXTENSIONS.get( + app.host_name ) + if extensions: + anatomy = data["anatomy"] + # Find last workfile + file_template = anatomy.templates["work"]["file"] + workdir_data.update({ + "version": 1, + "user": get_openpype_username(), + "ext": extensions[0] + }) + + last_workfile_path = avalon.api.last_workfile( + workdir, file_template, workdir_data, extensions, True + ) + if os.path.exists(last_workfile_path): log.debug(( "Workfiles for launch context does not exists" From c9c21849b0b792b59c645ef6ffe6e102d874100d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Sep 2021 11:54:01 +0200 Subject: [PATCH 037/450] Added possibility to inject 'last_workfile_path' through data --- openpype/hooks/pre_global_host_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hooks/pre_global_host_data.py b/openpype/hooks/pre_global_host_data.py index c669d91ad5..b32fb5e44a 100644 --- a/openpype/hooks/pre_global_host_data.py +++ b/openpype/hooks/pre_global_host_data.py @@ -43,6 +43,8 @@ class GlobalHostDataHook(PreLaunchHook): "env": self.launch_context.env, + "last_workfile_path": self.data.get("last_workfile_path"), + "log": self.log }) From d4150a6af2ae31de8db6a9e2372ec3e6c9e990ec Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Sep 2021 16:49:04 +0200 Subject: [PATCH 038/450] Fix - order must be in range --- openpype/plugins/publish/collect_host_name.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_host_name.py b/openpype/plugins/publish/collect_host_name.py index 41d9cc3a5a..ffb8125cbb 100644 --- a/openpype/plugins/publish/collect_host_name.py +++ b/openpype/plugins/publish/collect_host_name.py @@ -14,7 +14,7 @@ class CollectHostName(pyblish.api.ContextPlugin): """Collect avalon host name to context.""" label = "Collect Host Name" - order = pyblish.api.CollectorOrder - 1 + order = pyblish.api.CollectorOrder - 0.49 def process(self, context): host_name = context.data.get("hostName") @@ -35,3 +35,4 @@ class CollectHostName(pyblish.api.ContextPlugin): host_name = app.host_name context.data["hostName"] = host_name + From 0dee75ec876cd485a13d039cd1ffb8885e2a28fe Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Sep 2021 19:20:50 +0100 Subject: [PATCH 039/450] Dropbox Provider --- .../sync_server/providers/dropbox.py | 366 ++++++++++++++++++ .../sync_server/providers/lib.py | 2 + .../providers/resources/dropbox.png | Bin 0 -> 2081 bytes .../schema_project_syncserver.json | 41 +- 4 files changed, 404 insertions(+), 5 deletions(-) create mode 100644 openpype/modules/default_modules/sync_server/providers/dropbox.py create mode 100644 openpype/modules/default_modules/sync_server/providers/resources/dropbox.png diff --git a/openpype/modules/default_modules/sync_server/providers/dropbox.py b/openpype/modules/default_modules/sync_server/providers/dropbox.py new file mode 100644 index 0000000000..31459f1074 --- /dev/null +++ b/openpype/modules/default_modules/sync_server/providers/dropbox.py @@ -0,0 +1,366 @@ +import os + +import dropbox + +from openpype.api import Logger +from .abstract_provider import AbstractProvider +from ..utils import EditableScopes + +log = Logger().get_logger("SyncServer") + + +class DropboxHandler(AbstractProvider): + CODE = 'dropbox' + LABEL = 'Dropbox' + + def __init__(self, project_name, site_name, tree=None, presets=None): + self.active = False + self.site_name = site_name + self.presets = presets + + if not self.presets: + log.info( + "Sync Server: There are no presets for {}.".format(site_name) + ) + return + + provider_presets = self.presets.get(self.CODE) + if not provider_presets: + msg = "Sync Server: No provider presets for {}".format(self.CODE) + log.info(msg) + return + + token = self.presets[self.CODE].get("token", "") + if not token: + msg = "Sync Server: No access token for dropbox provider" + log.info(msg) + return + + team_folder_name = self.presets[self.CODE].get("team_folder_name", "") + if not team_folder_name: + msg = "Sync Server: No team folder name for dropbox provider" + log.info(msg) + return + + acting_as_member = self.presets[self.CODE].get("acting_as_member", "") + if not acting_as_member: + msg = ( + "Sync Server: No acting member for dropbox provider" + ) + log.info(msg) + return + + self.dbx = None + try: + self.dbx = self._get_service( + token, acting_as_member, team_folder_name + ) + except Exception as e: + log.info("Could not establish dropbox object: {}".format(e)) + return + + super(AbstractProvider, self).__init__() + + def _get_service(self, token, acting_as_member, team_folder_name): + dbx = dropbox.DropboxTeam(token) + + # Getting member id. + member_id = None + member_names = [] + for member in dbx.team_members_list().members: + member_names.append(member.profile.name.display_name) + if member.profile.name.display_name == acting_as_member: + member_id = member.profile.team_member_id + + if member_id is None: + raise ValueError( + "Could not find member \"{}\". Available members: {}".format( + acting_as_member, member_names + ) + ) + + # Getting team folder id. + team_folder_id = None + team_folder_names = [] + for entry in dbx.team_team_folder_list().team_folders: + team_folder_names.append(entry.name) + if entry.name == team_folder_name: + team_folder_id = entry.team_folder_id + + if team_folder_id is None: + raise ValueError( + "Could not find team folder \"{}\". Available folders: " + "{}".format( + team_folder_name, team_folder_names + ) + ) + + # Establish dropbox object. + path_root = dropbox.common.PathRoot.namespace_id(team_folder_id) + return dropbox.DropboxTeam( + token + ).with_path_root(path_root).as_user(member_id) + + def is_active(self): + """ + Returns True if provider is activated, eg. has working credentials. + Returns: + (boolean) + """ + return self.dbx is not None + + @classmethod + def get_configurable_items(cls): + """ + Returns filtered dict of editable properties + + + Returns: + (dict) + """ + editable = { + 'token': { + 'scope': [EditableScopes.PROJECT], + 'label': "Access Token", + 'type': 'text', + 'namespace': ( + '{project_settings}/global/sync_server/sites/{site}/token' + ) + }, + 'team_folder_name': { + 'scope': [EditableScopes.PROJECT], + 'label': "Team Folder Name", + 'type': 'text', + 'namespace': ( + '{project_settings}/global/sync_server/sites/{site}' + '/team_folder_name' + ) + }, + 'acting_as_member': { + 'scope': [EditableScopes.PROJECT, EditableScopes.LOCAL], + 'label': "Acting As Member", + 'type': 'text', + 'namespace': ( + '{project_settings}/global/sync_server/sites/{site}' + '/acting_as_member' + ) + } + } + return editable + + def _path_exists(self, path): + try: + entries = self.dbx.files_list_folder( + path=os.path.dirname(path) + ).entries + except dropbox.exceptions.ApiError: + return False + + for entry in entries: + if entry.name == os.path.basename(path): + return True + + return False + + def upload_file(self, source_path, path, + server, collection, file, representation, site, + overwrite=False): + """ + Copy file from 'source_path' to 'target_path' on provider. + Use 'overwrite' boolean to rewrite existing file on provider + + Args: + source_path (string): + path (string): absolute path with or without name of the file + overwrite (boolean): replace existing file + + arguments for saving progress: + server (SyncServer): server instance to call update_db on + collection (str): name of collection + file (dict): info about uploaded file (matches structure from db) + representation (dict): complete repre containing 'file' + site (str): site name + Returns: + (string) file_id of created file, raises exception + """ + # Check source path. + if not os.path.exists(source_path): + raise FileNotFoundError( + "Source file {} doesn't exist.".format(source_path) + ) + + if self._path_exists(path) and not overwrite: + raise FileExistsError( + "File already exists, use 'overwrite' argument" + ) + + mode = dropbox.files.WriteMode("add", None) + if overwrite: + mode = dropbox.files.WriteMode.overwrite + + with open(source_path, "rb") as f: + self.dbx.files_upload(f.read(), path, mode=mode) + + server.update_db( + collection=collection, + new_file_id=None, + file=file, + representation=representation, + site=site, + progress=100 + ) + + return path + + def download_file(self, source_path, local_path, + server, collection, file, representation, site, + overwrite=False): + """ + Download file from provider into local system + + Args: + source_path (string): absolute path on provider + local_path (string): absolute path with or without name of the file + overwrite (boolean): replace existing file + + arguments for saving progress: + server (SyncServer): server instance to call update_db on + collection (str): name of collection + file (dict): info about uploaded file (matches structure from db) + representation (dict): complete repre containing 'file' + site (str): site name + Returns: + None + """ + # Check source path. + if not self._path_exists(source_path): + raise FileNotFoundError( + "Source file {} doesn't exist.".format(source_path) + ) + + if os.path.exists(local_path) and not overwrite: + raise FileExistsError( + "File already exists, use 'overwrite' argument" + ) + + if os.path.exists(local_path) and overwrite: + os.unlink(local_path) + + self.dbx.files_download_to_file(local_path, source_path) + + server.update_db( + collection=collection, + new_file_id=None, + file=file, + representation=representation, + site=site, + progress=100 + ) + + return os.path.basename(source_path) + + def delete_file(self, path): + """ + Deletes file from 'path'. Expects path to specific file. + + Args: + path (string): absolute path to particular file + + Returns: + None + """ + if not self._path_exists(path): + raise FileExistsError("File {} doesn't exist".format(path)) + + self.dbx.files_delete(path) + + def list_folder(self, folder_path): + """ + List all files and subfolders of particular path non-recursively. + Args: + folder_path (string): absolut path on provider + + Returns: + (list) + """ + if not self._path_exists(folder_path): + raise FileExistsError( + "Folder \"{}\" does not exist".format(folder_path) + ) + + entry_names = [] + for entry in self.dbx.files_list_folder(path=folder_path).entries: + entry_names.append(entry.name) + return entry_names + + 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' + """ + if self._path_exists(folder_path): + return folder_path + + self.dbx.files_create_folder_v2(folder_path) + + return folder_path + + 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 + + def get_roots_config(self, anatomy=None): + """ + Returns root values for path resolving + + Takes value from Anatomy which takes values from Settings + overridden by Local Settings + + Returns: + (dict) - {"root": {"root": "/My Drive"}} + OR + {"root": {"root_ONE": "value", "root_TWO":"value}} + Format is importing for usage of python's format ** approach + """ + return self.presets['root'] + + def resolve_path(self, path, root_config=None, anatomy=None): + """ + Replaces all root placeholders with proper values + + Args: + path(string): root[work]/folder... + root_config (dict): {'work': "c:/..."...} + anatomy (Anatomy): object of Anatomy + Returns: + (string): proper url + """ + if not root_config: + root_config = self.get_roots_config(anatomy) + + 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 diff --git a/openpype/modules/default_modules/sync_server/providers/lib.py b/openpype/modules/default_modules/sync_server/providers/lib.py index 816ccca981..192562b48b 100644 --- a/openpype/modules/default_modules/sync_server/providers/lib.py +++ b/openpype/modules/default_modules/sync_server/providers/lib.py @@ -1,4 +1,5 @@ from .gdrive import GDriveHandler +from .dropbox import DropboxHandler from .local_drive import LocalDriveHandler @@ -103,4 +104,5 @@ factory = ProviderFactory() # 7 denotes number of files that could be synced in single loop - learned by # trial and error factory.register_provider(GDriveHandler.CODE, GDriveHandler, 7) +factory.register_provider(DropboxHandler.CODE, DropboxHandler, 10) factory.register_provider(LocalDriveHandler.CODE, LocalDriveHandler, 50) diff --git a/openpype/modules/default_modules/sync_server/providers/resources/dropbox.png b/openpype/modules/default_modules/sync_server/providers/resources/dropbox.png new file mode 100644 index 0000000000000000000000000000000000000000..6f56e3335b2871feb40dda6dc46ce5c88170cbe8 GIT binary patch literal 2081 zcmbVN2~gBl7+$p~h@g0Zwqs)u#A7$v17rgW#RX-xiwvwn<&bQWg^atKm;`qL6-K#b ztAaY#GK!9()E4o^6A`6qrOsH9qhpavpw@#Ks*YEi<=D}VwNo?m@?PHi-uK^s^1{Ow zI*pz>S|AWOsX~Jz_}R@m>__tNdnUzmesZ8gzhVUfXHV-Gw)e5eD}lhyh15iI(dsV| zj53H&oYE2^vw`N(0)c;^nMSdd1P5q|MAE1LAD^uP0TNe$QQm4;O)H5cGBlMTB2&XO zSn5hlj)Q>#fWH~x2@C{>0%n8W$RcJ1*w2gbYwI=y0{swfr2-7H3Iw9n;ee822*5`K z3o#h>0p`g?II2Z4t#&RTfhA%HmO)~f5QY({9FfU@!3X5UF}Mzi2wpHKjQ>)ANgPKb z5M(l$L?)?-ViF;-TrP)T2_%sSd4!NnF>Ru?V)>)DOfl8;(w9^!>td3?lS|fiQ9`j}_anbP~l;Y!dY^ z)VAYy1n|C9t8FsgtBb*46TxyJt9dg99r9i@t4W~=D1uKNP9(A(a7%L?i>eqDUi7nb=uxT_A!{j!=Nss0m@Y5Eg61-iUY}BK4jFixC(e zfT}5+)TO)^Dt`yZCk&2q=!cOwhUh59fbyo21~id?Xk#J>*c^x`DLuvTB6+=}Z||#= z%5a9#k$V1sjaWDzP=zRc#Bv`Wp+qF^S5&P=R7RFVjToT{R)D-)L?nqLTCEh5VuVD9 z>AbZdZs^wr9Jg}7KYPm23W3k;JOOm7WCp69qvrYXK-g%o=$D2M4#wkM_U-ttpA%Nk5Pg12C4-aVC@@ zf)aUe{b$BOyz-E>R0r&U{%K}hJt zvzP}lEvHty?UIdB*?*ppZO+hCJAl<`-8HYjX~}Pn-S2eC>m=&elVrEhrEjf&?n^lT z*HpX4O^4~Yp1VG4^6d7T)?Ugxm*H7{MRko{nsJC84rvbGdH8%$czzyIRA)3MU^rfk_0hdby|OW#)9aoxej+dDeK{PX@=G@+{SPOE$F$$-je(fRbz zt(lqOueQG2vmp(bSYxC#bx(2UCB)S zsk|n}eB8}&aauv0dWqwU-l>mf*DkI&CT{ZGVCNez$r)B$eL-|TElcXbGPBaZ4zUPg z@(-bGR*+N9#(SO3Rm;mu+>aB}+nu!&M6GBH?vIPF7aUz4wqo(2O>L9?8$5lpPB1IR zv^w}(VtSK5HGT4+v(T=!DX(c~*sQ=4&1&%_ zU<06aimwHJzTR-PZcyjjTWs}>NXtUt@ zVwFXjw9hlJ>&O_Wt|k^;7tWOBmg(~I4cytA&x)3{wu_3}FFO_P3Cn!+dBMc?!nBIT zVNoNxJkmM}6?g0QepeT>#q`3nN7?pp{;>&FeYe}vW4}H8!|7dDGp#>yRY+KH`TT_S Fe*@&?2sr=% literal 0 HcmV?d00001 diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json b/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json index cb2cc9c9d1..577efcaf13 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json @@ -48,10 +48,41 @@ "type": "dict", "children": [ { - "type": "path", - "key": "credentials_url", - "label": "Credentials url", - "multiplatform": true + "type": "dict", + "key": "gdrive", + "label": "Google Drive", + "collapsible": true, + "children": [ + { + "type": "path", + "key": "credentials_url", + "label": "Credentials url", + "multiplatform": true + } + ] + }, + { + "type": "dict", + "key": "dropbox", + "label": "Dropbox", + "collapsible": true, + "children": [ + { + "type": "text", + "key": "token", + "label": "Access Token" + }, + { + "type": "text", + "key": "team_folder_name", + "label": "Team Folder Name" + }, + { + "type": "text", + "key": "acting_as_member", + "label": "Acting As Member" + } + ] }, { "type": "dict-modifiable", @@ -61,7 +92,7 @@ "collapsable_key": false, "object_type": "text" } - ] + ] } } ] From 7c607784bc1e68a04e67ca88f1d9c83746b67db3 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Sep 2021 19:21:10 +0100 Subject: [PATCH 040/450] Code cosmetics --- .../default_modules/sync_server/providers/abstract_provider.py | 3 ++- openpype/modules/default_modules/sync_server/sync_server.py | 1 + openpype/modules/default_modules/sync_server/utils.py | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py index 2e9632134c..b6234c7bc6 100644 --- a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py +++ b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py @@ -81,7 +81,8 @@ class AbstractProvider: representation (dict): complete repre containing 'file' site (str): site name Returns: - None + (string) file_id of created/modified file , + throws FileExistsError, FileNotFoundError exceptions """ pass diff --git a/openpype/modules/default_modules/sync_server/sync_server.py b/openpype/modules/default_modules/sync_server/sync_server.py index 638a4a367f..2227ec9366 100644 --- a/openpype/modules/default_modules/sync_server/sync_server.py +++ b/openpype/modules/default_modules/sync_server/sync_server.py @@ -221,6 +221,7 @@ def _get_configured_sites_from_setting(module, project_name, project_setting): return configured_sites + class SyncServerThread(threading.Thread): """ Separate thread running synchronization server with asyncio loop. diff --git a/openpype/modules/default_modules/sync_server/utils.py b/openpype/modules/default_modules/sync_server/utils.py index d4fc29ff8a..85e4e03f77 100644 --- a/openpype/modules/default_modules/sync_server/utils.py +++ b/openpype/modules/default_modules/sync_server/utils.py @@ -29,7 +29,6 @@ def time_function(method): kw['log_time'][name] = int((te - ts) * 1000) else: log.debug('%r %2.2f ms' % (method.__name__, (te - ts) * 1000)) - print('%r %2.2f ms' % (method.__name__, (te - ts) * 1000)) return result return timed From fdb739e52e9f0f01a6ec0c15adce4a5063a06be5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Sep 2021 19:21:41 +0100 Subject: [PATCH 041/450] Enable gdrive provider to use new settings schema. --- .../default_modules/sync_server/providers/gdrive.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 18d679b833..2a4d1e497f 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -61,7 +61,6 @@ class GDriveHandler(AbstractProvider): CHUNK_SIZE = 2097152 # must be divisible by 256! used for upload chunks def __init__(self, project_name, site_name, tree=None, presets=None): - self.presets = None self.active = False self.project_name = project_name self.site_name = site_name @@ -74,7 +73,13 @@ class GDriveHandler(AbstractProvider): format(site_name)) return - cred_path = self.presets.get("credentials_url", {}).\ + provider_presets = self.presets.get(self.CODE) + if not provider_presets: + msg = "Sync Server: No provider presets for {}".format(self.CODE) + log.info(msg) + return + + cred_path = self.presets[self.CODE].get("credentials_url", {}).\ get(platform.system().lower()) or '' if not os.path.exists(cred_path): msg = "Sync Server: No credentials for gdrive provider " + \ From 691fcc4f31cd64647cdcd57394c249adad33c084 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 2 Sep 2021 13:49:59 +0200 Subject: [PATCH 042/450] More pictures, more tidy, hopefully more readable. --- .../assets/nuke_tut/nuke_AssetLoadOutOfDate.png | Bin 0 -> 38359 bytes .../docs/assets/nuke_tut/nuke_ManageVersion.png | Bin 0 -> 71506 bytes .../assets/nuke_tut/nuke_PyblishCheckBox.png | Bin 0 -> 7732 bytes .../nuke_PyblishDialogNukeNoteIntent.png | Bin 0 -> 8626 bytes .../nuke_tut/nuke_RunNukeFtrackAction_p3.png | Bin 0 -> 26720 bytes .../assets/nuke_tut/nuke_ValidateContainers.png | Bin 0 -> 76046 bytes .../assets/nuke_tut/nuke_WriteNodeCreated.png | Bin 0 -> 34442 bytes .../assets/nuke_tut/nuke_WriteNodeReview.png | Bin 0 -> 12896 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/assets/nuke_tut/nuke_AssetLoadOutOfDate.png create mode 100644 website/docs/assets/nuke_tut/nuke_ManageVersion.png create mode 100644 website/docs/assets/nuke_tut/nuke_PyblishCheckBox.png create mode 100644 website/docs/assets/nuke_tut/nuke_PyblishDialogNukeNoteIntent.png create mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p3.png create mode 100644 website/docs/assets/nuke_tut/nuke_ValidateContainers.png create mode 100644 website/docs/assets/nuke_tut/nuke_WriteNodeCreated.png create mode 100644 website/docs/assets/nuke_tut/nuke_WriteNodeReview.png diff --git a/website/docs/assets/nuke_tut/nuke_AssetLoadOutOfDate.png b/website/docs/assets/nuke_tut/nuke_AssetLoadOutOfDate.png new file mode 100644 index 0000000000000000000000000000000000000000..f7f807a94f7824fc2c08374089ca3f24890ffc17 GIT binary patch literal 38359 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Π*QVsYGnG?hdX6a@ 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

    9oN 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 GJ?>wbX@OS& literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_ManageVersion.png b/website/docs/assets/nuke_tut/nuke_ManageVersion.png new file mode 100644 index 0000000000000000000000000000000000000000..c9f2091347e2a76053736f0fead1151c6145a681 GIT binary patch literal 71506 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%0A{1<2_ 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_ValidateContainers.png b/website/docs/assets/nuke_tut/nuke_ValidateContainers.png new file mode 100644 index 0000000000000000000000000000000000000000..78e0f2edd7d33e1926f50340d61c99b30d8878c9 GIT binary patch literal 76046 zcmag_1z1$y*Dwqpx=RpHK!#DeyF&y)x+DgW&KbHJkw!o|B$SjGWatKw5CKt;p+P}F z8l?L>`1{9wf6w#2?_4m~oW1s5d+pVG?R{djG?YjP?-Bw4K=MRcK?eYEUIG9nF+L{P zGBq7C4K`Rh=oxw%s;fy@xj6AySi4x-@c25pf^YzklJ#}9u!7roLM(0U9Gs=0JFT5i zh=a8>)IeCBPu=yAjlF}izq^gDzlNTbKio>(8Y(M8DCH{wB5<Cm~ zFRzb}508%^kBhqi+)?ds)@_>0-viq{5V<7DIP>A}m-!_WIa``h?B{FfSM zkAJZXWE}4e1nkcDzq)%m*#19szk&Q8-CgZnJY78OU0nY|)c=~|Kj8mkAjt0j`w(9X z*Z*5*b@l)E?oLksYZ)G%irygO{w0L}Mbp10@X+&fwc*vV@o@2Sx3W?62Dx|tFXL|b zBBABrYlARUZ~&R=0ZKy}$}c3q_x}er`X69n0ipjwAGsi0+;u@cv;oKcZN6ci#1khA zI~xNBYftT|{e|4nhw!>lh^NjD;EDwPi_Y-BFlnf`2&jrTYVvoK z(f<|nA8APZB~abP8l+F~@2LNRo;-S_}~t)GI0C)i7XPk^7BPn?@iK#yNgLO@i4Pk@t8K!Wcddj9qXEy&u!)8hZD|BW3$ zq^zwZJRLj{Hh-V~v5SWY&wrl&c|dS3yV{74s@Px`Zd%*-_RZu8I%)G=x3Lt9%1P?mlW ztsCQk@bifC{3F%>4DhkH0g?Sb^Og61uH@g8N%8(4wEwg4|KU`?asRY|&I(^XiZckn!W$e)s#zdg?ho{GCSl%d}HtpW8C3gMYdJK#{la;<>cH`y2 z+Y8_7D(@0bu16?QsgDq7;%g#TgyI?_X40%ZjZX9?3kPu`T)3t67ir=snXhUL$)du{ zU2c0bbBO7GP|?vO8XR;h)%9CGmO-m#j}OgO_Bf+7h~-~WcJ)3e#NHF^s`ix)I{BS( z`{|$d-itdm$raHPY;S7i!ybrB6|5AuaPafAa0n`XSifHAxV!hztjym7aLiZRN8+tF z{0JBmk|k5$KgXv}bWLJkwIehCZF(?VvoepjDv@KFUO!2i<09Apf_l7Aae&D-vH#~G zHVpdJnp18PaD4*SLUKxepSh#3GcT&cdHxP6&A_qi5(iVRle_VQ{hT2N~LRj#|bSv%4&_jJ{u^Kz!nXXhC{ zQ;)>QA^02)Q#d&hq@aE~j9fmtBC8h7KrV06O8ruSi#qRBbp*M5WydK~%t_`BDz=pwmgW_CK(bzZs{~cAjh>)24Z|F$Pkms zm}zbsql*s?GrCm-VremOQ@Ox5r9KOPLYjtl=PbE&?5V zCjFUPrxbgtno@f`9NtRcus>}ZraRdhWzkDi6?c%)1;4`_e3GNV0TotXc&|^^hB_JS zrugx0#-fvlHbK%kzvX_w>H^oeiH_`c3 z?(ZxEEdzx$L5!A0(=*enZR%Wcusu=&rXKev2$1k9u^Rn45G}LGpYt$mc97B=s+6~1 z|84sc5dNRGA$HRn@Q~ZUPej=hkZXb-)X?@HE~vyHPJBa7oML5mOn%y-YHD;13jH{m zYbH8=S@M12_T=2?FHR5}d}2Di_(oKDreDf6lW#CsdC1>Z-82%8?NW#i5lYh03nA3H zB$I>4Gbbkwf=uR0essfphl~pirYtH@Y+io(@H<5Sxg0h*gvqkv!GuiV zO^bDxefg3x=s2||qJ*G44bn=lj0o~YN0u?^aB>52TRyCp)8BhMt{1NNKPPsYzNW`Y z@}^Y49?cb<^Zs})vsfempVMXPso(yl#Av!W9oqr@}-|H>pN-?^A01BX@thRRlvhxToxJ2a^ZmBW3xDFxk z0adX#LpH=Z#z6MuCsqYId|>HprCE zMuQ62gU^gtCAY5!-(O$x)(VfK#`Y?vRok}4%pjr`!x0&T1Di7w9-EWqZut*nKn3*& zX9cRz_=*T40-ybkA2T5ft2^!OSH8bd=R239yJ4(TU<+AJ_PL|g{q2jPM=s(mT_~b^ z%;8Pm+|)6KHw?S_NoRKbyR#LqYe=kD(m7;C>3U8&cuIEP}AJ7 z-^4gL5P=yUuz^agI_5?Q7=V0YzO}uMig9A}{UylQx<7EvNC$IfjbuFk9>v(2Qvq7U zI8{H3Z6ABdeX9L2PW`BVWMyT@9{PbA5`5_*bF!}4A$T^ngE851ZSekjIiS*Ju=*%0 zhKlW;q)6tP_^ScyqeCePvFN;ez>60zq|dhp%*jDmB-Zh4ijgJk(+`wY)_efz!I1N9 zyzORIL9yN6U8oO1?b7?Cx8#Lp&dLng^%vYJx6@A&Tz+#Zs%zX@`8Z}#HcrG5>!p)r2#@rKb>cy?)h797?4R_JOa9$j)PAxOJ%&S$pJ+3z91RH zBrb8#;k;&$jcrHoL0y6qqyO(D$wr_nB_Cc_{aYJ!GwC2}(IWB~H@>q9f)?Pv`;ZV5 z;W$=x!pvI#fz~$C#x|~3kbPz3)6c4BWjc6f{OMdd;+(T}?ip>HPC4h1363 zJ~|E+%%LCKE5xbHFC|r3@2sEQ&U4-CZVK)FerZaQsc##D4 z@3KKW=}%ChYMNxY2@F`aJ#4Wr+_Z-mf0(1}78Wt0hdQwET&>N&DMx})2z3hg8#3Ej zDo5QS@R`{T{QW5xa6R}*CM$ydBT6a%V)AN6*5}~K=0fN4C#(E8A3Fa{k38%U~bhtd_KS5w{S)2ek-1Mewo)Rb*hnf|2N>j zBYv&QnZiygb!JZ?r@13d)X#>CcaMP#`)6+ZC2oIpLaDd(N7+If9oeG)ZjFODOInf`Y|zR{bQojdDnfj ztBZ#H8`HPE%ray5a9W}oCe-Okxx8xG_XFPeUMBS|yz!d>LLJbIKo7PhE`^-^Y(^+X zZ%@1dK+exktMXymAFt9TcA@AEJxM5_Xs*`j!nCb2`*M^J5Hz-WQnq$ZZ_791n_J$u zJ`>$pC>`?9jb=Tgy9XPxeO|eN8+;4&m+TQ~*L}FGF^hv!*CjaoMOz z#2{7oQQw`%=;+7<+G}NJMx-3eC<(ys8>=fJs5~$xRpo0`rLr*3l3ebdpG-cNd|sIU zIzbyT6Ef4l8)SoTe6+LRet_!5B7bDoe5~vHtAl%nXQMluyue{aiEZWmG3s-+QAza$qLdVRuB;dCkr_RuHn00KGgmM<2ID0H}ulQf!r*GEo7KrI%T_3iZWLMiVn zhqtP*ZP7OQ=>2tz=rzd7OR~(2!)yJ$NmKz>FKAr`ZMNWAWRE6ylg@WP;f>fC5|fxt zTrZEfVD0`|CFL@KEj9#nUZ5_4&_c2hU;Igb2O7}D!?;Zutj61pb9X>73I?Ce`*hf; zhf#sKP&xoGLq<{dzaLK9F8Q2KYF5w?!GIB0a3>m=$rO`iP4wI z7Awcm!KgEhVXkidkOjZ8{%|%n%i!rqOJZq_QlI$%#6+7Q>30XSnISJ#apcG0lpqYj4~XLu*w^-H=OX8daP1*B4<^J=J8M$=MiN|@eS@) z=rbfOe+p1nQt>}xhixQVC)Kp>r zS0^CKAltTUS7nfBst`9LbMW=PkMkY>&8vXbv>`Ad{zf%CTx95aPl?_b;+$IePKjIY zu!VFLuG#lg@M++fW;zm6&fMxgwB|Ex5KxA9g*{X;q7>0!)O=i|CqTiPLTRiB=@+Dq zB?kKU8H!&%O*cxylq>44#;u5+^fCqb_FC!hP&QYoPRF-qJ8l8rPNu#+-qZNrDsB1@ zOYL)+84ZPz=}yKCvElZZtIIBxo+*#aTCTe?{^78y{j;BHxT47n_WH>^2Yup$0pA(P z)hF_Do{>Cw_N`F+)vY1iqC09@0D_92f`VV^u!y)}ukw=u$6Cw1S`0a~{gzzPAY>V< zTBIhOGS5y;0ftpgt>RZ%S&w$b?(&*r!zX!>Itr7lBI*)=%}iI?EQ^W6-p!bO_i75i z#ZUy+8VsLFH*NLaY`F*ceqb*DoAKX#eH&6Ev>l3-+1(tMrY1E0ZWozjhNd*P?0L%N zl%q|7$IT^Z^u3U*XcDHshlus%z~irlrYS$5pI|lwsvmRKq??Y^BQV%h3av}DB znqGx|ve>;AuoXw0@);G{V-tmH>dH}gJts2Jn8oVtGpe;KKstaQy!*lx}DJfQllVTW@=mQ zq%&i{cZc%d&6fNXjo9p19xMtuAzy0qzf3=W6Rq4D3{7*uBgfG$FJ{IMdjpGy8{ef= zw6HJ7xL=cmVeZr}q|kkwTAmA7;fle*ytRK`p3memoKr}^&bL(6f6>F%Cb zlVI_oX+I`xNaWew;YKJ87HVt|j3-Fxs9j`A z7U~_e)gnTu%gxOq0KBYE*nmaG7dX~qcIiIj#y4^A{zRTsHV!m7ZB=cECD7pt8^%q= zC7umXg{Gsap-;>7O^~X(R`n`Cs7n%C1UZfO@tn_lFPiM%tg@$1G&M(H#|1ICczEx( ziKyhOE6aFpf>`$JC;YWNJvnNzeY#M-RLrkQ?NtHi4QOlkMomcqFnXQ%LFyZCa3c{=7`KalG;!gC?xSM%o2fE`RZ>4A zrrT@;LlZn(+6%d}lvx57+)RLF_pDTT+M?vO@FmUk&+jysejFW%C`npSVbJF1)b$y{ zU+n1^a$_ks)*-_6Vaz5Yn4kB6wu?W5xl_ZYNaV80;jbS=z+;VEozKnAGw6+$+Gws& z1tEBU1ZA^X&l{lRuA3OR_!JWmQUFf3G2z z7gC(DVQ7!dqUsTXci?sH&P)q1aOX0B$~*F2AKs_XS5rV33ZsSDPPkDLz%h+^GB6ah zC#R?HMzTbQ`O3uTyXJ-q|Lpm$Cw*P54|jONp#v#O9^exIoTr0og};sDN%D(7b)Nf5 z@J5}f%*ktUQ&Th1+j$l+pE=qPqJ-<_al~ut1H?4Xa_{Ezp~)%rg4&b$-l?gp7aJQN z&z_~{Gmc=Hi(o_f92$#}B%!N*)ac_N^>P?dSMizQzk~%QhfKq;Iji8GY8am~|I--f zIPFPh+wd|=Q1Whw95JA#B3ZR6kw6*-$TjO&L>79{!bUs*J-9F*Qv_;{U}8^>8b~4x zGY_osTp4nJV?c6M{AIc-qoIQUOvGj+M`mVvo!Nv2d&CVx?PX$oLPCe_$VUFPwE-NXPY+IM!!t{`mWbZ+=bCtSH!|n(y%p8IUeR(j{m0E=}|ed zP`w2)7-dP)&=~SG0?Ohc^*IKoq?kCLnl#YF{-uwoYL=>PjH(QCNVb6# z?Vqsyyz_y56 z^3PjJ8`OnS0Gef<-FuhV?8_27##oTGRyskt>Cr2afM+!NEEt}qo$$@#f$ibfQ->A0 z%m7tnaEp_B`7MQ#dZ+uL4GrFDYS;U^non6+eP&Ox4W8ixFB5ZT>JD2x;TvT%W`A_q zf%^2LH$q<&02RvOlX6Kx1`Q|$?UH@=%Whw_e}@Wu)EgFbkvXn5eQfl^Zf0vs6q<(u z$D*hXSFT8W6*(CuesCF-vulxm zm+JRmz=1p=Wz|t}u#FV1TE+MZV;$p8S^ZKZPnXsYyUl|c1=13yuCL_-p~2Y+1|#T@!7_9t;PA=tE2^A!Wb8`I)fbHdFZuhX-HS^YmYZcl`6|a#^6XQQG?>dYxa_}v`9*2(m~oCKFwmRi%>EZ@@y z;`0e~W>{}FNCKo6xHVE?(=!Ftgk6Uz5GbE04P5!Tm_gH}x8gMQXH>eGQG? z7@<0`dTX<6c*003VB)x4Z{xcj`y9#*fglbgD70>gFJ%ebkXx(P#{fJPSjr=lMkTm# zfVw5Ntw}a)BT6ISv)7F7v(fSTyHc74aNL`+B$zd`KnijteVi9M=Dg+x9ZK~$a-{=~ zdSWO!$N53HWpj4+lMjOHnnTfAL$FM$m-Yy2+OCs|_r7I%dPK@=FlNj*^vHTiDJhe3 zy?9jB5@u$0_Vm|g)cq8P4dqf8CXnV{?0Yo(tH+R*wwP9K^!R9V`tIY%4+<)jSXw>i zwr~dIr@q@zp;T}BiQl2|IOLC$M3M>}_K$KOI{*BTX^?ADGSF|9#6n;^CV zwahJN@WRmmA62(C(`ZmJPV>z7=bzUc8H3C2Q?#{S8))594b$KkH_^^rxYo5qy0ZmO zkizZKh5QRmNhUL!LF!V|fF5+5uxpP23SDNK*qZOKnMq&(M7+F;N4cw3ISM{000oeD zPs8?N7T3l~H9K}OL%-?1r&3)`0C$@d>xYs~*6`&g6K=OZU{e77UQ-I%WOnCOgpc<`RUVy-FG*;Uftk2ZRrDLjJ|)UL#);Ri^zNx} zew3u4c%ryYQnwvJ11UW7-HC>KRe+NJ^+@XxHsHa!7pt?Jv2~_v#easfNpv6Wk zbzzBD(UcJ?QQ@(d^rsVX3=R%!)5tn2vl zHbM(rTaz!iwye(xY&jUaPEo4xH&0=8o!vl9_T}GatOg5Sx@&q8xq2M8LsnOssz$54 z>7jHH(H_*51Zhy&-0H~5NWd}8u~;9YIiaATff|tWF;-Qj!sfl&7bg-M0&}b++kUT) ze4ceL#O5u;DxW&Ce4oa8rR*PLU2qrM0tq9)1XiChxY2L1jJ?w^QgxH-&U~kXbigS# zEv5&a2~eQJ*1uy@6gb|Nql)zdjZL75 zZ#A_Lcr~iv8nocWAe)8zN{vN&VQkt^57eH>5lS@$VWJgP_gy^fN1 z{|Ug6R)Zpj8`IbD7_k9WOGcRxVg{s%EC!%rP|6~gu-e53_{M}UHIwREzjVi!b8xSe zuGNM|U8JI!9PoftLdSY*jEM=i@iBjO3?)XcID_ zhjuQ$>>wtqEsUbX67~~(+Lk6!R`1*XsHFINZM_?Ld1JVspL#M}`GlXLuv+9<-wOgl z>!nkQp@xp`C>%I|rbT*cAs@fBs#MH*sTJmKqOPD~R5gNOAyO1=Xherm9g3NW9F=gM znf?^sSA8hz^_U$oZf=)gbgvEPCr-xDP>DnU+;qvUGPpH`C!Z$ znv1Jf48tXV%u-c_!P0+-@v0^v-&KnD-S_>hevSO9?~VyIDU>P1-@|}gh2)l7T%z*) z+*xW>2+eG}^P{atj~*o-{}mEbmB}#j3DZh4KZWJi#ajN=TG;r`^-1EjZ%jt^!uzzJ ze)E^d3)cr@n%BK%7ZE++X~!aF1A{MkAW=#>=kxpbSD23?=!dw5O~X?0fk^?Z9*9m6 z@JKGl0E0+1I^zdG`_;9sH1*Fd830+g)TCf8$Jwt<2-GsWXJ(SnPhglM@QL8J*>%hT z?Viut)p|sQFmet9tar5DRCQ2A>_PxSm{y22$nuWA97rxp#mThjD%FSw07|;0)4ZkA z@jp~aSP;F0Rn{=aaRKHrb^xuQqAs3Snp)F`nMuni8+0bb+V04$6}!~TVbkme=FLjv z*km#%KSEB(t}#My79>{_a9lMs+DfIAkVs`vD!MV)fJG)7Xiis+y!QFizr#S=X(_OR zmJ^ASw?Im|-A>x0T7(K!pd|#?Qv~IP(xbtD3_~j<3R2SKi^Xmq#san9>cJ7N_pb9^ zv`kllE43C;vA+uQrr)wR2#6`#@z2}vOE{vR7)wA;V^F=38X21&hu!Hi{4TFcq;9QE z+Wo13ItxpC zBKhc*I}8j#yC3je+7HpKI~}d}E^dQ|U>sJ8uT4gGYt1Aje7EL!g`re{M&MR0UMK&F zicNe#UBa(3(tA>q8L#*1Iy*Z>TMr&i&0oF1<88`ln%tw37xHRRx_m7Kh3cTQ#aJjt z{LBZ{5LtC4zPGR))D;qzlJsG4fDi(nW~ygXrFsM8AH(X=2qbz>o+&Q|8fk@Owgk`u z6z9Rvp8-SZX;@tXlx2>zrwOqWYXS()=i^l_l_)i!fQ?xFd$L+f9=H*~*wy7O^5hs_ z^i5d8-4l3cr=*$KCiIIrki1{@RZGW+*0h`M)>c*(S123tefUdZ743SRGAQsBy5K~} zl+B)?t1J^ijwxsJtGP6;)p@SgRWWb1!+*D8gehQ9*t=7RQ+{vHC*%S?Kr&(0csGVh zvgLXvq}9215MTIVNI95T08n&{*ZN>6*l zhZ|jL#b%Yp((fin*Q#QtX#iFM2^It+DHEz{O1=W-ktw@{UnQp9gs|SBUvBt;$3(rB zv)%$(DkWq37)2N1vd^k8-=%`tP0VO>m->SXJ)%(00L~W$Z>VuBnn3~_5u2O8vtke# zJEwW886_o<2*C*QVOBRnrXHd4yuh>4OvX}s1-5xFr(QPdSgp;q=B}K~+g^)sGs$nh zflKi_0UPllyXXy#tHdS7>(S*09>4Y@<`E(pgQQ$Si4c$_&*iU@x5xLR$b!xU{2Njc zJQ<3?5qAd<5095?0Ig4}N*KUvk>#3ZgbP!?OZR51>Q*3SzixDeiU*NhA7jGClYU}y zS}h1rNhSD*;$iOqei40fJ(}>a3D=yP6DdT&F5lK-Z!#37Oi}AqMv>7Za!pjmRK@_QeX;fKQ>qUzWG+YJK!IsXRm>^`(6LHo zh(Be-1`C)qVD=?@eX-v-@4sPoIc}DwQe3~CKyJxBoT#fDZW#}y>e@Fu-|WAxtQ~$Y z+a5AQLUuXV8M4snwQx=*dmR&RKR#&>bYpZGM1p*!;c%e{JFmq@T1SCb6Ga{=Z?&`HD148xBiT* zBpLImXm{oYHv~V*9bR!4TvebVJbli4@0rv9bi${aH$(Pf?W3Ucr`qO@R)0Efsy|Vk zzP_GU&sFo-CT13BCTxCh$xadhwJe20;vc$b$arsC3>?jmzwYg>H6rwgw9+4B`-Qji z1$W@+=y>%{6e)?h2IH5VCcRZ&u{MvseW*sG^6DyyDrk9~J_oy{WUF?Y6JldI8_p|I zv`qSsYX#ZYdj%i<5Oe0A$CKe5-_St)XU=oD<&&hx-{6nd&_A^;up{<}81zkAv;G^%n|Ss6H)cLv1o=dX!r8@A`o^(p zxTO=6ipW(26L`$R5+!k|s!?LdrINIltosymNT@WkAr+IhP3$xFm)?v|Jm43qPnORG}0SbqH z6q+M;f)1&9ZBC>^sBkD2sRM)S#NH`?HWW4m6LVvY7sdv{`RWpuYA|bKUdO(CuE$wJRWaE!@h_r+%v;Ms%V^B6tvnS`y< zK2{bWXE|RO-UUiQDhABsdrUSYF-}&gN+37Pt6NwDuc5*WY7UtBC`vcWMLm)-xoS1P|J^o!MOz~ zDFjoX$TTzu=`+KRh$%y>N{f%weY79Zu)#i<6*8t$Kh`44DJR>4Yoh!ECzv+MhI|Nr zPJm@;(rMSY(fmscU^UJ46rL2|`b5ypC&VIztCF%&T{t~GV=QWW^C#IlBZ8Q(_JbQ7 z9}Sgk2)wSEsq<1&V)^)^Y#;`;w!m-9gHZBA-ggr~SL9d(t9o7*7|&x*p-{QPX&(PDCbE$?QV`V^NIo5^qW_uu~H z$SGaz{tUc-$7+RrmDp%DLVap^mC^M3?z7*XgLl#|ex@1oC`Jo^?-`fwGKT|(h9ac3 zOvT*VZAo>OgTx>8-GW%~P&RR&{&><=RyK%yS8+hQ)vNLu!RaUBGUNFH!bgwBN2$hX zuC;*aR*9N9V+LMS^XeJu?Zb(Eph+Q#U;o2hQYY-Lr(dT@;@Q!T=v0gc^hm1lZoWz! zX90pPg(TGX(Rmm>FHD5qCxMu*0X^B(<42j7?;6BP_(n)Pj|jjPDG}n=&*QR@5G@kS zI;#dx@+jGgy*DTMW#v&Gs~QVtj(HwP{eZn*n%6Vp;P#qMsWapN>-=~JJM=i{PfFv} zW`Ln6MJ$7)K&PvL&w}xDR?^_3o`$MC)w~S~&YSqWImG*8iMCgfYa?mcIXi^_Rjod? zDQ`p+(aq+nExJo1_N{xXf7s=-t=pO+74@1Qz2c|gH8k{*H>$917K_)n#fEqgGsE!H zDp7kosK8SnRi83aSOz)3eyWAuIkY5KQ%mQjG?shL4Tywd_J?F*xJtc!aYrkh9LO{@ zmNTNPsRwHS4utV{B_$=?jy!{YlEdY&^&tr#0nyE=T(w5FsA&;T7E#afz7b62vjk@B zw>W~7Snm`!4VT1tDAFI@UNDbi3RW1OMb7qv*e>o`1_=GyRh~fLl!CX?HwG9 z7Q1xHJi!Pmj5L#v_IfMkd(m^)>auCL5nS0)7!~|p=^u#(kvnU>a*}$M}c!;#@j=lrwQKSd-6Dshvio9~3ysZNR;+s@`Ak2|O z1EYC()iLtG6tb0GiL?0U*Q2@j*K3W}6rrmvPoju9 zBfaTyAbBpB#SDDoG|b~@X!OfbXMtSYH+4ssJ-k7?UqXT<@|~SR{^WO%c4e1aMAlo- zHtzxo*osLuL|UrG0a8G5t=%W=Q^tD?!KcM@F0({k0h+aa#`q{9IC9l5!xduo^MfLR z96l2Oq{YzWU9D9azb}i;LR}OdUo(MsR7g4xo1?1J-C`-t&Zh}-#P;ebS-+n2H#Af! zeR+TECcds6+k`DIC2Xbtaxw7R;6+z07A*qz)%`iAM+H5<={Ld5e3ntfW+-46e^ zYSF@@vp-GetA}JyzRHQb-e|87&Dhu&F?AiFVnV|x4UU^eWv4`6fQ3K!a_hq7_MFc` z`(y><dLfVDJ(XAT;PV1*z}`|wxe)mp#Zt9CbJK+QB-HD~ekebCWT2zqMe|J5?PfJErY^WzU$I9Y!$v-8pPu8)wI}ic)WEo)N$y&u66#RfXHcr1=B`PMI2ojAz z>N`1s2Skjb&he+aSl1n50*kDa54kTEf|nlwpNj_!p835Md5S_jp#W^FA$doQCUSd+U`re86< zkp|wls!Gqh+*vo;(5Q?#yj8k8Lt3de-y=_mg>^dLVFKitGQ*f=idCMg zq*<7~Pa_}Whi~4JU)~PuXjxJIpX7VMgesdNdUx;z>thWmvMv~w;j6IvLrcoTLs_}d zdI~lkSQ&vi9|ijsU=GhBcZu78m5e!Twe%>6=mf8?h;HGzZZclWY)L}c;$m$8#C-*A z;hK(}47f#G1h-}y%bWt0665?8r%yu>ETQwn((brwZ?QRlIfNLJrBYw#J=?TelBD~> z0!au-QhSi>(EklR?cPjm-sZbK^xJ_qt6W1pf#xy78Y0XdZYVh%yjX#xV$Z$NX2MQV?jZ7E|uO|;7#3X zYmupJ($|C|DaO!M)+k)fOvm_U6c?*8IX@kEl^HN<=!q46fx%)AK=@un12rtMjcoS0 zyH4bjs7`N(^gC4O3iS{>s{!B-FC}df-VmmkG)-5DY`sP8KtfZ8N)K~ zxZPEzfAex!q0UWo}cR-6*#PZSx+!SS(ez&Y6wHQ{OBErVJz zcwJz%tZd*ag`O4-MjpJy8 zS-rcEWnZvj3@2tMc>`ltN^_UKkGW?m&7XNU1mNR;o|!ekF}nAsK!yF4EuDIPtXw*` zLAip00`PemJo!@42JghMD*_GkyV)^|&5Nq6^oj`^fU+UnQ$VP}FvpVtmpq(8`Lv^P zslK&w&XqE>HsV{lTC4Z$*n8rDyp7W*0PnAm>ZKK9);PQ1-JYe_4ehvHudl}g7S6sL z=SyF%H-GD1`^E&kki7&e!PF7sqs_I;d#H(dPeI`0%}W9%9wrl}8$9@K!klR;WqKSM zzf35~^vd-(kb3Zq;Z3pLIab*t8rjzJ#lLcC{krz_p8vT!Kp1h+_4X0g?=7=0rk)l* zzP|k%0R5BYm_kmz{=6vZc+sq0d(^xeLx>ecY6tF%_nw`8#5(oo`}y@PC{`Dqj18oc z%ng>VWOnR`=a2Hg^efqT#NVNg0QXBCcj6uow#Q#Y$_M}0#ku=zG2BD46Z*0Bo$B56 zKIcNg1o@u=JjT&a9|at5J3VeqaS@vuo7ih7dx-(*-%IJ(0Iv@aguQ7;GIVi1Dh(UO zj=tmeJo?TaL5Xk7*T+p^08~o~FFZANLt8A54o|dFn4acUs6YwskFA}6h0e2j$qz~V z6oN)p81~4*H|f_`KK+{QuT)9^+K)rp)?zjEkC{SY+z|PI)fLkKXSZYmAl7LF?vt39 z8*p_p^4Lt;=g(?CM$XxkCfDSlt~Qd87Kl0M%MAeY@&`-eJMZb3*y0&%t@721&esl(-^9939o8SJ?*33j@$nbGW{zXygs$ED95K`+>FSqUx_kHZRoIK)F$K5j z1Q~XN_wB68_}sGMe{K8yem+m&mtd`z>Mm#6lpI|{u1AgCIGxVkJWW2Sq@aECsS z*fjBgtCgjfv3P7y3arDUbyOR+23R}jI(Z-5idnul|4WUKKMIV?i`j4Mz3G+M>(LAp ziyX%n`yRG4qYfG3f`OLPl{W!p&2Q>C5jPhH?b&(~vq zc?Ix5~UpxqcBq+<8z#fQ1wm%(QTl%I)DOK59E1?sr^{ z`I&)HS6AQ7QEbtjjiLs(J*GZOaGwruvSt}_49IWPqPY4QtL(|P-jtfU8UV1@Xd7}$ zYIG|+1=2>FSA$Q6GbCGDs%FxZnK7s$qYC(hk{lLZ`7KG#n`U!in#)WT-8i+w`&lN% zi5K$GlcJrc!`#LVvz}w>9Xn*cx&&d`<9UQIEEo%%)^AZCLx@~J6k;n3-b4LFfb1HvEC{ZqU2PxrV53; zQ?TesI@FD6JOHoUBqlTv-wSa6*!Sg)PSAcp$nDUDQEg65Bk(wb4b!}mBFJM+*=lIFV@_?#&!?2%<=q=R4?{^leJ{OZxkZoy{nJazT&+lMW= zwUN(x4NC|E;n!iZt-H@=k}kr#U zZb*}DkP>F31D=y){Ca=ywR|+5n%!Ah8OF)0$J!7Me`{$QIEerJ*lgH|@Z}%939UaH) zs%DyzF>@4uNicXGav7CxdYK}#Kj>n7nF8Vvwm*FsyZ&i!sds35ZZ3w|$5{WTTArWs zX=!J$zvo%86GJ0|qb6@_#(&n()GhShl_gzUEaeU$C>X^%AK<_KW!p^Z2PV)>v%`>Y zsUckU-`<9z`h3dT4C29oMTCH|r;DGuw5;X&r%$>{-F-6KU!qz|S3PrN4*qB+YA|kl z^Y6N!9%wnPessQ1^l|Rgb=C86-^-G8N4wRJbCUd5--DK`)`B8!rA^=D`bJ@G$0>bw z<21dTIo`~d@eU*eUdepDEWz5oC2ce`bZIQ=;eadIw<33tX`-2IGm^-pQV2 ziKMuI4z@tN!Cw*g_>mo6eqPV7D=f5HYx&aI?q~`gIUjDNZ_@Fs?%pQSq0+p^_+)#! zRr1A2-g&?poeTh!ne7Dkzn?hHI@?Gp-H}W=)3KxBJDj*L%i!2~sJi68ImEy5rb3`9&lXP&1U2W7X{@XGJs?^W>exs1D3 z^Jx^N%4z7@%s+4{6I$$F&PWJ1WkhIdcET3&rZ2C(y@RfY8f6aLFFY;=)-F-|De;%@ zRlS@@26?d>0?*F3Pq$WQvi8i;W4+TDKejK-%RBbHobOtk+WZ*!vxeM~bcP>nrIZ!J z3{UK~ub!}0>SI8@&5fCg`(2#urKD}=?cbL9LRR>1!*Wx1c&1J$DIER<33O)!yZkEF z9eZDt`JuF%y~))0ZWby1M0Iq2iSyOs(d(t45Z9HNd{`7|-+|{~HElFeQ`H*!z{HDFJJJik=ZXU-z9CDa$nFYRWlR7DEjZCy$f z*_o8sx51@$z^kQ`TDXF4<-mNezf=3@zc`$rcC`2ged}=?a^*9;;$N=dW$pSPYv3hu zg7x3GH2+%EVs@kanDni=xvce1%KX9l^mhmvTTF?G6?N?^xj(aAzcG1F`=RWMXcVvX zv%J`!FDELH`X>0QR7l*Nh zf9a}-&*Dt`JbCjmEY>mV!*l|hdsJHM)NZ_m-Q$vc8}F^Tw1LfOQ|aJ~EISQMxq^T} z$-$wn^oF2({LcNgw;7OxA0w5J?Lh5|js&0M+0o5B0@|3Tyw=BEu3y*2m*|Ry zSXi;Xc>W7dWandr=Jo5nKGp4n7won^=ONQ#!kr8na|bb>eXgv3ALIjas|;K0sTh2` zqZ&&CZSt?nUXwoVmytf$YRc6_7}rJgE)1`3?RffVyx&g3cr{fI-^^O-M+G!efjSlq zmqQtvZ16b7aTcfQd?%NEcB(P5e>SslviXg1aQz;b7s!y1 zlFrv$lW5B9&7A}>B^i2KyePT-ZF3`k|=l9pu34t8;XOnbGA#(9%#> z%Hi^rl*#2R;EHqM^XvEENq;Yq{S8skoamS1(&hL2^Tj(XE7Z7Jrh65yi8U{NHxfSj z$ocJi%Nd1yWgV-O%ZL1kqo8vv@Xg?^nbQ32i(%RPJ;c?QkgL~{ycKYlkRLHds}(Ng zqUT3=d15=y8~ZP_YL}rJC@t1fI>$D#!sYAy_G2>VCAA%?&bgC^p-^`DWai|C0TwQG z%YkI4c;}QXNfA-OP?z88N6r;IIqhRY70D#8Du$HV7fRf7l$BYY=e*(Y*q|$4am39u zX_#D6={5n?3SEisT0WVhUPHhgP?;of@AEJVA}JX`P@_x%!SGyQoCEI4E#a|iKPX-m z8T+hW z|KR}+JljqJ7?5iNFq@UUGA{g|*9hYXWbOaV zun+K)%l9MSl8YWxqLp3ekZ$l_B%r_q!p$L)q+pBqzktEIk$MOvAO*I9JI$UcfvotB zLpwx>i|m-BCk;OPy||QH@fm5el{71&aS;`{iYx|V8xvIisBBYWR$jqO47~r8O(KMf zXzwqKT6KK$*b>Ps8_l@(R{$crdlK5{4-t$H5zHo+Hf}Mfu#qC<^c4utBjomIS#NdH zmqhL`lqItxoa^mJcFf;{o8F#TiDf@I9q&(jbUQ9gKWuy&bDVlKh&y@#Un%F~9lf|d zS`CdthaQp`s_*h=60EiJDAow8&>)adde1)Mze)Q>5f8ZNH!luN|FA5V*&a? z3dqX!1BeJLbCkMa6x{yo`6?gix!AivlJpc+5hgtsjPpjhK!-Fv6A92QrliZw$z966 z3QqcY`wUoB#dkN5f_Z%n|9u4cbuxj^7MvqKMXAK@P(F8FzNm3Q#f|hU+SErr4y7iB zv_8H}WqoW@cKtB=DgJwDGhf0W>?z%`z}m&CmYAWLz$#3$6~<}sQ6fKZ-9Oyb#t)=x z+7kgi`$l|mU;{S9pTS*h1j*{NB#rsaiFI=nW4?CsYW*VKzkwn#FgQ&3H<~_9feV{DDalG_%DzMdR z3Q##yvB%2i>-@qfS68val+}~9#oU`(4M294RVvHjSUv6Sm3Q7m;5}yeYOT)l#Y)yT zz3oAo!?%K)2rLB!HO7m(1j`gTX{bLTJNi)MKDS zjR_!&_Hp81{p-4ezV5ww64ql{{>{QHlN$n1KA?r>=5(IJZic@fyhe2Mp6)L^c+wNc z>|Fr6Q+&fX%I>=ynJ5*tf%>WiC{Y~8h%61f2yCEKy*o{nf|3EWh>W?!eYeU9vJfn9 zeGALo^UQCpl~$kr`}**b{^ZQ>zEf`bdX8Jay1sfPpRqM}mUs6$_L}d4FzV{*eidNZ zoc?zDstAG?Yg*Eb#s^VY<|}b6w+a7-DtW#cy~AWrd$VZHd}}_)Qe`|%w_JZJK9FL# z(**tWysXnZ8)vUEO=Or*T~*#L*}1D8$(ak`k)tWdn+3#1Hm$qAS)*?><;bZ$|LI&u zy^%it^4e9_*4Froop;qoJ`{E=K{tiq)Q@@K(_st`Kdh-$)c;HlTjLRiL|`eRl@%J7 z5(f`1fJn%({zl7zX;)mAxHJmWnaR8i(E2-ZSa=j*J%D(oTK?pGB0Ii2y47>{?xSMP zRo(YMyp;WHkQJDP#*9%(I#u%lG~_mA7t|)Y4hSWRi?=2``^KTrZ2sW)JFw~FYS;sy7Or&6 zwSk^cnOtf*8@f3eTwabQb)dw6$IkK@IH)1&%$;WHqCea7;uYhmMkYKh^xi@`#g8a3 zH3GlFk&t?fRbzG4uYAiQ9==dK&rVL-x^b8ijl0|RF#?mB(~5oRN(hd>el@H~O>BRd z?lo3TY2m4yP1Tboc}^NZRvyjFeRB0tsO~EbSq`@gxYV1&=cdV%rR8Wzt!YAi?&nog zLJZ1d#C`kNBj%M4;HRzWt?s5!uH5h~sckolx8ru;@Sx*U3wb6C#_hXiYlBiunLWr^ zlG=vAQ(b~EKphRbcy-U7oV3~O48`^$G$G%`m&6Of@E#kX$%yxj!(1Wm_NdEVhy2b< z2|DzJ=9dZV6>3n0o06wz>X6`%keSgU$r_(|kXA3aFxG;5dU=G(sQrGEM+64?x;LsA zs_m^TWhl45!!k1Ovd|!d7}>Sa*+8%+(+92y1t$ZCaC`VU9C{wyrAmT`vb5h951$|$ z2NpeOa%0I=cAyF&eLe*7RN%AlsPm__zy6^pJ>i=JQD20^@d{Bi+uR;5cZL-YPt(2# zyR3l9-1d-PFX!91?EWf|SehkkIfSQi1kG!;&%|`)&I^BYHt5u62q1#d&DYgZR~PS` zHj)yYj(-?Bdnh{vgg5idgBDs3mWR!CzOWGF)w=@scTRgnF`IWhWR=}fFV zvw4FEN7m1cb8Yp%u{kh2iBdMdTV2GxFojsqxNay!G-QzFzM}iWnL-W8^mu8N$Sum# z6NHd$d3U(96jvu^qumDZS8|m(*f*uO(0<4*k+tv&xFpY*nFRA1>WicQ2$Do`5-Cv) z)&*mhRo9~%pa_N`tvyj@3f^)_9#YzXcI^j$bqS$*SL~>8nMy7SlWH>=&`EmyAA@g* zNIu!nVg(IkPx?~2wrT>_KQ6-Kr)=28BY3D*8wQ<^-4h{)CN8&rl*8wbZfi#J{%vdB zKX)@~;5I+-Q4P_Im5HPBYH9RXxrN%w%2i&`vCsHW%>Gj*04Wf$8j$JP{I+lO;uq;D>v{?GeZ+{MAzN zDZj5+ChuElsCLVFK@l{|XN;{}9-iabyU(yB@IEGbxxu|WJjM=_B8)+ks3bTA1Ctv% z0>n{4uv-pxEJ=Y#5xT@Ibf{oNBtpX3^wRFT1$g-QH$tH|1UA4DnuLWPE-5ZP0^!}W ze_oLC^XvA{Uj})dU|%3AyJ`E$lES3-X_*|w*iyM3Yonh~+u89F+Yl|bUDv0lDZ_0asVYd>Bw3)4KUFJr-jvkEr@a)rv;yB_efqbi_|abapEy%(G0DVv&1|1d zO(l**H+U9P*DcjmbNOOo#DepPB@~nv5U1=s3?|KLsRR*xNBxS6i!P^Eh~+LWyXJxj z!j@!JhPr|a9gf6+qZHln3J!-1L&tiB>qSW;@8HYhOJpZ(<@YNMa+i#Ib+UmYk^!t- z9AQ!p0x5EP?mf!E$;l#jI@HMx0C$<*`%;r>@<@gvr59x8CKtL%ay&Y5EE~{u8^|Jv zmwe%q+4j>Drg_Ofw`Ac$mJ6>8m-+o_XD+85cWZIo(DCxu*`{dT>I zfOFg1i}r}@*z6@P@lEP&V^auTVgCX9Drsaqio6~l5v=y4Bn-_eBhIBXQgH~?U2K||- zQ-UQ=1ZL0s+FaeEv`4CXf14*OLbkI>=CPHl;WW;C*|+mr`N@*z;@!SPHdp_p)w7&C zq*&FOz4$xsnsuRW>#kx3LOxF&Oj7sMt*ri*l2Q+ETDt-?xo zI#L^U$jBfm&$rYNrLvA+u|$CZ0oLDzy%A3rDpJXp5E|Mv1<6v5hY-*4*473`Kf`Qx zR-B&q$fP5mxpBg#faXqD*QRX&&A;&1qB*q*dizIu6K7i9CGzbxmNOhv{=rh_+gn%q z+t7d`diCv(zBk;DU%zB_A1AYh>CwjgjX#ptJb4zLyG=HQ=SAt^?_FEfP{x;8Q13dd z+f4M%e{1M}Oo3MF*M6#|sl|1?nP)Kq9ZR{pzeTSaz)sA1n3b$HxOZOt(90vcq`_u8 zr8#*^q=}6skH$hfj+rgk;$l#|uYX9L0pi*nnbpH}ZihNvx945cYG^gZPEWblv{uat zIKOH)RUfJzValjknCG`p7kBCZ$#Mx3h4EPsn2T?$L*OM}^rq0~ zNivTp5V~9Mk5J`D02QWixRBwzk~oF8yshPGJ!%Q1aXCVEfTf@9=f0sqahz$uUAmp& zS|*F(-ye_PzCIv>@HidNOeLt2ny%M!qe&p&a7U@?_dSBUBW{Kxs1J?KN=0eZsE#1&1_j9!qYAQ~( z;c-jnTMmJFfDmxt+6ggbiiKj2w}2;=v9myAIetJX|uNfzchF5Wo2O(sW-hsidSt9-FnE z6C{iUr+|W-{`gj)7%|B92)NFH(qc=srj)|~#7CPbCk$=2cYeL1X}h~Sb7@8XwUnwk z;h4ohN+7KIFtLD+KGKGEju^gJr`TGu?XhQikd_3l=a7)@ylKmBW5`7B* zH6aBZ$D_Wa$!CT(?_o=k=O)vWHDHEdP^Q$^NufYoXh-QkEbM9CH{T;)zfm{L+2;Ko zGk_$sumifW#MFH$xwpiO4JV5d@dGUfpb!=;IQn?u-gy}?+c#dgx=K-#)6L$l(~!~Z z$#fjgx!yju^#7QK_g4oZ#JijUO6ok(JqJ*q_s5;AKP$f$Z+e-=Z+3>))kWivoPAFX z)1V_C{xXYOKb75nX`iec3#}f3ZjTb>JY)UbK)Ic(HveP&+N>k`+Ao4eYakYzuFMvG z{n=@wDR=ps9OM>{{_JU7S=rKhpqwoxrbaJ63qT2YhlGTz60m)UAu5T|Z*}(a+t=a-I-r|BLHaXFl`=&@C1yp$nIld9p`{U}fz-sq?rzl&o^_lKbep#B zl(ol`s9@CSXPHM&!)e`B&K{|Dzbh-${pf>smMhnMbb+7U5=`7cvCscT!%X$PQ zXNzzc_u^~(W9CaDw_BGSt<3{+{Ed@<=(Cef`(97GBiPTNkof)+p_uKy%N`ocy|k56X+^tV@n@&lMTLyZksp9T5r2L5hmB0rMA(yYPH zqA0*DQj`3>LjVvoQu+C!Q|kwVIkJg9l+_IhHui)}<5~FQ$84L`xYgH(-;n2j6ylZ< z0L57tT*uq*oaP!Wh;O<>_Yw@pE7cDJ8hvQ%tv+wva$k&0{z+QC>+$CxvmJDwIcbX; zHS&sUd`WBVKbzVi@N9l+wG+MD>-uv zq$lO$HRC_*CwhNpWq^V8AqqT^dJ%^MN<)9X==f2eFl6W04EiNo7@FVgW>d;LUDHLE zlcea}vhR@$FGlFb{jwO1tJxC?{PM7^?9q%m;nyjOHYR2-+=CfJrTTG21;W$$=%dAJ zFxQYgW9BOC4vm^@V;*XEjU$ftf8w;&v)+Aj0dpOHt1N%w;oSoUdtLnlsDXp?h!Fs^2{IHw{ql5{2;2MmH_C8XbwwlQA2 zZy={p#|Np;Wa62p$RgI9pE#?nIMdXJa>AQhaI9u_oZTPBZkX=Cn*xp8lS`x`s| zBQ09PB3sa5{*{mF>4bXq{ze_@{Mk|?02#!?b}<9uo74|V0#e?n!tu@JbAHCXH00mIw-@R! zVp~(GA?2P`(wPo=4@32FH-(KyAx}u2l_Rq=uGr(YsZFV4*^U_N!}jizNa#2jg^D`C zh^{vO&Ru;oReFok4Y4^8v^#`jwlVI=oriZFMs4D4Xq(s-p^Tq_v-8Lt^&pFfQhz7k zFv8~S3F}n1c~N{Bmnp)!a8nY_g``5i*DZb2eUg63edL-V1^8@OEnO!TaR^mdO;JIUbYdv+aS)p;Yit;t1xF`jbZ5OFCs+?3G)lL-X9uV8p5VbF}(hxubou zEXhi&!*SUQ=-A>HJ@ZhqbUvHX-Nx1`W7_+0`i7_c9N)LE4_`q@{aBO zv#Td6Db4hI6zvH$^kTSn7>@pg^+5LEWM2RHtD8_jBG(GyL$>bxg4>1m;5xOnDJ zHnw*<(i(g4SWgXdTvHw6m|ra9o!;@RT5)a~h5YsEz*@V0bBsDsvvO0ZKYuVL2^!FN zi{43beIcYNx`TQLI)6h(0L~po$Zf4>KWNgI87m0F9e}O^!ot~A%q(;p5(2HA8JJ#p zj9gNfGeZU;vHHcKmMRoMsp2XzS!mhkkHk@dING)+<1wKe_09d7M;Wu5A9IvjY^*11V0?AaASNMb z(%=3*5AS3#OL5vBj`~Bo0}QI2lX9HC)9)yvtZtG|?}PRc_PakS$c^i4|2zo7Eh^~e z1SY-&a6*O@0Y1UpO@`-2896Q$vm}cCJ@Jo^T8#s2ar(is374e5SlL{3<)Pf76169i zD+W?Y2@5>)kY|Q`x+fd)$MQZpPtz_R|R;f zxf7AtsHy2oVsr}xrJ-Tpj<7RjCOC9_WnxC=_){)%IyfJrj$>dm%PlDU6SQxXl^}v_ z`Ab(Q9`n1Q4YA1xoc?`_=8ImPjF9Bl?TmVd#Xr_Qq{4B? z-AC|3K3wIL-SCUVd|*!QT9mj2U^F=BC5)lKVgO7l0vrGySlK(eQjcjsh#zqzWiR%? zWFZwchS-uxy;(0QMHAuBvESy>gqi1PKZsX0WbyYmQ;Y5BZ>(B0yM0=QWo6?q!&a{# ziux(w!$s(}CQwuHmvmnOA$>v#9WuGR*1B^@=UtftI7ix8cWVTP@E$iKRu*>cp$}0E zJx>xX`?eahZBpTA^2@+Ga0x{G{EscV{w9P<|M$kF^b3N}Z7U6q#J*OV{!taf_!k1K zAOYe}K@d>|>PVY64{l!i75tJd3QW@2jV*sMB_LZhY0kkA_5O9Qj=&-M*aU}R_WsI? zs+j!=@aHXKva+!yoeO<80JfrAcv88LA%Mmo84*AU>S-GK)Z_p2>!;rK%yJN~m9D|} z&FBzg1bkp$(;c^ZjB{6wK1$zp?qJREY>gm?(Zm zjTS_}9nQuL|cdAf4m>Gn$HJ(=fWO zeA9ye+8wBBHuo{-zt#A^*CQVvwExFvGkDUU|EBAI-}Tga{eL{%vo(}C9jzoLp2k%( zi^NHfklOCzVHBWX$DK!@>Pl!%*<6{}miy|NlR*T@i^5KzWnN?l_^*1HO&4SnQFTYv z8VKiPK9cloLh62~FSJn5Srj`uG}PeMl<`5HH5RYFW&NDDOzI$0sa*R@@0>5Nd_y0f z>&Y~DC!M@k7v2OeI6Nin@DgW>$t4|;F@pYO7L8()Ujl)hOP>72%fpvo33+SGK#%|0 zT4(=xb}(}=^S7XXp8=s0B-$NO9U;5juT$yzAbY)a6X^Gu4GI#-QZ+))iR|(yG%h}p zU8BF3=9pn8IERiYEo@&9OF_X&Knf8}tT09sImF-oj2?XPyES;KVe0X`>!<8B#b16p zO}KmTV6I*%sGc=)k&~grFQZ=^zR%qq_EF6UCdQ$lI?ZuAruTnlgRl zQg_pR)m2AR^%&g&O8lVc`Vq$t#UU{$MJ>{kz_g&1rMbr>>6-l36lIoA{5dg@?qshO zbMuKXIPClj9lvUO`Y=}!t}IgU47>92PY;x=gc1vh!DW}*5FsaH(GTwwMUo=OeQf+j z^(Rv}8d9Lh>xe;a1!)%PMQ`{9w6h2ry7IM16wX#0uV2ait2j;BWI>ril!!y-;wF09 zJ{d72O^D-n&xAuE#**J4BER38&%20|_#>H_nf%d~s*=wBW0;s;X!HXCV^V2c?rmlY z0}~I$djY`Lr=#$~a9XY*WcwJs-c|Iu!ElP@l4)G>ZFsrFsjUWKxtnnc2}`WbE(Paq zm)Dt@qR0Vb@orRgxl_5p9<*0BauU>6`9)1Q8cNqU&zXyhCd+eY15FMTas>SUTkHWi7LDQ33~(u z9`fYb-P6mfm5sn(Fi8}YTU_EROfAVkKR77?tf1+C{@b$9$eYm6y^ErCLKJkd9dhV7 z8JS5Nntl;Eh4lQe@i~w7b;8JdUC|cJp)=&rJO42>8c4lE+1+vMFIBSo`HD0m8x=XB z(Bw~JMTy^vbzpO0hM#NX&lzpe7XCLDzz-EdJQkZdh16NC-{V3=t32x~80nSBSRlU@ zILG{Qzk*oFn%4Bs4X*^e!b*g#wy3;d8Mw4P`0{4W^!*s*d~Y8~LEf$2*6q81+&m$B ze3sAONju|9g2gsy`7WHq_YKAAt4RR2FQg8SXAk@r5gGZ%p=Z|NA3I@FM(F(2Klhky z15bY$7?!L43>Zu2W?}k8`NrbZ#t_1Bo%gxtV4A;UyHGAVfkQ$08$G+CIlPmWs9Ww7GIBD>mBl zl^>at4rdoDE2omN+1wuwl~a{gwnMmyS@rpqQ;zLs10VIO8 zAU0;0v6ZZrQbr*0=7p2KD7!Wi#LvwJVl2M!h+bg?JveAm1qCd^qtX~EJfpg-oWZJZ z&qOp5eqkT7NPENV(R#PNA94;pxyRxY2W|OGz+bB_z|J4$s>(B0=jD-p*8@)}(Dss& zg!Jmv{Vm!|NxF4ZnVzEb?skXXx?lkb{)yRc(f!=Wea}lE+ps3qh@3-4#xTJD$0;p7 zZw#Ap?f^A)jqA$l)1Gf1ByOk)7g_|SDAWgC`!Kxzi%R&p-rS0HWXZr+i55>)LeAK< zpK?e3Dhm0pW%TxUG6yT=9$Ty&%*-wihxrpyAf$gb zC#0sN;IrAmMDP5#yc}PNwp3gyCHFYr`3%(krRo*=j{KpTHm}Ct$n5fZdv>~7j~`=M zf2Lqk-mO3p{aa^(g`>kQ`S*2(!v;1rLs)T*_>$z%L1vQib~luQ#clhgPGHXK@mKc- z&>ThJwtN^~E6^G8NdnLQ6V@>Kt6Z{;)mgLL;k{+dQnSvzqhVQyp|^;yUh=IyNuy?b z(@lbi9(+Og5RmU?Tkubvc0xB&0%XP?&`N#aqcoRgvYP!Leh*cx@EouelD0Tg1VHoC z^y{?Eb>H0l$@S**0u4EJnDHcu_CYlm@6*BQy{g_jPy-Pc?A4Wu?^gqL1n9bpm=5{_ z(M2d--W543@rg@60A zL}+86>KTJk|72K{L?q%8^Wk@^--KK)ca7Ap|KD-Vg_X}u{E4ZpF8j|1nU&Dh`=0yH z+Y{RpyzX`EcCRKUjTVW>h=z~Am_qkoZ{PCoEJ@5Qh|}u0&o~qgP9eLv%KkQB%W1lW ze(iMEvtU>*_9KPV|z*j4O$jh>!-o2NmIN(`p_j^tt~@ zVMZp&YNP)+-}j_K+r{gbri&$e4Gw?6k5E$j^*24Cb} z{t?z-Y327v6PapaXrZ*Tvf3@75U{zs6!f*BLqQ{>hhSbJBQYkLa6Rn)sxEpTqkukI z2KiIdZk%};K}00&eYJP%SC${*L46UWE1-=8bX%B*bnuZ>s7W*1eePqMP? z1JK;6OIqW|hBqaULJ=EuCK{AT`umX5xIAC4J4HC9{&ERVJ_==2zTm+Cy zc-Ck1*3S|m`wBxoP{E`)heP-GE}xiV9=|J2ho>rzv*M%d6Z7_3zzq&SxebVh z^p@>z+o;5XAw|TfTHzk5nPKk|TW6-{zvgO`ZkHvAqDD3hJe4-+_eUB_YiD!5vE!w8J_J(l z2j%z!zQcDl^}~vXhXIQBS87CR+D!DRVZUV9C{wfuowJFNnN3wfA-8jp&2-S^3xr|6 zzrXl;2uX5bWf1wHixGo#dkUv~cdm*pcZ_-pezN<+;p~O?z~_ZDq;f21Ry-mvKBe{=QzQh*30fkP}f4ly4Fw2T6PFtp6Qz4qs?_T zpc+}B{Lt0~gJt!t3IQ^a&Bem|^xx!X&5lIwh0cc?h62{a5oo3=qhG5EA0cEyh3{|}B^z3c`jUmPH@9XOW1e9~EhKq*jdl|my+YXrkee4WLm)ErNYU( zNt4O{kN)>rK-bAB)nVv>WQ=6{Sln0L>CZhlqS zIbe8^7s|tp`jM+z)UsGkuKB8sFRj(iwic_M2+EmFB`orCzF&<)@t^U94=YF-X#SHt z)&wixil!;Gp6qAk&BB}+yMAz+h@JuT4G3t^g)*-zF#eTS|5)5fIr9I0H870#zlQ$n z9pL$f{MU+r%CLW|BoJj=&xetikO*u+5b#o^Gl9Yh27kylB@IitcY;$;LSWT7dBI-I z!mQ46`usXngrHrAfb@WQ>> zgAf^|e0+}|pcLhyyo}I5q#V4;8(M0`7@7qLvZT|WbU)Dee|t#->=(!w7ysM}E!BuG z{~1O0<{w;$G95YomTNurnW&VHN6_FAvdIOro^x=2(C0uYxO>cT78vh&)GRF)cpv&@ zs3Bm0H!FK%HUNST;1b**jeDRwj(*q>8yI}Q7WSf{N4+?>^eG@p54vw@8xWXKt59Pe z*CpmFL>JKK^2-e;4*tZgJV5{VpJf4ZD~OzuPW0&r2o^yS%uVQ^D~LRReE1iSihhDd zsFbh+;T_*Kd!^20mq}mi^KKa_%PeVWh{awBrc({6I`zUXd8|4i6Cf^==PfouQYIq^F?3cTRkwlb$Ff zCH3q6Lr>r|c!n(Y*Eb;p;`#k7p#rn9^-6mOo)E|8B^GCc&!bY zg5EO#(9;v9mD#ivmDDP6M67!b5~6&D@e_LNj&Nauq6xL?%SFQwv{onMx)=3qrG?C~ z1*2n?)yqYBpYvuE0Dh=EX>d468Z}rpp{e`w;PyI5lD-1$QdPfiu~|_1+X8$@F0JP- ztyOrjI~nuvPOU)9Z}-v7(Bz~q0zl$sU;2*k%_>;}Pml3tX4&AMp(mB`21c(7H!*?E zL`(Q6uph@*6b|t+@@v{&ZlP_{L`}$VdJC13`f@bINC9g}BlU{-Efy{eLOKEpyo{Nv zP{M8Cw4^esU|F}wfUjUf4My@QDfRTy;{k_Nvzp()03(&5Rkdf!n(SFtdFt@t5k`8b zAgW95cc%22lao4Zm=%cS)L6SrMu&`})T}NVxZx1i?ZDJtaB!;?eks*+ty>B?`Dyt) zUFx92{^K9hS6UwlQ1hs58OMUU3{gC^oA5(itaOy~i$Ak;wZiP3HK|yA2T1yok$|`WL`@7 zbucb1XH1s|P%OgF$%yTFpF|Gi#lST?KhMlnmSL$$=}UIlmEMZN$AS_UGkRyiZUtl% zl{$F8Q$j3&3aR^&{N*N1G(^ykmi?)|2Qx+WU#9! zU4L`&%uSR0TKqo)+pwD=Q3285cxR|_1PR~82Zt#$-%`54#|TaY57UFbeFtPr{7auXAj;}dBdFZm;wRP;hRkOm$cK;=eOUQu617i#zr z<}Cy&F`|*d@$zu%+QOB-Np;cqjrTn^Kot_QC+B2&?V-WCkx^&<{PQ7PE(X~YV~hWy zpZp*Bc5DYwWM=FPJbrZfh8;$aDXC!D*qKoKn7!>fO@1OlIx1gEI{6~hJD_;5HQ zWPWf7<*&~v(#A5LF5R3fpjkHd!OdVR!7Dlbc7M`WJ;dM0 zJsZcZ&q@gGrw#=T-8+>wHowDCCk`YmxkcShCly{mKK-bBfMf_Axu~LRNU~!Z&Kb*Z zWO(}_uM!+RSdX<^FjFyTaJ^xwqFf9}GBL~ocSF~a?>M)Q8o;><8^W-HXt_}+h$ z8%_jX#4kLB59=@@2oS-|W}n_wXkCDF&5uo&Tb8#&<_Hiqi2c^-q&3kTJ#t3#N)qNu|MBND zmVfOcsNkWch`uOpv~&l45HBwU77dkO+v2O2RKQcKoE1w6A&#m9*VEnsmet(JX!wa{tlAzO0qB+1ZOn==Z2)1%Q914bz8?LiyXX0D(z14~q3-a)~n}^I`VHxeURw zwxOf+ShS(7kt3DHoNeHlCwPJ8@cKt6$>GF7IwEEzKm}C*lYIi9-SS8GBFzSd+g8fu zKYYa)i56iJls7z(;00clGas}W6ldit9MD1Xib{%%h(&Pj7k-*whFS0&{42pKsu8j) zak`CE37j#!`Z{!~4Wl5t>H91aRjHG~Gm45GAxHvUkyU9YXSaW0a6B*@(I=qQ!3jZj12GmUDn-VSvsX+hZ49rTAveS%z zSd@U`Km8yf$5=}IC@mfh3V!&s+XDN-xBj(@MddeSs5}BCSmig9M-=hl@Y4t>#M3$2XuqIz{qlb&)P{Hi&K|`)V8f^bW>F5 zte_lT0#!IX9hyLqXJgJ&$W)u+6xXq+0@L!e)b(yn<8-bhZ|Q`}?5 zWpjjtS~HBt50M(?i?wE&qwL4jAk&AIX`S(%OPq+R2H=}(2}VAnL`T+zm_sh|T z4-LK4i95~mXbf1iI3OWCEJg0#-}W`9HVvv^MoEVz=A$dk&_*s8zmtads!bbf84+29 zrtk7cj&Ri)R;iGHbwdGU#d0j^8nQabhHnv%>y>Bh(2N=yH*A34hzIi5pTBfglrs;X zJ644PB`b9TOZE~T6?qMYBReQyda-yyT#E3k%?Pyzl)rqqS6o>T4##fq6#Brg}DNDZ&-CdVTJ*tvmlLB9wRNfu95= zL1F$`MSVb~Zo}6@jk>b7rsm8w%iWO#6fk_Scw)_R;t)4G<5s+k*fF*g7k|3eWFMll z0FfT=Z(ACqCGJWX< z+v*93zSTN-9#hlgqTPA=hnA$_5)T{4K`_8V2}~6^;-m1|dDWk|J2(+1Ft;^-9A2`i z`!zyKzSVYU@ldg|p&;^p#~I&6^kf1wOK!FA6FkGm@egqMzqBec0B5?&u>fZREt%t1?60NTc9FGR`8VQ3$R6QzV z3J4oCJyg{Mj~v~LKch0dS}MD(n`uP{nKv&jt1i}Ql&W%>Ixc_}6b-TAQywX6NGvs7@6@6Eos2VLD1? zO#Wc-quw~~d?-D)`SzK!?$fEOv9o5^axM5em!W|Cs_CO}Y}0jbV?PPsn$Lm*o$lE} zfFuh%32ej7^H6a}2=I`F28YsCPR`#OY)z8{J>A+l)YP-5BGtPpeUFBU9Nq!eIIKQ{ z>SeR=5fiA6r6m)!Wk8o$Tk7wu4Z(pVMje5&4rOJl5BFO;GgKhgyAu`F(YmITQ$g*9iV5^F4aH|#2ki-DGNrN3>@nS&&S#WY73PmT{w=638rKg&%d^Sk?& z&Zt*cXV<`p++VW8!&z z*0KyOs2eDAxeT^|j|3Gud{DEPTCZfWsy60zn^Pe6jEP!H@bI>xMEw__v}pk7S11m+ zK4AibUu%fsyIw1LU@I434C5H=0+4$jd%Ox_#6tYhTFDbAF^?4w%n0wL-K>hLZsNM0 zvYMci4(g%O)LO%#VQ745@#E%HIt@Kb4Sf>$rke*+L@(!&sC{&5oI?<*=d!w^n|*JBQsLMKdy;yGVb&KltK&47< zPrsJ%Bn?AS^%rMHaDb=p6cu$ISd|&f&$tnQy6Nj&9eBWpx2v6#N0I)XPH7~M_R$WM z7=+p(`oC;PtASKgs3^0+q}kc7;6td&I3)v6DT|wtOYYB*AVcIRLUtwdguB>_pZ6RVyfmkxS<>)pWsMe{e=%87SgRe zQb=wX8F4swi5Y;9j&sR<4%zgzBtnb0;JJA6qzSN=`2c!1(lmKK33jK0rHKW@btoN> z))r5hlFzD-aQx-Z=twRr=?NlEdjWO_%W=4wMVL2L{BU7Wk)?cPQ!n`)aQ z#ew~;SzJ=ppw17etz?QDEn)?kQ!_vHw7s~pt;a{Fk=%msUpq}vq&GD;n_JvnPHBL| zHLL4cFN+_pMe}O2@x7)GtnBR8o+cxfI9${BPgP?)Km2{{5)RN}*)TC}&Ks$BIEIQ; z(-DUlRBAnE=a?2E_)9UV$tZI@0YEj|EiH9JIwlZEQoAnv|_oV(X*vK80LKz@U3)03Gm9%K<11- zu>Ki?L<#QKT?iG2(B~QiQd0kbKL^P1!v3xVlazOXe%PL2MRd<))^z*QXFBzp;eV5= zb+T1;g1gRl-{~|hIEti9np@bB#`x#lvaRtn{UN9kv5m(URlNm(TSMsH1vA4DGo*3r z#Uk!;uPWq^d8cm)NdF^VprCN1u~SKPZCRA26VU^H*Diwnsy01{4ILe;qHMNY*VOo< zqGr2bzW59nCu)Nw9~xZmrdLl=$1ngJb-b7y^gZ1Wn=!w05KHDGdw0#LiJlo?>jJoc z#{o z?f*3+X0}8m&t_M8eb($Xcuf=^%WQlJnN5sVWFTq%pzZVfuMtp?*Hy4A28U9?5 zT_EN#O=A;POP38swdwy7p!XevyJDf+i~Q@$JTr631G5}9_}rQrsDDMKr?DAwU>)U7=aY$wEXg9b>Gsl#CF6y^6wSj_R7gzhi0KXQR|6Fk>) z5e;Ca(tW9TkJ*yfHA^X^>*LM#o94VW@*}8IZm$0$Qcg)jMK^(El;nSWDEhc|nV(L* z+T^}Bcf#&?y4LDw>HIBv&`_bzK(7-ZH~MZ5E>~SC?i3XjPuDb>Tpf)8T(q>5v}#bj zsGK|syfPqK0OoXNQPQYIeWe4g>H6&CKu_*IWLNgB&}&nagrzAH!RNpqrG zxPom-(s1dn;`E&|<9cIrw2r*LjN0;&TFtDxd6^U@w`!yzJ{6L1$d+F^zLEKQYg5zG zR8vT;cWvk7RF@YRc){VJs)aq(o=vSCP!D)jgNmx8`*a_8K*RBI23GHH!6ea3a6IM? zl6;)8vYd)_fKvYU!tX)lSJ>&aQqz=)r_W{FMwRX=vdPl0_Q;mZN7Fy+@n4d%jxP{8 z!9j4y*smy#Bln4W0Q@6-(#qukY^tC-kbTpVyyRGVDub9-ysk9f4SJl}q-y&ASaBmZB7y#-KQQPVAY zaF^f`ECB++-8GOv&_G~tcXyW|SV%|+5MOszLZsR&;pQ2yNaLRMNv7z@)@nJ-IMrUiYJNAu0zcFZ@ zE!d!H2ZW&|Pf(B~gFC+8TeiK#%)nywe-U?oiVJXBIeV_TOc$_K#&?4QgFNERzpsVw zf5n~cnR)AjBFUcI4|fHTmWX~~Ew?S8Il|)U@Vm5HBdo;w>M7I@UbG6;shmFOUy`hC#eToUhkL&2nPHHh0Z;*>1(N-q8mX}gwx{Hk`jFJJK;tgs27ELqu z=XIm|f2a2VJO0_@;oE`Q$Imp2a7!bH0^Ir7Re%FJa>`>)<$QrA4&BNM!Qi9U6nec{=8ro^xss)5Qk z@P7QoA{mEh-aZ2J(~aI6HVVqEH_cY6!%Y|^WE^~Y)*zF9M7omUisv^TPLGQCw#-)V z<)1m`QABfb3|VFwhVRz$?Y>|J=I26Q05!B{B^@+TniHw4WueY;;5>@+2S3~|4`9JR7i*j?aXi6ExTv#MyXy>dz*fS>oM$4K7T zb>=a0OKf@6{O9{m$^74}bU@*hU`hXF%l7SL+S%<`;5|MQ);qGGPcit4&Xu2}!IsR^ zySQkWQkdbhXS=R%?D#EZze$HC6+ZbjHCmlNS?0@->@Jkbdh$?qaeid42TwgXNFlE~ zl6@vukN;ODsC08m&FsbB)ENh42X%G6?|-d5X5DnruK)TflqVM@R$05$+8Q6!hmNc{ z%qEA-WYxcx@P86B*DyCLs02C0RM!2X25;xPdzM`ON+*Qr-4KgZL>na+5B#1B$>^|y zRGyx0>k3uVVj$%t1)f6wL3B`b&~?d87@@0l*Ntxw&TTZr$Ac7_nd|PbvF+#MU@!?- z36-1Ii^M58Rv*;E2b2*LtNo;6%rheV#9wruPn_cb{9dBfb^P7|Hv2PYdW=Z4$w>+? zir3+z*gr>Wk(y#9ZX6F5Yq~kr>=woXz|qh^DB zl$G(o6l>=9fA<#Dk268db=mj5{F>G7TjKVaCjx&O;UA*51n~ra2Zp83G&5!6ZK`QM z>b~LDCa?X|l;Fe-vjgdxq@ zb-TvXD0m|hy!hSI-8dG zb3(oMgDCUIu+~0#Z+rSc=UX9Gp`D zGXhom1Ddt!VbH8~gtfAhGFR70ix{wtt;o$jGBh_=Az4S4o*kj4t!ju5sOfNlY1i6~ z@_pgKl`NetE#s+4LNmq3hML8W`^5+IRsNlSP!)j+7~V_q^Cb*g?v?6u#`pdL)<&kk z=e;fMOU$jKpRJl7I1!>7v*X92P3Sj+pD3l6!+8?e4N?`dCDVwoN^9ajmVV(QT^Kv! z-?CyjblCYyYRXmT>jySj62XH{yoyKvpk@6jECR)!Nm$}MeDR(P(DOUOO84J)Pw<6! z3oF%eyNE~cexPDwvueDE-gBs8s5`zR9Id*DelrK@7eo&0(&LZu+C4Mda1=Az53c{l=aykk6FRTGQ3P@s`l6 zjSwROGU$~aW9p=v264?$DA2^f*sGStW5t@Bg!E0mKR4-*kiBR>AKh^t?#z5<5a&gF zRj{xQ#!B=c%koG(!Rv3$G{uF=$Wn9mtkf$(U>)cemD^>Jj*@(^%U}_n=w08wy5ZDA z{vxxNTHmnU^4`v@cam8gpB+o zR9diRW-vcxcA}hr+MXIhV`2r0~VH9d;Hl+lMDl;Cnq%yW@AIH)!vNG?A@>9F>71X2J<2Z zF>y{{*Z^Pgc1rdr%#IN)ExFnQXH8yej^g{!l8rlY6gIAmWiFSmtgS9MgY zFUr?=ik0@>tOW(6P;x1V>4M!C*iN6Lf2f+JeU38x5gqD`0WHdxCiQ0H;6)htfd$kQ zSz5x26jRYG_2Pl982V`{kJ?=umqJHdw4u4VmkKGf^q{r;W`tk?wWAiXd${4x6S_Ax z$^eTPo@&3ZJlz&$>nAUvioy&b>~2%( zrzoo07?S(PM=-OV{YE|nd0{KuDg89$(3l=e=08Wo4^Ulw-J^GxBQQGv@@J*@7g{hakZ1) zc({_#plHnvrUx`hxYh7+!lT88wc@Z$)A3R<7zIs}I1y>wP_t}>319P~Zk$j+%Jp8G zZK#z^s)NBqgfmP*aV4gN1|6W1o(yS&;Wjq|Ry70UlWs~e^z$DHheM0jLyayRrVF+b zA?`paG9xWlJVb+z3}AOwniNVftF`k|bCSb_5cb0Dlsi#9MWB#}s}OqmVdnz}j(*`XM-Xcao2e2kgM>F4QN{Tv(W5^+ZSbBU3m{M z0N&kpOf+DqnE~eNP$_~ym4eij{}O3~FBL1b+XJvMfbe~pwktJ* z^P@qltA#q~`4U!htFIJEBztQncugrqFZzC4a7l1XQvpT#)uR4KrFIG6a8K`VW)bpp z8%}?#W@q``0M!!;Bu`#Gg_Oa8esj1h5c0Ri^HDPM?=cytd)$mfAs%vo(}|Hw>_wAM zaCF&-Qz>|800Ja{bAWyT)+T5PX3L+1VTBs6bKVn5Losk+TpdE-$n9_DSwV1F^hHUENhZmW59wdC(G=+U18GdGifc+3^%mM)FK&f%#~QRUc|7>W0%)#?DstPS!h_ zg0fmYP(qc$6W+_0+>I!gy-}p!K)i5!gI@)5x}5(|BREJ5)Ak=xa7}2}`8+B6j_@IJxppo-f*Pfp;z-!8j0c`2dNK& z=V2v;3*dikH!0B)9BTEGI{6dNSotl9MGQsy3ljMf7Iv zW)3ON0XL0sqt`8;-wPLRlv0W%XB8<*S>-v%B%*H)@yU5Gb`wCW*G_liJ}0exOV)Q` z#A$+!a!>PCPM5Fofo-sXsnk4Pj@do7ohd!WUxGzx#9)c&3DSJa4ib)>85DrHT58`fk(dVMUU9@q!70mMv9A zsBboFid+Z22;u@Z;vC)FmIS6Paa$mJfp7#J9g*XAYCT}cR>wBA?GJpVc@l$Y8a2j$sVFAZxj@@mAp zF}+a|YVS2SsSk4UY9vMP^!tREK8O`!dK0ILemA=^e{Sx2g@ zQ-(!)sTq8XZ#Mzl&~%*O-A6TU+a3UVwy@UIUqifrUVO%m^kk@!B6@4RbW8|-i=4G9+KMpfJK#ppodNc&D*NB^SB;afA2E0Iltj#ceL-s zJKby^Jt)M0YNV_Y%I>150q4^u0QC4R4Kx@c-0hCZCeTv3GIAATboqT~X8|iky%gc% zd>>&x&!ZmV)*mfZZEkekrFLbrskp$vM!X$kyHIdPbsMNoh);qA?5rQmn#*|?w$7u1 zmrmRKXBTOVBu&ip!R!=t^4}Rj2mlD{22tzYereAZz{h>Y%lQL1M2SLk{%O&wDu?0t z`RT_tlqH-Hq@(5_)j7Q;Qpm+o*And`5~ORlvtjk}Z~lRYc8;rv?nwJIfL~mY6TdQV z?TqfPXA`fCD5anfnUpqZxS#JU!S|}8o||5cYK~O;j4we`U_?r1!X6}>GX8C)34Cw$ zMZ@I~l$gl%8sH{IQGc5ee=tO6alVQZ(%Q@uX5PNTqU6xGvNMv%6pr>JGPKpo&b6n| z#LUdBPy=)iRjpe)JGw&4X1{1Da1k=FGS*Lk5XPp^A>;%zrAh2bq)mb<{2CzG_?Hp1 z$W}OnKIEGakb$K=1%jLtZ+$Y#FwP=tJ<_~32916sL7FH_cxfaCl#3Ctt9=MMqjIS2 zU#KW?WqdNpM-1)rO2f#&Ky9W)=(6>$&6kIZ=5Gk8BY+wYMiI5G>a8_%_E|tKrr#og zq1NuZt{x(-!rZ>wx~sN)eB`_$ImBJ5*L=nayo79`m)Cln8$% zU{Lwwb5izQ2zhDeB9&FKW_hACoEZ~ef`Y4-@66iC?@pYFZWZ^j&4tYKS77w; zJy0{0p@5$^kT|4){@nbP!6q2evb(i@Mp*jho`Kc%A)f-E3+XsfMIlQdLpB8j32P*M z9QJ2^_u7b~0n(fuUjzxBH?;XK*5ke*gqxh4`*NfP=z0hPyuBtC%7l>^sI3z}auKED z1PAj$@&hok;V;g$Vdv-;GR0Espzk)Gof#G3KPti>(jbNwZP(z}6m2De={TSk3EIXm zZS7fJg!HXC-Yli@{_a4a^6&ZnT)VjllQW~#TtCx`??$vsvl}}S@FFzwlmN3YS2TkO-mA;x0+mTpL`!`0{oh_Shb9EK_dyDT1E+r_>}AA zKJ zMnRK&I~kZN?#VBa&hzsvSuB06euW6RfG5xVSb4ie0(orGtnl2dkGsaWMqE%iZ+8k; zZGT^Yaiu(kopkAJYSD(p8`rYT>2({A>LguVd;aV)^VBkP{gdknM#yM*z&>2!@4;$X zp@FMW3KzP*dO6~i-Ru6rotlB_x}ti2ecxK!)MNymfcrHpj=D>U2LFxuQOgA}C6R9g zZTgV}LYR(?r0oe8lA_`mz=D{&fJ34v;l)|zY9;=3 zI$0i`-K-qR8yzcZkT%@!X9j~~WRZ}9qa(~cGeS_M-(K)@1e?<~!DQi`+{C`0+*t`Q zk5!m_F-JFXko|fhVk@U7OE}1|N?Xk`9-~4B(yrJKRx%D{5{F#tKqfiagc9>u=B z2uMHXau!LOJ1z#z}v1V++TH{k|WiCK`MnlGLLOkj6kY-H;r3T1;jU z2R4rrwEoUag3h07HGC^I;?XYbs-Kn4|UwNh3N^U+@efbMW4sjV`k_U>wM z4-ua%;3oA1R<0SYML;8`bldZ_;*OLb7<$i^KF9;4D5SW`qteb+(N8ylK`lLH7=Omb zN$&LYv>A6?3c`<_q>>lRfbl-5Ns2-wxiZmf8C1fh6buCRX)>r`z((2r7fNzM8-wKJ zhTCh;pFoO-E`R!D$oF1zGt*IZc|=;bFRm5|=0J7bOSg_aes&NkiU3NJ5=ZI5uB5RK zKHDHrbQRtO2;dzYim9U-8iXxV9x|jee&EP%3qFllJANQ3Ud6CBEeS?aGeG8AQGw{S z{A9AzpdVDu7A>S|k%zRuXcJ_=w;h_EhAA@6J6t5?%Hgx9jB92x#5+`5fxM#0+kw!R z?bj+qANcBmOgxlL_~aR5i5al`FkZxVS5#Ew<}wcm!BdDize2Rs0k6%7i^ik+$qFz% zTa3ssK>x^jk#Igw5%<4t?p(Z3+pX<5K*=7h)>{VY4Bn@RLv(6ouD~vCZU4p}kv-h( z-hCHAP*@mawo1_Cbl(Sy5LiV1HfVwD#u&osNsB@_)UPB#+xO5!W*He2q)_UxuV6F9 zDoC3$zzFp3=rYJpxW~CRG?>Q1gXV#v$S?fWDS3=`E($Njup~{S(I(yIjXl@hsXDT< zBiCK4NwRIl6Ofi}U{TexBFc^DuW!QoCH(FmbSoREByISsSMSC3(;Iz`Qc7yWd&gIt zrpVw)dD)|`K+Gs4yI8M&V#aUpcztfp?)=&v5RSiFRl=8PbwSg9=0I>z;lQ(vXtC5z zKxqA_+eUV_lm;*7WzGs(CB`hkDfv~r8F&v0QbiucyB7j(8j4>Z(36H_7a~943zJP| z@H7LclHmhQbcp~k-(`l;(S(dV%GaOMQT1)tpT5>?U>RNizGz^5KBY#6_g!)2ibvrr8L_F3+}Dyul*p?xbv{y zh6VU|N`R9WZd%zc_U6tSI%6T(fZqYlz0@S1zj$^Yh1>I;7fm7a<0CT*4c5ICe(Xq+ zwDmO?AmlS4{`f|`I7Fe=|0&f|dI7M?4)pQ)j{wk%teN~NK*r119Yc>aG*e z#-JraE}3u>SqOe40=D!f=Zatm=Pq$%ci&X*`%(-(JXCaxE>dZT>&cRlQBpkGx50ja zVnX5AA6y;p->4Hp!_RNmBqSaL_V#wDJd%xxm(>)6lLsxzl7u;2P1V)J5_-2k>`hhM zDjfI&F%Wj4BwhM$*xP=mfiJ3GY3lD=?Jo|*{kPXa`Px-LI!zPD?Y1S{qWDYw-SzZQ zWl_rKR|po;x* zbEW1O{}C_PDwxmhf)zZb9aag#0kTmIoTf7t(onz@u^ozw&_N4W>@s4S!g1-4$zmf5 zh=7p153U2|8Tp}4i9bk%54dfDo_n&0VLq8(Y7!ZVmlGFYv)`k+JnRhc+I19}sO+~r z0nFI>gy>w;Chd}50y%p2wl*6vW9|pO$4R(LI^5F}Tsuxe3jwR&i?9al0T&p{QRQnh z-Pk+}gR9O)Bu?T$Ps>G9{R=zCgQ+%}1c0})NTuI^G7x^olWouqcDTF0en8+nUA52m zBK$uv7?}C+-Acx)w%^7|`M-ayTBTEYJkMgxRmeOdND1cJlWxiV@$!}+z%Y-*$lz&B zM&wjiBDzNd6hV?Yk-j^r2*l0^q{40)h%IvT&F1nDKF*GiA|BkOBy08!}mrS?vlHQNMeg?7s(EzPv= z6Jj5%TnoX2fZ)WbzbW0%iSTL^D|@ldPkjHVA-gcM_3%(G8&2ldz2UW+8`z6X`7jAV zK<_Kuw*SXiJRVvtcaT=(eJYKvkN$iJ93c|nsxeOMMp-$ipN?Sk+Km&4B(H^#6cDz@ zNf2`KOOO8b?qKLJ1EeSW)`+Fi2LLN?sz@$%M)O=&D~^ zlnao>{>V3yCZqfUKvi9H!Bd;`EjhK24RE8f;m;fxOciIC-Bk)}ja^vPD%bs2m-#Qk z`F9p&kZI)!fN4ga0dv@nFaWlZ__Aa17SqCE@!cO)vRr0lfU#S85&#r8Yb2>(BVt+b zjHq$DLey~e373X6T6d$<2iMEUlY23;u$0g<5ajx^V9@!k?YWxu7Xe2J2z8eU(fyPn z^c_5~UH_ zk5g>!L+Sy*jCM^DPiZiwm!|dkqm=vKGlBje&LyO2@mvTC#hVrn=rZ2%0h+Gkb|2nc z)(-uQ3+O;HA^~)QMg#GH>>bVf)5fhj?{cQclGK03i0>)-h%cV#L=pbychEjV|3i$D zKejda&$Iv53QO(hGCpDr{%P)g!C5NUGA;h-@Wm>hga$DPS_PN%bACV%hFo7)gMrL- z-nuA6zls$MGb3JVFGx-?olpq}AW@BvRXgP_ctuS%I1yfcFn`5@yM5k4ihf;ZTPX8< zRD_6%;|bL^o52xHN!l5%Y+*JZrbEo}e*A>V?Z*dAH?!1WU4g7pSp5^(UhVta0xkvB zOVooUw($(*+N0+I&y#gzm(?gHhJwb68lDREj)hd!Ki;h$(XYp@L6>szL?YKH@W)@Z zbCmN}Lga=y5d;+I%)Of}l<4fvCB-4Airbz)o-Wwx>D6el8d`cGA$YiqR9be6y~oPQ zH4t5(!oH5bfxDSzrZm4ut6xV=6+wB3KFX2UUVPUkA*xwNCdR*iQM^#Hr+WGbG2jRKOF^-fwEgYeC?6B?}uK zBE>$q*ux`TC*t5}4{v(esA?wYHb$&lu#FQE90V>u)0Uk^@qN1cf|z*Q^B1g|n2Q;! z=B+O!Q2fGsI3yrh1{munbWerf3!T3#^pkq!o!O-;Y(|Vj(M?4|6!O-7Z;PwmRs?{R-aH_+nL=_k^`c!nao4 zfr+o9zDT&Y3d@B1B=8sj(1)(=D{a<^NpAQjrbhzziMDHcXvZzMLy(^-OghYd;^{6e zhk{IAeAv``xMK!>K1j>7eL7EwASc8)ajJ3=G*ojUocL=0;JZ{S1hF~q%)MIar(Q#` z9-4Z=mfJEGi7oES1{3Vi!SKa-5wO910v~Jc1%@LPPpD^=g!UmYa?0Ic~OZl}pCB&Q55^ zDRnPl)p+avXA`)|F9$T{J2$l4-^8a++-P0VC*U+o_!X>#O{6W)K6$)sI+H|Ld3VD> zKpzVNdXr2h3hb7fdiNxE#}g7;mgp{I633~%GoOoaENWnW_N5I`s~GBY=O44Sae=%B zfk4B3%!20NHxSetT#ajH8KgG8@DI9+eL-XlgL%V=VSAsV8D%RqIqS9NNxWEQ z-g`x%)83zxdlvROJ7GmjYK@8i8#k4 zMP%v-`-BBrsDEc)P7Fn1p5}6U&moOaX)A&7J3YK>=RP7%ao)Nmm$sMvBg0=8@?OWw zZ&7U*emAtVYEQa1elKEjii>mA+0oG%7Or4GQ6C_IebN=lM0Yt^_NC#MWfWQm{kpRIKBMOI0V?)g;D$l1w z1Ffw5yu7fJ2#xx{%--Q+w7j@P%`G-p@k~W({5f2LU{B1&&~d%h`pWul zSRJjG@k;B8qkkcIUBIpGHo_m1n51hK?Vrnl4sEILludYs(SGEU7RtCpL@=Zs3Oo*h z9nd|Mu_`_~7jM9pCpie8!NIve{LE~kafF)2qF#JHnO3|A`9p#1&&fvJvs{ma^x_%| zfrL0aZdGUPh8M)8`+@x67@xu(l!7f@&DHO+c_eb0}cf+L@ z{rCR&Besj1s*aAu97e$dAX>S z{tAf*1x>zAtJeDa?qUhvvJEX>Va>mOL25RPWU;akp69*S#SJS zlUc89yfnvHIgg@+fI5$xYG40H<6C=MEnFLq@XZn?7Ha6}y%y}AqvA#Gsfd~>_U0h|hzUe?#^{GiLUoF|hGQH4T zjG`FeT|d$AZj9{gIGyICq0D^VOWGc-6RWot+CypMZGuPb=jm0Q!Ht9Tm`XHzlK!E3 zC*AL;MPOrMp*%-@D#<-P8+GFr&U`Ta5*l=GC8AzB4&itV`@`Aw@qObKE>j z>OJcA(vBYpg+=PFPagI%eJV#TXre^_KKIf_HOrvCQN#bPK@D{<=-UcQy9E8W_dHo2*RLUcfm57gwlJY>G^`=FOx27tt#VbfS z#P~B!<@)cub%v9lh4CZvg+Hc-r>4BjOFvT8^JN;nwI`+0aUVC)3EDh7{F;$r&`aPh zR6jR$%lc~BW4)Z#gd6 zMk^vEyb?yvv-a;@m|VNGTqI0eZ+v>k@+qXR=sD7YM~~YY{wxvBPJvO0mT_ssaP+W@ z2RU1OVrqs{%KeJxz`fhi$=#0Sgs`g&(%LUMWmeY#j1N{$D=*?43@eZ4c{Z%+xvM4o zL`go2Vf|kCAVE!z-Hfe{-h?gpQHJr0WQZEbn5O1xzA*l^D{&`*5H7kJnw18htaATrf8{B)iFA-F@vUQREL@}rWWL6n2FWlp#D|s ztM#I>FeKF5djpv$SyH~h_mwYqilj$P-#w^uDI4ef)noU;zr47@GNzEte05|}7=T43 z8y?=SOy`E>XXO=H=(FMBYk>p?Ff8 zV%iqBe1@s1-WyFi=pHV8d`;STGi(|sub%%^(41F+BPmL4XiH2HUt;|5wdDEg@KJ`e{OQuy;Pw2@5-XzC`-SC_ z60jH7u*p35quS}O1?5x9^P5g+N$~x$m7Cw(v-0COi!Yky^Kp}3sZTFRZoX2_Jz?X0 zIx8R^(DvQg30u<5%d4w^oQj${zt%y|PY=KkmAyYq%;+|Gr5gf zwmd`VQicf;Xw<-r8h_$2QS3em1_@LQ(F(BbcIua5qy@rHucC8qnj&$cK<5kl(Td{pmqvC|frnjHT$Hk6wv}v= zR(4*C4a*F3ccnB%;MWQi^ch<8-JP2HM($H$8ibZLm9QuwDr$Dl%P-0+T0z7kd-@~r za*E3}ZO88&b`;GtBqhZECLn}LMVlS6s7jPR<Yw++sGU{EqnN?uKUP_M(5ITcXkX{DM7Wm!Jsm zj`jhIX8ZC;M@&Uok1VN+LtFVe!X)+mv^7|T8cIEDsQe3uS>k@7Fn#{Loo~Nl>cptf zoT;v4tk3(&^pq-2USn6!@-sINiujL_mT+ZH7dPIm^D2S}`lbD?9cK=1I8M+X%0CIoL>iS>g{ofDg|}}y!aG<| zpFc!fIsUdcg@K{ixvVdQb0tVie|tf7HDX>swBo1cCzL+)Y?btY!=sCOqMM>4-{vj7 z^P~_^;z;uL>N@RREVzjsEo5O{#S)T=yXSVREq*^4Nw&1@V*ZhGA zmpo4Cf@0&>_u`!YJQI|ml<=DoM*JEr8}(CIJ@+EW956?CM_Dgl|j2d+U~qx&ZFCo(AO0&!;l1Db%;g7H0J0czL1iZ zLKwwod4CmC_}<`q@K4EC!7J8OLimUkhKDuSV#D-~vKB7NqVaDV$LjWbDRG2~26sQO zkGS*Z3huYlODQvw853H83zsUQEUH`7SlHI|2;r7S^eeGd89Dd)U_Haamd~6xSmcyLQEo4roZ_Xyv zU%sP~rO~9RVE~a<+McW|uc%Zu?D3`GjSPMS=^sTRP=b#fLmscOmzGf7sa~Z|c@6CW+(=z7t_@E6to!}} zTKBs;KE+~|WNk33zlWo}nDnObIaF8?iPN3^FQ{SzjqdCbpRk(WPy_uiS z#K!*C)|NAa^634)BD#M}WCSS0D8V!%%zPfP0;|m~?%dm-hEY*bk;8z_-6zEq-56?Hc{9`!(ox&9Iu-t}SiA7(-Zs?x3msG<-Mr&i`~(W)+C zfiJ_pOaH0KtNBrct zI*QW8!(LlbTFhobg0k@vR<*nDg5Y#<+7}u5)dW63(arQp_uDfr;g~;m$EqdOS6<9fy_5-KIvO@+`F6!7imw_(?&+!*jd#T6q z7_3+zQ(3j6)z9YIw(Wribos5fo#NSiuN5C(yf@7+#QrZsB$N4w)0gqb$+;jOvnC!! zCqK!KXcZwB(JhLV7r$qm2NM_n+2^Xy|HP9+(y9M@-`PVvE~@6c7+=46?RZm9Me;P1 zNl<+bHGORNAy4mmb}XjXao^SSL>^xT>G=PLPjZVd>Oj2wQ1Zi(PmKiZ8ILHH?pYZ3 z@9;CcN+S}G;pY-g>Z-X}mT zviYkXRuCW=a{l*m3X6)h0j4T<;q6<~)9~sicROka(^?=#RgR=j`7A2meVLB|N5XT9 z-!|iDI5|gom1XMcc+jEC4|4v5AJgCC<6b_aYZtr#$tdzjjxiYw9H!nakc-+EBLBq7 zbb>lo<=y^KH`oPrP>gcn{Hi(hfa`HTc+t&%Rbj+Lgml^$b;d-vl*tsz238b^CX-Q7 z!I}!o)ak*o!9{5esSb~ZJ&a*#7)9B|7<;(8%i=Da@(QE>3$#v*!?LsRdNhCx;E+_UsC%vyUz~o1Z6J_x{$%SSWk<0AqIW-!5=u z&DFPLygx^QSA;K&GKM4yopelT-&D*7qJhcE(EpeC<&FW&Fm}~kUZAwjKI{1KSBtrb zsfql=Mf;T@XWe4>UCCWm%F4P+$=Mi`!)wkdigVi+X%NM!)b}9(Oq7Tc=Iq+&u1V6f z7rX@(FC=a_7f9l(sevWmt5BFSkOYYW__0gXAFZJh=}SpdEt|8iC`{(r78i*lk9=ik ztoGN|`q4cPcAdp-6ytJVaD50i9E?#WKdvc`Y)x;mzf2%Yl|QeVck=PrFWBTo0vE_o zGWOT(>RbAlc5&M}4UT*ZjlnUWpMN`HWr`KDUrF(|re8gWy{WYyRIvaiUr0>(1aP~m zTx6*@FtLr-)(BAXL-w~TU$cuj7OW-f;9Zn2Akm8XTG5Y|BXAhLLzo+_-;yx79{T0& zSEc(}n-tJXzx{FZPThkoD64OpS*r(!&VSF#*8?+=Lg=B+c`M*{SZ-suL5W6Imc#4x zzDR{+mzAT@|2`k`X_WTCZZRtg5xQepdjftMai2!4rd|kllt*P+`g_@RJq@LsfmCW% z-iPu7n+f%TcTOQ)W8VBEUKVp={`+ZmCT@RS2t}~?cEHwnf+7cwJjxS}3%i%Y?lUZj zv_0>XnWRQVmrky}6Vhe!@^S*eg5>2i6p~wVd5YR16@rRNM3#k|k~7Uor1q%-36#ow z#KR3)$s%vI$;$a11`2Q5@OE+9Wi#&S|8$0f8F6PXq-E&~;>!=R?R6k$?A5W^x%0~E z0BSPD$?8Da)}Q&5T85JZH#qWf#I8*1tJj|>JGYLi-Vk6!%?8UnTb#imsweyY`4s_ozp6S zwJ<;^KH&4t%1@8qmD}ZGNFbJsi0o98&{i23;~zae^s6~jk=R6qp4xHlUBeuzvv`_& z9SwaNy6>|6*@4G3ADTT*UvO-Dn*&egzuQ7H2dyZ)lH=~3=aJWW_$8iR`e?qGQE%;F@*$`;@^OoxxFG<@Yf%e}*T-@j(cqTy&C!gMG1)uVkaxwc z1$v2Gwc^##@QF%|6lUNhF~%*)WmS>qW!Ih!9}I}6d-xNTExxV1_Yt`F8mTkG*?qiK zZ3Uag(jJlo#i%djhKIhT$P3Slq;@gdN>Ek>Sz2(pj3buj`4A#vtSJAoq6V7=_C&P4 z&R{r^xwY!In4hid!EjA2mU%`P)K2LEjsP_jtXQmjdVzQpI>xz8Q#qE7Jizo~6~K>@ z!T;R;l1dC)0(*2!Z6o7}1ci{b-?&o8Dy;)sLjcY9Z_c)lPT*%W-ShVTB?ng(`WS6X z{^jFSA!ELGkN$PS)lm^0 z$y<)4c`^TX&hIGOlWij$F%ih3vV*wUvwNbc{hAsqd2n7k?(uF+o384#N(=;qt4v*e zi_?%2F3bKsq0zF%nrDHhcNYC`Dk?ncll---sGs0}quKiw3h23zuDyBN2|HnK{h+$N z5?xrRUz`U=soBOrMjp#03c-Tj@*H?l8Z*nb_XWQU?L}4Ft!F+dl=nNmE7YIJh+C_o zp8j?_L0rxvwlh65&S3E+Kkg#4f=9Xks@Pcsfo$-P^>4WW`uQ~R>?aBDL1(bfQ%016 z`(}A)+3H&x6Y{Xu|A$%!@zJDyE0lUc#vH0tQ)J^PUi-IVWDYe`+Xg=hb;3~S^)^nh#K`AWl8@^X z>x_)#+d!g5B9{c-7w9F2{Kdb25eXtX(k`h#%aFB%Ryq4MPt99oY=YuBTjIyim@*t( ztMJiP=NAOZgWQl$%7Y#%rSaJT4Ex~dK0KoWMU@wGB0Clg1xGrw$0N1$fBMCuyDSp) z0_nENP2zJ5^o8aj9d0~$zlW62km0RL2@ejXza4GI33cD81K-5hGqTy%e@<}qP-dOL z^C@*uFn?t3Rlk@4XvHoX8~N`F&%a1~5gyq-Q)6|E5p&e$q&>+8)y8Sg$znJZ}7wJv$ ze+huRnf?iYSV@qeRVD?hU&2aC!+J0n$V91w!3>vn(wIwLj%w@X%H#K0{`mJLvVhP+Y{1g_cVT1yNm%5gz45#LfQ zI2I5?n5oS}Wa2#Oj1!gmWG(n%GIw>! z>+`7V$lv~}1u&28ZkeeGOUd2kDXVr}=TgHA#H2=!MpoKB7UET}Mn>pL8s-ahy&pwG3^EehW#mM)a(~^o4!%mF0^?z?gn5aG+oocqR=_Nk(S7Gi4^zN zMwGHQz~Yf`f`#fsXJ%$rj<9%=E6Wk8JdR8p?ld9(rleAHT%h9l;FtnjXCzH5NxC}* zfNUKUMujQ_m)nfnYZ>Ifjzt**z7H*wUPmJZpKa?9<;R=WfATW>mcrsd^*^dEmM*yx z>9Jnh>w76M35&33|OZNwCQO%f0I1QapQD-#*Ccyqe&lQ=#njS@^+;-;@S=8$N!^ z)pFtSL0#-6U#5IR*OGL|n%F9}R+^=&P{fzJeRj*O)=?f6 z4UDhk80Sd>fA|c(zO&5w>#AVlM*mKPzgrD%Xs9G8h9^3-1IoG8F;o=C*4ND?H!tH1_*)w zqm7LLRxl^&6U4D>2I@T~`1$zSK>=17V#g7tEhc~`c62Y$a4~bfHw($&0FS*%_5V|R z1wUSG?T$6+XRbG3&t5xrnq)S0|DknMq*+}2wun(eNYgFS`F`7!EtRj5qZ);`!~UPX z{}ByoVRNe$2(`~8evy2|^qlsTSf^0enNQ+Ii^piALJvuJ=H55_%#S%~Zi+H5tg7(8 z>w*9IU-DPWDF0t~qJJ=I1ve~`sO@b`3K>j}T_nedxK(W5%)##X|HuGvLB4_I4S#fw z7X75IKmvmjSy0_kTKt9iM#EvpTF)BWDe_?Tt=UygwNVw*gdKSx zn~$=84i)rvY#&D@&T;-_$aPCD4C}(~@>r~6G{3>ghMn$@w|0$^rJ=27gQE|7l1KcS z%AlW*=uHDvs-c4b7Q3SU)d}UWjQ?5QZ7d{5SH*st1teT|XPjRD5kc73+e2pJGDSnJDH7T8|TiN!?Aj>!GgKJPX& z>=g{$rp{G;wl`mY-@I*u6=0A$AU@V`pW$&+u`pWFSpVLBRE$dCmptrk z@=R(fIUhpC^ONnrx9S&yk;iBU)>|n94K|xRe3jmv7Pgb2cmR>gj!Ff{OB6d=;zVJ1 zCW{EE+ggHyrT*uWb_9bYA#uwexiKc^Rw7zCXMB8TW$%9QP`{zr*+EpIUjMQll6&oWr(k<6G9P zxKn-W=br#FwW}GP>cy5x0;DT~+m5U*nMr19jSmjz8NLFJ{0zOxdZFxkG4HI%IH>D9 zPaLa(rIxGw#)pk^rKqAnbZGunCAB?&UgQ0Kd)IGm6-)TB9glK0)^z*-85PM3fPxoI zjL(+sCYqRuX+(&}3;I+@aWGmyP~zz031QZpZMy%(wuPm4x#IT4Vp1(SFf})Rq^oZ} zcttrnjW#<;=|b&hC?Z~n>(-5r5502y%`cEA&v?6AK4d=3MLRW=#u5=qm{nWzrbV|Fq>F)7M^#2 zTeYJSX?liT7vZ4H^&=JKla{V4_&`o9sjVyi7iDi97G==4fvzZ^gruZ&x0G}*4bmmu z-Q7qoE!`j~-AIFgfV4=rbc1v+=UIHe_r!Ou>s)94V%cYA=b4#b-M@QgkQn2ni4@k1 zPS?z3TQ-?p1~PPG5OtmV$GDDx&16O?=fg_4u=>u^Z%4>-=>vD8Q!rcGt4X+OE$YMUd+jk2Xe~rY zW-Ki;{wJ&QA!D~oY0c{sqPV{myx!uD=|{_H(*rcqpme!0pCjFq^PWSY-TL>A2X19G zXJ!dAV`I85e9PL5LINr8_~hGHo!VyTT;hbF)5H5_3->1z&ZR@m2a}jnH?pq+o1&FI zU@y`7bgkEDGU7zS`}WaPH_Z!*AelHY*Eys1acrqwnSM;En6*{7y-MYHEo%tgzO&k#I$7D|q4A z)h!Eu=3O5{ieIb8_|*Mw7S|W)Bf_uF0F7d>aP4#U;v6)Axy{}CE+IdJx^he5n1j>v z_)@;p<@R@qp7fi+46bKzr_|<^lt0=#hSZ3kDd#o8(_!XQyplzia)l}bRC9BmhmHX( zG9Y`5xQ2Kcx(3%_z_>#Qu|ci|_O2wZ&13)@R&WUL6b)vn1I;g|(XdW#91M=X2Ls!= zUU%ET*QEd(Xxw6t_P!#zoyp8 zVp6NhrU`k}p#*O>--J90C|S!1_r6G$`XFb*&o>|eU_Syt#}U3@UyTFg6W`VKyXc{l zCNAP|$a=pqO{62)Lq`J5>x>lGzO2qLx&+rB{YeRq&p=ChZ=)0C&?Q)EYW08!mzK59 z7E-l>Nae!TG3^Dzlzte&vd%o2#IfEcMlW{uwanT?qJ4$KH=nFAfHl%q0sAnZcUX|ogx8^|CCazoN z%GU^hj%6VMIv}ZF6d4*)vlZpzwxK-2Yx9gj;=wxX?)^-w3!;-)f&zf5=D!s@h!lYB zSqfL^!$3K>`6yL#-K_$2$-pB6Hx6qs*N+67Vvz!fp2FaNq)FTRHyUpK^)>3mP5Kr! zz#aHZzret!n7e_N4c%xpuRYkp#-|HUi+0a>HKX$+T*wCzIM<^0d(g_wZ^Ar1lg$x! z|6TnLG$eB!-y<_d6rjI>QwOdOG&&Moj^{6+Iex&w7)_9guOnu_e)QkCW!icB@1X9DW zY%iyJf^MtZ;e10`-rtJGWv~5;Mk_UU*Tea#O{)R@`UU13!Iic$>k$-CYmN^l#p6w3 zf%i-$^UM*Ad(LgQxxVMxPV9m#XJxPdhR;@yF3Y{V(_#G+d;f_S3->D}+ti4s{Iezy zdv2Zr`dqe>Fcu}5pBUJCBMh+Q0ve|OEK8}ieu~KZXbQ_$KOhxChxJOqn5bjAX1O2( zJc@F+h+CWpxD5B>WuIMpX1Il+JmC*-M48G%%nW|QR>rwqqT0g4IWCFM`o72Q?vh!d zQI-k|P9Vpik(|uwGn?hM-8Hnwd($TOW>DtsZA#+Az>;4RqXd!;|HW1aa2pNH2V=5W zl)&r9xz?+l_my{g=R(5dulA&sBd)7oL|F`csG#aC+I;WS6TOi9Wv^g+V|;qIM4;(w zGy=~^k6o*s@^*ZJt8-}S)Wk%>e6uHZe&upVoB|fjS9!a1+%S2x_qweDNpJK^+}3~Q zA$+Dy|0e!(ItZK6>4=0KgzyLCgN5cNG$d2sE2N{ioFq@Rj=l)u( zg5kTA`Acv5!@c+s7&+cdw7? zl%LP zylQ4imJpZPG>BQhBU6e}N1*{Y0}41K*NI=tJ<-|#1PY+%=*L9UHtz4({~CG;?f+Z( zNdp4e`OOs@{MhXgCJN$xsc>1Xan*LPkqko=mj zO5`E~0_h9O_T#@iy~t)E5~9ZLP%3C}mCQeO+jVfS|K?ToUhcyOBYJqyB)xs}b5JpN z2A4gvn-y@{P8t3yU>M5-Pjda6-a;L=3#ECTZ{_;!BXVPW5uJG@KjiFsu$=J`Wb*iK zL04WLlauF7XJ-iX}f3=T_U3pyo;@bB39l99a7vsC2e>1(7idXG6BYk?Bh{LL_CD)rzQ}zLDS2s-t{?SaqvzNxl zx2usJPjXoVJ?W$;rD5_x zBPi%}`gY0lGxTIA*8glX<*f1B+c5^4gV{!w&W}S`h#9-X>if>3>h z>UUu$|!ihgg61|in2ld&$^(jO4xx%C6wIrC;*=m;^rHV<;tjKx^OtcB%v*{U8P#erRF;AAhk5p7-_}PxF*tfd2FB?d@$eA(vjKU)H;5IG~Dp_&#HdXC|nztkmj$ z{O$EO+XeMyDd1b6Ov4QqfM^HP<;H*gQd}!6`1jvx9Y8K<;kA`O-({K}u_S6#Sp39O zb!j1`1mA|DQz^q{DEG6oTnoe&I zgIrH^k;K-pH)Tm@sSIS4w8;@PwDVsPVwgGk-~lbj3v>CG2eNur+?OL@srE2w!E&g}ISQEpe122j*1U zC(=kXDa!p)8O&2)6)NsN4OOwIgdE|w-|*5sq~zrO!svm(MIK{>aB;${Opj?<7rUa$ zbc| zCxn5yGrVDEvP5MxwrGrFpS1$P;Dp~Y)_hRqi1tgZi>qse&bE{?Ffyvu1%}n66oNVZ zM~f|J$Qf#4QdVS4#Y5!|tS>Y~$&6|%hdlR1w?#+}L4iCqy^roJET8s?myuUz^78^-=^eWQ?5sthz`N{)f+8}godQ@8lvFLLy zNE3&>Bdz4QN~7B{y*p_&H7Py<@YBRce5Rw_NuS9VR1hG=J5~r{2~HPLnv{82GckRt zQ}m7>KK~g65l(J?vXKnLQ|oDEUm~Q&9}R8)821z9Ltf>O8~X?;`KV`S`Ti&^(Xqm& zOqEDc9PU?2ON3Zwo&|dEMZW&1*Tf2YX{HnsjF|X-`W*J8z|>Ys$1B-{%N?F7-X6I$ z2Z4$?cUvc1^Id~2t*X2E8Cp^c4tPKAbw14bf++{_3qj}W2$!s^brW8X*-n=UaTqlJ*OHs75&wc+!v!W z0RyJ)4{3n*u=!5Htm*i)vV^Bt>xH(gj)=%=q+U$W0yV9k#lzHZ^-G<&LEesBX|mz$ z0C>&EzFSxOat3#7keabq|M?+N;}8*ix%g8XAmPKZR>j66c|eM{hP)>tDd7|0-2`A7 z7bpP{0hEA@gTAM$|5bloh5IP@0u)f&r7sHm()y#ASkRfVudi=2IbB(3e{icl*0)0@ zn7*@q`_vDRbvker(~Zy5;C$@gj46Km-rl?XQ?cQlamEnQE713OK*>L~zjfk4KuFkV zx76agKi${Y7dS;?-Lu8wIy(>^#>-1gZ_fy{O1OOV)G_P%W!r_f;o&2(w$|5On;BHv zFTiSVp5K_hl8?t+BVBKQ?gJml$Ti(*lkzP}SB+x&k)jW+YHkQImCy_(lA!M&zunXv zqPm<_gz5RZj+OOIa9JK-+B(Y@Nm(e{!^$y;M$mvr=A)5XcL==ELr@)?%+xr~cbK;X zRJUq<#0uT_ucJrGD~%8>2}GGDX&6i^AF^>18lMX(lSZE^MEo=TD#2BE##L>x9YKS3 z7#YiyI*|`DXjXM>VKFNPR_$#3lvyS;O**~O2@$G@v~FPBB1lIoH-NM3$VxeZf>enZ zuYj%=YxcdPQ8P(58bdZSN^LSpwCYa!6-bSNoVO@YZb5ZduEV$!E1hc*>MH#28&ICaBYoB4GVpPxB#qI zN-m4bfw?8K(R)OH>_)b)T6@xrT@|41Ow*n}ZX{za9*c84*+i(3{G`Y~cDtbUTwDn` zyIm3?L&l3~J!BSZH>ErAr-S&A^8H~Lx^e(L& zfn$FqcTNUj8~p0dMNf>n%uvEmz~(AJK=)S#X(_p*Ket?s6qs)%t8BW=AtALKAw2aW zx*a3oz#oy_Q_(E@9+W9|PQRF$Z+=zw^G87_Jb6)A^Y1eFyfi;b>9Z@eL6O*X(Nso6 z6n}0;m|+XWYd||3GO6pUz}E>^?417!*b+P9`ETt%`6LO67rx!>%4*;)Q&B80@EHgp zvW@upOnG_p#ObD2XUAv>5$+rkReyX_Vk~Q+TxE8(B4pv(`h{52-yh(oHUzuY=DhJ_ z+kF0dtXfy0`Dh@7ijCYiH^_&E@+u)-RhY%Wb*gi}tF02U}-1 zONI|ovSAgfqOW2`^T#E##LhnDkKU>L75nXL6p%sXf0wH1;J$dAO2F;dl#+2H&;;h} zMN*C__Xg3>yIB?Lhu%6JbKUh)xGxEe*j-3^;f>pS!t1KKX#> zrg)wH)^!eR4Bo^-nKn*-;H6O|3AynXE{@Os{%q#(O3~09i64RoSn!qO2Mnbp9PlU4 z!cBD5q(!sI^_Mv!M)X_mj@s%XGv&G}GRdj970waiz|Z<1HC(A%w znEdlUWLv&JCdM4b}=7>gPOWrRm7AF0ib@82tMx70>U$c&a?5fg99#cnVBc%zE zfB$kzNOuflA<)k~#>~F1Pwhg|$nHoSqj@xJ$%ugBIv25VTTi;!X_J| z1U|Dr&lz8wm0{qsju10i;-xKn_{`Qnhv4UVFPQ9R!}qIYR`bzOFQ8!zjTZdWGutYU zlj40%_eb#PM)|oK8IH-1q=ScN08AMGcv! z2u^0kHWu!0_*#$NB1YQSy~|NbwiE0cjxlk)G)~=)rhj+kTelVS0_^JWuG+}GS+&0o zj#in$$TTyB14sDAa9s{2Zhh;S;pAAof2uFq?BwHjP(wt?=*$?ud>uZB$U}`z#U7+R z$@2DV2oV@dv1?7tu6v<$c!iSKG#nzKcBuLIMJ}SiA%8JWHF;!#*gCcBLyy}P5d%)J zGjTN+yrxeUxZA6M@ne|_TBbAgJUa?p-cPobN<_RmBz$KM)E97RB6XXiNJm{JADbnV z%gc2Brb)>l9x$`JjL$}7j2(X{vXl6{*@}yld&vz(1{E;Qv7mY0#Du}g^2-}OVTU<78)#sAse$Uuw!F8++txa>2(Di zmUNIwKXXFBq*F>2I)HGV61Hneb_<1zBeVqR7)$NwSo$8Y?4$Q+`oOJEP93E&n=e zQbKOA2PZaeTexix!8;-B&bOK1F+ZbRJW9ohhFVm2UWW{uGX^02+1=ZtcSm6;&M$;6 zo*CvE~FMgXe_dEl!$=ChizAG!Ns9;Kx;pE~>j_-p3_>D3*Yy7?YQ|I9`zK!02s7P(wD zaRL}{k6vIMBj8i1s7&na6a(JT%le%U|HB3FYR^X5_qge!+Y>-rcE1ft0l4^)YTT}S zljp5PG{-FWVfyQl(fi9fDL0dUQ%Q#f5&|K19T&f9%dbJj@6WECUs!8|b9Ddw`SX+5 zm0*>S`9nLvRKuC)0sM~YQkdUgY}F7Cz#BY&#xN4A^Q4guV5;z`ym1<;HsYN`6U-2&3>=E@OY#sUJ@9f`20W*7;(Zgdrg5$#Hse zME(V+KUoZbFenVWqZVUCQXEVUM5{RE#uU`j=}My(rO&WT3DldEj<^SN?FuTHmAKkH z)BhU4S7SF?EKRuracLRClg499O^PC=PisphfyJOnZ@e6~Wr~MOFsb)zg{VU)jae&j zy`0t0%_@nrzgXlDaJQ(R@l4bo4YoWD2lA7*DC~({K&dl<^SWG!;M@Zb3j&Es#U4ii zZ1YeQ1k&`I5Umtu!{u~#)jS%{GP~uCY6&O@FARjfu>l5~n0yTgPeMqDp8d2EPTP<2il*8`cAd;V8}qo)SDzYuY{dLJAVW#@7(7%WiOfZc;U{spmT6 z$<^-?^}FnEKyhgxjxu)~sYJdFciyY!+s$mjT8@8a{df9^3NY(xBCuKiBJ(l|L6qzR zSZ=!a2utQvDK1gb&hNQKQ%~#h~&)L zn=2oled;$4=`rD9|3X??nprKH?ON5e*2EK-N+>>kr&X0F9Z_DE=aP6k>j3_ZlGm1@ zwR9Z-uB|K$zF-B+`pLiW7E?m5E@wO;R0M2dL9pARw^0UNM0w6;M0`d3lSNUEaMX@* z2e_}XB*?HejSL;XkV1MRhpQe7o~${fE3D!gOS+#7zV_Kp8VFMy?x(KF=aZ&*eC>6w z&luZw8haaPFGb&RMUbIqZAV}$swEl4Ney6>VIi}vMYvW00zi-dx@SbaJOMUNtAMy5GZUr1 zFzM+qL)5)T>OP&=1b4#beFFce)-PsEa9lIv%YlLAOcSF^+RGLUAnMgJe6Z>>(Y-_= z1uz)U6dXGYv=}t5%wpT#@*ttn@Ioc07zGaI2xn*?3@@nMGbE?LSyy#I3d$vlE*TdQ zs8cDTraS#|;iyz~js2sTVA(bdn+@gn)|*fYV(Bs9aIk> zgCZZkeHsRJLN*MTx~Z(Dfdyl*qWa>W;sXBsLQeYssXD(e2mnv2tI;RXyT7d7`qMrN ztWTz=^{GiEr6Am_Dda7s2E6NL4@_YOOa-98CldL+dkV}5=1I@ARoq_DQou2XQmjbLVY;33Fu@YlxU;(f|Ke<@W-A>Q8 zgapKb^7cm;jReFiCMyce8XKx1oy#OVUe5GDQ-tulMyKtPj%>SBw- z`}`)?E+MLT&aXqPEP*)Vxa`q5jJ<+5S|xJ@h3%ieR$^1B5MKaI(4sxf zqWX%|`D$1`!?fr7hhO-xpYpmka0!O}c===i@EmC9h4&Lwl`{IX=A}4^AGei6j5sfQ zNLCir!ufqfLkrM_U|w&(!U-{O8nyGkQRVzMsx8^Z)5|F|u@|#+5j1%e6ysxBgmUPp zsHoBRg9*$T`Z3^qF_Xi3&t*M)w(}o7O7R^hEHnz)Q7*|=!0HE8-}39~>Tbpu!n@W_ z`rp;vL8N?czgFp`GrDtwgo`Y3-kSS2)HgH~Dwk|+ZPkXntcC-e3cw~;GoAQXm#CJO zlvo1S5bwhzKq1t%O-Cg8Z#LpF-$S=7)z)JsudCIYZeR%v$rNBnNJzYX+j8akz_Wi0 zpEK6@Ym_wsHqx*{5BH?V7+xm?eG&a53CIKeKMbSn8Csa4+cg=WoCd=*J_FB}WA0;M z+^Kxu6Y~N-mBK$&EJKQogtgOa09zqX<^QJ){V(^Udfv;+OJ^u7G}H{2O)DkMcNhTZ zWL<^z;bgrV5<8a2G2+`9o*;g-y z5WU256=^AZ@AGK!=EK37_3RjODEhxX-~|>(gqmE~T~f#^?Oq2IQ1#HC!th=Ae+JM( z=T(TPWC;tqWfx%YhO{RQ{~8*rvr}k?TeD)ijkG>4${}$wYI1U$i%5Wf1V;6T$)6$z zqJN{d`Y94CM9TQUW++}mN>SN-p9##HD&CW)sCa`SkAxaIE~xY;9G>eE8E#~Ja8Lgb zD}?gm!W-r)P(rLoFfdNIQvlck{(;qYmXe&!2=hHpG0qcC+kKP<^bqwOY=M2@oyEe5 zM=;gUPbA$)ws?_ds8fvu?;j3@@8XrhlO`w6&6tD#(U7X2XgWR?hUyGu6@~}$1KtP# z#tI6s`jiI4^pa%Jr-t*8$t#07f_`!nsHo|VC1P7zI$*l$j!yC_Ob5SwGG1x6bc?q$ zm_en))NDpI2c?+CsVdxZtlp==xqvsJ&1#@5Zn~aMi65$T1nlSlZ4FFJO{pE>sVhp# zJ~Rro#zKC4^}pRK9B>MI4l+qM>vFu*kk!`K2JDQT9kVtB)DZ9#GDs19KLymUyu5sV ze*P1@=fzgOuJ-cJ|I?kP4}J!!PEL}dh>D7O%k6S}cI!O7f-IP0pPOa;|1K#83NS=y zz6X&U@S=e(8mvAIBJkILr4;t3BF5)}R75f#pB;}&cw5+ifKrWVWv74yWOljNC1ZX> zB@vL(q78H{N>Y4_4}!G?lyZE|lmbmH(d+9I2hrW#@NJ~qG<5bTCyD=2ZMfl%YqAauba&7W~x4CMZO!Cm1HxM!c%K4$)$BflCLr>s1ubxb)OrcD%Dr|^e^ z$O@^4Xp+Bm!zVqae~YWcm~Eu9hqy_`^Ldx`>=V1>yR$v~e|<3cU~QrhF%6vCwF&D3 ztSDiwuBVwhQn(e+tV>1J(j4@k48Q6*qKSJ)OUtL>z42m`EGJ%e z+aEFKKiEhDPe1$Ue3-3!75w)kw>sZUly*stwfB~;Nb}L|Au*yr)0g5sx^cf23SlrD zh%q5MJKN}M=zkTZ%@0axc6I7X%pOcfs)E0Ap6{}6KyPAT`M9)WLtrfEb#1BX^n7nl zz0VS`!4>S$;DmF|2h3ND!il7VOugJj=vp@s2R1>%!n6-k3SjIRBcspoFYXeCj!?RN zTAeOG7UUR3Ty*|gB9EBD?S5DVZlgOwbKU@amE>@1wr)<(E)!iCvNjjnQjV>F@e+pB zGIB&Km8e1sTitTI~GeP1PD_T`SbH|iy5yw&3}Ob;nxp_@VV?u7U++F;!NQ4Qz+mdS zb-)7rKHkX+;63(`Jean@{lTi@@f?Bs^2o?r@4TX#WZCzg%q#w0a zdifOvF&C}D8|xD?Pg+{@um}bL$+bDLc)&hxESBhKxaI3;U~6C}A2JX};{TAjR7q45 zYEqKKI*^u_dkO+N{j4=pg0evq$L57Rne)Lc2Z+?4@MWtKnEWTl+2^W{1uK*CoGaOm;zhC}`(D$Bm9 zz64ZUjTN(W+#FlpKU`;t2J|#Mwq% zm}Q^Ri6>wG4Zd|*yZ!XNFRO4q!o2Wc5xPXrTkIY2&50U5H4}f9wUQ7> zfGT}Vps&woF$4Gvv*$}=kJ+fuhjdjR;Qm)2jcJIt`5Kr+ z13(4*^Nfie+gocH1pT1lIf-0&AX4)G*QBNR$|mdaaPR#)tt@B0;Rq_oI4xm}s`YgP z0{{qeaz@9*JOzpS1JyAaWC2(F9xJ=JdV2>4jOwdE8oLY~0Z5CflSc!GTR8ps)jqaA zrsdCa?b8wBrZ>b+2ifU$)~KL_pZ0=9ahlU|;AqU2)(}G`qeQ;ffJW6+YiOf^$MD_W zFx~$`4tompees3Tlq;u?p#f+?3HV&j$tjpXeiswV`KSj3fto>0?atoWrxz%1FV5x~?x72(aY2TF0b-v9d4wQuBre@y57QM4TwvE5{}n&&w9LgON8{$_z}&)0 zF&KER$#K1J%yvHU{YVmFUdw&vnEU!X=YK@~jV!Uw3yeFZO{Q0}LGCDNFrM zsF1wEqbskjzP|pE=D*fJ^rJFVbasnPCw4h#6SPQe3)`)Z>j+OV0_=&p3aNM_GqbIr znaZUGU%q&3_@mRCqDq~#w6v~Ez-4?Y@amM(tUNsU#Z_16WYS9C#m=>=uF7Twu|Nj4 z`@ks%Oe6#Z>1qI+VU~sl!$~&R$jL=ir>g(Zz5@W7wQ&moI5RHEs)GXb2@}VMYG@bf zu^Kn4mTt1aG!pe7M>&ArI|9EnJQhJoXV>;{>+kJ*y28ul-)2bFqA$+1ayt^|?#0W8 z%u=9v!@szDwge2Ap>#w^jfS2y=ZdpJt9=&$ISXH8uyc|@oUW>CA={U8sHpNtmayE? z__!Q>K=XS&U7s)RrW5Q9{~@rDR{@K9%J3!ud)MVQzp>~slph$%-4NS!&xT`Q3t`S( z)6LEjQD^P&IFq985W9OL`bYr{%i}l4ixl`(DKWnrQFr)u&l|_}3giyh?2jDCv2tDx zG8n6jw6?l&;aYE6cYZDN9mfS`wgHT;ruIt|sqr;_3Wo?PJJP-(Af(8}PYFX->EHKr zmUweuNjEbD4eWSl)a~@#aVCI51NiV5(tJ5NeE?i8Cxt{ay+R`OqTBEjq=_8gh=b81 z(-KES)s>o?0)|-{spn!Olrw7?ZpfyE)O>TK5`D1Bk@*RO6*)Dbl)V5MiFbcGEis$= zZ0labTZafaM)zT5{y4(^8}g|B`;NcUBUiTCPyuRzNK2D+lMe}LdV9BTCMfet2=g`U z4wjV^z=;He86S;u0NGebE#C*52z6V>lkb<(5)}&eRY-b;ls#k0!>5iT#WV&obqE-7 zxlidmyh9+@7WWi?r3D9fZ#TP$zC`{=TY!$SPvVnk*{ebjr9_F%CWWrnFa~-1I*SWl z304Ci4441R+Jkz(6ffLjJN!j*c=$kzh3jZtocTg`-hY#JfPB&GQcD{$(OTHQC6vXO zTiMgrKf%I?t6KVK{aGWLRyzp-QQlU_Qoz9Q4TKbTEYy?iMu!Kx@W_L~7pua;lyI^>%fzY{TiDsC?m|0qjozc|YL25ahP9o0{LVt{w(X%;t`7viY3PFW^Y&m;^s>d2s5P zch)xs9ps(*@I1VX5&h-FkGM7^+Sv|`WfL?k623B>q2;|>2U2uuHp>b7hJsG$^yU&@ zI)v7hYHD#=Fil#O`{<^8!Ea39P<0Oq>{*Pa&)ok0hVDx#eJ|yJLy6trRk<2tF=Pq} zu-_-6c_Vdsyg9Rh;DyvWNiIxjfJRj0D)p(`53m-!y5Q|IUako7*Rnls>gu+#ljwO;P5CHjw2na?md%A;^OY8k=J5J=l+2ADVycwi+I&&ff#oZ7C9gHC@EX3$`t~snd6|B@Z$9=4gc#!^;hJ6(_FOZ~ zcJ()l`{A0t3s?6Uke%ik{e-@~j+t-aauWqEdCQOO0v9fa7iewScoqZ1`cOybRrIlT zXnK6p;*tF;6W;p{e=H2tPUw7GP0MkkEqTkO$=V*v?UheqHstphCCzA>S(s%yc*1On z!j|b|qv7i2AyyNM?>83pc)exO0P*JCbcl}k!-KIg+L7fxwTv8PlFN_=J1*3Fz&h<# zwbSsdFTBiHW%7fual&ENAqSh8ez~(@|Kuz~%)-&-!ba00pv{x7;XgrM2*oM2#edFt zPX-z9P(!li(TuwaZL+MO|5RxB5X44@=>`Llt^W966jikg+|_3g5awTw4ogILY?-ZW zOiq?tMGKN>ZI;y3lx$tPk&K6;66(pXe9KuKDWjp@Y5bV6AkS`_zS|NVwl}eDpKh5T zSNd476>V9zNSUzn%{TLLWhC6Za?=Xx;~e=o-|%S3RU%Yh!wjq9<-c4rgf6tQxTf%R zY;5l@Zo2mUOyxt6Ws$sd=&2Fp#i?85wC_b{ubrNm4-(w9CHKPVdLEb6*098*>mo5K$N7P+b`1vZ+9FO-z{gb@A5i_(l*v@ zMe&Gj9j2w7A7q=G&Bzg|upuUiOb7x%eH#L@K#t5qLA*c&x0#Ngo;;~0AZPN!sK;%5Jhk*=`-z;GuKr}1d@~Lf zyKb*h=(|n{Rx#iXC}*baC2OrXw2yJ?S;VqEoYHKyLl%Ir(d&j&ikpeO_-dp-f9ErL z-ix5Wq<#55VGCu-cO!;RP-nZ`w#a_-SWK&Nb$efVtmwC{yyr#v;0>R8I`ve;)SL4^ zZaTl-Wpzlo+PZFyXm3SXhf$A*x@Vy;i-TSE2eI<)eSCX}Na&^)GB;iK`64D2CR}<# zQ8yN5m-*jsS&u1*7L4W)oGndEnJ!(zENK_VD}a8@0hHsGG$Kmpf?ws%qcZUu^V6ex zvsHLZ%iiD8H_EAaMuGUVvON7cMv6q5nBY5M@JGM=`%$Vwsg1eptP4ZpCzPmkjY)ZV zaruKWMU4IVSLG7rv7r(95tX^6w*o3N%n51PY0?>lP+hEt5kifq1JUsFi|vUz4TbVY z;fc_3hI|FxF{bl(l~K^~Bp0uTY5&gE>6L;??U1prl~H!H4)&|J$TP;~hPMC#C}HZ| z#H##6n`QcAYKxO&PpG&2S&`?%m}FHZ*GNW@r?amvAt3k59F~2&n))*mjq#QfR!IzF zMA(V;J5Etf(A$eh@I;ZPZJ$p@+eLIVc(Sdv?AdQ!x`y*Fo>xZajiK>8$AHWEA1}Eg>BbIc zx)Y+ZM%0DQmiF=a)QNU6tG2465t~fig@?|T<@74Ui^Xebh3n(Z$dCSV>V*N;h;`Ao zac^fk+8J(&IA!iaQ4hg<(;3^93f=?zCpZJBH}l7H0Z8`3^M9zRW}x4>tzNL zK0H~=)vnfQdH>^Vy8zC%g^dl1+s)noZ~-`e@6OmSeQYM%>gjYw#%LGx6Y#ZeX^U%X zGZgbSu1RV7k3(PX3e^4U)vn^cq_%veVzZ)iHJjfWzH_$nZaGydg%hVGzpdv#%SZJa z5GSYxHdZe4o=)W?@jYepfsUtY$GIE+r#(=4B|#1lZ*;#Vfnh9P?NjyzNdCPY=v^c5 zRgJH+?qk*6o&Qp&uk7Jh0@-cGvri0UAyqm$ zI3iIJJ*@)BByj^??*Ll3c*lJP5bla7Y+-5Otf(*(r)}zk5?tp;(Rm;Z?yN&E z$l3qbI5P4rYLLYUB`pNv5lDQff_rra414vhm5?;q!QWSWJ{SAek223d22AK+PWA>- zbT!>K@c)DEJpLK*75km59>NDoX+6v| zzJ1^p`xcuo-cMC2Rim4yR`-Vd zV9-m!(JWWhmj$GGWBxr&9C6G*h6e*cHWwWO$mQRGKUq_jveD%#nu&&X!x;DyC%Hk4_$2QDp33-V(Fnte zL4)>)eF#G|5rRX#Iaj=%s>ZQyqI!Ry3P4&3xzxq`Flvtp_G2yS|{y0`@gW|lmkHfKd z)PeqJELIHGgVoyBhc_0}3d@(dJ^=T}|7#hDG`8yAcFiI9?aDVShaQEn8;zci}1Cwo8xmNHg? zD=X=&F1es{@=9^{NfM>ws=cWJ%@s7iQt}Lzbrg=S!KJ4y z%3KN&LBF@-M5T}hQ@Xe#gTaX$w2WH~TQY%)i6oKbI6@jia6rZGF&%$yxrTT6PNKX+X8y&z4c`lEH!h6 z8k!Pz8%Dgbb2eyF-2yzC0@RE|%ar=7M2Ajg3hE0hiB+{(R^CdDG~ek>Hg2xx;eqAP zH3|Ys_noMvS{qpTZ6XX~)!Dtd9OUCsdT8ANRSr7a_(uc-{KM4$%?|*9uv7jQFTD)? zJB*c{8=0bEND6>2nIs2`mu=9@i>tt~ZEXu@#z3H5SP6(wTptpENX@QN~o*$+G z%%gu_fyAc(&19_*0oA{h4lp}0j28eu}Aj0?XDYTLNJ>7zw)F*|D*RoQ+a&= z#C8c=Zm?2}31C5RPf2IkN7YCIf0zF3|Ec-(Fzo+8`8OxQPdotFXF!RJ6@35iqs|#= zfCHNgN0n_AdI`m|B0_cim{$ zxI9sR|9n^l&>yf@UdarHd)gB;qUrF@5SRdpG7jSACm4=c8LZA91c%&|O8Gat(q@ZOr9yq>JRXf<8mM%y-}tv#BpyEr477<%{n;z6PPb4u<{q_(Ap z$rW4uP_l3Qznr*P#NyN%w;g09@0?H!-@hFlQZ%VpV4wKqXv-7&<@;(Iig>bpEdEYY zhfn7|N1Z)yirUl4ftpa{x3z zC|ex~EJ02=Lb@gcY`6&#Rklf?MFNntL6a1^MLRsCH#x;!fsAQVsg#gX3n_KJI(@Uy zBC4LeEF%LW?h>AkuPo(q(m|Sy!eCK%_jnri{$XRr#{(0GE<{QueD%1mpkmem4Zp{$ z7{9+kt7v|I_k}K*7E+e?{SSZj#b9px*9h3?Xw9Q*3whlJbpG&MSygz9rFuPHd-Cgf zAu-7Rqz6^D9m)A27hj=-jKDmZ_eH~7wtZRDnq1ETvDt5O3dfW(9Xz)sFzhS=m^N&ClZ_=d7b?s0zTaz1tK|$Ur%xX(4%hqD5J4QIq@)Azv zqIyX~l$2?7layjK93DTGA!fRcp0FqLwgo=ArIqE!<1BiGvBbTFk)Jy#IRYNvk?Iae z?Q$9nNeb1CDTF~F@hp$KT*=m9fmww9BZuDJU-hBCTI~CN@>kE2^`YG-FT6$idR5iB zU-hBW(R*C%z{g-+M`Qdp_u-MCwc7YZEOmdIAa7GFL883G_LZ1ThBYLMB;AWlVgXkG z94ZjKray7q))7wa*nF3IKn#BLL ztchvwyTV_v&NqIBRtV6gmPbD8BJP1m-?S3o0o?plfAuwEb<;JpyfAavrqyo#cvkW`UdCWW2SHNq8O?KZy%= z+*a9Vwf-)8e(ZgeM#Ag{xzJAEuB^X;_QfTrL=ttgYupAvERLryw)=q>dbkgr<*mIY zv*EK}8NFvzvKE{86y3Oci87v?zpu=8lbrcXt2$lph%7i34HcCL4S7Siv9jn%QZcEA z0DeaVo%xju`iq(Hy3mGf(VZh42!z&B(JfA5AQTM#@rDZj2+Q4jkpR3*~Ph z%Z@HRVK^n$ohF;0oC%7vk#AlO8iiyH65v2*KQo~PS&jB0k8LLpxBEU^ZXTY9zpil>ltv?0#fl{}l^`wri_48cqafzJL;*4i()JN!C8blT}P@wOZ> z+g*3WEgqh45a5bE z(1>p{#tzQWF?=6Yvc^+Z&ZH*TTw0v>XI^s-t68>HotanNuP#OE7_corG@8pCW=Mm< zUxf<&_b2nhW<5RnbNRMQ&)0TGQ_WArhOb|bq)~d^9c0;0KX&DXy}Ke#ulv35ILYJZ zJ+2|Lb-t=S5#vh5C22RqS#iZ{mbd7$H?kYUhjUqX5tOx%{U356P8)XNdvj%%`bm;D z=emcL%3Fz3w}bgiCOUT)m>CBMEjw$Ikm(qKWD0UnBINF8XHsFJm7h_+pwOk znI&WHaC5GtpUJ*$Jt@@b6(2j-QKQcBGVa#-jXtwgdOE7Ij+^4}W=o(k3m=L1Zp&h9 z?#EXZOG&Msb#1$j+Li=5D`$)!h-h|qz{yc{LLA?8@(~{G4{m-^VvM8t$Mbp~S>YT1 zix)N15z!tF&1*A=IvyX1=WavL^t~*3csoiu94??@y`8fH-XHvKKZ@-fQOPYxL3g1u z(97ZMyzQ#p90L5SkR!W~_Xp+9_MLz2`CdQhjGvizdT*7;c2MbtY)>OM&JfW<`F$^= z>lBvGLxKzckEX6X9;z<e_fiViC9S#g{xmj-gI-AD26or7D$D2`a+zhDQ!whP%tr z1{RPk+`SvgRJ96yD=wR?gKl_^zwK9_z`C_xU%n((UF+_((I46CPVx@zqx4ty<%V2` zkJZlbT|R7XNs3<<4&K4{HSQsYUf9P(Wx^ZGoMN!um%<%=IilVgvGNdd&n7ii{KE%K zl2Zzu@VvZm92Mi)WZ^NL{%K8+n~!c0?i@(nKFw*3-y-H{+0`-El6wusNm_K@eR)WN{6Ot`b^e$cYV$;zr7S;@IuSEEFh zm~Ls5HDJ2^8X0WI4zP1~39T7&nT7Li@2ESjr1Pxd^Nn>+0a9yLu3JEBgm4s!toe<| z6O_2BXPn9DYT|-#`_FfN6xt-|-~!WB07g?}QL5B|(HE#ww6aF{$t?Z=8+7nXc-M>V zPlM+94){KMHOL6q%{#9SFQwTa2e)$PIEbDV$jM&d0&)|r`^R3mlrL&i|M*{HXdr}igFhciZp zPQ=!P*|VUsw3K(Q#O@iPHGy+2oP_IhUp{V+BH^agl$2usL{|VYiow=WVc$Yh*s^0u zNeN(vn%JtW-Levw{oAbCj9R)7zzGm(5hP>;$!covHZA^?|GyQ=TSf7--@aUH2;XINGa0VPj84(C@>oP`};dPPgeow0+hZL5>e&a9aUn5uKu>OwKBFOER1YC@GU<%6nl9B zRYvVn;kCz(CSDuc)Y~tE)?e+8mMBc<3?o zkS2&LO^^XB5TpYw#gdZt2Kc_UU0vVEC45$(wECV|r(%)Fa)Z}o?lvkza1)=tuk^Z; zqeERAU#C0=to-z8n}oVi?lYYq70xf|`uCfr6%}Aq_`hjRbhuys_|`<~yNK1h0=9fF zttUnJy1SmuXHWkd_ff7{y0D!|{C%pTp$V^Ii|NC<;i#?84_Xxsk0`ax4gc7$iR5i#XBG<6iBijFX@njS4^iItxp(rNHHis-qSYltPYNrvUPDj34J;010UU z0UUeA1E<(8&kq~(GP&)3HFC1lwI7aox^x2S)M%OzlMigl&JpKtaVQ7e6p2Ym#h8l; z+66sss>)+bwsY+4m#|_J*VnL|u*ub!wvy6NZTVwKT#Cm2({acZ{-N(oBz!?d?AR;J zB0xl(H9rnrn-r6f=vVZ+$&%D|N?MX^YHC{epf|(C%ggJQaa39GY0eaTZo0_W7ns(6 z8;OFLR#?gx3ytQD`dcUx=rQC_^%Wkr*hjdBR#=zGAV;!Iv$m6kj^!)r@m?$MdNV7l z%-yLVz2(C_PT%573?aBwDGmO~{&|Hbz7yy!|N6 zb#`fZa%;=q)L43}08?LIuN=sF;@C~@jJRJ37gHW^J4YY#;WK1h0p{;jkDCNrW|EYf zo8ERcevDhc?icbn#cN=5i}^6sH4`_`5Wj|}l~@`w)krwc+=f(3#QQf-ac?2or@pfF z8mexh5kIsF`3qYcf~3ON+uXz46?jn3PKbh!Tl@it-@ef_@lyU?Q}&_S{jsY1K0a62 z;{8U4p*5OqMOl|t#6KVLC`PR*Hz_#D{;Lw zDosDWy!Xl;wU*4G?6V+r5^c9b%J%%N(bA$}o+)|M#PG*V_>s4o{`FE2bit@Q`Bi?t z{nB6w%;ffMd0AOgV`Dy8>Zv<%qNYh!iT=kaiFs4?Dt(*mH&I|`#-})^;JP>I=mY9o zW7eeoI8q@2`BqfNMfs}0h>Vi?xdbgOCpWhkX)qVvb^wfg9+zZQ$ndb#v6mk0a!6Ni z_x)$VSbknSUHS4?$9q!aW>nFY>CeTq=VO?92@Q0f7PZ zM_^~7)s_9<=G*UQ-fx>^P5CjVuarxG>eMvW#RnwMV_^n zRQY<4n}5yBi|EluCPY8NZ7bwT(Iac0up$_q#IUllP1XhXE%;+FVldl-wVlUf+Cw8= zgxI9c?hswar61}_W1-X9zK?suRa2ZrE@69nvQJ28j$_{NR!{$(^P@$!iT^9lv4tS2 zW;Ag>3{lg%Sq;aDUQCG7y>el^F4&K+af2R;Tv(Rm!47K)d?$A`b-`m^w-<@Lu)>Ci z?+ry9EJaZ5z#@FUX4L}o=m>L)T@hA-CBT{aKJ4goU{5J! zoFWhKNK_RSIaU5sOq^n4iK{dB3Xs3!7a@NVsx6rIa;~xUL6;=IDlDAY+g<`EF#gRd zC?>6Dp7lC$qs1|$4Iq`leTfVP=-Sy29o1#~9lGAGLyy3jO3L+IjiVIj8GBPr%_9## zJ$hddBQ1ltUviE^5w7HXs-p?HlMqp0-drq-9c?d>j}wEYgN=P$pLsrE{r&GKE7v-A zyjTuMu?18jq6NS)rtp+(NWDL_qLNZ0Hr8jPux}VK8h}!i#_pOJvt~E&RPw{rnV67)ru+3lT8rl4qpHYWDQ>R4vcdLOApV#X6oL7|-_v?*L*X0Gr%58+Nj@ zon2m4)!=e57-e8Ehp7?IshVBV@GV51sQepPAWkg|#}?0w_YQeIX~r*&iU=*dm+QE* zr5Vh2FIXm>YaXE0Ins;01C>0U&ND$9obxIb-f8;xGMq_*7@J=dIBNpg8-cK_TCg=8_XN--F0F-5va-z9aGs#qrp zbmTY9d?Iho9T$3CSJV*9&Oi^TvYcGiPsP)sZ+IDxDKj39!2iN5W@g2+7~qOaz6rt} z`cZxD=Z$Zr{-p1DekW6_g%CoLy6cX55BEKE9V$S7e9k5D!btySut5bP$!wKwn*K&p zw%!S_G50CH z==o5;0xoNY1^SgGQY~|a57=cv#`LwdGvD*0qBM980Q*c1BV*uS67t*_k=qF(nwkyR zrPqE#PB^c=&;27pE!BDEno~mzB*33OQj8nFcyzX z7B*8KeqJr!AUDMJ4=ISoJ*BQzn6 zN5cS>kgr!20~8$pNJWMT_+%opO!^LX##*-uMdCMa#5?$j$-7qVUG>zI`{tcVmADFd zo&X&yilz?;N@f?S?&~!UXB}q-uY$BePmjE#-2V6x?nVWeDWJNToE9QrPE(e*xN~Pu z@se$WnmpXr((;|7j+vR6pNvcgmQxR!@pq9d{o5pOQ7NinK!9<|V6+hefdEW0_bTsj zQz#sPwsrG9iaTo)RxUkzzkJNh()t!cDceH|wheWmW%m5itAlir!6MaGecF=Mjs{@< zu{%3AQ2c{irF}V2ROEYa7cr>LDRA(?_F|k)ma;RlW*JU-*P8g##lHNL4(iBEos{bZ^Oql@RM89}R6YVEL{Jrj&p*$U=##s~xG)HPi?-e@K>=-MiO4g@(d(jGT&0q8yybd>_1R@q+`h|JHT=~Yk5P_y)g H-J|~jBH&;k literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_WriteNodeCreated.png b/website/docs/assets/nuke_tut/nuke_WriteNodeCreated.png new file mode 100644 index 0000000000000000000000000000000000000000..b283593d6a461d7c6ae71a66eff19f2f844e211b GIT binary patch literal 34442 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 MN>TEgxKY6W0%TQr-v9sr literal 0 HcmV?d00001 From edf460d73909667c15c81eec5ac429f37740f6a7 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 2 Sep 2021 13:51:44 +0200 Subject: [PATCH 043/450] more images --- website/docs/artist_hosts_nuke_tut.md | 150 +++++++++++------- .../docs/assets/nuke_tut/nuke_AssetLoader.png | Bin 49946 -> 67174 bytes website/docs/assets/nuke_tut/nuke_Creator.png | Bin 15493 -> 30511 bytes .../nuke_tut/nuke_PyblishDialogNuke.png | Bin 52744 -> 66349 bytes .../nuke_tut/nuke_RunNukeFtrackAction_p2.png | Bin 87912 -> 0 bytes 5 files changed, 89 insertions(+), 61 deletions(-) delete mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png diff --git a/website/docs/artist_hosts_nuke_tut.md b/website/docs/artist_hosts_nuke_tut.md index 5743c8c756..d285d8a6ff 100644 --- a/website/docs/artist_hosts_nuke_tut.md +++ b/website/docs/artist_hosts_nuke_tut.md @@ -4,120 +4,134 @@ title: Nuke QuickStart sidebar_label: Nuke QuickStart --- -# Nuke QuickStart -- [Nuke QuickStart](#nuke-quickstart) - - [Run Nuke - Shot and Task Context](#run-nuke---shot-and-task-context) - - [Save Nuke script – the Work File](#save-nuke-script--the-work-file) - - [Load plate – Asset Loader](#load-plate--asset-loader) - - [Create Write Node – Instance Creator](#create-write-node--instance-creator) - - [What Nuke Publish Does](#what-nuke-publish-does) - - [Publish steps](#publish-steps) - - [Pyblish Note and Intent](#pyblish-note-and-intent) - - [Pyblish Checkbox](#pyblish-checkbox) - - [Pyblish Dialog](#pyblish-dialog) - - [Review](#review) - - [Render and Publish](#render-and-publish) - - [Version-less Render](#version-less-render) - - [Fixing Validate Containers](#fixing-validate-containers) - - [Fixing Validate Version](#fixing-validate-version) - This QuickStart is just a small introduction to what OpenPype can do for you. It attempts to make an overview for compositing artists, and simplifies processes that are better described in specific parts of the documentation. -## Run Nuke - Shot and Task Context +## Launch Nuke - Shot and Task Context OpenPype has to know what shot and task you are working on. You need to run Nuke in context of the task, using Ftrack Action or OpenPype Launcher to select the task and run Nuke. -![Run Nuke From Ftrack](assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png) +![Run Nuke From Ftrack](assets/nuke_tut/nuke_RunNukeFtrackAction_p3.png) ![Run Nuke From Launcher](assets/nuke_tut/nuke_RunNukeLauncher_p2.png) +:::tip Admin Tip - Nuke version +You can [configure](admin_settings_project_anatomy.md#Attributes) which DCC version(s) will be available for current project in **Studio Settings → Project → Anatomy → Attributes → Applications** +::: -OpenPype menu shows the current context - shot and task. - -If you use Ftrack, executing Nuke with context stops your timer, and starts the Ftrack clock on the shot and task you picked. +## Nuke OpenPype menu shows the current context ![Context](assets/nuke_tut/nuke_Context.png) -:::tip Admin Tip - Nuke version -You can [configure](admin_settings_project_anatomy.md#Attributes) which version(s) will be available for current project in **Studio Settings → Project → Anatomy → Attributes → Applications** -::: +Launching Nuke with context stops your timer, and starts the clock on the shot and task you picked. + +## Nuke Initial setup +Openpype makes initial setup for your Nuke script. It is the same as running [Apply All Settings](artist_hosts_nuke.md#apply-all-settings) from the OpenPype menu. + +Reads frame range and resolution from Avalon database, sets it in Nuke Project Settings, +Creates Viewer node, sets it’s range and indicates handles by In and Out points. + +Reads Color settings from the project configuration, and sets it in Nuke Project Settings and Viewer. + +Sets project directory in the Nuke Project Settings to the Nuke Script Directory + ## Save Nuke script – the Work File Use OpenPype - Work files menu to create a new Nuke script. Openpype offers you the preconfigured naming. -![Context](assets/nuke_tut/nuke_AssetLoader.png) +![Context](assets/nuke_tut/nuke_WorkFileSaveAs.png) -Openpype makes initial setup for your Nuke script. It is the same as running [Apply All Settings](artist_hosts_nuke.md#apply-all-settings) from the OpenPype menu. -Reads frame range and resolution from Avalon database, sets it in Nuke Project Settings -Creates Viewer node, sets it’s range and indicates handles by In and Out points -Reads Color settings from the project configuration, and sets it in Nuke Project Settings and Viewer -Sets project directory in the Nuke Project Settings to the Nuke Script Directory +The Next Available Version checks the work folder for already used versions and offers the lowest unused version number automatically. -:::tip Admin Tip - Workfile Naming -The [workfile naming](admin_settings_project_anatomy#templates) is configured in anatomy, see **Studio Settings → Project → Anatomy → Templates → Work** -::: +Subversion can be used to distinguish or name versions. For example used to add shortened artist name. -:::tip Admin Tip - Open Workfile -You can [configure](project_settings/settings_project_nuke#create-first-workfile) Nuke to automatically open the last version, or create a file on startup. See **Studio Settings → Project → Global → Tools → Workfiles** -::: +More about [workfiles](artist_tools#workfiles). -:::tip Admin Tip - Nuke Color Settings -[Color setting](project_settings/settings_project_nuke) for Nuke can be found in **Studio Settings → Project → Anatomy → Color Management and Output Formats → Nuke** + +:::tip Admin Tips +- **Workfile Naming** + + - The [workfile naming](admin_settings_project_anatomy#templates) is configured in anatomy, see **Studio Settings → Project → Anatomy → Templates → Work** + +- **Open Workfile** + + - You can [configure](project_settings/settings_project_nuke#create-first-workfile) Nuke to automatically open the last version, or create a file on startup. See **Studio Settings → Project → Global → Tools → Workfiles** + +- **Nuke Color Settings** + + - [Color setting](project_settings/settings_project_nuke) for Nuke can be found in **Studio Settings → Project → Anatomy → Color Management and Output Formats → Nuke** ::: ## Load plate – Asset Loader -If your IO or editorial prepared plates and references, or your CG team has a render to be composited, we need to load it. +Use Load from OpenPype menu to load any plates or renders available. -![OpenPype Load](assets/nuke_tut/nuke_Load.png) ![Asset Load](assets/nuke_tut/nuke_AssetLoader.png) -Pick the plate asset, right click and choose Load Image Sequence to create a Read node in Nuke. Note that the Read node created by OpenPype is green and has an OpenPype Tab. Green color indicates the highest version available. +Pick the plate asset, right click and choose Load Image Sequence to create a Read node in Nuke. + +Note that the Read node created by OpenPype is green. Green color indicates the highest version of asset is loaded. Asset versions could be easily changed by [Manage](#managing-versions). Lower versions will be highlighted by orange color on the read node. + +![Asset Load](assets/nuke_tut/nuke_AssetLoadOutOfDate.png) + +More about [Asset loader](artist_tools#loader). ## Create Write Node – Instance Creator To create OpenPype managed Write node, select the Read node you just created, from OpenPype menu, pick Create. In the Instance Creator, pick Create Write Render, and Create. -![OpenPype Create](assets/nuke_tut/nuke_Create.png) ![OpenPype Create](assets/nuke_tut/nuke_Creator.png) This will create a Group with a Write node inside. +![OpenPype Create](assets/nuke_tut/nuke_WriteNodeCreated.png) + :::tip Admin Tip - Configuring write node You can configure write node parameters in **Studio Settings → Project → Anatomy → Color Management and Output Formats → Nuke → Nodes** ::: ## What Nuke Publish Does From Artist perspective, Nuke publish gathers all the stuff found in the Nuke script with Publish checkbox set to on, exports stuff and raises the Nuke script (workfile) version. + The Pyblish dialog shows the progress of the process. -![OpenPype Publish](assets/nuke_tut/nuke_Publish.png) +The left column of the dialog shows what will be published. Typically it is one or more renders or prerenders, plus work file. ![OpenPype Publish](assets/nuke_tut/nuke_PyblishDialogNuke.png) +The right column shows the publish steps + #### Publish steps -- gathers all the stuff found in the Nuke script with Publish checkbox set to on (1) -- collects all the info (from the script, database…) (2) -- validates components to be published (checks render range and resolution...) (3) -- extracts data from the script (generates thumbnail, creates reviews like h264 ...) (4) -- copies and renames components (render, reviews, thumbnail, Nuke script...) to publish folder -- integrates components (writes to database, sends preview of the render to Ftrack ...) (5) -- increments Nuke script version, cleans up the render directory (6) +1. Gathers all the stuff found in the Nuke script with Publish checkbox set to on +2. Collects all the info (from the script, database…) +3. Validates components to be published (checks render range and resolution...) +4. Extracts data from the script + - generates thumbnail + - creates review(s) like h264 + - adds burnins to review(s) + - Copies and renames components like render(s), review(s), Nuke script... to publish folder +5. Integrates components (writes to database, sends preview of the render to Ftrack ... +6. Increments Nuke script version, cleans up the render directory + +Gathering all the info and validating usually takes just a few seconds. Creating reviews for long, high resolution shots can however take significant amount of time when publishing locally. #### Pyblish Note and Intent -Artist can add Note and Intent before firing the publish (7) button. The Note and Intent is ment for easy communication between artist and supervisor. After publish, Note and Intent can be seen in Ftrack notes. +![Note and Intent](assets/nuke_tut/nuke_PyblishDialogNukeNoteIntent.png) + +Artist can add Note and Intent before firing the publish button. The Note and Intent is ment for easy communication between artist and supervisor. After publish, Note and Intent can be seen in Ftrack notes. #### Pyblish Checkbox -Some may say Pyblish Dialog looks unnecessarily complex; it just tries to put a lot of info in a small area. One of the more tricky parts is that it uses non-standard checkboxes. Some squares can be turned on and off by the artist, some are mandatory. -#### Pyblish Dialog -The left column of the dialog shows what will be published. If you run the publish and decide to not publish the Nuke script, you can turn it off right in the Pyblish dialog by clicking on the checkbox. If you decide to render and publish the shot in lower resolution to speed up the turnaround, you have to turn off the Write Resolution validator. If you want to use an older version of the asset (older version of the plate...), you have to turn off the Validate containers, and so on. +![Note and Intent](assets/nuke_tut/nuke_PyblishCheckBox.png) -Time wise, gathering all the info and validating usually takes just a few seconds, but creating reviews for long, high resolution shots can take too much time on the artist machine. +Pyblish Dialog tries to pack a lot of info in a small area. One of the more tricky parts is that it uses non-standard checkboxes. Some squares can be turned on and off by the artist, some are mandatory. -More info about [Using Pyblish](artist_tools#using-pyblish) +If you run the publish and decide to not publish the Nuke script, you can turn it off right in the Pyblish dialog by clicking on the checkbox. If you decide to render and publish the shot in lower resolution to speed up the turnaround, you have to turn off the Write Resolution validator. If you want to use an older version of the asset (older version of the plate...), you have to turn off the Validate containers, and so on. + +More info about [Using Pyblish](artist_tools#publisher) :::tip Admin Tip - Configuring validators You can configure Nuke validators like Output Resolution in **Studio Settings → Project → Nuke → Publish plugins** ::: ## Review +![Write Node Review](assets/nuke_tut/nuke_WriteNodeReview.png) + When you turn the review checkbox on in your OpenPype write node, here is what happens: - OpenPype uses the current Nuke script to - Load the render @@ -139,26 +153,40 @@ Reviews can be configured separately for each host, task, or family. For example ::: ## Render and Publish + +![OpenPype Create](assets/nuke_tut/nuke_WriteNode.png) + Let’s say you want to render and publish the shot right now, with only a Read and Write node. You need to decide if you want to render, check the render and then publish it, or you want to execute the render and publish in one go. If you wish to check your render before publishing, you can use your local machine or your farm to render the write node as you would do without OpenPype, load and check your render (OpenPype Write has a convenience button for that), and if happy, use publish with Use existing frames option selected in the write node to generate the review on your local machine. If you want to render and publish on the farm in one go, run publish with On farm option selected in the write node to render and make the review on farm. -![OpenPype Create](assets/nuke_tut/nuke_WriteNode.png) - ## Version-less Render + +![Versionless](assets/nuke_tut/nuke_versionless.png) + OpenPype is configured so your render file names have no version number. The main advantage is that you can keep the render from the previous version and re-render only part of the shot. With care, this is handy. Main disadvantage of this approach is that you can render only one version of your shot at one time. Otherwise you risk to partially overwrite your shot render before publishing copies and renames the rendered files to the properly versioned publish folder. When making quick farm publishes, like making two versions with different color correction, care must be taken to let the first job (first version) completely finish before the second version starts rendering. -![Versionless](assets/nuke_tut/nuke_versionless.png) +## Managing Versions + +![Versionless](assets/nuke_tut/nuke_ManageVersion.png) + +OpenPype checks all the assets loaded to Nuke on script open. All out of date assets are colored orange, up to date assets are colored green. + +Use Manage to switch versions for loaded assets. + ## Fixing Validate Containers -If your Pyblish dialog fails on Validate Containers, you might have an old asset loaded. Use OpenPype - Manage... to switch the asset(s) to the latest greatest version. + +![Versionless](assets/nuke_tut/nuke_ValidateContainers.png) + +If your Pyblish dialog fails on Validate Containers, you might have an old asset loaded. Use OpenPype - Manage... to switch the asset(s) to the latest version. ## Fixing Validate Version If your Pyblish dialog fails on Validate Version, you might be trying to publish already published version. Rise your version in the OpenPype WorkFiles SaveAs. diff --git a/website/docs/assets/nuke_tut/nuke_AssetLoader.png b/website/docs/assets/nuke_tut/nuke_AssetLoader.png index cf7f40d5db39f7cd6e1b5d4943056463ad77cf49..e52abdc42835b3d680e6915f85fd8536d01195e7 100644 GIT binary patch literal 67174 zcmagF1yodD*EozKAW8@*UDDk-l$3Nxcc;J%F!T_LQX(MTU?3pfAtl}2-HdcI^n4e8 z@jUPU{eRzg*J3Ts-RJDP&+fg?nJ{%#IqXNIk5EuhuodKGG*D2`YEe+`T|Gnx-YiqZ z^aC$+?6vgV^i-5VmJlZnb1R62HHVjzGXO_H5tZ`1x%^$oV z;QzbVoHW#brnotX(denDQ%gf!t*QAr_&K;}#2-y%-pY(b&1UIfFPk zJv}`+Jb5`FuC|=q!otFwTs)jSJnX;>cBnVl&D@I}45ht8@fQvmYpA8Gy|bG=1WbL0 z)7%2$?j}Y<1Hh^OMs8*KFK%adSI0k?tt>gM9j%?L!ER7aZVqnF|DJE{W&a;)z|g}~#Err$yS*K}t)h#LfI2XX$FsQ>Yaf5HE2A;9kc`w}m6=l|Pe z6_x+*=}u1nV;fL6Sr345e+%J%(Dcs-pjzI})|?vFP>8#$rM0XFz&+YOjJx9tNZsDc z+EGu&9$+dIkcJoyH=h9i{{hzjFEBrs;D4af5J!lsCZLDbz`8%L@0bTta5A^G*0r~C zv->Y}|A8o5gKh6X|DgHaS{gx8esuI74OvK&no@@E9hTo zfc_Au0uc4#;oiTtM&B3-}G+yjh);8wuj&3yKl2E9%n=-`Q%GyyiQO^JI2~mJV6%~-YJ=6{2>iu6|)wFi`pI`lBhSb!5lnZ2Tc_$Sy8fRCC zmAj?2)gL|r^#28gLTubT&0Vb}Z2`R!qmi_+u?OVmO|5=sFx1=}!W@4|^4|rXcGdv0 z|94(;{`XG)p-hzXf6@Ne#{bKa0PFsG2RJCe({cXm>VUz&&e9qTct}^k721Q|tOD-# ztv#Ry|4aNRC`{4r(RE6wl21aK=srMb?99#a=nZ7U@B#%0F&kg7TRiwC(Cw}8UNo}q z0cKTT;b-cZbCwg%A)OM!2eNo=swFxVuY}?F&n5dfc#n4>p273_B_W!MGr5OSpx9CA z)q9h^R?XT7WmJz+j`~;aL)o^SEm{;*o>K*0qqgID{(FnV6$PY^rnZ9Qo~jIDsy$RI z+~5nZ4w{X(a??N8n)=p@76f4z(^(-3C!oJA)58r3G=n_zpl1=%X;#!wd)(XWlB4Op zju4+vN*V8)D`*3ks$xlfB>2@%_wB(U@2?Us3BU8-iO;l;8rrW(%3|_ECz(E%O9j#i zi+hrf7!+Y1{{=ToKQK1lLmRw`Hpb>n{4-Uo2bs^(+37LLmt=>;omcE_MpDF%lsF4 ztW)#vOoaE1F7H8#ZMVc9&ud1c+irvp^sS|AH}2&^2ZVE0cghECL}A;7)m&J#Bj%wWy>LQkUvqV)iorZEUdYLX zKvIx{w=F6}cz-?7rR*$uFvUEmftJ?Ogk>CgnXJ(ZqV<=c2Y*k%BUz7IBa9BcxNd_- z<4DT<{k$w4iyIqK*gJV#48LsOG$uHc=)4)+rD{XU8C|3 z5qNkc{r6+%Hi=x93vNW+TWRZuW12r6XA@xXIl>|vJdwQcsepkiZX)OP`EoNPn@#^w zfll2GH|6ZBZL-zNZK6^AxOskv&fxaFCu$uZH?7(vTZeDb#QR&ilLFw4ytkz4<6_%A zb+sjDz#4AvH>IKfK3#fGC=TpgL7M>qu}+yeF=Arxgcd*E_?95u29yiGXQP=Bsu)6+ zg2{?Dw(_Yq4pxefzyBsQBh)p?9EhY^>~q&O?H|`&9N0G9Nh7LQ*hNbBt_Ww1R}2}0 zh)jgSZ>|u~^Ps|OzllWQ&fVSlZI!mBqdwE~&)K_AE&j9r=;0!m8n`-qW9+GhG@GM} zUG{f;GtNjsiIAORA%f)fh4=v?PB5jeHIe|;(hV{+qAr!x|s zv+A!sLQlVb4GVYp@*OP@=8D>8+h^v(GPdXS?q^+7x3Gw~)kMKf{n3F*SJHA~DAfx# zMc43;pxW~6QY#alQ4O_&+mK4PifZ1Q#|q?5@+*~o96e{cFF`W@@P)+Yt-{_6#m@QF z{MEntqGkA4XoVBz}o>yHAF9xLSZ<5P)YSuwRL)+fgCZuySYlO2u-!m_} zU*7cf-PDxO*KgmxG(@mA`7k^lwBIO$td}+4GG_|@4z8s6`&pH(_MpE~+PA%#dp75T z5WHp(@1($;y%6OmQv-YBAaCpBZfq8t6Yhwd8iYnQ^tew-{F*n0UbOa8spAQ5$K9WA zvJ@FFx45{Wg$(1Xw~Kw!%Z{bUqY4lGC`3T8S_6lfTC{#sgLY3gwIco45+RG%95-t{%7DjBfbXM2q!S-+7T8jEA3%U!a63)4!-ZND3vZ-2Ti%Jk-qG{67& zrytX4@LsI_^-100_9i^}*Js`tk9KBwD3OO$7fb5c#tu0#ehdB?bx0MKH&0PsPajTk z{`m2r>?Qz*!tSz9Uk3U8{d`sorTP{?sE;@k4UcQVW%cy+wQiH;c);8XLJvE~m8O8C z=A)}TVI#Csm(rng@kGvCT3U`E3hVgm%sDTCYlv%>^gqstq=Y?H0MYowdHln7X>tYR zb;W_d$;=r~@%|j1>#b}!$66VE;!*3@I_Uwb&`%2BH#HwO(-MbA^L|)mCQ=-s$G2H? ziLLbjBY4wGa+1-UkaJ=A%OImt;ss;a6CNoiD0OLQmwY1c@nhc_gm$CHv}f1WIc`_YgwFwm^Vb{smw_Lw>Th;t_WLX8F4n=VIhmr9DpGKOKz}o!jGW>~Ruh4YV*)guJ z4MPV?3;hK6@IZu;zNvt6>VR>7Lj2=<$W`KIufGIkHMYh6c79Uk+}X!|ws+2J_Y`Fw zf1Zf_=O?n_h77q3FGuxil6j=|+403h~$v>jZEs38KdtB5-iSAc~Llj-Q5^kS)oYO@4{m16B8Ds1h7{jkVB@p*FnO@MZ zd?WKZ3)t=kT|Ms0bbbT(C+E%oeeJ&-npQ=ThJ&GuP z@KXB?)v|oo>w`zP&XXFqzofwZ#zMZO*j*eR9y*G;I_k}vt>x2mL2N9u?LH8!!32)e z3{$)ha(~H#?mj4XD*f4fx&*FlAlWb1l{U-;pY1|PDm|GsEx+q-+lq?HW82QK&%)r_ zv*=h`f4)pOvOM9u+vR#aUf>oEp=i8dCSZFb($_W3*7Mewj5T*jMX&bL4xBajU|@Se zhekO+tN%Ew@=Z;cB~OT&Xw>!1Au_*+mwRj7WyvQ;(j;yF2u}w>1TdqhHCpTZR0R2OVp=yDCk^5 zM684MFYm@9@PP4W2um}zSy)5_Ma)~dR5ghkzLDpl@}F=#Szo<+^&C#&GI7+9*>Mn3 zmlHd7{m3-K{Q1C$VtDUt(2sAlYd$De>{ibqWsUe|M0GVmvib{(8*|^;@_j?|_l}fQ zLsB}Vz{b#fb$bRqd5YAY{A5K!Er7a7Ho+`8*CzOteoWQGX*aH2Nc-lG@OA*^GhI7e zH<$h0H{XpYAQ#Js@#;g!6pARBK$WA1IPNEvv*B^0N*dqw%dheI-5kbZ?-iyT6V@BL z^PVaF=rZUC=C3{|EE~W6;znfXN?6v|2TdbdXS1^1)L_vg_D}C|zA{)AX7HYVYl`Kz zs$hm8Zfo9AfaO9NhgOv8Cwp#U?-%~JegwDJHH3XTi9j((7}b7~CG#we4Pxt?=_@CL>4yN@mux@Iuif$%?SzbKWbAGe;Vws- z>W3bGra~u=`pcx#c<75mOb=8TTIWBya$%J)R)=;CiTL*Qq;a;DG_hRsOiX$GMgf6p zmUi^^&R>#Y7&f)lQE8*vKcCR+Dyw%a^A2VAemhF!G}k`RW2;}eqF)9$O7_&U1}AAL zM#GfAW0>rkSyTvy&ZPsp;^ zG%jjb`zff_I9yw_DMkDHGl2CW89MrsXpi+hWIaM7PGUXT7SGBPO4&S#>U!Q3jCL1^ zt|%niumi-yCc6!Kc=kclx@@N>!DnQ}rlso~*81xkPvYAj*Se2N`?9i*DWb;rw-`Yj z^&jgetmrQcIV9>_A#T0j6cg%WvC;1ZT`RnzqK^GW|+KMw*4{+tk#3WCf& z24c*7lxEo9I%@S4GjYk^4~|3Wy}}KyKl9-*a9jQi){LK{;jw7&tBF+Nyux3+fWH&H z@j2*cY?zt#mAKt3M9&_m4-L3pMa7x;9&F_MYCwvmM;Al;p>9ceeIT~*WBoy?rU~jg zy{4};xyGTMq|XYP9C*nl&b8meVC?%WhW&-+5Ygm*o+bg}3^Jy@qsYo+c2==NQP|{b zwP!BVHW~e}Q0&wMsdWyIOZ;yD9v31*9k9)?;$Q@$r>o>-{io6(@%KwVgcv;e4X+7q z7RaP<8XvJ_FSI16WY8S~PN0q@n>BNNx~G4xN7_hxI?=sl$H88pSDH^%_gp}$Pl8&PANmn>-TQe2Lr%2LKNrI<1n*rdVnejEk4>9ILVMS7l8~uXrY-fD zkc0ffQ)S^RQ=x#@{D?+;m2wdqF6ns5)mm>GHNg#|3Rm98y*Gw$PZ}ft35;q#n1AH0 zQQlxFr@kHC`!!wcP4i9K(h$qRBkTAEtdYtT_v&|k3sWy^Po&~-a|+}}z1^Q8a6-^nS@3btvCh&OsTQKkwH z$|?C=W4iR|y~kXZhrV8nUOryI_N{y%svuIRsPjt6WVB5d-Xd4>+{MAbne-UOAar7w zd|6%=J+Iq(**2ipYQrAe*U1MJ>2k;yL$I>>`}gNB?7>Y`MJtoV{H_P(L+pxHeE;M+ zIoe9U$y77)?jl>ROAlWRC9P^4VG*X5`d%+Ek{3Fe)l;IAfTHbm z|4GJa@COb6^aE{6s0$E<4(iok!gs12!)&}%;SMpy7W!+uA_t&Zg5y(zS|r` zS<-nJ<_C`hMBC&bG{DOWUr3cwE%cO{53^|Sc?KB-@^UfS^(yp7C=h9?R zR^l%DZh6ilBPZ2=%PcD^vPKX3SA;9M@X>(`lHJ`3uD4!FsMs9N{9~^cH)tm2SD7SJ ze&nBC$B9~^cZ5YjUa0q+VDug5r%<@2IZHyyY=K( z`*_P0B$jOwlG~aEJx7+9Dd5UbOz6T~$AX&JcDum76@0*Jg^@H3wr4Tmwg0TjFHUtv z<{fk}y4swp8{Lo+ni;>Hr)EO?WG^*p__m~yqAbV$F7tpFgs(9*J-lMZl1EJ0I z$YbSf6$Qpk=o`|=@%B~)#sLu5K;_U>+Q5UwRY!SMhQ{Y!`h!Be<=HV>vE=?_a@#)y zl@o~hg?843(pA$A?X&c+Gg~}n=GkkEPsb(%mRf#2oDxRv3Vb>VLZi!OB*-MMA2jpRMUZ~v<1+`Tqa=R_8WL91X%5CF=S~-B2vR~`~+w$6XBxjH}Hf6~$oCv-T z_SxcPh@H3)5)RAKoTmSeJk5EOIln*7=;13l~{PDB`D&Y25#jOVFbD0OBW!%m=li$)R29Ava7U( zTY3|r*LbO>RMLN}3OZ-7Pxt^HO^2Dl^{yVw7~P0{BuTThagOBNaMFrao`ug>&(}u8 zsxpouyMz0Q;F1f@d}ftxXl?$vJ;BK^ywL{1jGfqE>H1?y!f+z?L#?36Nli8ABJ1Km z6|#&B4c+UR3VVuJ1{KnbLj4$dHMKLF3FEQJiOU&Ry_gXNwKFBct4l?Mdp$?LyXscT zbMgM)d2V2EwlJkC$k>WABWnHNd8D6-l9O~2`-6zxJOl?BUjx+eiCPNx+t|h|@0!lT zGzXD%f=F7v;85YQD!=1E#H8oP`_=2S`x~G%_w)Y8k=;@nKBQyb85oPuPaX_5HUw8^ z0a6iQQD%Q~9Jw*GNY8b|ZJ{MsC&AzRk3OX5r*}_^ox8Cx3wB@*BnX**3;n#xq58!Y zY0Eb;j?d94=7yy_yP4+f`zsnR_J!)5({&>peMit)6-8Zx_VFX^UHV#vpl4Cc+&}8h zV|L6b8bHTb+pKw$_R@LQKFVebJ&i8%&Pn>U;ny@j8>f96ly%}3a|8Y=e6Y%D8UJ64 zWBQs7u9vsE#nk)9gfVoA=eKJrrMVmfs`kn4H!X>+z2$YOS*1Y;KLZ|K^sQ^1QAK6t@WL?Orn*&9TYIM`a%W_Ma5+~-#!85} zn;i)1T5niJL_GTHMfWG`M3tlP zW9}P9;oYatrkR=56%DPjGy_%8-`)P+CCeY*s%z8LRSOO6(tAoo)LCB(6$au^SmfSp zVcyK_?8W)FsHp~9T=qPCs#j8e9>sJ_Kt+7QXUjqS_+Wq>ru$(QCru>DOS0noKyG|-A z&KBj#UicGdzjld%EjyWUe$5@cQSz~SwL&rYdk(wFrT^OEXN#K!Cn!FF`7cA^F&YqB z#q!vhY979)Wng?BU(5sunuezZOH&QZh=Z|~itZtjk>?abVB5%8yUVBI9Jj>h_;iD8 zD*RDBre*B)ij-sHgR-~%C7J-QDJFkS)MB>2H2?Cp;q&JZMJE7mvG}d%flrb(YR56% zv{NL_&P(x&IQ60FjkZ3<#nfV;bw;u~Rs=|``2r4S;& zNCJo=s1uXXnE}+`C6z(+$v5LLdkYJTOZxVEhguF!t}b1@>1g^HB&XQXdq3^0PMM zO%vkcR&mC%X?2oQnC$04onmGw8PzArP=jbM1MHumV_?v@?xO8W<_&3pAvBy- zirp-^zFt&ONeRPm7L_{&XwXNO=6y)KJ?;d&qjR3S)FVvP+ku*Q69kO*O35z}&wWye zI_B0zYkVIWPR{og<7CiLvy&6Zxmu9>)ltBIr1T!S6KPh0DT;Nld(mZOtUi)vAlOmT zgwh!u8q8pbD+Wl^0qdrCedFhZT!hl59Q_p>atLzF z&88?8=&IAd(lD{Hot@5j#63MZ435m0cz)5|{#4}7+o+YwR=ta%5eW)ve|n|HijuSZ z>mYNlLQ^X%c#bBz%xP9&YkM0tdwqR9D2;>JerY?JWY^u@)@lX9py;&;2wiKcI{@;H}cYBQL} z4JS*%`8n^sd+4iOQcW`nklrZAO-<3ra_SS&%s!=>-72M|6+hxK6~ztmjiX0R%nq%f z=Rq}}NSq*Kag62{N43UI${QT;=5?r_l6%Ddbai2)4&$wpme0(7Bgf>;IZ6p!0UY2O z^~Y458ETvDIlunelW5n(q^S5*{Y<{PvXtoADGz&_*S%w@Q-Z@x$_nKgy_SyO3Oz(8 ztEHh>5v6fvWD(rG2j_c7>*llHeZ9`PsQk~raCVE$NmZhCclXmfY-yDp9r-$`#Kgv; z7X4!46Op8P$a&h?RZ*=liYg*MW$*CO1vEH76`3<6W^Ot;=ZeR z3;!mNbFlY9`havg{$aCeoxOM2+(UJTYJ6|j=@(o{(4sztr}Uojf6cZ zs4w_f@OiPHUMay1<_olv7qy8s%gdzv?c?L)Wv#8Pr<_jL?5w}JtttsC%D^{M{2Jk_YvTTlgoFe|^z>x3pm9Cn%>^(LJt@z;NT@x^ z%fIv2IQpUy5fKIF3y78hc&ZzXK3PuzP|no1kh|}W^O!eF>W8a_&y8-fU5>S zBxevy6y+&>jda9hA~6Onkgkz9ScIx6VvRm(Q=FWA{giunw_?y_!09%=hi&O9Dm{vI z$LEQY%c=(kwy`xlgR1G*@DjV~=j})%?Qtzz|A^y=#EV|cMIE1V>VNJ6*tO9!;1N+o zh)~v8wIW##2cgoerd%ZKEPq9YY>S z1$Y2)fZWArZkU*SPQNYg1JTE3| z=cankR$pw;8}WGJa*72rmVXk?yy(b0KgO)rUZ;o)`5Yc#71c#D3M_ci2vJ{OcX=U7&XD+^t5~@7z|gZWgbtSplwIjNRMS z)mN7ZYplv?Hc1%D0N^JVa()9q7X?5~GDq;HhJqiO!X*I#QLCqfmhh^U2)sGQ`eWOh zg%6=0v4AXTYCOdHlB0&vGl}fQ*k?MJa*p(64lK+E$#Av9nsahEVdhtgP(*#3Ax) zE|hG^UTy^mQ#Umo)M|3aB^O#!3JxJF&LO@*P;GU zq5J-R!3UuZGY}A(SeOipRHCa4Q88r9`ZQ#D{Us_2%~^*$D~wJ zot+IHy@dH+R6iDuf8d$xgrLtD*rgKli36+($e0Q~^-_rYCXqpEz6_S$EdU}Q_}QFJ zzIM|)Ku=!3_h~{90hQkzQ28By~}BM<&Lrk2Y<5UycM9|5RIpjX$%x) zzyS*l4gQNe9h9-+dsP>6$jHk2IDA~u&5dt(cvwbO_QF3h@&gTsp+k_Jh2;bBz9q?x z5eE@FJ3BeR?!hRkC^sRVz&}MLO~Y%?I$@^~I`ONMQTY%6@yNG|QPtL6S6L7i9*GqY z_tN|Zgrjk!Y5FWm%}Vu>xGjSrGt0Q*pNon(0$+8@e+IVn@sVOlx7iR?gP&!kL7ld9 zbybx`M7ZVF*pFx|6`&WO3r-9dXjqJ5{UTnM`xWLG_Ths@pkWKh;nIy9(xd`|!Lp>I z9|HZEm)x<}76fvVn2~tGCIKC|DYjEgY!&89Z6;dp=Y{F+MX>zyA%eY_#e|Iw`S*8c z$s@FttR%A)ug4R?=X`9`9G%%d{DC6jPV&ZSLhLv1x zGhRW?vAKMvG*A)y_GJt-yah{)i3=+-F>UT*d8%s+)qZ@-zS#CkYe@lo*RR0vVhi5( z1QS8-qf~RQNd?mXej3^3eZAB6aDo;^~rdjez{kEz37O!{Ch0Z zyUvHu;;)6|nqwZvptV^nau#h0Qy&p3Rc6##CD|%_3y>nybE;7*l-WmujSBu>OkHOn z`UG+kF+F$9is2m7Fv>ISCMb=cM}1Lb7y%>BRs8+J!osnj8hypA)nWQ3;JZ92zsth` zW!8+DrIT2EIPGzl=RZV2H8dB$tA-fvJ!|lf&sBxt$z~ksI0gkxT`Ou)(4)l(VL4t1 zV4*$=cQV$lFz<|Bo}9ffGh@Z_$4P%a5cSr&MP;;YnW_2hyX#N-dt7jJ2o`GV=EIqs zJ(rY=_O(?1D+kY&%mM}pDgv(X@aEd|N&Pvb-sOo1BTYJ>bwCFy+>KZ~ID^8;8I1@Gr^8MRfcvjN3oA2F)5=NZ!P)Zro1`%beZ zBp#yP78sGfWCFB83F@=^s5umMU`C|of*EcW)mPuIlnDq%HHWu4%~4eAcZsh@ z)67|nU}SPH!#*F=zCiTc7QyTCF)GbdmViy{;_P^kU1Yy)m~?br?C^|xsy-wv(L_8* zb1POtVH~uoih*M*{<3MXwXq$Lq597?UBIqi68L$L)M<6I-|%y@M*wI4bbul~xND(p zw%Jc{VZ)HDO0AW)T*u4V#=k~b_@t*1W(xKvPEnc55;ca6u} zN)S5q^NEM3?hN()*DLqVZpDN2g4c+{yS(tWVivQ;6w(|xHY%9)qaP^&U#EL9U&?Y8 z-|%uYtO%CIR4Z1c$3|Nqp!rfXop*JTOYAt~!*olF)WbbI#|=%pDdw^egZ{U)5}Cf` zZw9U_%vswu*uG{#8zJOs+m9A4TYu?N&yEV=K%%UE#w<06C1{R^95+iYL4}Bw$pm(u zK09ZuXwV+cP6w+&0;(Y!nvS5r0f517OH7z`I*TV)dME3iU^nZgZ)%9PE1cnQ}GVPpzQ$@Y7x|82L`_FU50TE<=+|5NT;Mi#8rKM3b z{V$mTT?4Fk$+eb-BIe{>yG64_>8G@`zBp#h&`ho`rrUC|ve>Vzct)^iC&>U?LBk;f zx&l-8H-wm&59Z{{L@?-OJsv#{XU5T^{QMrjyqw~}TTBR`THlg@zM3hT=eY|VzK;Y| zU?z}zcBN#8wVFJMV>+vN!7R}K3oQ72;ERdy&XiNo1CMqW3r-eL<=J!8u%JeAz=nZh zuWCYvg@pxZ$aW?1)APo*Wzne$@VnQo39?(V6pJFr8eJ~dqp@BAB5H$2af)|VL(Zdt z2sStiX^@qZlgn!TqzUZIxz>nJP*Bs*Pzi_#8-ZxiI7%u0DFqxDAw9W48 z$AgTZ5y^j>^jKQC9xxi7Ml5h$43jfsoqcr`?p6uSH)A(LKJHWQ&-vLhR!<+gcNYUI zy5zrm&>t-?w*sB|U_DJu0!&QIJ6~TSso+{|Q#Io1^LX#6U~W^LydA|8C{1t;txKCu z`%4fhUUbPdl4*=86i^GEkw=#v7+>BXsUC1w#fL06P8DF zKfoy(sdoGZ3SOLVe-pZ6eRxk2Zs>JRBgpHk`PF;#RL{S}crfJ8Cc5mtaSC?an^)KJ z;a>Wv3NsSHh6^$^v-6-7jT-T#Exv%I8C+pW2nwRn)!dt@-T5lO5w0#nLmLqgj-NlC zmq33<(^r6|t|_8tgv zFsp71LGvyUAO1Watx)sUz`_mIC96+Jstw8vLklM&nAFC1J?EO=;fit<%=Yydw%+Jk zT~O+Z2M~EC>fF~~E7X@ZnPlq`EHbmyui1Q|Ly0J^4J`P)DaCFd7`cIaF(C4!E#1Mnm(>g$D&wb8 z^oG3ISMX2^H4W^0AKpCgO3ixcS7iCee5!1Z;Z;rKEuC#># zdj1W8)*ya+ZW%c~fnzR1V|#3&p=F~JaW*Y4>!GA@5_!Y66H zI6z;ki_8A?BX{Kei|t5?XdLr4Qio0(x?MFXDXA>!CwS2=)F2Qkv^mhf*t3Jg)*1La zIGcrx4i{Zrb`NcN#?;M|-ezg_qD3EA(1)mQ+49X`A@>Scbjl{hTppE?XcQrr=*1_U-cnxh$zq4~ci+exZZ3l?NoQ zt@E*|J>MF$+;mJRg|+afNM~a|aSR$tOR5rnkB|S7lbE2{=t}Qh``z5f!8SS`ZT()@ zj-jK?)BsUV4o1DuqI$32~P!MP0nb08&B{fwi^A7Tpkro)B>))mB!)bop$+S8-N z4??mkMy~Erd(x{FoH&SST!?xuYLobb;vMT9SP;e2Z3L`-WM-=$BH}b(ZXSEjHQ5k& z#}7h1?FkVfU|h`HIdh8v>dfmY@6|l2*Cfv^&f%#*QsO=j@7k-kXdPYtsSeDrI!h;==Yd zw|eAX0wa|s1Z(#jE7Q)_(jEOFxz%E+`I8)9vO}^i2~q`Ic0W9mE<2t0X~|a;xY%TP zv7?7tBq_!Tl0ZF277U`&&^Bjv#|j_!@nZPR<>oRC40gYgt;PyFys4F?>HL$>nrOY5 z=L~n8demJ_BBaW!G^bQH&BskaANMKBp687dbF$dT31>4!E2l}6`K2uQ(xdL9or%wU zkJ5a$XBGCL<`#P^$81-nHCY|*Gcf3dH8Y^c*u1?+^u+g6(pI0aa*Y+-(J&&V4Qy;= zkV5Hx{rm?XSz1F3Ta)NWx_AmtZ48Nwxvxl$0vwo1 zIr>{S8Xx~G#=xN`m&1;|_Cv|B7f5(!yAtMvJd;FrK#9; zTkmWJHny}$uBSc2pa`Xy{y@1dgvrBKH#6vz3py&fO6>?SC}Y!XNRF%T_+%13pWhk3 zL#>ZMi3hvxw(FJos;bARP}8gUSsLjA{%_cmGDI#IE5BfP?NN^&3m-@z$}Sq1zJ9ei z-sLa9xZ3|(bD1(d=MGPQBiNj-Ie6IJ8754uQdgNCycyA^;u>AUI5$yWJU;kqe6sJX zlFPiC1$=8T`4~FsPzA5No%buRoe^oeti-97>1rSMqRQ$z)G=nZ+xkl_%JaVO6Aezd1jXTadiy&*ZBq{YCXFTFVzJ5)_c%Q@N ztVz_HGHy}68ImGAohIG9s#N>g5fzeA`u_N=a1}%1QslI#h+QSgi(9ZLr6m*66edCC z@Jp6(v3_6TJm-VyQ3vm!^WF?ey^AsT`sd!=5J@@55d1ls)?BfwcbB!NZH<&lS`JSq ziEiH__d|$|gQ}~JqTb_}3Frz;WQgcqRdlZ)s*eluR;x{^lF z_Pz3+X3Y-p`NpsZrjVFX7W~T7A-6_S3<1=rn=ijH_ zx2EbO0Zu+^K5Xs9BW+wt5jjN*v%&U(-`Y}ZJj|+Ry9BX!mU;JXNKIv|*ocA4A8h0^ zu8jG}x(`3^h^&-Ys=$_jpPGbsd#o}+g2R%D30Tj~ObOx2qN=STvtEda+4GCZK=27a zOrSiue+MI=dEEb7J?|kgb|*C{YiV0-c1A|)SNjbG`Wo3_eZZPxdDUV{1mCW8G?h9{ zCMJvR@-7tHheXcDJ>AdZ@TYmXAv5^s71tM^``nn^jofUNghCiI1oQ8=x}6j*S%&-y zmL8c*^=Aa5r^Jd%%w|f+_>EX$c@ktE;)n<$=Z}TT;|a!fEJ#W1Or~15y#Q&w`Vr8{ zd2qv;kvkx3GiYwOH^-xjo3J`poFoW4^V9!N#ISV&lbW zBDV&a*P_@EqWOu~OnGCspJ3qZRY~>nuR|MYgcnaXmvwPE6@_!OsBmo72k9M42F9Dl zzJp!J6yoB5-Tf}#a8A4W&WXK!rlFxB9{CB9E_^JWp%4`3A$G zZko0$B0zT^?$%UlW7^tZjwrp&raCMt4ipTTc+<6_aE6U9tNd%#&H@yGbp@i_s0_%W ztFz@a$Asy@p8g(%sRl%u67T7#b}*W|Q5Y0V*rfO&e9i)>PV@Pw4ot5k;jL?QYzaDp z`oo>apAAL9pXVc3#k==K!x;p|Hef!PxaDeh4iA(Lbmx9ze#c7bVyFeKA!eJ_J!}o` zcUvi0-BevZ*e-YSG0+t9-A+zQ_p7;q-?Q=OzHmEzwYOV}C8&nxZf^ek0b*U?n3-?q zsqlf;W$RMqV{WIZ5>2h8%%L+KyHt;#={WBi{D2hIQUKonW5e#peoz#r9p1`0xLTd-JpCFEN6t9<+)6DQIH(=ZxL0aEdiQSG2>FO zUH!fkpHM{AeFX})p({?qknis{?EBjU+AV%;Z;yja2BN&Gg)>h2=*kj|WN}nms!nli zO(Dgr&rPma4QT^3l_3(FeyJaX%ngX z!KgTiq}w?3NxQt&ja5#M(AEwp<*2U@w@$C0M1R32BRg`)c~11~nQVwSTMspH2LdbB z*A#~F+wo9c>jBUwa3h=mDc1W{rPIeHwY#~$KTz@#*^Qfe%)FDfIi$iUN0!qNdOw7H zsTSd^uCerBXNsad1KI&w?bftTVZeb&s*lW!-v2(uhba`6);znvO+t7nI(flN^G%^? zHvW83zQOAP~G$Ei%h~Hg$t}idd6O> zx=t-Fe>!uroYmDmJTDf*%{PSw>!#Zx3h{-Vsr{>$*N2K%oA((TCvU@mEIL7?M$o;o zIP>IvShC*l^&57*s7}uFoBWOd*}=KeBZ>bj$BYW2&GhI1^csl9n-m5rTugu^~(M zev{Y6xuB|Q_C%@ahU)ru`0Bi4J#p3RYIl!SfU475w4`hj`O>^7cz{gn-k}0rosk}c z8eN7>Bo)-Wluv!%cs_av+kE}`Ob^wVKhoH*#&32cf-yt}=wR%+>{hVZ zfB9Ys^J%$E4w8|xctAryl1kd&@#e!`&6M8a{sMf^H*(yG-aDpwZ4LOnsNqHZ&!F=s z2>PT<%d*x63f-^aYhvAPX}=v6R=zGzxg-(K?bjr)YWGW8*4hbq^m%H^D~-r7@CkZ0 zWMHsST~EojmZI;My$_(+->eLHnDm(Ny}#+>8C?jMq?0jV8QYVGkNuHlLrYEmLx)>F zQP}Qj(qfr6)B8`h>qv9OaO`7aOHQ^$ACe9tFLpP*P7lQQn{Wes4`mzuTBE!!5KUN3 ztjbNW2dKjxB}I){UVVIN~pH|DY6zC8krqq7hJb@w>fVEbV9+80}{t4 zm=TKlin?R?cLi5G{ zK7p@pQYSArRH}L%#6~fuT->a^{9`H7KJ+lv;kmtQF7PuR1{?I89(FpF0W9AJ)epGk zz6bvgV_z8*=N4_5gkS-J1-IZ1f#8}D+})uG?(Ul4?wa75;NG~qHSX@NjoW&hBuwMBUSq8OTUWOP?TxK)<%eEdsn&`ry3Kq=U0+ zVUKFB^OC!UVN=){MP27|Y7vY+{$(~38Mzqny!X{jvZBD8`c3WPESsr zE}eK5XkqXIPQ@VatIxHqPdsw`kB)st!lF4hH?A!gODC*O`;!7?7t1TpBZ-V!9V6Gz zY5M|I*Jr%hS$5XsOhsWI#8}5<=t`q2{;}ERKC5{-eHjYDUHqF>HEV@uwHD__HZBAF z0Re(5>gHjP7~Nq$bLR|18TuhQ3c#X=LPP}rPIvSqrVmhat9>1aw6fhL(r%2 zMU)pZopUfl5WHTD3$JT9!gw*nl*}jeddXOsx>dSFc>L{phGkwOTK{lnYWx#1SlfFx zP8*dmI3U9vUosoq@FOG`T3TKG;fK?~jM6{uyucP85z`cH_Z_Fx8%^C({LtNPyQ9vF zIj@V2!{Z4ky56#rWu0~Uia(vMF0XPgbu4Q=bc8qMdPLKkN1QjwZ7=P?_KEkw@n|r0 zKiDb5t7j*}XZgVX?5zEW0dA%LFwSxG>lR+ZWc|ULZA!0H;lj zhERO@Sl-$JhXNb5zle;x9@+LPn5S9Hvm^wpKV`XpWZ=H*&XrB ze)cH&t349*F$cL6|YbBAjAz zr@BG_#jNXA)D-kx3Sbm5Qtmav-o9(uRP&)2&?_n_*#a@w``=z{v-m9YG1cGbn?vD8 zR&9ybI^&|FV}%M&R%Rt_JNcNJk(X(oZAUQ{(+aPTNbjC+d3I{UV|48S-j*-$h1uZf zy7NAMC}`a=EFLw%iBZ~dGYLLjDs=@UemNS1vf#lm{$9kag~M+)|X9O5B5kj>m`*7po5T=6Bd0I+IywbYX_&Tt;Yn@%n8< z2@2d0yft*0G>PaWT$&yJnQq;-GQD7lvN#zsyym^ApXN25 zlCp4{tBbSPaUy9gu!4tNz>O64x_A^;pEQL_uWXh+?%`WsPMQKhRgWu%S2&h@rz=l4v~O4vd0c+!IwKQJpn2!+wQHe1pnI#!(KiO4MN6OmCrU0~&yM7OknR6*X*p zYz>v8sD2Ij@_+AoQ-=V_xY`iBDr@Ig>WYat>@hK27s^#bgC~qCaQdZB_VOD@z&4|XW@VW?x1+nnCoj!_FO&QH9>&EJvWwHDKpTixPGu#)^PGwN<- z{gP}NeztN{uTq8CEW5x-X0sZrJ$rjbj&fyCdjZ`1bkQP%77r*3=S@GX9hv$G#|0+BR9ov+szIc{uVP~y#REuLtX zo5I10%auPPt_0lakM-`a+BP!Fy$2O}oGn)c<)G;5Bf2Ix7|*FgPWUpoOGl|=TMD@{ zwpLk`;sSSC(^W}<+b+8kBj+_*H*Zl?dHL1uzMAN1R=`8*=NMW>kZht3L}>i`KGLd@ zb#+0{#1vZMarDD`ur}ZK2_~H&&70|!8=~1fQPelf1%DXRoyu-3_RF$_7}|#-I~MS_ zp^Q!o-A57~Y8_=89jJB~j9FE~vX<%`RX3XdDfYh_FfUxtG<}zPV4js{CCJV$Q!Qp} z+Is{FhK4q6ac;1EQs?T6R$XS>7;|OcEtOt;u3hxHUSiN~;^H@wEKF3KI+JR7Doyf@ zRYcZlUCSensEgReriu9hh1nLeZaXx zri^#eS>>}i4Kk6tsg+_rj}Z&XFN^a2mRj*T5U!pzy0>~d-x4 z<}KH2_hmD(hgtr5cX|6H4f+D3sU!FpR=h~?i$#Wgx_PSIQa`W>dH*5^rpz?NuTkt%i^}pMwU}AD%U{(*{f?L%Y{3_m}k6E91N!fBCqeX zM*nQ769dCRz735m$+HFF43UlQQ!l0Kw_s5})q`%{O#+)QNkIbk#}c{)W2V9dk2L+x zqS95b2WXoMjWq3cPeRhOJ5E;!a{3WW#_GeJ3WSO3E!QVdxZ zr6kndOH(S*6{$&E(>_E>ifN3uG3jUQYl=30{BOwd$fLZCoHtzVDD_TaIxcdk#B#3R zEuwlmZ%1T{2{`Q!cgH4C=k5I8Z#f?)AFXgqD{4)($8bI@n5^ui{p8C}NIb89K9Mbi zp~WZWBsRJ|C2LcW`Wt%a(WDPpDCqLynh6*tfg@<3)&L(tmLDynKWaA+i1HWeH$-73 zI_)`xE4Not*yi(K?TJ-|^ ziswzi$=2$eNW0~}jv#1IE~EOMfGBrLC*~s!&pRS`vyuoF4q~3jq4ninqDfTF)0xR* zy%kyNm4$PzMlnJp^om)kFTJ|-kc{ap`j82+#e?I_OFh70_RIwq<4&Ax5*0ve+1YvO z^@Bv&to2jeA8-kYJu)(Iw)D~Cd?hd;o)p|K>U_MXk9PUt<#S0RV?pG3@E2FPEz$4Ea+x6noY}#lClW6goMHcQlJF5%Lq=442muqv+a>IUN(cCa5hwf z4$G?V907-lMCwE#LLvQd^>L)Hhf)n#4XRRD0*;jJ)QSF3eda*o!cVI+aQ-yCquj0! z&V1VwIt^0Xh^otEX&Iq@ucD&VP;EICODR!cF~Y#Svl)U(i|5iVE`s|7X990=HWAhuZ5^=MoLajZtcP@QI;b=R{PeJIJ6g0Aj} z9I>0f&+QeMn`Da}YkG%fgs5K%bZzARjeLv;W+D{DwAo9$g|DeRtL0NPI^nXY35~Ge zrysR-X#8KEv6ytv;3c4N@0ym^48A@>wei1zG=l{Wclr!|p4~lo36pr-3JQkIHOCP_ zGR~PL5tX&(1Qz-uSF}$rG41#>3;wX_>t;ES*exO6A=H-iG+c}FFvyLrwOZSNu1}lD zNaB*cL_G*gzht$xI1BTpW%*H3f9SV zJ+!Ge23&rVt2}y6&!Whcb*UT}7`W*PMBzKT+0YKR8w3N7>J9XAMoE41-hQuQsc{3Y z{@w(6w7HtM&Z-ZB`gH8|P#M^S)V+xp+Be9VIku~pdIAxWrnr+jsWk57(9F2is4XBH zH*|AyT8~TS(r6D$Vjvat+1z0&n;Aq`F*2obIW|rGZ1TQpTwe7U;&X?oqmu)=UJoR& z&aBI&`0GGDaPT#EWSp51Vn_uhFfkyP_34tYn5{@5Fmt3L5&94ptlw_UvSwM0afk6K z#Y%mMYSOMd0P}4t7OaTVYMXENr@)H!OXjkRkkzj5Fd+WaBMpR@Mj+jOLZUl&UP(_+ zw_DK^RAv)CsT6=}^51sPgcmWR_p+kEE~l7!bP683N!{<=KTz*pcJH2_(=srM&G3ir zK*xLSP=k7A0NV{0bX-cpiT4VYrUo}uiba^;Ff*5JK8D5$nY7Hwi`3{&__Zv*NZuWd zpxDB+J`EG{t=r}-&+6g*$>=vkqr8?sR#Pj=IS$-=2N&6#rW@LFyQ7HP#AQ)>3qnxS z)AxZK^!W_aqb&E~S6u$NV!h@`T~6+r*JsOqH5}gmmZNbl1Ldg4vg*fvkEQCVk!KVmMk&p^!u>BI-5BfxRvnfhgg%xA zD6Jz~PE`KMTMEeK}kzm8lqy(7ZT3rPpFY%a_yyJq`OV72d~DQK_fz7h zqI=dyw;*e>;2z7$chNkytX3!GLR6wm}Dt> zcfM~vk|7A$yIz($@sD)`j_vV4GEC;U1_>^&uIwLXD?SWM2JcpW*yQ-mLH|bZ1G57q5Cbq8c(!0(!mM8TRo*I!K3sr=;k5%g7kv!hlj!&Ai-F0%o@)E&J( z{2^P?mX{&`8kbN&HywXQn310bZDXN6M!h!TP7(CmtN3;6?N@?S5Aj z)gIjrxzCw-r>5NVOo-8ufiCkZr3?ZPd0i+oDx9%X5Nv%f!7AM7mD$*p7;4j%9LZ|o z9^I|A8?hK;iaiA$riQt&VW-N3%3T4fFn@k?;n#@*(mVxkQYsptg|Szu#9JJj-_kUT z8kIhbjm4lVYm5X8j_2orf?B%G?g?^C+N%#iB=&g))C3bBl+>rq?W2mQ!31Q9^n?Pt zxnuiXNtaf=Z~nZek25`L4S^i>Sh40Lr_#Fd-|${eUBl_m3~^-9P)Sn}I6Igs!Nz@7 z=y7I!gddR_KF~%Wt)xr?Ke!L-o7mwvD`y@hX3m?MHJ;NR{>e0{r`|tnH`}`<7d+7? zju#c2zUZch{=OS?*&n+y@6Te7_wRGVk0=wC@DcGDdc_F@N&h-F*-KPw8{+QpaAdTu z!9}**QmN%IK+~*}m_ge>W>%^#AQet7p4xnGfLw5wPDmG_J0MhWW zC}Ck}r}-tLp=l^O8tT!U{&fQPnOG;LRoL07k^F)sM=&x1J+>uP0rUNwAz5C?RVd!) z7Ur*k_fQj4`jHrrf3CYHsekf%AoLqDmS~joz5rA7t7|UP#>-s+COzA`KG?%RWEkYV zW>bAb-6)mzxi<}x1|~R{awNe7nV3T1QSFqpi$Vx7MZhV!PKF%2#YYC+%C`V6Id$Ji zy2ljS6prRaJB22>%1;Hq0yC5Iy4H5$b4pE@6ee3b`!X)d;uCBbdWRL;@)A@Ol0rP9 zLpy;<16(SNC-v9NeB8RB*_k{g=wB^Bh#wlW{e7y|BMCt*i)cd9M0<2*XCw^qeny*jsF5wmXL1B7VTHBCw z#x5mWPbxPO*nLrtx|1u1hvNX#ua+wn6J?!CgWts_9rE*k;;HTG+*`ee zur;;?$peQ4{kQA1jlaWDr}6Ygew%M~k|ND~l0_jqW#W4nq~kQeey(M_>mMB4p!=)q zZ;4(D*SC)xpY4@*zNo4C1% zA?5O1Z-rbIRDBhf98Zs4pJ~1~UVT+IW-6b^_|0o00%$n9gekvTllqI#f4p8qj$+5Q z`((IcttAL1=I}q1%EQOMQk$qP4LMx2O*lFe;qZS_NdE2fG2P(cC}#&y6;n$-q}ZuKiOnm{snk%binUrKzBYrty>(>gE$2{_IOTQFZtraiaGBrsnP+eOx|LsJURbY7*UMqc4jeQz|(YkXd0yV@b= ze?(wGk55R*bIj@&eusoqzlH=_JpwNe%q9y-zs1o^r_@qH>z zlhb35&a5`G!Q2DuNx+glN?A`M<8x2<;vrA(1Y%E!>ORHniD}Out4u~Gt~=Mu3b9vp+~;jhRdr88W6_864ygj4!+w{-1Un1?WRh~kNui$g7TGlM zY{0M&L$VVQSO)|1&Bvw3oze<^lDz|J4Mo6r5hj}Rz1>9c_nBQ-SXc_L3sZsI&T@0O zkYU)v7Vv1!$!qk&@Ty`4f3M*z$)QDaC^gdqeTz8Rsojp2K3E$kWjid$g8TFLwc6)v zE30;_d8T4HpGGQEQ-oBX#D-{AiUcQKSG`MH;o+&KgnNo^kAYv+;0M)G!()f*%5#>} zDa1$k=@yQ2Rp@oPhkpVDwif$4Z+YzRjC((mc|MOYwM$vGFY6*nq`h~@$BfvYOJe z;#b*8{-G`m#y+Gc9u>#=coVp#Np5#FI*X3vE!Q(FLySd_a%dH$yjGARG;V94L#r)l zb8^@oU0RY+Q&StbYjVDG+#deTssDU%bUu^SXZ25LOBemk|Ak+89R&Ytf&Mk!UrUR% zmM?9EC23cI6asux%1TSqLQ0uwMFEMmkCT&ZW6N-(qDo4K=M*t1i=%-bKBqKLnbgNp zEV<%D5=4Ekzeo4!xg@iMU!NR{>>G)sXAsA*Np;|>xz$ZSH}P%N-a~bca^@rT$Bp{D z<9afpp$%bq_K37$>6w7M?&8JPx*=lNz!O9{icL6va!^S<@II+Q<|{)Kug8cn14Nkq zs}H^c*S8RdhglYP50BoZq)JOXO)DsSUZR@XMbAOmXEq8TuovSCr$%2myuhNA`{@z` zufA3;mR)F}+3L?Zi6SAx8!!{oVN#dO7*d2ORkX+ew~^Rpl$B6eDhuiV z7W+G^WkPuNHRpY|B=^ndO+$(uFv1Qj%jdru=@3oTQN(FU3-x9i<1nvmEV!T4)6^7$ zJW>jdA2^sv5K=FY((M`F*ZSPm12t^1Thm9>X=6Ldj*n}xpe*u$S;*WtXmYa$EWfG5 z)P`k}`chatuu(Yeo5PJRxL37(U!pQ$xokL=X_}X1nU6Q zJWR#HQjbCbZ~e+qQ#Njof}Bdrx#Z6!ILsQc(0VDXfJkyNT;S)DODH$a_;|7Y+h^mU zzAHX|G|V4`g};=q)M^YR#-A_KG;~M$F{u@~A8?T?7dYD5MPcB3J-oX*;($o}=*MHf z);}Prt@Ya!Rl}9lvZAl8BaQ{93fX1{vjGCaTwEEQIxM;-vXW-G9R|=;ScWI-dg2Q6 z^Ae})_Suo6i;KZ0+@8s6z1xp!MP-3UgBh2wuiix^UfipH-m>EKJk$9ym zckTv$5~g>(UTfcL*4A(o*snP4b@2>kZifA$pn%TL&riFjgb>p-y27uPGy*;~{I?+= z&4q6KLgOy^PsH?aH%FbT-A|EVxpW{s8OB6QyLQ337=2~eW5E{HT_A8tOJ_VR>AB9; zD-l3oUH5Z6Ha2%8;ciU{GR}M&`q}YcF92hbv^7f%t@`1+!l|FX;vPf##d;0O-k<&{ z30jg!E8AKvx*daAFU3CAn!z|isAjKdVbpx*OdxX*cu~}e6Ba&W;@TK~kbqN^I6guy42ZR|S9->|FcSt>2sdhl}D57kkPOFdR<$JMUu|d>r0ySi%U|MOu>C zXLpJb3mfT;!2?e^F1y(_HOzv7M6|{SN zEZbS^FW&0w`{uae-r5+Fp62)){B$Fh?sYZCHX?~Y+T}|5TOpnPB?*j)k(?#DtVFo+ zsceX6YHfxh8{0S9{T@n|p_Ap*V>RHYpFnH-fa&|6Jky;nsy9^?O4yt~PpPS`Z@8rh z@I|=FGik!V7rQv)8ogUcHu@#pnf=tAK9b?LfqsGcaV_+_(kZ zHqikDP0`0l#o*W87LNDX^=_|V@8C$rwWZ&1-RJS-S?254*Ub{C%>Kbl^Cmd`yV!pf zRa6p=Xv6z~SC5+2r1Ch?sTbVe9h8|EySYjBYE-+j^hOfPm}Llsi-`IGGpy8y-pK}q zN+{U!rs*j>^vWb#aSV}~H5YbPOEkwALUmm_Hm>oT3M75Q@nTh88gqQ^@pc2PKVZAKDrMJ0Mr@LyS=eR3T5-6laYqwT>sn8+-5fD7mMlc41k zcG>%&1c1%LWGN;6C53;mVPWMzV4097`@t`@6fd{s_lFMywr=A=x=hUq9t5}otO6Kz zYhKtenXf3uNcVnQPfrXSn6gDvE3E)~<*Px2=ZZyZ%=+EF$d1^AQ#J^bvRFG9@zeJg zhKA_r!m})IEW)bC(*CaXt%3q0sm6?5FG)aMnFG+?HdL$W!`H(XT*kPR()d-Sc*04Q z_3-8I4L`6lU}0e`DrzCMG1dU91D*JuvQ%`^6!-kJbRwE?M6`qkqt>u@fo(7vSyl9> z398c|M-AP;9i`SHutU>h4iKth7p`B(>J~-a)eqWWI%H&22=ksF;}CL=3w9c z6dD8Gl&5bak(V!Y6XHjJiZN$4Kb0lH+aw-GvD*qGQFLwMv&dtoVnBg8CbzF>$ws|BJ|@wO_!;3fbuue-_u3}k=u}(~wOc+Vpi0^aYfSqHQ_p978EyxKENd-5jS=A_}5SY%XtUdB+FuwF6uQ zG|?P6_xYY{Izemh0LbB6lEQ^D)d5rv-2f)E%>yR^g)sK$Z!j$lk{ND{Mp;(S*l z!ghz_QAeoKPp`I$rY5t9dC>K;d#Aoom(qG05rPLJ2l~VWsmoGwHNT(clFqQN{hY<0 zx^H(Fj|+URjNA*Ckfo!V_0o=ooGqRbladTps)EkX&&yxAeEB%4qo5Gep-`j_vdOJE zJ9rT_TT24Y?;7jdNv=<;j}0f_!cNqi(@?E-`giV|T7KA?Ha@3Fl-MCy9VV4TovEr` z0_g}2>2g66x5){qXyh<+?Ew9e8rS1-ocs`(aADfFXi4@lC#c@l2>k$@Y}#xBlX&O8 ze*ivPE<8EgN{PAn^xZLIE1U_ndHR{F%XG(Vsefc9>9mF!(x#;Xalaa$eR!XAALx06 z-6HCDc`#{ni~6*<6oiAC?od^K%)_ipo5k#prjJpi*}%4-lst;fp#DW6CZ6;|O46-| zngLL1KJa#8eVp8U<9{|^pT?ESVfoX~n)6t%r8G2WEM(Z&q)sm+w3BBU3BCUx9?RI0 z!$Y{)94lxFh0CtcnZen^cCG(R@dNCXSw!FLRl=7FdP!F&LYK`Oj8bfX$~qfJ_<)6N zzcr9h;o^+}w~hQM_c~xOs6ouwO*`e0bcpyKO(_y5SoiN+nO3;r1s=FN4@C~`Ga5yT zfx$t)*N5Mjo8v3FDU@sC3nM}2moCj8;^Xyq?@*YD+~SRpo+EO6m<&gP!_Ba20DD9Z z;zkV44dhFsAW4B=H?&t{;`8WJz;^4X2~Aei&C!^n}@c&s!uMzS*wVlD`!DHPB(nLSyXo1E!H`LI!pa-YrMg(XpxV;b60hud58W|QC zX*6y6wd3@$uu~F>>tN`?InvWZF@Nj#_ooUi7LxOYZ88Bku4lD97@ho1x|*@)~og%t)ldmPpq78)LuWHP9AwUZAQa4};GkQVhc* zH~*aj3=|S8C^RAABOBG?5UMJB{33=QEwbFpczeA5^kRtkIq`n-6<~-U^67yd&h!4= zEpvB{Y@Zo68-m1RLLN?r%Le--_) z%{AGk5v6J_eBo$&-lDAgD6v%*XoVKGhcg6^nTfl_2NM}{s;c6y<^H1zf9m&J%nE~j z2S8L5dpY$V1Z{&QoYxKq-(ldZ*yu?W8FlMpm1%i&#iOi)pHg7V_IwH_e3He+&N{lN z^A&)~2rkx4fZhW0%nS(oyR5Ex5jF+ku_YJ5RlX;WZL(=TF1--W(|d3JKm&L)cAM{I z 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

    1Ol*OUL78Is3 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 Tc*+&}_tm~>a07kK?(zQua`x+j literal 49946 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 S+fal&TS{DBtVmSf?|%WTv)8%+ diff --git a/website/docs/assets/nuke_tut/nuke_Creator.png b/website/docs/assets/nuke_tut/nuke_Creator.png index 3cb7f01666ac8bfebca6c19c0f69c087e3f24f95..454777574af5083c746858eacaaef537c13d3bc0 100644 GIT binary patch literal 30511 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 literal 15493 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 diff --git a/website/docs/assets/nuke_tut/nuke_PyblishDialogNuke.png b/website/docs/assets/nuke_tut/nuke_PyblishDialogNuke.png index 21388dae7cc44c1c20dceafadff8794bc0229042..e98a4b9553a489ef47bda955b36d02302199d961 100644 GIT binary patch literal 66349 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

    4SBPS9ty2z-ZdWc_ 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

  • 8jh8c%*RhB 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 Q6b&HpuQI}Ag1UbH3#et^U;qFB diff --git a/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png b/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png deleted file mode 100644 index 47c8b61824e0428424c6b6fb4f8ab97fd74899db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87912 zcmb5V2{@E*+dqC=$|z+jWh`MNMp=diW3MSf_E2_WF!p7}zLlZUSVFQBSt67*d#DjY zj4|0mnvjH${eSy@pXYg>=Y8Mb|9AX44o7p(bI%S6D$V+B~^bSa5$CQfxYW=hD1ft97Q7{+gVgc7igm(i1bSm{2I?Bw1-jVD+Y73y98vOD02^?_ z;%x=|-CW&$6#SJ1|7uqO{Cx1Wq@ciGRq!s#g12?_1+IE{V+CX+WF(M+Dn|sAyzLzn z46mvGF4Js;de>G!`o3(N?u-G5-BYyEiDe#5cdgi$J_dgyZf9w zXyWfSu3>%bym6j*oQJ!>L7TRBJP3GYK|!!w;2(|K+x@F;PlC7WUoG3)Nn%~GZdiA` zkEE1@l;pqH$NJ;`Yc%dY|5z??;Uo`A!0O2VsP5_Hf%oun^6>muFaPnIe-;0aAL4Ni z|A&4KO8!T6aJm2Y9{g=R|F_CII{*LG-Q50T8a{Y6UvT06u?hb%g@1P7V-(Ye**;SSbC;i`wLhxbkJC}EWarT*({eVjkm_4YL!xYj=4CMgTbNJ;;< zQqzAcMWO#L{nxVn-L8&@J=pA}zkc{%MVeQy>U(=Q;9S8nAH(Yy0Zp~5vQqN0vf|Pb ze-GhcB^5N?Y#p)MSa(Ofld|AHJNU1XTR8iJf`5+duY!X~x)Kgfm9aYxT$aBF^;gmD z|0+`!l$Qn1^TApBtH|_!Edn=AK?l4M_O^K2YqofBtkOtnDRCrPTuR^6#eps_$Wsa|rmq)jqfd0!sFF3V0me75mrI-?!q5EB^1NzaL$32UkJC)z;ln zSM`|GkEl=^r$;Qeg9 zu~!_y)1@qU#lZmwZa{#5{=qF1kdly>_8yEwQ4pryJzaviaaB~Y4 zceb%TPd=P4R&-eAgLD5=RmL!-iJKU+2$N?5&T};Vuq&lke4rd^P`4{S1~)nv8reu6 zeB?KRe{M|pyxFjLr7o>y;_>UU?5$6jD=jW6^RsHG zE$s4GA9#7Tt!z*-cK^=#Qf%bx>~DulYO{2eZ#T?N)z!Uv$G7Z`$1pF?+==#L97u$# zW7nm^pI$LF5Sd)fRJ~#4eLB@x%jspjpbkTAk=l!__!JIh__?c{KlUe^=R0jZssl^- zx%rRHRI{haJ~Nah#WA%P@!nfx==G`I*-;@iKC~*sH3fZYK3>QvBi?+>k>?YkoLu#} z*8WtjaN;j-Ha7!Lt1GK_svT9gdB?BV>om`w^!0sa-CHXe!Vq`J@y{ntXg26(Sv@sO zeE{J2yMsT5Cl`n5g$Au=B za)oX=RStPS3HeLB30J*8$n!(5>G>J2&CSm*nJ(~-&GnvBGvJv00bf#Jh|WcHH9V6B z)C`g?X<-5I(J`qD;7~K@MLc0ZEFA-Ya613R=oM$Hcm@s86ByVGWhaO!oTfQ7x@kde zX!KT?9NA=HW>E>HL*U6+(;78ME6h9@J1U$1`_*De31idu?U32>8LXc{(oGG;X&E3T zJv}o!{S?NA^mD@cQCP`RIB-XutykX;_a&2Q$foT}>^Nses%RlPf4LC3lcKL0W*nXUy zCnQ|R&&bYB&rZjZyn2_$n)>8br++B#Ct%YcGr5RQ&6K^aIe94XkvTKrX`+2J)LXpv z=b+s6biH1Hk%Vl0{&PrtT!{D;E2T@;Mv|n|^_ulBL(vQvXu*kMU6{b{5o=U_mC0&0 z@4NTFWn?lS;hT$)==RLZT4kW zziz!t;r&ej-d?Ab{aoM5>v!6p^_qSfliP?JT-#x?27u9;GOM~^aIS0jR%U5Y zwx8&?I)Pz@CqXaUjLOo-P&yb5 z<92^6pnuf40Jk^aU2e8mu%MusFcfS}ks*3E@ufr=B%N<7LL3prDnocjzh&GIRqk60 z=|lCn+>*Qzt|Y0{x2ha>(lD9Ljwm&R(t+HC4iL^Zsn_Xg4dZHIc5?0$R#s*lh7P6^ zMYLmA9Z9CibM<8l^@fagA8TF;6mivCaj2~&xS4j-ljpBM-#^BtRi}BRe7j_@bPb*C z;_P531bJ3(PLojl1J~!W6MSl$I2kg4CuMQBV}==xOspMj&|7kC%SSI#f+Y;nJnb z(FK<3G{4q?t&A4@=widp&Yk z7`d&@0v2`NE*Y1WY`Pj^Nh|hXwtKmdmUI%pTaI>fpPuDVxWJT>o~Z`mb8|gOTFt$E zJHNL9qW+SDsOIP)&>***w=?w zFox~!Z8TFB*iB3%IMhC7RGih!n7j>)=Sy~Im-AzO6(Dk}q!IUnUd=$Z}1u!@^*b{*4#!AEQIlLE5r zh}{(*R~Z4QfYys=JONotlY9Vghr2Ad0f*nnqEfdw1sxX3W!KnP1$)0dKOc>QEt+)K zhy$OBzM*xS_%h6`RzfBTnNOZPGVFRExrx%zq2SLtdf8@4g>DWw=keJG@C`b9C4B7b zsVi%E^X9F*9NW2QK~Zd;!T#^WI4*k9KwP;c74N2$RTLQRt#Fn~%E58l@hU%*1`O4` zEId&6a-yW%2Nxi4jA!LAQriy6JJHZ0%dH2Z%taFyH1`cr^B#wu4XL(d+_!+Ao9+$N z3(>iXlzzmq_FJUFZHCJw!;PW4=Q?WqeEnF3H)unZO4lLd%|3o1y9N!^caViNSyj15 zzzK|Vvm6EI;pR5k(6a6{;{{V!Y-ljxu)UWkG(J9FTD)q&>q9ON!!J@6_|Gv@XxidA z?tb4ICO3J7IMn(xQ{hA#l37m_vwve9VPWVj>#N2peUA5~ihgo@9yMm-_d3bsZzEgn z{Z3;`?1v*BCtCnYorIP~6-87kyH0?tg@b^NgNBm>Bfy<`J^<^&H4aO0;na3i9z$ z)J))!^SEyCLqXyq7qtkNR)b}{*O6{+Zazv{rapP3nda)YBtV4GM@JPQaFSPAfoZaJ zgZHLlc1SMImc6ubzqNdIY!}dmET#r)iP9}-$4KC;3l=_i-*~hen(Z^Ov_|858Ma(f zPK-Bh*tbDDqvoq~WVvIYWMVub8r-0kKPx5GEDJSmwPrmrF+J{nQBqdbWsMS~X`$hf zkK`vd!4}3S&OU z<$J{WuK*)!!buDSp7J$d?>(nAoT$3{wcZvcGEozLqRZ5MzQ7J``q++m)WpfihfU;S zi*c7aP&R8;FQZck2|qfKzK+TzIG>GXg}=*DfIsJ7zhULOa5jRw+=}Tfy6-4Fa%r0w z>hnG@>$+&qz>l|<>UqnztS1}&!rZ*PT9GmH3(1DgMkRb2@P0~i$W@vSC8@%Bvm;}5 z{@!x%()9b)-93SBZ;Yr0tAG?aO+6AS(~>OI$=R~iCEc^YZ(TA`i|9(Pb>Co#Kx?*a zZ4KSV0$rlMP9jLSy;7>!tGbJ~Bw{)e>xO^2mp?7QC1V$TgEmTds?ADA7h^>diy3v1 zWSCyz&29_vXzAEfmK0w?t(W=H<)B7EA;BH7h#w{@d=wQlC~dAmj5i`^BdZV1_@vNCq&d%bU% zn;n5L+qOAtQX<10XV&Ae)@<12P-#%D0qG@Js&4Aq5RqgHPgcWo+^5)1wH>++Nv)UB zB~NDD6=8p+E7Sc7LB$u3M#V@K5EhwA1D6dkG_ zL9t!AM-NKI=@w4eTI=*|aB|W|OTYO`cUJC<)t#pi=GVSV>{cJ8_mx^SetDw{ivo}^ zwl-vaFiwpkLozV-yaKVsK*;rF#=9(ub5-2oL}(jq(mLpqd#y^nFII__i(YU0o)`tVP;6rH2i^`= zjDE?cVV96*gn&lEy9%*BpDSHcPA?Y}&I^%p)$_8j0R|l9T*eG*eb?>_wOdY)7mojCAg>)!Ll38iNvjwjNEm4aO+l=&rhd>)bD*?a% z;P~&6h}*zhFJ6)tZ%6YIgMg@-G6Z;r)&MR`e_jC zM&i<90`-$WBH6S(lV>S&zX=I=ff#2Ch!&)k$m-yv-n;Ua6<^GrH#MP|Lo_jsbg6^c z_Hwb<^2n+qF0K|o?>kh9H_tO86unf+$=EXl!VS5ZtBZHa76M`FSW}gKs#23X56qVM z)f06BQ)Q-ONy$aCwtpgw3lwRJ-fw?w`#tHCkj}yy05@8D`MKtF)MI);hR)chlsjST5!($pw&^K;;~w0I8(h!?td6Uqx$P>;b2kN0H7I@|GuV$uj-;&eJ z)jg$)N_~2^583RL$jz z&Ot*7K16XVQi_-_@P__^MmxY3ezVM$G)L|$={ zZ~^RRV{-6q(Ybe=VoIXM zZQ2xOfc&{Zw|5FiEif&$+T6O40FF&*M(7d%+_t#==WAlzBc=WB?42L@-WF?}V@>tm z%k#6{?fu^!!`9?1eo<0;Z@-{zIvjIm^sVkNPyq%x#+GyUc zb2BZ#WewWu@xs3*F7Su#^vh6CZ0-KD#?F%nQEU@maPZ>16MAb-FU$c(UW+TAtM_%j zXdIo}7^hot&GgKBnw>l0?-brXM+Sw z<}dYSUv}_i+~tU_?S-#POaerGE#0u3Tw=z^6&YfDzZ0+8atE#Ku+08W-KH@6*7OZ(a<=(?)K9+GLF9=p z$*c1=EUG%qx`Z0lJ{6NLx7vt)y!udz3@h2QS2OVZ4g=wW|f<&1V;8W=2j;m4G872;AGoo{Hd3Rq-*pZVd<3$wfT zFkWPp3L4tI!1vq$kWw)0EXxy9Z#R10j5B0Obw!3d%D^B)`yBaFj&tCO63MH4vi7WEkM!im zG0UTQZ@z8Um#u9vkyCJKGLxJbeVt0}O4^cqY>5l#y+reCu(l-Sz6JIzLygIe?uv{k z+j9`SrJVLTwmh2l1@h;bMJ37LzN@f&T(S@=`He|QY7_&cfbt2Y(~Sb-Iy|@{D4j|r zkHLldXN5R*)@MltxOw#PHU!(b(He2e#}ziXtGS8QR|Agh4-btwX4}{2n{vX#y(IEE zG8}Pfvvo_GS4xG64-&6IaOTPM`m}=70xmTvx`!wE5J}%OISJYU1|nrM`daOX~9xtGcD~)?LZjfO!%}d6n9KxTadys6Fv==lcz~=m=V|gYe*npq)#|4BU zU(yzu=><454s6c`MXY<^hyB0{@Cy8_zGrs&lW2bP5-r%8w$_Q)LX>W*z~60z5WQeg zrdfxs=ujrUY9EH(2=Bz3l~7T|Tu}ze*kmkh>Vij6T6h5k3g=*!0TA6M;k<*+S#(^Q zaQ?>%l9Y+GFtN(5dqWAsN=4X-byqZg&_nFELG-ie0aiaUp%T{;gaWb_>LrL8l_bM1 zdA$m4ErFEpQFoEOkM8+o(d%qe%4R;-^vGvx7nUugMM-68E74wOCjOw_;+SRfWz09T z1W}^K05brAt7WBwetgSGNtn3RY@N&|gs$1$S|T6;BDD3*1)5?_CTFCZDR|?IZ}N7$ zsNmPa8E{0|ey?|jsrPJ%f+yiwh}&`$NGR}$;L-{xo!|i|p88Pn@>FLFvdlZbFzy5f zg5xfwI%h4IREFLmf#{44Tv`LIL{zo-Bf~aW$;eOzI7KnGb|Fnb-yk`Q4kt$E{*s&=x&h|*Q@GEl!HN`Nd+&#)l@17er7nQW7= z^1%1BWCv3a0wwlFLvs-~$aPT6k>%%ZU4;B3i(kd1Bq_2y3qV0mqh*g>g}~3hi9Mxu z!miO?J1@!b!gCZ`CN$&ui!vN|=5%57>q3_aj2FQhTWXtgy?DBtl8XQWWA4m(Mcv}d zVhjG#&`x=H=GPRC>9==fWV5$|qe5oCfAE**`@kn>S%M`?Jmy2I=bz^lD=0U~bPX9c zBZH)_&Pmpt!P6CwsbqareX6Kyt51BxEu#$=T?XVQhz8qsO=eDL%Nf)d0{h zr;U_?j9B-|VrFL3syZZf4zE6Q!tQ!$A{&tDtJt@S)?Alm0{RF9OMX4!;&a!)HS)3- zJ$VkUCfnJSQAZFt>lk1p05Y9H$JA23%kw34nmCy01{<=e|6kAwhslgg(1?^tR0(lc>1~K3*>{ghe7M)%Xn<;(}15Gd- z3T~k~>Bl&8hg3+xA_CoJK>`ww zInJje*7uTNq3!kf)tvjF3QZs>Ni+4m8ghnJA~Wqthve;?C8h|CrVja)};t_uIsWZ<8ZA>`sB~rh02#Moy!;NmWD7+ zZgAOwVNC~nb$x+p0av5}he*;%(b=6biw3HFMqI}!OIo=_vr+PSjW#@;u7NuXGJlYi`z#y`8Y+d3EmwaJg)eyTCEF76TVjhuLZ)Qh%yr z$@L{}#F zVu>QgM__D+M@L6XO?=$lB{WZJ%`G-;K;TJ+KviRved@Qom=2;1yyROLQs=5xzbgu6 z=fXb33KzI5Ra4y*vhzJoO1IM0&u?R3zRRB5-Ob}K;wxxlZ&$2a^n6m*f}7h~&~BV+ z{gA8dNT~zI`?%T~m1f`S(ah9EUmCxPc0!dw`8nd*(O#*?Kr%KhNB7WdbhB>dCVY!^6|6R2JmEIoIdkkSN@JqjIZ+5W)RJtbaH!oPF%&8y(fu76Lk-iOX-0Cf=2zhHu%jFO~$WL_dz;s*hPj}Pgz8@?Q<%tn#yd7aS*Uc0I z5uNRpSU(CZ#L50ty)8ntXbOHY?8}p|&~n~ksFp%^jG(A+!{elr zP;QU=yK9fjD1o%=5S^gM4u&GHHUdK)u0M}Sce-{&6GCQ=U@pGN{7HePQK*I@Whc{0 zx(;I?r|#t^5fg6lUL!tEs<0}T{Mh5BG{GA~PD(&^yH7T6``fLK4H#0NuloD=*xFjr zs+w|l6JK8i2^tE>@qqr~5ocd|GV4hz+iJhwO?&BK|DDe@ZtY06t0E~!;7}uezj76Z zjO8iMs^ve+oYtXKel}4mlx(PFNh!X@zz8R*iweI9RM$A6-apgR12$P+s_g%y5{w9j z*H6kBGXNU)G_i5nB6Y$Im(9D-98pr8eh%JX{iaWQlJ#zjs}&Tsj|Wq*sL{DYWuHb2)!?OYIr#GE{hdogF^2`5%rRDRSF1#ImV4(BtU z_~_lclHfe($)=5zR7-slmG2TId|n_P+e^55vEn3yU{XriT`5M^dk>}Qlh2tGaZ+`P zbyt5G5I9F`u5{&!g-p_9bRfNTMI)b1V6dG7D8gCA=u;FF!7^?J2DeW;+{z5vd+vJo z6k)~>kCgfz2|%86*uu`X@LZ-8(KnN9fFuXgWNeL_d`$$hxSnAEm1tOB)(xAS6wnoJ%k`sH8VJej~K7IU`n9$R^);h*-Lg)IEsedRUGuF!!i#QgKZT!>1rIpe? z>$7t?H}7Rl-Ue|1+A7rQEc)xRS54Sdc1Q3WHsgb>l~>NLl~@PtEZ1(omBI#rO)2bo z`>AKSEx8EE#bIxhdSO`2?#(PE5N&Lme@yrR z%!515xHIIhRW%pBByi@7SvNjAfIiP1T>!>wZ3%t}AeP~nbu+Au0}}-Qot(fqpOgZ7 zsMNga@(=+Q>->|u*pB-*-1MKx+M=h~$G6m;C*3EAox%(jgTXwa@SOqI+q-wmz; zPB^b;!%iRQa!kL$d#jYnyV@J!eB#NB>kF`h#(NPU0pT>r)Zw*$KZt~ELy*A1va|QR zdh_a4V|!YqaJW#e9}oflGH?y=f~R5 zqYG5SKibmkV9wh}ezsd!6P#ztadDCloNH!#{|V^{=@I?hcCCRHe$lkPU@lGl!|U9X zqzVcyjK11Ouq>aRIq@WFKEJ7___8MCv7tijDP|eb*$G)=qkN*6q-0&0`C_CLm=Y?$ zX^BP%c(!CNp3Ur?&3_N_995i|aoM^662H4shi(6~ac4IX+XApzddQ?xZOuyyY+>`h z$3Vn7_Dtt;fvpn%5h?OZG0M3(M1IoAEF5F8$)K{sLq5APTy=2YG3V+Ev0DeJI8h?v zk+kGWzdz_z>PG#3%EwmLd%s3#k^0;3$4OYc5n$$z>xfamnunLpOePH{tFeMUDiX>* z#7}6rRAzUnm;UV=?`VApU4q_L@v={)j!(URz4s@W^>uf58-GLS1x$qhsRc;q%X_CI z>L!z(wV)8YsPJ=VN6b#F?*^&g2F)niaOqO*(!^rb=%X8tuvVU;Lo|LXbeJx!Di<*z zrW&@VBp_7xD_Bo!^Gj=>6$kLi38k|ABeA-gC1cg)R{2hkWN2N(=*=f!reAmH8P{cl zGw6V_yNojxO3qSeNXrrTc__F)u0IO(-+VH)4(n$Zm*1wz6}vmpHQVi8~hdx zfdNWc_=QUcAx|spk`85~F=!)<5J1SMI?HgkKD7=Vcdtd{^YFw;T3f|T8&RAZsL_ef z`36lSvivOQ>zP6KR@IwckMd}+65}hx@KYe5XB9|RP5pSRlILUCY zuKM)JKC)v55mXMsl!lv(8EA`(zf4ATl&0j~rB z5-LVZRDQO*^d1;}f|>V|88>wr2n)JdoCh{y_TAYu1|)e!DNKdw3+Zgg zJAq$dg5Z*6ISl^xbA{{g+!|LcoHAsHUwM9mr2r?B9;4KE0&}lhtWSi}Io;M~uk@l` z&uz3h7-vjd!@;W4bckS((cr(__N2SSD?v!pd3Z>-oF0?~y@TdGB(o2qH#~PT_VpVi zf5+>jaH(m`OVz-ty6PvK@B2GTfN&g`n4YK)k?W~g;I}N<-19^o21M;S-s6(xYfiuS zqN;t21OWIuPn0(&XSrpQ;Q8My?SaZEsdH@i?M7=>l^kc~=(61vAP6`|denIN!XHqF za+o)b;d{NU`TA-IAj$SPNnb15jou2T0ON*PgFW2DyQM{EBi0C%Px73GU5b874gtCl zuH*Mjv+TeiL(Sm#Rzy!vDF|i0)g^gLXHL`w#Th2h-|vj^lKn$=pV0$=`1}Jbs}0gH z-b$(8)1sgw7jOIY%F{bEtq6Q*dae6p%*<^qkOK~}%v0-x*?~FCsf=^sR$*^pB1EwH zet$;5@8@{ms#3SK4u#7Omo`yPFlRg&+<|1f9M3s$mHgd5z0A1X6LcHgVC1d5n0ac27)$#U zUZK>P$AI_N^#{2S_*YPL@wvVKffShiBG{8Rt*XDhf~RzV5$puSE~8-b_ahOUf}C6j zdU9-}4Q9M3Z;a4OV1{#ah?4aV?-2VFQMZPRx##l`U8BSZhi&^fC(~p%Qx%eoom%Dsn_5wG!-7s{X{gf?@EE*Q%hLY% zn9$NMt2+dqO3)Wg2FY@bT{2kv4&6R}_0?ZDF?7lFfQ--0-~fe2|Kn+Kf##P;Pueh8S}Nn(H7$_1XJ4P{GOev(Jng+mAK zRUD1i`J&sfSntlE*4~fSOsK7tUQATD&u#P^QP?@L^KeN4-o=_%vvBtFVB%~*GIn)! zwMTt?RjIhY$@FuUf#sR-=Cy&n{(SJdb*^rkq;~B&;w@>Y(zL?W&0E%Qwl5Xu`^9~} zY@mWP4hdQEGm!b(jP&GJK9d18Ed3D^yPS7(Lh`-?2nHGvW<6k{?gXaHsu4EmoVQH$ z+516M>cawtU8~(Uz$gmE&GdHchbxphv6CYU0|9TPS!vqgI)%2jot-gP+D4;5k($?! zdR>F0?{jy4QVDF^Mi&n+i9c@+OK=puYv>(7I3GPjKlKv9U!1CYol@o8$|i~}q2hTJ zyDQFxG9Kt50!Axx5fl4IOMM?3f{r7!MsY^Ly>?Wm@+Bg=$!&dKwx6=|E%jFalN)+l)1hatAJ#ep9Szk4(`P zj#1wo=<15N&(0D7r;pXVtQ#I4^;{6y~t)FjiwxXP>+_uxq z>zU8oRAI^sTv{yXlG||zBG?xN#Q>7NAVCQ-rewT=41U}z6FjS6Ef|=6Hd8y-_bA6I zk9Y>lrS`bEyd{Li9sn+jf|9eZ0inEVGs{9+C*kj`ytdIms}m|KJ3UjokSY~nwzw0y z+TQwV*m)}}9b1PrO)?}x$+tn4Be?H?hRpLVnf;txU_g4f1ZGTAH;?BtGt~P}?R*=m zfwtAS#u!@Co}S@cY0OC)3J!B|bOe*j_J)>eePuXscaebquV2rW8nQ#Ycc=1Vs85b2 z;@;F%@8&k_N~z!08g`YH%u=mg_NQztlr5+PS1o_Vf@J5%mE3tIrP@Y&^P%-#cV&o! zV?%p?5!JOm_Ecp5%D{k=l9k!*&toGAWtZd>8|s=Y+3)?-c0_agof%0}AMH6aBi47c zyG9|irfk6$Ru_A!f%Cl$63NzNQlcZAh=?5lNs-~V4-WEAOFxpZFXo?g4d0cDo%wUi zIwVGF>!+W}jfCG|=JYu(Fk=!Bc$*DQr1N$ehH1~|zy+%`pkuV#<9+%U@sRuioVtM$ zpB@7I;1Hk!J5F_K_nVG4xNLY5?!z|$a7F7I16ko334_#6`#6c6NCr@aDST(|Sx z6}xt3J5r+1;#yFqZ@PX0nAi1wh^;RCI%-u+F(|c;&RbCH-Vy>Lzlf04uirto*4B^ zDU;;$t!T}GwfpQ8)H$M&tw6UAh&s??6T@731&}M{w7A?iql?aIvY=# zkIB}&a6mva5fRNwj!{1cxi^1Q<<-ewI|zZx9|##@-IZe9kJ@~Hpmh8Kl_yu{*WRv< zWoBj~VnOw!ayjBrE&`=vZgJabaA7SFdH`=dxYe5sI4!={J1EMM{kfxF+C%b?Bv27S za>#p(4Y1-^%lR1~A#5+5`Z<~EgtDMvN$gQq;Y4SMVJ{=*Lq7F%?`DsAZ64xCdlA+7 zq}KiRseV<44zK83n~ZbBT*MHnr{d+5ClQJPvgpFNG?lYjU+Z-fgg{paW~@TbL3nAy z6BBUaAP+$lRQwoGoKoHs*Ss zB*?60pe3r)4zj9PQQ|uosNeu$txqqP9aWhj!v`bUTBKk`wKA$+{9a$+64#zOhT@SRBs~L~>teQC zHQ@2RKHghW>pPgRDsnzuU~A-7hwTIjLjsa}V{2Epfts0G=QUG6yufHf8Dxcn8qfj* zEyNYblk<5*eVwZiG$-k`T)>^Ah_mqwMtR|&kmRG5gW0#zQNJ^1zO^H{kKboGE`mlf z{aI(|U*Kq+(qx*w!T%M-CSfuhw^9?bU3l+A;L)c7z{@fVl=wx>leC%{p@2V5GnxBa zNyQo=?+&y0G2YY??;aT&F|#uB=1qqV6w$!_yC^)pCc(5*%o5N+;|HjO_E!^FZjRYf zfH}g;$y6Tb@=oSVouL~238SMy*%4asBp*0H>Sg%5u^%t7rj_Gd9bQ|zw@7=AX9TXj z__ifu%&OnZcv%BzgD@Q)$Fr?kihTdr4ioV*N`8N0ls8c17kCU!9Sk@u9`rjIj`!s^ za*(8inZd-3Jku{ZcB9e07oHizitL>L@4l07eFbYEb@NP)7>|Ap>(t)D#Y8mUn>Kny+#aJ6CyyG(=0J zQc|FzLPf)GLCmJY0ixfN;Wh(KgB^*QH*bgcU^ug?l)&|-UMgI12A8)F*TtINInM<;OcR!;f=`Si_D>KmTyx~ zqscP__^8Kg#V2Jiq`OC8n2LG~Ar@`(711$&~+ezd|Pd5+9YI%6~@+*(C^Kw(a zNmi!~%fuv2lV`X0rrUNR*6yqR+4C#6YUUN`v_bB~@b8%=6a*OjX}*;_J1{W4w0phf z?D^LVf%`xAHxnOmN(S%EiHs^X$V~2c_Ju}CZMJT-JbkCx*`>N$*K0=``g(&x5Dr^!tCT;XZD?)3k%u&fg+&Z+}>0rRhJch{x!Z7yiSct zufc}w%@yvp{;Cc8ogTL37}meE@u*^Fr*eUZ$9!XmZ)c0W`8s4Xb8@fWMro^4Z+~lJ ze`slk%pR6$sD<2Q58IsK-~Dwz+o}bmGdK6LcQ;!^m~Y5nZW;7Owd|#5FVyc@?{V&b zsnjd?A@AScp9|aD`X;VreVz&J&jF@-W8Ie4LGsqdua>hzN&^inm` zcz9=r7i3O0%&JOQdekeOL8^3dU_RQ-luFt+{`!PP#*GPpf1Qpz`qboBnMM5|uC^ra zSxD&=wRNM0o{=8)I&fK#d|O55TFG#gm`hmk`a~P5$5~>nu!J*1NEBOYvfb;LU6U4* zwiLELCvtRkwQOdr&d1NORt^U0>Ui{oZDEF5tgYjzsVocZpd($Hv^KZ07$od2nAh^D zPH@MngCL#WvLndW&c918mrK|AYvzn3uVlJ)NC?xZoyQT@_{$zzK^)31;K%Qk<^}TlMTO~q4J_Y2G zzO>de|5EY@z=>*75V6NBCx^!^Q_>$zBcih`Bcq@Li@vgaZk}+-3{{c*DzQo$e^pux zs?UwG5-b^=8yW@xu`?fi9lw={DsI2Fr zrH&!7pU788#^+aW*&LPQ`G_Dx(MNWx#AU)Gr zH+x?6yd9e@GsyAv_V|LSeqsU|qc!Vx@ zTW_~+aK~(2zg52Xr%=`Wb_&a7n5`%X<+H=qJ6-*TP;3}Tk>(c1>{S*ZDL(8^_Wo}7 zQ8u-KB2mKJxqx=cCTlDy(RlLCgE zz;jPD5*jEtYctDgw}tgZMM&b4qxAfwL?l}tNOyFw+F9foX-M(Kfp5bo_?lMubl<={ zKjUhF>eD*U5Q|^XUnT+}^nk!KJP9hwda!c^b(`?0zi_ zqdLj9Au|?yBa>`7cGvpCqE#Dyj+25GSwjm9mf~qwL-|am? zw5GZ7twMxo^hky8QWp@uP+wzrYvB>uYv5;?`Gaq?KtXNqX)G(CnNTyj zSRp2Qe$lsb3uS!$N(uGjYMalvl4NvaUcgEexA!^?U$$!3QChYVt>c zFCD6zTr(DsE22{w`?GOjT8MyZw(yakt$t7_W@OPs8Dc>by&=mRQz54Ms&Kh8C6#W% zM`uvRbjDF%!vuF$E{R#C}_=<#xEIgX4B-Oa2OqY6X%0pkg}+4jiQf-Td7!f zshd%l$z4OO&x>oqiJH|Jc#iiCyP+^O!!D3OM;!_m>w^fBUiUkJ!JQVl4aplzw&k!R zl4rcKI5EG(dZMS(m2wc>s2;1ZuSEq51&h2*#{e7R*JicalF+%PKJzBL%^lz}1a7oZ zo||zVROlImfr`~@RNvi?4lp~DGe16@hqK@2Xs)QQ5MXPo<-^>=y?OwTiElQ-fKliw4yA ztm69s9ExEC+HU7@z>SR2huk)L%S)9Wc8;m$HM@)Ump9n2CGH(ho)-8{&(vt(=pRJzlb&?@& z9!nf*@Y!dFJbMpe^kOZr;XxkEdRI3SLl}xT)b!H-Y!nun?ZTQmc|L)eE#1-?oReQGKF_z)kT)zi6O_biW(g)DV8v0xr>N+|l+G<@>f3*LKuWEUj=OhJU~1Lso- zHcFmdUysG6g5aR@cj4b*ni{#l&bKQ)CqHnyOBAzm76BVU_ z;y^eu8n}W!)_v81LE(q4%`Dmv6l3K!vUb#+`twORClE@sMa#)?svDFHYb5-RlnHmH zj?lD!k8T`(3J<#40S-l;^?od4Zjx^Mr&XoVnq%Xi7(;4lstP8C;C3&W@`W!aruu%; zbYv0VL>aSBi3eX$Y5$fP zdNw>f{*;xzHVC1a&ss3W8y>%YPD`T`4ZnRvp7(g%@yEcI&%SvR=w(yt1OoHnr@LhL zug>1>I51|sHZJ=N0lt^v$q(cYZZwvbx{-(LX0aqsjt2+VZtg>S$e3dwmz@eICO3a)Kmaf;A%Uiso0=D&nNxZ}6pi*C;vHiVc=II#e8Vk9 z{Z&oP#Gb4u zd}(z03wJnlx__o4DZ#27G^xw>6}5;RbKOZ6b*(vt-tzsC4Up*r>2UtN)+MkX45XvR zk~VyFId;ZBt7M@*X`n)kMkslT$WO)wWR>93)aNF*_4a1K7bT1zO*Jm)fim%J`WIar zZ6wH`J(NmKP3&}6z%B9u8P&d9Jn2FA;fbEIsN^LWs!+E%#;B?eoPes$}FUBGP- zAm1o&B(q@yO%K~(-}i+MDqLEOE3-IZ_~Xa#y%iDF-6{5FRXOmzxy>F$KPk)s_U_Kj zh?>gY8@AqQYFWLXj3aC2x@>_+Ds;OxG|yzUkn)Na)(RR&gCzP{h2N1!v#`ShZ96}- z??2Io7^pR}t9}9mitWlH#o291nSdfg8*I*G4Q4LkoBm*4+FGLsP-vZS z)saajp-Oi2iQ!2w>=~IDEJr4bgy*dP()E6 z!P=pD+dV6>BZTEKgY*|R4F5|QkTVs2DZ-7a7K1q%vEqfw5 zf>2=v(KIf>)31_cFZ2LV;dBNr>dIT@3RvBF?dtGkjyGS=_Qu*EajeWevi-`72&O^V;A0 z+^uq1Il1bpDqi`(9Wsym_oW554=8P29lH0Xp6b?ztL&k<&)}=QcO!Uc8y?@6Je)sn zWl26Ek&j{#`DxjG<6aBmJaR9~S?(?r<;9k~J@ol9!v`cOc746`Mdvla{(=7w2SND0 z+{_r$T9?Y@OR#}zt+UyNR5JPT5=MwPH#hJ5ON4m{qyZVm(f|m41)Ve+HdW^VfC6_R zv>Uj<1u_n147s3Mk!l6BlV~TXRzNKwmIgo;lnq0iH=WjcZaz0RKVPlZilyTG{QTh1 zuvE%(y-K+H7TVA2Zx9V5l;^e zMM=4nQ`2=9P#tyDado(GAy^oOxm>QJt&NE0=X17Yd%g$1_5pw;Xh~iO2mnZa4FCXf zK@$J~C;$MdsG6tH2fw;S7p(5ob<|Nu9oG!2EIL;Nq96#Qx?u6tg`t%eLaeR= zAR^1M5{X0*_~mj%2oaCRs?}P=+(|@+VI&g?L@1Rjk?%f2T^ZC-M;+G{h=@qrmSdV` zsa%E+@Jp_C7nGIZDiT^&HwPvvg6mn=ok}IUyStLfM0!hVKr4zL@TEkBgwcL{}F&j zh{nA90QJYIsH2WL>R1jELRgj+IVUX3YRF_P%gQ!nOw+V&n=y7hxqz+@aK-_kT&@H` zXqx8q^o*3}R6%>57mGQ`WMX<|#xzaawlb-d=X%pLvkKD_00J4L!+1GIWtuIG-21AV6PaZ~}5jwMbxuHO6N}-4v*!j*k~2;$e!#wAPMo z5kWqm4}zdjD43>cnr5+7sw;qV5LN2V&d#^CGzUSD%jJovTrNwcmU4cQ$)s&tuIFXa z=`ai}%W_?>=GIme1STL(s#VI?P^ToBbk=pVT8L~5tsoA%L8(T`=Y^OsSVaK4fOJiBqp zh`11;lu|0!^NPh{Q#PAOBn-g;kVs4Uu~GnxF@~s=rZx6Q_`dJ^J}hQ-R;x8ctkv8o z>C^Lk&-0cpj()!Sng9YaC|K{Q_K=Ll^ehi~(O_kc0fI!lkOH{iEx6bU2mv^d&=k`u zqjXXM0vVv1BpGB5SbW|%@69aFL4)cDF+i+d&GCPMs+vgCIc!MFam9nK4Wb1utr8di zxn9bD!36;be-raBKV-*zovJO2#6rnfa^B%rVhRDEzE71QG)Y1i0Ys#fZf|e9<<{G}H*BhsaEVQ8 z3Iigg(er_!A-Ld%VcGFyx~ZkTd+WAc(^K!fweS7+-w*xZ+7#0ffF_8Ww6!a|ZGEt% zBWz4aB2}&_&sRzj6WEkx+q;AiJYN$7lVaa#^Sx1{y-B>KXpizax0?4LS(U| z(YqzS^Ir4G-_wY6`1Oc_ch%Nh$A{s1Q~-!*S(as5h+tV35HQA;I;%u~RZ<2YFlSZ^ z%l%I0y#nVtrWj+EWg$WkgiA(AWN^VEVbR5>0ofetjEZtG`J6C@%?0Uyfh7R}G$r)S zee#a=;kqUn6VeT2&C{VI1Za)pjcwdAdD$}tX2e?q;_!%3sNn^y0a&ODW*7=}WKOKAWOsx<@#K!6gB5fi-&$67Uk!D&~Ie0*21rQI)Q@yINiP>_|ooLUz30MwD-b0{-M)yVqO=k6e?00hyVc?14CfK z!S)9K#vcCA&DLEvSex49Gy7w&9JSmK*IEZCt!>M^W!J6;9=t!hp+9el0Ts*#ex(-p zN-3=Y(O}HtB4e7(oy}d{?HhOQ7;}F=kqpX&TXaKbcG<5(xybZ9A$3w~o8-okhPA{aS|oCB^#l?oytLg4FC-nsoDLl}Dh3&5|(Q|ZSMm(vpR`ceQxkNl$^?+O|T>aso}BP>~B02QyTXk4-OCdJ9^;0El}~<&U*C_vU8K zl#10b)IbQp7()QBQYKA?X(A#Jg<6*af7-3(K()5F-Fd?eZmB#xI<}fRKnCpQ2f~(~ z(oeocUKN%dYZj8e^a7T6)URN0L<&87+io+J(ZBiuh?OQ6)e+%x6u@<)Y!T5gjOZD< z936rHobxsC7!UwRDGkGjoILS(tX!^GmZg*;tr;*ab>Ib|AA$xMgdU(1bC8gT06|F= z`KFXq7p2=n(zXT$paCSZGXG$PU*xiQVweAw-R{0)m2baT+umn9b*HnrOAtV7mjCB} zDxaP_n-R+pw)Tn#Z?d=ds;xc7Gq2VD?CJ9V@Ra_YPmmkHi$|;r_<|5HV64iOBY{BM zn#2{5|45~7zwNeNcij8)eJ6*fa*T`4hV+h2-7(ulh5%6n0s#>jB11(4K%t_l?@yO2 zg_;|ZB;vy4ULX&RP2Snl^`+nZjsO1N{xa|{_0(P|G{Mci!8h-(R;%9s`Q7Tv?*^UC z?9p3ot>NXv;l%t!9f{c_K6JC?n0R0y{MF&0;?kdf4ZnI1P8Io{Bi5?7B8sTUKmX(t zcRcp+>u%}Y`59M*2*{#)i0HfS+}OzU@UUUo?fw0kmR4aHK!gN=R)eKNG4Ste?fmlB zzf!H$_U?ZhR?inij8ouI;GK8yPb3nVOolDer37J6DwV3W+7(SrO6qEfV*i2~k6$Oa zjuZd@SeC^Z*O#4Iuoen{Xu$|ThSh4#vP{D;f-n%ArxHoDu|y?1X=8$gVaU_WG)-j8 z15cwOA_!3%I_AWxm6~DM%f>x-;~<6YY)V_ zn|M^9^trp7Af*5Fo$}c1g8X6(k~ZTEo6`KjoxH1w{pEAj|N2bjKYpG4_9xhQjvpDn zQ2VrK_Go9H9u1OTjSYiw^!pVc(DApemqFe*jAYlt^z zl9Tz;;Ox8%BmfAL`=AbtP29b0`|jO$zWCyv)%s>NK}$+~T+MEQ~mfc^fboz7uEJKXS1m?Wj zo~h9%I{Uuz)h|y@PoEhcS=HRCbIvGHi0<~=Z~ycYPclTtI3l>NJ2^3N@Zf=W4!l#T zRIWh%)cfmw6u7<=0B4Mu=H+t`uZ0ngh)QYS_cduLWjr2pT^9(HR&Lr2+d^OXp5rmb zB4b5z2>?nfQy9pY4pgyR$Rw2KmUNv{b#?v+(FCn&^|gCk&-4E3 zXSF+SuzvT^cuSgTy^zY1a_~#{$J~Ja;)j*d8ASw!z)XSx01*fV+j@+D_u0h%_V?xg z_eYig^iBTd-Tc2i8?X2m3Scgu{Yn(Tz4zSXv&6e6PchB`ks*uQ7SLjo28?beAR_8W z2LLieWSn8zu^rPOpn>T*87f5Nz)Eg#ELV8&(MJv)I+V-h`HCGuM8Fw5yxqTJga4O5 z@qV`7_XBFrvPW(;x2zL~2W6ppDY;X22>>>C@!)3jubyKk#?%`}!*Bh}{a?SqpS;ER zs}~c&>IDO|)?FPPpZmgR2MpXlJ{f4mkRgCls+^mj8XBA(9jWB=G6(=bNtvITtQGRP zsmZp!{+4w;P9lMbj3DoMFHVhpb>sRcpLqPQzV){&A}RS$AgQTqp{Tk%{U9D#_cwVLaBL}VI<*1A@6 ziKv(?RoW{$rh!9%!P501EM%ji0f0y`9oiCNz=^XN)35;$RHM{sZJaSL%D#Et8UPUC z6I;R^UBQ2Ox|)b#B>mMgVWkPuG5q=iu}qx3eKM$clyunkULy(%M8#qoJH#J;Ch;eK zUHHN4wJ+T-_8c`|K6(W%{!2h>oo#Hq`L;V=JUk>d2!qiAbHegiDlC#LETqInZURIN zfCON2*5A^YE7xW#RYU*~%)pVkVo!h9?%lh8@{?y*MFF&D)Kj;)r^kbDzvvaK1OS`6 z#l}uf8upzCM`tgk0Om{j^&`Q3JB+SowxeI17*k$AKYYXg)Gg-YJH21NXCECmR>ct% zbt3xY1NX=KdtMzr=_<(?BdsgN;>^hK%Y^Iqx?%zO7mn3b0GxBvFw`2PaI75|W1Mrw*z%}jRC)#gn#irWqzMvK z=_pf*7Cbe}Nf;J=J7FNB40;p@X`qc70)h}!lTs~`tSit}M9`GfkKGiU81>&i8UFh( zCVN`B`hcX;8XA)9i}yI6*kMiOwA9qq#LtF>0W`s_8;vJ-J3ra$K5~ow=#F6jX~SKO z6QNRi^XAPJX^c+gxM5sMtq~9rAutWdFtUdy6x8I zpMOEg6(6t>ZeQ>BHwXX6Klnq7@sfMC8kq#Ec=YDc(Dg6%y+{Rb9t(42%EZ}y+s*I4 z<|_>|1^thEy#Kb%yl;ztV$!&9mhBaSh}v3P?z#WocPsg+T7?0)9l6R0Du)4n`fXAfC!N@P>K*SBKFY@ zS~q9_fD};&!7!i%p5wCh3iyji!;KwkV_W!tKjZba@%wjJoLwNAOaPiFW?^%h{llKx z(6qX5yZM>hom3oQv5~!P!e{TYpWEl|dDs8+ZhKvmJ~qx*erQF6EjMmGH8!V-48|xr z(qCv;hB#lWjLj4r%V=-Tq~Z?Yg3Q$bN>O7nVIT`b#TX((hB#FyZ|QCC=x854GrW@1 zD-qZR-M!hL&a3C&38Dux700`_8m7RpIkoS22mtKjOm{?Zn73Gz{ck_ z0K9f2JTW47ZwkKqrY=<3swjZ%TW^eaw!b%gf&f+EpMLk?+~{zvRMuK8Cf#2Ih>@Gf ztCWkgGhG`uuDjvJD$t?A{M|S2YHMv79vxfjDr=>6u~f_#^7&%nl~-PV>a(A3ZQa$| z+be`H5bwJCp1XG6-O}0$2<1}gx#xcJ<{NL;jezR`t7!osB4g}2VQ5%huX_nFc0MyD z5gAsjA(M{BtM4GUnrL(<3jE#<#JjDqmpzHygQ& zn9mTue4isV<;z;>sE`-qHhb}1|0@s3Egkacm{?(_Mm_P%jSn8w1rMz|nr z9b;_1SUq%V)H1|;v9f!|1^`$hxYnp`!7a{RH;l+$0lBASNx`O#8wXDhu0(Qaf|iut z+@an(B?l)LBE6ft_zm3xfDRAJp(%Ce4d#YUcFvQsTrLq<2ABerroJ|D(|Tik-UopB zlHPkf{PKfxU8BlXI9u`k6rAt6>844Nb8d|@=7-_*@ENyQ0Ax$?^yRBsYEZO{-vh%S zC`?ZY#+sYktj6rgLhj)WTQ~Ie4Xtu0_`+bwkTC$LR4cAqLj=Px8Dn?capxcW;lJ(e zUFZ9ffD-Z8*y!k+Z@jt6l?ruS6+V(6aFo8hK%sM*l5=&CHVP8KxD* zdKcRlUtr)+sjBO{USJwrYmE$v0ErM7qDDjpsH9e0McJYnX|CdSuj^i+fKVFOHOhv# zdiS*Sf`zB#j?HFk0}Ca*ayW1-{2xE(>|8G{VYMPcdn1zyG6{CqjmGou1X7XG@b)SB zjfd6xHu=twDOM(3i71gs^z`)}tCfAF3?YEDmU8jnzx?Z>g*7exy!`Koptbb9HkSls@kLrI3yHP7RM_%n-LMWx_j3dsyIHKk2z*;I-hCo=;`au8To27g$B^1 zInQ=>Z)|Xs^a@qKxocgE_Q{`|$xB8TVQ;>Y$31RQXM;X;O6Dtirl3!b$+PcXrT_%P zw~hyHK>zBY*dII*uXwcgm~R>g2ydSZzxzrps%|9$FQ9CSjnAu|CT$tyT?xJO2cflY zYRvkI;M`!|f*p0Pq#+=|STe{rzx52fu5p(Vk_OnGc&c~5*zEXFX-I#;d9y+oEvgSfap-AbyzG_fTk;@&aO^F2&FGC z{opFs50+(b+qS*4qqDiS<&j4o>+0^9otb~ec{*3t{axDRNm^S4)<0^n!3`VlH3vt2JLMt)xao%`|dw{UF=exI*VTL&zqy)-+dK5bzz1 zeCK+B2uFt1i80yH$p7vI&oS{5&O&4$7~H?Z*w!m<=oT9~`Ak6rK(VU*kg^FaIIZw} zO+b$07>-@3%w2|NUQ9zk09tnWISK!4Vkn4Ij1$2jL|4e8T~2-2B`-~Ii669!>J zL%OTGyIQS2_v}v&9y|~P{@m;g5!|?K`{R#4@%pQ;4v&qhRq3H!$7DDT>z%*F7T!{%4QWU*J9n^{yKOT=W2m?)wF+hz#)k?uKGP!&{o@_Kg2Luds zp-=+GO2rb_cDd#`rjUzV5m&GkC3QCm#O$SAywJAF~U2%OfZ3|R+VV5IsY;I1Q zTuHxF@&!V%RCTh6bVF+)=;5-fvOUb{p9reQ2=?gc~;@rEu`dqtN;o<5?4Vc& zxqKcGfBDN7ZrHM=zrXLBzw_HS?cDW?U;OO#*I#p8uO0xn9`I4b00>Z+F^iQ-70xiM z`G9sNV@-_bAt|hwZJUCKkQ=rcNGZ|eRxBQ~gj*^4VNk7B63LVT=EP$n2&|al2pTJu zEISqk+GU!!oleJ!CGW#maxM_j_a$;O>4DWUy$_#w9GNIT<8;=jS8ISH9|y0MpP^m*ElZ&5usEpeEaYIrdTRfYqiOViQ$pq zd_E5VTI*lE`YYRZ9)0|=?b~-e{K!K+y}bzV;+{Qvjo6gxxDtFML0}>RE0(C1rz#Sf z(vJ3mYE^57T=1|~3#4TVj*J@ytCU^ejOoz#eZNvEYG!lJqGrdOF=R~ouCL;@X(H$G zWZbc|?*=uO$typVA);R?&z8g9{vH~gcB^H^SxaX(4+@2nhs_JlcO)w0XLH{AY|6|w zS{4IKX&{x)O_g2Aja7&N_#x)2*w(;o6EB^pzJ&MBggWE;G@HCwDyiKwr$ zwXG?Wj62adhTt2z+YwnqDt7eDWN&A4+4B#bnE*nQvE1DJ3MCh5$W<6)*pgY0&{H}6 z_NnlOZqc4)pV@6*x*iD`lq&S2H~j8qwrjlrfTP25Xfc~86Gz*`Tot88ldY6au~Myt zRX1%}w8S2`tZ)hdoLF2b75E+io=v$$uk8l_$2C^~ zL@2C;O~^zlmDE*b$74#DT{lQ16Q;!zA_zid3Jyd{gK68Qt44mR2muQvNk+E z;jUjigE1lmkm2m;FgaGbsl`f){Pa}C^JfbShkRN(4=%44yZl8UR6HD+=l$(Gn`Gnu zW%WGJ8b)Vj5Q1$k)C}Fa!T6Ier$SkJ>5xwZ?Tx&nk-u}0`{7CxLOAC$Gjoo?41+Nq zd7NRXG1@|&(t=+|cQiL_+t6L}d;)69q%6}YSKZ0keAV?G+iY!2Z(ZNj+L+$8xwow` zJ(Vkt&E_VH<+y3ij81E!mE_kjgz+2?6~D2E!>a%Qu1{|q3BP!cEd(fpGt(-(c#alh zFfgv(J0tJiW_C66P|}-6gZv_O#Kul;2sSc*$(9e72ShBEN`<+(wyx#{nRw1IpBUrK zE$u{la(q*i;ti@DqahX*Zuasn_pZ?x<))3evW=dOud*R9rSQIqHB zQxJdda{2AI_Ya>s+1%Xpn_v6V`i+~CiDWL9Thk@gaXGm5762i)lQ96&hLgye znsFXaB%~r?7>JNbrHKh5LMGy|I52JqE>dX?$k7Uw=A1J`!!Q_VU`8gLCKQ4h%m@J) z<06$xN==+a9VD*|i0G9IBO@}MaH5E>S1Xq*K1c?rK?7N&TNRsy+}v0JFi|Pz(_(Ub z%9UCx1vcqMO)*+9)+M4wxI2^K6klIZw!HqEr@_f2i&>E{GYy* zD7$+9iSXtPM$G2N#<|k4!n8%k*nB?kmWwUf$;wO_`M7tTCOqbz{#@_8o4FTdxfc zu4t72jM4DCn6L1i>qXqgsz(H{|Ad^(>Hc;;Ij{e(@6>W-eKB>22(v{!Q_v6WG#wLX zi~9AWVHD$M3~t*j+>j4Ui7QGpyU0XXbKO%XPi@}R=LiulMFJKgy=0nJLpCc6GYo>6 z=?Seh5Ez!#*4f?C+8%~MsgRHI<^Tv6ZG=WA=13YFA7AUz99+ykI*;Rlv`(fopZomh zi-p42*eGgBrPGK|DVM`=HQ8S4xN3Y9E+9m{n6}IX2Li^0B^C+-xUonEhs?CB$X~>` z!GQ?C;^Mp>yj(0?Z<JBy%GnhXsZ2nq8D`U>dP&Mpby;INQnJhaPt`A`tb%la&i#e_I3jfj9F z+`G-bcbk7=R6exJnkk86lVVl&o$q;PP7iGCzhVCP5ElXov;v^0unv$d2rtfAC}j{z z(-44g@4RHlI&h75w`h65t5PdAijX2++d)>72cqSNmJQxK71n)g5OU`Kn?>}&xM`}%qc z`SPz`eyLC>))m0@fa`?~beX)S3%*F_zXIo7zx+>EB>m&Dn9}+Z8HY%qJqiLZ$b?7$ zF@R=VHUL;?opUt+AsA(07GwIVj)x3kU`iZ3V?1(;{SSNG!-JQV$l+M$AMFvr4QOmu z-L~1>y~TR|h&j0m44cu&c;x-}zkKKJ!)L}c00UGEAPR>r90Q3l^n&o%(70n;ZA}d| z&wpwE(RYpyH)S$g`r9o-%onQf4UF#De{Ade&bxPR)}&uQbmH~Hr~2BOrbmWzx!ejJ z7YN{o`05Gs!ENG+JFLS)QfsJs^xFHuLpPgEDYk2a*mvSX?(NR?qPLYvMQpR5%_e-ndtxkJLL_BulY> zYwhSVO@n~q@uU-rm!p0}i@a+Y%ifesAKv>)u~b?k|4$^rCnm=B?R$HCbRr1PCz(P- z*R36X_uY8xj&wSW0E4Gb@89?KPk;KXx2pXf>bPoLFACtJ0ugnzx3wtsp%0^!VcD(( z37`0f$#jio)l3zVZd{I)=dL1|9TCbNK6}vm)6WIpcqsNCzg?=hAFfCo69oso=rLjl zMDX;h?$J^CCtpeg%KXJ)%a?E!p0N*tbAEboFh4$eV{gZMBjbV@h(vnP0zd#j#z0A( zD^-I~0zkf0Jv=a~32)le-QJW&faZo&p!r#pDmQ{+t&Nz@BBn7 z#a7-D0O9Q+^Z11Eg?p_Z@AVH4N&tBCXqYSO&PIOsR`VbBc$c`hhQPaSG}8%ItmxO? z3!{3mt|tDq2kq&S`T9v~wR*%e#&Y@mi!Z$R)j#^ZzI5hrsg%lO0>c16ZWyUl0|G|Q zon$KA-P>=NmTf!HjW$fPsil=gISNFvWC|JE-qZ}a;=x1jDW%sm0I+Z0zKO|6&#hIf z)$?RJW2{!IJ^jz$du#8TsZ<((a=F~l(9rDMY+d?aC-|4H001B!k6q4XBq=@@_d{Pg zQW+QZ%d?OPU)TlP4(*&>I;ToTHmz3Yj))H(W6IKg}#O%%oO?e_s9Oj z6X9<^;{5M_U(J>EiE(veRCP7;TQ-SxE&S;4MFYXj8NBO8gL4?2)$g5=0AQQ=l?Uvd z8;rkx#TlNxVy=-(EXkv&pFPP1j1ZNGY z1Y=A})zRG06c^wA>+kuVCswZ`*vm()+t)i!?p7zq)VF@_=F9pgd;R7NE7vGyT}l|m zEOY}pG9;gUD;S+si17IB);Auu4~^JAJ!l7Nb^JdO5HXj}KmE)P{;;d#k)HL>X`M@? z{9K--I$K;9722_7(jO5JE!(yn2M`IMEuOrmx$VrMBm3StATR0LykcBrXJ2~DBcj$i z&;cx^3D%a$M%)OUSU|uE^1e2q!1Sw_bMs@FDu2zf1`S zf4<)<)h@i*5W%q#5WIi}Cge}|dwUM~Th@u+|8#t!K~jD!yx$Sb3gkx zf7E;1roOjMo@PRf<#HU+Y@tbexIXymm_|$0C^BvQiEtbb; z^W80(Z5z56Ljv5~-TbY;e)jahz$&s=GK74U{rzjPjt2RApQf^_pLx~){_Ebrgd&3B z=}RS9%@*{3eY$1|JT@w&f=6z({`hlFK-S;>Dn3zIHFIhShXKSD&DeKpU}hwlPJ zymIQsI#!NVQvi&ync3OGnwK4ZoWKmjbP|_U%X=Y2wXAyw41>ph>u&h>U&IX^;@P*n z6QgoD$3p}nI5VZ5Kj0lY9UeLzRy_Two2^H0jg1$bzkAhrf6Q15`Yq1+$nePbzxVXl zzxfTlY5l>0GZoK0H8F+6q|~7=dzu>_xqW*}HVpu_X?)_=8>i-rZyy^zHaN~0lS*|q zr5?L$#|?cQ0O0%ni!Z$J(n~LCy~=KDL^wWf{Ov38-+h4o_;Zx7*$>|EezxBSfJ-~p zidA~yfDZua82jSA*6)AXNhY1Yc_ndR$Xp9yg@{V)SKrtxgm~=pPd(YY@lD4$erC8l zKSxqWeB+B{76^dIkQSVb6gSNEne5hhx^!~z#b53jo0weHvo=B)uvn7C5~`G2<i9>EUn`MSWOGS^L)?qS8KqJJBY}3VwRIwi9SSxK;o+>jH(}d>Q?^6`^2vG z#;@M<4-AAuQ>s|iUOB{=Rq<`-*ZaOi8}w{Lbjo9zRm_7C>k$0vl+Yqhfw@tp$) zV~+E{W1qU`#!Y*Voi0~B(o9JT12vs5m1~}92(1Z#dOMoG{HZ%PbhizT&v-%Dm`-e4 z-+9yK-ek-f8=w5{Ge7vJ?|!dRsjN<-((*+DXo5FSSpe|0yV)OnhOO^l-+#@2_q42i zSfMp%cw-NLYPa>oomNQB-@FpvbHol;)%kPIQV~7hf9Y4RRVvj7A9?7LH*DOHYB+pm zaCmII>Ms&|;6kZ25^%=irqv!#^e0kjFC5wb?%ucdjg3#N^0FACnK2Pdnj3b=>5=m~ zQ?3X^MB41^Gduff-}7|^aE-Cb4PjPsv|j_P$_M{eWW`@}1Uf;W%LlcO?MCT|%p5F(fYGYRZ( z=eKVXpV(=x@3c-$+dq6e_RgT`g=;1KBQQkY_xJAGKRrEj+a0&waP!VbI{PNmjYBij zbCrsxlmyKfGZ~MYW`k*ExZ!wVX<&HovE#>&pUM{stG^Su&_hSq+<4*>pAUzR2VV!<(`)gzI(`X-K(kIzOr=!B6KyXdpG-c zZ}ytve!e8nOob=M)#SXccqA2Z1~CiU8u-R8*3)Ve8cMHD-sfd3 zl>i`-Nc8or+q`*GTXz=;?rMpKfr2AM{H|eGtrchIC&$J|M#pC7=E88z+W#WrCw~(j z`Fi#Jy_J`q4o6RuU%MKrljTDi7H{c}-}QL%u2188uhY|i0dA$P16B)&SS=T;<ph!AC++a@){b#s!MlR6nAlM?{=Ar@;mTV``*man2O5iZ+IU%3K^pp;4^ z6aD@D8*jKFk!|Kq!c|zR_*zq?7AQ?^4XG_Xt@qu$C6japP7j?NIK6k@{&x=@nwgn_ z#U{qr0Gc3fQD2MP-W_gg58Kl+ZiZSbsiKBLAQ&>vJZVf7#lV#D-l#D!Ey^x~mD1>4 z`BF78hK_A#8yZr{gk@Wn?L^Iy!Z37ewNj~EER`yiYA7YFsTEEd(v9qszo~cL?-%C7 zk&~)0OL_&O0~t?d%&rY)Yd0TyU;pCUFn+o&{Z|on1@KW~Efm1wTNh5Uf&pWTEOi7* zqtXyLe6C0gtd;_R$P@rbr&28~Eln*gsfMf_i(8iCIC13MtGO!l;wGz<%Y#Eh;}a9} zb8}%BUW1OcA6O>LWT(_iEM}ule=%huafIvab!(5e37Wj0L=PFq7+2w1~wOK6@ z5nIIKH%>}xtuS01yoV09tEWtkqbpbw*<@pCwvo zu~;k=ibF#~j$_B;aoe(O+vc2mo?ETC)oQiox=KnS;+(Iw^k2Gx(GLqvLlFU>=3~|8 zV|jjP1WZnrAjUuw1QG*@p(LeAa7IA1mN#?hn*so(G$Ncg?9zhuqQr;AhXkTzAcW`` z7rk-$fYGO3`8pS-o14Nne}>23##o9O<~bgqs3!ywGIl}Fkfy~ac_EYyNJ`hLP?!O? zS`YtzB&aKZ>kdR@7)B_A8|#aR!T9oroOA zi8;=EE*Fo-vkjRb2xjNzBXN+;HY5{?VyRpx6xnLHv9;FeR5Fv!gi6lN&V}JwQXR&a z);bJBDP^ftUJ{ov$H`_JGU*f|7K_EGY;d`SwDihqRSY3wHk&P%OIlMl+fb{y@mQ=_ zD#=BM2?A(MT0sAg+sS6Lp$zBd<|At5mgZ*1w)2HTxl&o- zI*BNg$#BMU`8)tLH#LS~SS*#!et^YVs->?xx;dQl)|O_|H0N` z{&x<>9EXS+G8w}Vz8?U9<2a8$^6*VNwjFx!{hd3u_xJT~-@es#z1i70(=_kieP?@n z+f6&RjgF4FuDg7vsn$@ZGyq_T#<+>&9&M(~mtGbDRz9*S9n`jZaL*vO=7&D{fMr>??Akd!JyUaC!!Y8pm}Ob1R7!B} z2Z7_*4VjEqdgbvSj85xmMe(uT76bgm5w$@xO7jx`nDj5WUWm$6tsE7N}gZ>NYKI!2~qV6Rnk(c|?a_QF0&W_vJ9Rmz7a1FFUdtY7M<$aonqlfHn zGzK%k?4V#~kl#-MpX#iv%&5$&_(hyJX4lBg(cxV~y*=H-yLaij9*f7r!QhGG#~K^z zckddUoSL?k_$xP32=U5`FPu1j^xDV`kH`Jr{^=iUnzpdGR2zwCn#vjDoC_g*KA)l} zp=+AtLL|Bm6x7RrBa6u z9#~)BaJyZXE?+r(=%AExd1Zw&*5B8cFBHt>l>j^Fj7-9Jv-OZ*k}q%Rg_=8{cgTcV2s&!K~a=cDxFNHzxO-e>gw#ccH^cH zVs>s`)6{?WgYWbf&H@x@UM-G*VkC zmkNdACl^0`?Uk3VULR>|ZE0$3ynOZQ&CzkgFs7zv8tUuMoH`K<1{q`V`1+}n$N%*w zKf5tH{_0CF96q?ev7zCY@4R>V_dudo05gO3~mc$7G6?vD$^3W9CH=R`iaGsy%3 zW-=MmvQ8a8W}0SgB(isSHvkOp9`gJA_4Rcpj~^`*i_f1s*V^2C=-~dLfdMIHu~fQt zeZ)3yOij<2rpdXow7g72rfJ?B8w&=4sZ=_dO7H9lNG0;h%@Pa-!WUP_x&xx#-tJgz zZF*+*+Q^8<GgPm zf#8LUmo!y9b^Q1l@alQ?Jl zhKCOw+;3<7Yin&ea_Hcpg9n-#8=rsn?16oI-EOzv=N%dvn3wkeHPiTlD$u_TLRg7J;@C4s4j(+Q8e0=m z_`KfQn#jQe`#U?@5ANUh%#lMK?d^zIQ&Y2V&+x9HAppo^vr|*k4Gr~k^9zB1e|cr~ z*3@*gHWFXou>0a{Hs|rU$Hpg8sZ?WQ!vkJONz(80`~Ch)m#^gVg>t35w6r|Dd+3Gd z&iQ=4x%v4M$BrrrPiM1=q8vGNaCUy7y|s1!-aS1%-HnY6b{e?ll@*WMeel5kv!_m` zQkhb@eDcKcf&M;K;mK6`^vUBMkH<+R@F;Nv;4y*3iX2U}GdPMG3)q$wZEJP$;|rhk zba&O))!C~8exJ|h_ah=nsj8~S;|&G_iB#&7OP7<$BxB67tU|G9o6*}^TV8$X#gUPl z(=)T}?QMZTVDFycg9rA%^Q&KHGMU=~Kq!MkdTi$*VmbchEqUowI$9g?dc6(xbpT)( zhG`fJ3yX}i#)bwd<>=VN*!Vc-e06nodS+JPoH4E&#?0*8FW!9TgOAQ1Idq`0p<#4v zoCp?|mJGvasIPOow1vf`OP4R-onQW|TxLW7SX*0LURgCwi!oFS#^bI403ZNKL_t&( zB~nu(r5qcyedR;cZ}9EXt#nDVDBURyE5Z^|OLup7x0DjnrF3^LjYyZ$jkI)k-Ou-T z@BI(%-PgQe_j%^bnR(`%p{Aw;%%R3OShvo-`{xf2+w)G9IVB)ckcV((}FhL~(Gm_q@GyLT&*l%GUk1cK$t!`o0%_ze7UoZ*Wpe|M@66 z)5|Zv;((*$^zpV**Vp4@l2=h6HfZe=B%?_5Wf?B3I3|43VaS{+ooggW!L3-&uX<5A zXOOO|b$hwb_dx~N#TC$1t@n(7_v-}1v_iFXxs?wXCFs;=PM%CZ92`>_sRM8Xv+w8f za>&|A`9GOjEoQ=)jH0ozF)Sv&)a&w_z+SR9bo zPEoSdFs(JBDPfGBdJ%_G5d1bp3??EYCjG!lLx?AjiZa3&1o%=~gVpwct7S_sp!8j^ zw!Ln0ZOQk38e^mmx(x(HB|N+@_<1$qO};_gyVNmzjx%j3Fg_DV(B;<1SNGuj9Qx7s z0Z44yGc&ztue?MYs^`?+M1d3Kmb_C*wZFV|0%lhS)eNs6x=Dot&!*-K{rB!%E$I_7 zSmvtUC6>0f{sThSY%-l?&XHodi-Q!Km7UGHOoKIrl~#360j`>!{hhkYJtlHExeg4q zss@?zLXoEA0T1Rh#-ra2IB+zT0J7&ZXG{=KNs%IF*Y`iH?0ZK;*Soi>U?aZ z*j4=|?v0IoCf3f*OqpZAVZq(rw$-$lVHFr>M43S`y#XP?@%Y!qMlW0Wi@y?vwOa4} z{u$*eqkonL5=mx9YUYZ+*c#+VER(7wEOKG^?ZnSCz#fPN*LGBgya$LMrt z-m6sIML{AC-xBYZ)}{J`5eOHZ9lsF~T>?R3!y1KwF3(*1?8-}|vzFK}uFvF~8c!4? zPn}(HIV0n{p`MO*dRiJ#)|hS*08>QYhV1OXc7T@H#NWU7cx{)MSar`WSBsT6Cf)7r zH-K7?5ykW=ye(g{utijH)X9E-I=0^R{^U8)7HIw1@n#j(Y-w-rtW6l(N!+{Cv9q^l%NepDGhKK3H<%kr9pDGJiBO4o z{RI%|-P^YSd;%A~Pn9Vh%Ai(?HcIK(C6ElQbOaaRma1DY5bMTNe^MF)Ck{#F#>;|% z)M;XS_65TIZHTmpi(jl3xCm|eX+^b7O`Qn}F8x4s6O<;2wvU;)9-Lh*!0%k(i9=?w zXibRQSmDXZGk1M`L#2B&GxNj4`-jVZ%-NlMLXEAhvwv2LoSt`dPr!+#>dbt6Ji?+p zYK+8uFhxtAi;I}ns)^CDU952R_x?})W6%$VItB}};Pbo3>C0GaK%u<1aCe7hO-r}m ziU&+kZewL#Knv^0b>%Gq%Jw|F<;I`Nk`g`l(V?oM|2+KhUdF`6Viw=T#m5408&J8m zv9q`H^K%HW_V@2lW~}O)E4H*?8_`+Q>T`6;QC#UmImI>eNTcij0@Q_q?gXhIte%b8$YP= zPdOT?d)jaEw-es_Id0~;)JIj@Di-tI`D{6WuE5UGUex`3erx)2n`Q2h=eRx2gGtSo zegU@L93mOdwd}lA_6xy|+HxRyL8NnFQ-#QYSrfbW)JL=|k64}obb9Bf8f{Vwfn@AR z(FmK6@z-a@n`^HHJk2D}%+J`)3GKda^jerg4TzH9v_w6{g->9EdJK@_@er_sLNDdO zMhssOompHpkFajlV+Se2h=P{cJpnW=WYGjjF~GCg zNH9@6b_X8pt1kV?y>HxG9uQakS0~!tZY0a*e@=Ynim>xHHT7`lId&3vweis06rXBg z=D#|Q0qM^1lcHw{x!1Lo*B8Qtz72`o94@RLvoDD?IZovC1PAnT>N(7Lc3zR=_07&6 zKSJw)n4>g0`+MXzk4h=<`yI8p;x|cSzRVyADr*n&tpq=5bMLSH(rH*ci3UvcKW%tF z%-1#7AZqUFl>}a4<3?%$6=LD!N&E9Yz*IR5JL>Al0dAI5#+;pf1Z4b)9AP!)O9!&z=Edtn z+F4!+BMp)&z+-Aym?NmE$YXC!!r4Z!nRBEoTyC>$HrL+VBxs93?afsNYH63F`(^`Q zZ5SZv?eBN&IN!jUs;}?pSZPd%Tsrcw*5S|W@Y@EccjsS^wz5-NN-CjipKc(P_XKwe ztoam|m)94)tzg8YYh6Ajc}=PJrH{i9o&s2~v>J?MOuw4x#tV9fn6kT=HNwoJ;y4@x zW|x%QNL+rsZYE2UuouffXs)=u$9(w=*>ZuSqTY)?-agLQ(km){iOy@`J^J;>G7LAu zm>3g<$vju9;pu>Fq)eST!gvb{g^rmxX8iQUHae*eD3d(9o_uQ6UD_G|%suW2Cdm_z$QH_VXI={8l*?dc8<36hBvgtw(}%QP|-JfV^$N0nbZ8>U42Q267H^ zn36=O|NbMb$a%pPziLL)#w9eiTbg_9ch%e8B5(_b?kP=-u)T~decyXkt?{{lG`hZX~>xjpBZO4aj+@`g7*L-TSf73{}_rXELCSU7C&``T0%gp(bgYX?EP-})e z#O+{Q%Ra9lp5W@2(HCJ&{w$?+TO2$LNl^1iV|M)TE8GPt!$pA?XTHgGSD;FW`gNSa zUpVTUby~go2`)6%7sTb1~sZ5C!nbQKdj|0zO_)XXNgl zo}P84K0MI>|33&C2u*4pVOacBkkkb;hqTQeOIv4!hW1Vc(}Us=n3yPDeeN}8V%V*y zVjm_gFORXw@L3bfjy&`SDng`yu6%@7uU;9FeFRbQLBb*dR^@-c<;+xAWCO6b8TG5J z+WZ~~qc*!%QQG+&iupiH+WUux{e%55Tp-T!|LD;Enw8IdSmHr~Bhe*(RT2~wTn<+w zxK*K#%4_8Fy}?H6dFXl$ihM7;NVc0zdGy@?ZgT@fpsc56aT#-|o5jYgSwmCPe>Ety zZ-a%Si!`RP@`=M|z9ydv&PoWS&4V`CF06X~Yf)D&cTZV3sr?P9($ zTVSposT5@l9D`yOS$Dn+rck@BmA5xTg2F2t9383oUccH#FbI@UWRuZPDh{-%cy)Zw zf7RZ%zK3i@<6|T9D$>(c4(SSvC%0Ayk8PwfQ>`&)i|-g5|m{ZQfe*#Sa;FJ#z4&3w|ZR{$_%VB~X#4%@AR~@}=4?e!YXNQ=W z|BDDZHefS_jfy{H)~P%XfcDbn|AdS2yX8$^WlgEJDO#uZsS|l6Wh0v_@Nll#g+BoR zXKZY2vCe39Kz@9_y}y6_nYVVLtEb1VIUHGHO^)aUD@_6lsMS$3*DH5-syJO}`jA;j zt}hhGf8dpKRjI+Ld)kgHsF zUvAoe9j)pl)j7bSTpeZ}i}EOzm^8L4UzqscH4_n~G2!~ow&VI0B!ViY3G5Ss!jIzX zDQEgfA4WZv_^fJ0Bjctz`pYTmb#mMO#sJ(r$Ouxb>0PxuuK@TE8J1tpQz&mjp zdmQx`zZ>^Fdc2_Gu%{k#zIStSYT+0!`ThS2sG^VqHhP%N7xD)PNZa)fSF6I|W2V-l zbL5MtAc6eO1g>4^dp~!e`xZjm5Jty{)^51l-`vMNtO(>wa59zFH+2OQVzfM!4eWZf z>8Hde=c%xh6}BhneX6dkdWSzGRr`x-p$2N9y(&qPH2y^>DJDN1 zf5^!q^WH$q&>)TKJ1Mmy13?Y_w-x&C<8l$qSs(5ExVZ=I!El3QXJiazZu26kgAZ^G z+|oMMbmX|Y{JM`rU|SohHjkUDS9~8bSAu;|c&DVW1m#%eH^97g?O7rgojfEQ?^5ec z1&Veoc1N~{EhkaLl2FU4RM%=vUR2Iez26?cdFZZpLORaIPaBMF+P)F7xbGb<4@LtJaq{xcsl97BRmO6|OE=o?CBnzCahGtU8(d&2JS z#bOi4(dfi=xpu7@T4kcLmBW&9-1!?rnBy(j5mPXs9i^}&9K|nfuc&Pnn+@$xWS!>n z<&=oOgQAoacF&p$B(?x#3}+`WSOrRLw=c#;;Q z#;&4L4k=hvZl^^SLOLNSV8EH<^OweUrk@WHNjykZLwr}Ex>avtY&y?z49)&yI=#R3 z?-a%~P7FVwgILu@EN$95L-I0UA`S6%GFi61`6dV*L9a4U`cYhTh{`r9h9Uf5D_1Ku3AEx;T*+M8W` zYGS3j`0e%5YWh@+{S*vX3jE_~J4vPON;?%qo&f8ls4 z(#?c%hYN25Z~03WNb&0phEu7214&>WpC{=bLmMl|rH0oi$A;^TACEChdkQ1wUTv9`~9Dc!DS8P_P!{Vcod%MGq< zu@SasMMkcxbC1LkNb^4R&iar7>n<^z z3?>;)Z`mF1G=LP38%4_n85ay1-A`{H_4M#`}BcuObbHp#*0`Q6&KPMSh2Q#TY6RZa$<3};{)`(mv9rxn5sMO4-eK( zSZ%1^ji{CNBM9ay{Q^59N0e}GBo@#_feVum!Q8s|gl5s!+XIPnt*vOCF9U1DDJ{;X zm+N|R$e{T1ra#od$M0goWWk>8SQu0}X^42(&6zJNuHj!ZCdK`g?*`V?`$^cGyIR9j zek`PE>80jl%~EF*R8x~kHa}+GJbkkr7MLX78U<0&85*N@$Dc3MPeZ`IY+65_(yIx-z8^&_PGU$ ztb-6nNJn{pE#DwLRZ~3PP-4aXd`8fCj?n$*K$jnXedsL}O7vi9F?ef!ppSnc#L`3j z)zq|$$4SUF-dtqT7%m+x2D+NU%R(ZeLYZ=w<<$tW$v&?SzW86;@ZloFd+w-BOwN;x zgJgAjtc>S`R9AF)KwEcTy8l(f4W7t?l74GDKE-hm`X0!?+= zIVOz8S{#far<#~_e*FSTbk^W1DYzhbxHpk^WhrNY9X5d_fAv0lRp0bcHOu#m1&eC} zrq_s^bgDMO)48cLDWcF8448m6sy0!)i7qO-gz-H7Uk8){0*#&@%r-%|c^#GO954HP z72X1t%gTbN)5o)aFZhmo$ZsURw25yV$9dfgoh8;r=+!6+_$;cbc2_##36DGE*Zi5C zB);N})oass=R@g-u6(|V=u{-Y#l!Wo6u)9!Qv*r4hv(K8wD7N?bYDbu&pVc}gCMw< zgW}@4?-va3GYylT@4rAhSp%K=uXZ_nuiaQQi!1HUu!PHl`L_d~MNi=V84kbubZ6&7 z(C3L$#BedgqXv1uBqWsI`R)AidAAxI@8acEa&TBm zy)%XS@hiHjEaC|71L4tx&Z6K(VAl5p5Kcx!h%zF*8Gd}cycs?mS*aVP1i6(x20tan zN5A`}($U}2&m>Qk9dV{-k61-+5y zr;B6P2faB<)`&vic1^X@QL4)P;8M(X_rCeT;g^fu?J5}nvTg?x@6QSj4yIlLnJ5r$ zTWLBBUg{@0=clJ>7|0^D840TlyKY;MgCg~)ZWc5RJBv()lcImZ)Uq%>;wMAL^IkeD z5AGsTVoGu3bwUkZP6Qg4LLtZ?j3p1Og0XiLPW;-BLta03&y?(v43Cz8H(L6Wr+raa z>CYQAe7EqsgQuLDOl^+|X;u5B6VX$Bx)FXkdOBI-jJJ#wM2b-!OpTN}4*@*Hgh~>b*Tbz}O^LZFPI)*Bu-3O&L zVRTSX3o3rkq7IS~x~fLUODZxB?Olf@G`;;SutP;hUJgWRrG~mh2x;Lrt+G6x8NX{G z)op0a)bnji{_5l5YQXxd&#+77{dBS4iR@i}VhYFR+z76lHYOMW18GU zT?RQZSQp-@4|@K+{;&UO%*XX>h(=di4Gm$7zC2=42V5o)&W3vGQmmV0mW!7ilh*Jg zXe)j)oMan_l}877fj!92KwLaWU3D>reI3`CQzAhYq8+@p#3G> z4bgjN)R=+SK^>pH%AWkTIWI|E+IA~)Sj3zqW-zMBEGZs@P-6lM5WS(tiq7In9oBHE z{~R%Xd36=5M-3~>Ez)I6DIt+Wk4KgW!XEtT8+L_{r*>)ej*E!Z_QDn}gcP(NAdc}@ zxw*4=<@VD{kzsZd2!!_)FbqY|nB5D_w28Cp3y9v{3x|7=STMcS=Y&SIUx zLRbd8e}lA2lep6xsx-$oUEj#L+&p*iWThgvz}`kG;D5VY#`-emFkW5o?PXx&5Y|!9 zs*jcOJS%f}jcsH*hL4`UVvWb$4Od6di!Q{Sn^!MuDE6!=gnI`B93!-`(V_dEnYHbx z{$5lCnT@7(sq2xBoYpkkR|_voK+q*1tyP&tOoi%%AEtO=u;fLhliU-X6qU|rwO5Fs zC<$|~JpG3uGw%-B*Md|@i6N3Nok)sgPx|9T!el6T_~`OU&Qq|RSEAK8mn-h14Nun5 zGD;t|%<$DBSt7KRRPA1RMCs=~QkNuqW??ErHT@DvX2aiMqSLVPb^Ner1#P6L{8MVt ziSi(6%b_)*X~I`sAr2~|#b_tbrkv#QJ}?xYRg?tbo^I?J!_I*7o*}) z^VTN$$>ZBd{alupsm%3nfO>yTbWe<@#4u~NY-wj8h8|>yRWSEv|KN=>A+SyXaOc+1 z$$0bTKv;NapJw6z&I0%nL^V>jsW1|*cK8N*dKwB+Nd3}d7vSL$<@XRxhH5lRW)!)f ztXu@Z^SHw0BGA1F1KS$9N#j~qUc8;JUEn1xe8{8%%r8sltg?u{HPAuypm@10WIgV; zD#@dD@xJ=Rc+9Mhk-T2OTvXw`F2?5;h;v zocxG?TZ9Nzv$YnC9F+#qJ4NP6AV9@IT!NjNrRY(O8qQB#xSzuQU0uI9H3UQUPpqaYW!2umTR1EVpWETk0H6eSZ4h!UOs{VgB!5=5Pl6&Kt(mRZIWle$E_Iyy)^;p z(S6(qqL4KmEzHCH1x`SWLCNn51fA`^_E|^wp@aVJ<9{Ga{Lzvm+cxVxIODEh|4(wp z^aMyefSZ1;Iy5!5j!BxG;U61m#GmEmLl=01Y|Q)9!~VxF=unt3^4}?~K!|9UnV3;0 zDc%0D!?(p~-a=8~x?MS`jMj9NpKPu}{Fw&dA+lj0Mg)X3a74G8iUcQFeWc(=`E{t= zCzONb#>U2S7OwvCO*`pGC=)fg_Y!3r17EeOLOHxaWyyNFK_~eA2mx}HL*PQeK7Z&J zi<02T;6`bVZRs=w*~6;lp9ao*DUJ5~LSh9n(xaSva{}1Oy@s8~d)6D7EVG^%|5fei zUVreX|8!s9FgAg<!}$83d3qrs>ketU^b8C&>Xk8D^Fb`S;#i_A1K zOSz}O;d)kP0qMFDlqKmJ#Dc9T7Ze*#-Z_j!FhoyXZH#}92Xc%&|+n`U_MbO z{>D(xvG3i~hRO)U&Kq)aQ1@D6ASx2~cc6e|w!|Drq0uCZMC+)SpVljnafT>$2StLy z!PRYr`Ook%23*9NFD$~U-;PP&J35-8hzhlyVPz{o!s)*af(n%{Bko%Z3zKayKp;?_ z4tp6^ewfvC15w+jl+1`&<$AZfNG9F9NRY%Jzkk?Pc0I!~e$f7xqNpEjtN1BdTFdSA z0RlPmg+9Eog}*)v#ICbnO&Fd}e?Ft4((`ZyOLm7Ad)SJLRs*x$MrKN0>VH&O*e9%C zb0}Tl0h#36Ywr$VEP{5NmRL7xK$9i;d}Bk((oz>l+5m8gbYe?5G@qnfe*KuApjYMc z{P;PQ!w>?|ujb|Bo1K-3h=>>%aFo64&L#o@jE^5?z#aKdwbwZCon=+Fu$H zYt9-^VtanIn=U|v9|#=QltfTX3Nb{Tq~z^vBo!RdIhUunC-g9YkHe zxVXw(SfL|lF@t$i4|)QvvHw)k;31eE=MWsPaNwypm~#uA$vyTz92Ezw0nQ3xNt21Z z>5SX%x!kcNv4s_{Kq=-6+Zv>rgsB6B+}fO=A6x}+LGB7E{!oeEjtR8EJc|nQ@1hnO zyF8#j7>4EFP+LZ0fux|{cr4VRRsghlLH9pHT|3_USO7b81_8r<8>+MHiA+0x{DzDaSOOQLRQdWYzDyc z#@ap@xQdcPA-OKRtln$Lf`*8^in$5Q45`2DNtETVkIRs1Q^>Rx^&MhzXgDtllYcbZ zGdN4Cg(@aU3wr|Ss9eK_AVDLq-a(qx;P*%2C1zW8f)eSj?SrptsDw3sN%KKG&mty6 zIX*Io=BO{eQe%E^wV+_t;`wA8t|)nvhE5w|-Ge4D22pw(l6UCuCZuV~9%w2SfIZTR zIsh*XKNEV48oC1Gxd;(;baZrebdE@HRiGv1gvw3qnKyRPEM-A^(?TDXu-RF z4h}Mr`}2XIAg+|5ZCI}|iW;j7HW{P$)Fa@_OpW9IK{UrRWiFtBf`$8d=GLtAytj~n z4xZfj*Mm4m5sNDr(T>?rdAV#0=!-&OFy=ctQKuK?$nyvY1hz?29S zUpsl9NJzXHJ+giNfK+7MIjuR8z%XKASZV2)|J{{=6nhcTXiF`5^gW$4vM!Q_>(9i? zZlYes^wIkV#1yl`CXkc&u5lr6ssK9#(I3Ha1bW`9U|l5SGks!jAZJkW`efa1N&qjI z6p9*>W@Ls1;*2rpPG`16mNKwfH7uiSnW{xgV6`CnJ^5!f5(NG;nGD5bt5@&(p5}Y; z%!8Vj5gvux)4-`~kr5rhfrJai{tvd4&zru`EIIYuLyaf0}^)6B8 zGT!xznRz8>xepG};qMF7|AK*wKV-m(x-{OgM}(!1k5l?|J$G-3Mw?X}h` zoF2j&gLnuiQhoT;Z_<{OC|oyDHu`WwOQ>tMYny5Te@FlG;a%n9mz!*@m02{&l*gKXY(Q$v9XDFm>(Uf z>XY-z)D(n1Zcu1ttq(_N$;WI;+gOU$Bm42%*u8tTU$P5g%`t^F7mCUNVUS2WKGWGL zSv_Z}y|K}UwYfA1QY+jDbqcV{5S@$Fdc>B&`RP8cbS z>^{2YECS;nm}zcQp~luixQC*{X^^oAg5z-o)l|;>2uJkh2U7%H;V>qILoPOHGl~Q& zmdg<(G)WMRBz_3wRV)}vGb9^gb=fwXlDg$THL^^AjEjs#R&D}Krl++jQzbErl&Ley z2+i(l(wjGL$?B zF1AqKwh5KC#_Zf&IU`8c>^2kUlbh{rTu^t(hM^F3+6#ou!fAJT>{^pZr z0p<(rHzN(*YoQB1*;9Sqrl$#smfRf}6&Kt+Z*yj%eYulfx!o9ch+Q8OausEpIQ-Mj zuCTLx=d5P8T`4$UzQybZSWpgQ9Rbt^8wGJ4SkCkzSe-fW>3$mykE(1j1mH=~=h6#| z9}BTTMf2>qS>+Iay7cq)wdEvz@Al6LP}9*R#3v#m80IQJ{)u~DFn#c({BOKhUV6hW+J;<;5=syX+R=36cp8Qu%ddzSXoL$HXiTlpxn(&a zQDHQ~D~HrF17${rAJgP?-!S|kS4T#`E6MtRL=Jk36;YaM`n_|#+%fJIchFB!VGnS`}=7Doz{S-DXWI{-?`F! zd0`hs<=_c3%u97EnPg*_}5khrH^Y0?1ilH zH23ZM7h~?3)|*G@8jpt!-w~8{u*7xO{*tGHbwtTOy(=mzhM)g9His3qRxnLZfAGx!53dc@dQEhhsG4I@1P^SVtz|4Y1{eiexE>rxx-CvBxhzVfAp`; z62y(v2vQnu z13I!qd?cbW9eO-2;P(Ew4@}dP=~PmnLIx$StWz2snGitVR=3)9paiA>JWpKL#Y)tN zA&(eP(ST z8A<&tRJUWrs%xRkSvO}lET%X^ys|J-m#?ylZyf(r@C zDpYH8tnElEun;8jC!S*9UQhwK;Z>cRn?>Cd-|kQRvoGG}3zcxmXr#vV%#+ST}>4&&`thUBy3GnW)@DEZM#pJU~wIHS`| z{AjtJRZJd3w1?MIFv1ZCv0LljGUs7>AL6OZjmeYrcdm4a!?cl5DP-flJ)QNt+B&PZ zlfCX``bHU-w>1t9dVSlETPF@JGM_ORp+|(D#K8?#44*??t3JIOvr8C8ecFjQJ2I_`;1uRB z@#l6}?X4?ec5>0e@#50VTZ^?j)_!9I^!Z`DGq2w*d*G})VgthvLp90C;a|Q8g4CI} z+x#Zx_xC3=9iNl9l2kRA%|R(c1!8wwy=P~5)pNy+(dJRm!la2^g{g{GP=#ZK;nVHh zL(WU@#RU;)x}Y`ZPd!8LfvSc}VIDhd2z;v0*V>xUDvAUO8k^>CxI!lV3!xn#g^aj+qIAjE!iU;SVx=rW6`) zUz{M_ynTNrx@A38@%C+>yZEbqvCVfRZcOXLNn{g)ITisn|E;~*5XJn-JAImuR5t9) zV66YW5hMZoJ43&?*HQJ|z*TYJPN4J#4Pf(jHGr^*+5F*7UsMp&c>5y#Nc@Phpn*Hx z>}@^I!_vx$|KAi^x<9^MKAtbRrYq}T;RV|;z0}?v^K^ty8w@X7G zuvmL!>EvX>z|Huiz)eUnSBSAKgKn5H`aSJBj*g9K+Wx`E<0iTvs)QO=E*h5Gz3H{X zDY>$RB(a8QGntdfxXKd@3g+oM%^@P1wbXzA5H{0(YxHL{Q@L_Hh*sXNVD-80-P%Jv zy*9mh8WF0_&~UZIz)_#ea)r9%E@rEmjKF7<>pYfy3vw-#s4(*gJ<)p^z~1O7G)cLS z0{2=7PQ&d=GlK3w^`JGKZFVlE8H(qmM$_EX5p+t`NY=%SO{;71avA06n0c03?OUYT zH|A|?1Ji3YpAFac6)nf9174mcmQ+W`=~>l�`ul7l497JN6CRceF>W#qXx`GjE-u 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 E8}T?SegFUf From b63737a0e3f2a9b6ef13a9450fdcf33b2a6bd8aa Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 2 Sep 2021 14:21:36 +0200 Subject: [PATCH 044/450] render on farm / local flowchart --- website/docs/artist_hosts_nuke_tut.md | 1 + .../assets/nuke_tut/nuke_RenderLocalFarm.png | Bin 0 -> 66946 bytes 2 files changed, 1 insertion(+) create mode 100644 website/docs/assets/nuke_tut/nuke_RenderLocalFarm.png diff --git a/website/docs/artist_hosts_nuke_tut.md b/website/docs/artist_hosts_nuke_tut.md index d285d8a6ff..0ec1e104b9 100644 --- a/website/docs/artist_hosts_nuke_tut.md +++ b/website/docs/artist_hosts_nuke_tut.md @@ -162,6 +162,7 @@ If you wish to check your render before publishing, you can use your local machi If you want to render and publish on the farm in one go, run publish with On farm option selected in the write node to render and make the review on farm. +![Versionless](assets/nuke_tut/nuke_RenderLocalFarm.png) ## Version-less Render diff --git a/website/docs/assets/nuke_tut/nuke_RenderLocalFarm.png b/website/docs/assets/nuke_tut/nuke_RenderLocalFarm.png new file mode 100644 index 0000000000000000000000000000000000000000..4c4c8977a0bdfbcfed3c509c5aaf2b626c2b4341 GIT binary patch literal 66946 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 Fe*pnbU4H-o literal 0 HcmV?d00001 From 8be9838e011f187641ff4cc78c087c2fc1e513a6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Sep 2021 18:47:03 +0200 Subject: [PATCH 045/450] #1794 - added fixtures for db connections --- tests/lib/testing_wrapper.py | 42 +++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_wrapper.py index 1ff42158db..a389741ce3 100644 --- a/tests/lib/testing_wrapper.py +++ b/tests/lib/testing_wrapper.py @@ -11,7 +11,21 @@ from tests.lib.file_handler import RemoteFileHandler class TestCase: + """Generic test class for testing + Implemented fixtures: + monkeypatch_session - fixture for env vars with session scope + download_test_data - tmp folder with extracted data from GDrive + env_var - sets env vars from input file + db_setup - prepares avalon AND openpype DBs for testing from + binary dumps from input data + dbcon - returns DBConnection to AvalonDB + dbcon_openpype - returns DBConnection for OpenpypeMongoDB + + Not implemented: + last_workfile_path - returns path to testing workfile + + """ TEST_OPENPYPE_MONGO = "mongodb://localhost:27017" TEST_DB_NAME = "test_db" TEST_PROJECT_NAME = "test_project" @@ -19,9 +33,11 @@ class TestCase: REPRESENTATION_ID = "60e578d0c987036c6a7b741d" - TEST_FILES = [ - ("1eCwPljuJeOI8A3aisfOIBKKjcmIycTEt", "test_site_operations.zip", "") - ] + TEST_FILES = [] + + PROJECT = "test_project" + ASSET = "test_asset" + TASK = "test_task" @pytest.fixture(scope='session') def monkeypatch_session(self): @@ -47,6 +63,7 @@ class TestCase: RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) yield tmpdir + print("Removing {}".format(tmpdir)) shutil.rmtree(tmpdir) @pytest.fixture(scope="module") @@ -105,4 +122,23 @@ class TestCase: """ from avalon.api import AvalonMongoDB dbcon = AvalonMongoDB() + dbcon.Session["AVALON_PROJECT"] = self.TEST_PROJECT_NAME yield dbcon + + @pytest.fixture(scope="module") + def dbcon_openpype(self, db_setup): + """Provide test database connection for OP settings. + + Database prepared from dumps with 'db_setup' fixture. + """ + from openpype.lib import OpenPypeMongoConnection + mongo_client = OpenPypeMongoConnection.get_mongo_client() + yield mongo_client[self.TEST_OPENPYPE_NAME]["settings"] + + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data): + raise NotImplemented + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + raise NotImplemented From 216d2ab30e46a08b87bd0c33774cf058e1b4ab7e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Sep 2021 18:48:00 +0200 Subject: [PATCH 046/450] #1794 - added basic test for publishing in Maya --- .../sync_server/test_publish_in_maya.py | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 tests/unit/openpype/modules/sync_server/test_publish_in_maya.py diff --git a/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py b/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py new file mode 100644 index 0000000000..2aca0314dc --- /dev/null +++ b/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py @@ -0,0 +1,206 @@ +import pytest +import sys +import os +import shutil + +from tests.lib.testing_wrapper import TestCase + + +class TestPublishInMaya(TestCase): + """Basic test case for publishing in Maya + + Uses generic TestCase to prepare fixtures for test data, testing DBs, + env vars. + + Opens Maya, run publish on prepared workile. + + Then checks content of DB (if subset, version, representations were + created. + Checks tmp folder if all expected files were published. + + """ + TEST_FILES = [ + ("1pOwjA_VVBc6ooTZyFxtAwLS2KZHaBlkY", "test_maya_publish.zip", "") + ] + + APP = "maya" + APP_VARIANT = "2019" + + APP_NAME = "{}/{}".format(APP, APP_VARIANT) + + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data): + """Get last_workfile_path from source data. + + Maya expects workfile in proper folder, so copy is done first. + """ + src_path = os.path.join(download_test_data, + "input", + "data", + "test_project_test_asset_TestTask_v001.mb") + dest_folder = os.path.join(download_test_data, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + os.makedirs(dest_folder) + dest_path = os.path.join(dest_folder, + "test_project_test_asset_TestTask_v001.mb") + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Copy""" + startup_path = os.path.join(download_test_data, + "input", + "startup", + "userSetup.py") + from openpype.hosts import maya + maya_dir = os.path.dirname(os.path.abspath(maya.__file__)) + shutil.move(os.path.join(maya_dir, "startup", "userSetup.py"), + os.path.join(maya_dir, "startup", "userSetup.tmp") + ) + shutil.copy(startup_path, + os.path.join(maya_dir, "startup", "userSetup.py")) + yield os.path.join(maya_dir, "startup", "userSetup.py") + + shutil.move(os.path.join(maya_dir, "startup", "userSetup.tmp"), + os.path.join(maya_dir, "startup", "userSetup.py")) + + @pytest.fixture(scope="module") + def launched_app(self, dbcon, download_test_data, last_workfile_path, + startup_scripts): + """Get sync_server_module from ModulesManager""" + root_key = "config.roots.work.{}".format("windows") # TEMP + dbcon.update_one( + {"type": "project"}, + {"$set": + { + root_key: download_test_data + }} + ) + + from openpype import PACKAGE_DIR + + # Path to OpenPype's schema + schema_path = os.path.join( + os.path.dirname(PACKAGE_DIR), + "schema" + ) + os.environ["AVALON_SCHEMA"] = schema_path # TEMP + + import openpype + openpype.install() + os.environ["OPENPYPE_EXECUTABLE"] = sys.executable + from openpype.lib import ApplicationManager + + application_manager = ApplicationManager() + data = { + "last_workfile_path": last_workfile_path, + "start_last_workfile": True, + "project_name": self.PROJECT, + "asset_name": self.ASSET, + "task_name": self.TASK + } + + yield application_manager.launch(self.APP_NAME, **data) + + @pytest.fixture(scope="module") + def publish_finished(self, dbcon, launched_app): + """Dummy fixture waiting for publish to finish""" + import time + while launched_app.poll() is None: + time.sleep(0.5) + + # some clean exit test possible? + print("Publish finished") + + def test_db_asserts(self, dbcon, publish_finished): + print("test_db_asserts") + assert 5 == dbcon.find({"type": "version"}).count(), \ + "Not expected no of versions" + + assert 0 == \ + dbcon.find({"type": "version", "name": {"$ne": 1}}).count(), \ + "Only versions with 1 expected" + + assert 1 == \ + dbcon.find({"type": "subset", "name": "modelMain"}).count(), \ + "modelMain subset must be present" + + assert 1 == \ + dbcon.find( + {"type": "subset", "name": "workfileTest_task"}).count(), \ + "workfileTest_task subset must be present" + + assert 11 == dbcon.find({"type": "representation"}).count(), \ + "Not expected no of representations" + + assert 2 == dbcon.find({"type": "representation", + "context.subset": "modelMain", + "context.ext": "abc"}).count(), \ + "Not expected no of representations with ext 'abc'" + + assert 2 == dbcon.find({"type": "representation", + "context.subset": "modelMain", + "context.ext": "ma"}).count(), \ + "Not expected no of representations with ext 'abc'" + + def test_files(self, dbcon, publish_finished, download_test_data): + print("test_files") + # hero files + hero_folder = os.path.join(download_test_data, + self.PROJECT, + self.ASSET, + "publish", + "model", + "modelMain", + "hero") + + assert os.path.exists( + os.path.join(hero_folder, + "test_project_test_asset_modelMain_hero.ma") + ), "test_project_test_asset_modelMain_hero.ma doesn't exist" + + assert os.path.exists( + os.path.join(hero_folder, + "test_project_test_asset_modelMain_hero.abc") + ), "test_project_test_asset_modelMain_hero.abc doesn't exist" + + # version files + version_folder = os.path.join(download_test_data, + self.PROJECT, + self.ASSET, + "publish", + "model", + "modelMain", + "v001") + + assert os.path.exists( + os.path.join(version_folder, + "test_project_test_asset_modelMain_v001.ma") + ), "test_project_test_asset_modelMain_v001.ma doesn't exist" + + assert os.path.exists( + os.path.join(version_folder, + "test_project_test_asset_modelMain_v001.abc") + ), "test_project_test_asset_modelMain_v001.abc doesn't exist" + + # workfile files + workfile_folder = os.path.join(download_test_data, + self.PROJECT, + self.ASSET, + "publish", + "workfile", + "workfileTest_task", + "v001") + + assert os.path.exists( + os.path.join(workfile_folder, + "test_project_test_asset_workfileTest_task_v001.mb") + ), "test_project_test_asset_workfileTest_task_v001.mb doesn't exist" + +if __name__ == "__main__": + test_case = TestPublishInMaya() From 0b02e5348a8a53e6ac2326cc3bdc76acc73f36c3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Sep 2021 19:01:03 +0200 Subject: [PATCH 047/450] #1794 - fixes from review --- tests/lib/README.md | 6 +++--- tests/lib/db_handler.py | 4 ++-- tests/lib/testing_wrapper.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/lib/README.md b/tests/lib/README.md index 1c2b188d84..0384cd2ff0 100644 --- a/tests/lib/README.md +++ b/tests/lib/README.md @@ -10,7 +10,7 @@ Folder for libs and tooling for automatic testing. - file_handler.py - class to download test data from GDrive - downloads data from (list) of files from GDrive - - checks md5 if file ok + - check file integrity with MD5 hash - unzips if zip - testing_wrapper.py - base class to use for testing @@ -34,11 +34,11 @@ Currently it is expected that test file will be zip file with structure: - expected - expected files (not implemented yet) - input - data - test data (workfiles, images etc) - - dumps - folder for BSOn dumps from (`mongodump`) + - dumps - folder for BSON dumps from (`mongodump`) - env_vars env_vars.json - dictionary with environment variables {key:value} - - sql - sql files to load with `mongoimport` (human readable) + - json - json files to load with `mongoimport` (human readable) Example diff --git a/tests/lib/db_handler.py b/tests/lib/db_handler.py index c38f351b76..9be70895da 100644 --- a/tests/lib/db_handler.py +++ b/tests/lib/db_handler.py @@ -19,9 +19,9 @@ class DBHandler: if host: if all([user, password]): host = "{}:{}@{}".format(user, password, host) - uri = 'mongodb://{}:{}'.format(host, port or 27017) + self.uri = 'mongodb://{}:{}'.format(host, port or 27017) - assert uri, "Must have uri to MongoDB" + assert self.uri, "Must have uri to MongoDB" self.client = pymongo.MongoClient(uri) self.db = None diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_wrapper.py index a389741ce3..b2a89edec7 100644 --- a/tests/lib/testing_wrapper.py +++ b/tests/lib/testing_wrapper.py @@ -88,7 +88,7 @@ class TestCase: all_vars.update(vars(TestCase)) # TODO check value = value.format(**all_vars) print("Setting {}:{}".format(key, value)) - monkeypatch_session.setenv(key, value) + monkeypatch_session.setenv(key, str(value)) import openpype openpype_root = os.path.dirname(os.path.dirname(openpype.__file__)) From 5701cfed2b6018141f8c4a89b0f9428c917c7686 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Sep 2021 19:09:21 +0200 Subject: [PATCH 048/450] Hound --- openpype/plugins/publish/collect_host_name.py | 1 - tests/lib/testing_wrapper.py | 4 ++-- .../sync_server/test_publish_in_maya.py | 20 +++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/collect_host_name.py b/openpype/plugins/publish/collect_host_name.py index 17cb59b212..b731e3ed26 100644 --- a/openpype/plugins/publish/collect_host_name.py +++ b/openpype/plugins/publish/collect_host_name.py @@ -35,4 +35,3 @@ class CollectHostName(pyblish.api.ContextPlugin): host_name = app.host_name context.data["hostName"] = host_name - diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_wrapper.py index b2a89edec7..0a7c9a382f 100644 --- a/tests/lib/testing_wrapper.py +++ b/tests/lib/testing_wrapper.py @@ -137,8 +137,8 @@ class TestCase: @pytest.fixture(scope="module") def last_workfile_path(self, download_test_data): - raise NotImplemented + raise NotImplementedError @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): - raise NotImplemented + raise NotImplementedError diff --git a/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py b/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py index 2aca0314dc..612a657c12 100644 --- a/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py +++ b/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py @@ -122,18 +122,17 @@ class TestPublishInMaya(TestCase): assert 5 == dbcon.find({"type": "version"}).count(), \ "Not expected no of versions" - assert 0 == \ - dbcon.find({"type": "version", "name": {"$ne": 1}}).count(), \ - "Only versions with 1 expected" + assert 0 == dbcon.find({"type": "version", + "name": {"$ne": 1}}).count(), \ + "Only versions with 1 expected" - assert 1 == \ - dbcon.find({"type": "subset", "name": "modelMain"}).count(), \ - "modelMain subset must be present" + assert 1 == dbcon.find({"type": "subset", + "name": "modelMain"}).count(), \ + "modelMain subset must be present" - assert 1 == \ - dbcon.find( - {"type": "subset", "name": "workfileTest_task"}).count(), \ - "workfileTest_task subset must be present" + assert 1 == dbcon.find({"type": "subset", + "name": "workfileTest_task"}).count(), \ + "workfileTest_task subset must be present" assert 11 == dbcon.find({"type": "representation"}).count(), \ "Not expected no of representations" @@ -202,5 +201,6 @@ class TestPublishInMaya(TestCase): "test_project_test_asset_workfileTest_task_v001.mb") ), "test_project_test_asset_workfileTest_task_v001.mb doesn't exist" + if __name__ == "__main__": test_case = TestPublishInMaya() From b6754d8827a1761d2a70792713bf4317a5c6a25d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Sep 2021 14:46:15 +0200 Subject: [PATCH 049/450] added single selection option to task type enum --- openpype/settings/entities/enum_entity.py | 57 +++++++++++++++++++---- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index cb532c5ae0..6c0e63fa1f 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -376,11 +376,16 @@ class TaskTypeEnumEntity(BaseEnumEntity): schema_types = ["task-types-enum"] def _item_initalization(self): - self.multiselection = True - self.value_on_not_set = [] + self.multiselection = self.schema_data.get("multiselection", True) + if self.multiselection: + self.valid_value_types = (list, ) + self.value_on_not_set = [] + else: + self.valid_value_types = (STRING_TYPE, ) + self.value_on_not_set = "" + self.enum_items = [] self.valid_keys = set() - self.valid_value_types = (list, ) self.placeholder = None def _get_enum_values(self): @@ -396,15 +401,51 @@ class TaskTypeEnumEntity(BaseEnumEntity): return enum_items, valid_keys + def _convert_value_for_current_state(self, source_value): + if self.multiselection: + output = [] + for key in source_value: + if key in self.valid_keys: + output.append(key) + return output + + if source_value not in self.valid_keys: + # Take first item from enum items + for item in self.enum_items: + for key in item.keys(): + source_value = key + break + return source_value + def set_override_state(self, *args, **kwargs): super(TaskTypeEnumEntity, self).set_override_state(*args, **kwargs) self.enum_items, self.valid_keys = self._get_enum_values() - new_value = [] - for key in self._current_value: - if key in self.valid_keys: - new_value.append(key) - self._current_value = new_value + + if self.multiselection: + new_value = [] + for key in self._current_value: + if key in self.valid_keys: + new_value.append(key) + + if self._current_value != new_value: + self.set(new_value) + else: + if not self.enum_items: + self.valid_keys.add("") + self.enum_items.append({"": "< Empty >"}) + + for item in self.enum_items: + for key in item.keys(): + value_on_not_set = key + break + + self.value_on_not_set = value_on_not_set + if ( + self._current_value is NOT_SET + or self._current_value not in self.valid_keys + ): + self.set(value_on_not_set) class ProvidersEnum(BaseEnumEntity): From 64e7dbb3475e326d999b094c88d2c9579b36eb4b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Sep 2021 14:46:29 +0200 Subject: [PATCH 050/450] fixed valid_value_types for providers --- openpype/settings/entities/enum_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 6c0e63fa1f..ee54bc6e02 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -456,7 +456,7 @@ class ProvidersEnum(BaseEnumEntity): self.value_on_not_set = "" self.enum_items = [] self.valid_keys = set() - self.valid_value_types = (str, ) + self.valid_value_types = (STRING_TYPE, ) self.placeholder = None def _get_enum_values(self): From 8399de95cb9cb71e3d8b185267724aebb7256ce4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Sep 2021 16:51:11 +0200 Subject: [PATCH 051/450] nuke, resolve, hiero: precollector order lest then 0.5 --- openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_workfile.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_workfile.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_writes.py | 2 +- openpype/hosts/resolve/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/resolve/plugins/publish/precollect_workfile.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py index b0b171fb61..80c6abbaef 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py @@ -5,7 +5,7 @@ import pyblish.api class PreCollectClipEffects(pyblish.api.InstancePlugin): """Collect soft effects instances.""" - order = pyblish.api.CollectorOrder - 0.579 + order = pyblish.api.CollectorOrder - 0.479 label = "Precollect Clip Effects Instances" families = ["clip"] diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 9b529edf88..936ea2be58 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -13,7 +13,7 @@ from pprint import pformat class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Precollect Instances" hosts = ["hiero"] diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 530a433423..ff5d516065 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -12,7 +12,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.6 + order = pyblish.api.CollectorOrder - 0.5 def process(self, context): diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index c2c25d0627..75d0b4f9a9 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -8,7 +8,7 @@ from avalon.nuke import lib as anlib class PreCollectNukeInstances(pyblish.api.ContextPlugin): """Collect all nodes with Avalon knob.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Pre-collect Instances" hosts = ["nuke", "nukeassist"] diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 5d3eb5f609..8b1ccb8cef 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -9,7 +9,7 @@ reload(anlib) class CollectWorkfile(pyblish.api.ContextPlugin): """Collect current script for publish.""" - order = pyblish.api.CollectorOrder - 0.60 + order = pyblish.api.CollectorOrder - 0.50 label = "Pre-collect Workfile" hosts = ['nuke'] diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index 0b5fbc0479..47189c31fc 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -11,7 +11,7 @@ from avalon import io, api class CollectNukeWrites(pyblish.api.InstancePlugin): """Collect all write nodes.""" - order = pyblish.api.CollectorOrder - 0.58 + order = pyblish.api.CollectorOrder - 0.48 label = "Pre-collect Writes" hosts = ["nuke", "nukeassist"] families = ["write"] diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index 95b891d95a..8f1a13a4e5 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -8,7 +8,7 @@ from pprint import pformat class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Precollect Instances" hosts = ["resolve"] diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index ee05fb6f13..1333516177 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -13,7 +13,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Precollect the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.6 + order = pyblish.api.CollectorOrder - 0.5 def process(self, context): From 7e088d6b977f02560001cfef82650b8a47d16684 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Sep 2021 17:45:32 +0200 Subject: [PATCH 052/450] #1794 - moved to better folder Implemented comparing published files with expected Changed approach to userSetup --- .../hosts/maya}/test_publish_in_maya.py | 101 +++++++----------- 1 file changed, 37 insertions(+), 64 deletions(-) rename tests/{unit/openpype/modules/sync_server => integration/hosts/maya}/test_publish_in_maya.py (61%) diff --git a/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py similarity index 61% rename from tests/unit/openpype/modules/sync_server/test_publish_in_maya.py rename to tests/integration/hosts/maya/test_publish_in_maya.py index 612a657c12..fd8882c349 100644 --- a/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -2,6 +2,7 @@ import pytest import sys import os import shutil +import glob from tests.lib.testing_wrapper import TestCase @@ -28,6 +29,8 @@ class TestPublishInMaya(TestCase): APP_NAME = "{}/{}".format(APP, APP_VARIANT) + TIMEOUT = 120 # publish timeout + @pytest.fixture(scope="module") def last_workfile_path(self, download_test_data): """Get last_workfile_path from source data. @@ -52,22 +55,14 @@ class TestPublishInMaya(TestCase): @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): - """Copy""" + """Points Maya to userSetup file from input data""" startup_path = os.path.join(download_test_data, "input", - "startup", - "userSetup.py") - from openpype.hosts import maya - maya_dir = os.path.dirname(os.path.abspath(maya.__file__)) - shutil.move(os.path.join(maya_dir, "startup", "userSetup.py"), - os.path.join(maya_dir, "startup", "userSetup.tmp") - ) - shutil.copy(startup_path, - os.path.join(maya_dir, "startup", "userSetup.py")) - yield os.path.join(maya_dir, "startup", "userSetup.py") - - shutil.move(os.path.join(maya_dir, "startup", "userSetup.tmp"), - os.path.join(maya_dir, "startup", "userSetup.py")) + "startup") + original_pythonpath = os.environ.get("PYTHONPATH") + monkeypatch_session.setenv("PYTHONPATH", + "{};{}".format(original_pythonpath, + startup_path)) @pytest.fixture(scope="module") def launched_app(self, dbcon, download_test_data, last_workfile_path, @@ -111,11 +106,15 @@ class TestPublishInMaya(TestCase): def publish_finished(self, dbcon, launched_app): """Dummy fixture waiting for publish to finish""" import time + time_start = time.time() while launched_app.poll() is None: time.sleep(0.5) + if time.time() - time_start > self.TIMEOUT: + raise ValueError("Timeout reached") # some clean exit test possible? print("Publish finished") + yield True def test_db_asserts(self, dbcon, publish_finished): print("test_db_asserts") @@ -147,59 +146,33 @@ class TestPublishInMaya(TestCase): "context.ext": "ma"}).count(), \ "Not expected no of representations with ext 'abc'" - def test_files(self, dbcon, publish_finished, download_test_data): - print("test_files") - # hero files - hero_folder = os.path.join(download_test_data, - self.PROJECT, - self.ASSET, - "publish", - "model", - "modelMain", - "hero") + def test_folder_structure_same(self, dbcon, publish_finished, + download_test_data): + """Check if expected and published subfolders contain same files. - assert os.path.exists( - os.path.join(hero_folder, - "test_project_test_asset_modelMain_hero.ma") - ), "test_project_test_asset_modelMain_hero.ma doesn't exist" + Compares only presence, not size nor content! + """ + published_dir_base = download_test_data + published_dir = os.path.join(published_dir_base, + self.PROJECT, + self.TASK, + "**") + expected_dir_base = os.path.join(published_dir_base, + "expected") + expected_dir = os.path.join(expected_dir_base, + self.PROJECT, + self.TASK, + "**") - assert os.path.exists( - os.path.join(hero_folder, - "test_project_test_asset_modelMain_hero.abc") - ), "test_project_test_asset_modelMain_hero.abc doesn't exist" + published = set(f.replace(published_dir_base, '') for f in + glob.glob(published_dir, recursive=True) if + f != published_dir_base and os.path.exists(f)) + expected = set(f.replace(expected_dir_base, '') for f in + glob.glob(expected_dir, recursive=True) if + f != expected_dir_base and os.path.exists(f)) - # version files - version_folder = os.path.join(download_test_data, - self.PROJECT, - self.ASSET, - "publish", - "model", - "modelMain", - "v001") - - assert os.path.exists( - os.path.join(version_folder, - "test_project_test_asset_modelMain_v001.ma") - ), "test_project_test_asset_modelMain_v001.ma doesn't exist" - - assert os.path.exists( - os.path.join(version_folder, - "test_project_test_asset_modelMain_v001.abc") - ), "test_project_test_asset_modelMain_v001.abc doesn't exist" - - # workfile files - workfile_folder = os.path.join(download_test_data, - self.PROJECT, - self.ASSET, - "publish", - "workfile", - "workfileTest_task", - "v001") - - assert os.path.exists( - os.path.join(workfile_folder, - "test_project_test_asset_workfileTest_task_v001.mb") - ), "test_project_test_asset_workfileTest_task_v001.mb doesn't exist" + not_matched = expected.difference(published) + assert not not_matched, "Missing {} files".format(not_matched) if __name__ == "__main__": From 124429c5eece382161231f98853a4398278af2f8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Sep 2021 18:13:06 +0200 Subject: [PATCH 053/450] #1794 - created multiple test classes Refactor --- .../hosts/maya/test_publish_in_maya.py | 85 +----------- ...{testing_wrapper.py => testing_classes.py} | 127 ++++++++++++++++-- .../sync_server/test_site_operations.py | 6 +- 3 files changed, 126 insertions(+), 92 deletions(-) rename tests/lib/{testing_wrapper.py => testing_classes.py} (54%) diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index fd8882c349..86b26ba5e5 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -4,10 +4,10 @@ import os import shutil import glob -from tests.lib.testing_wrapper import TestCase +from tests.lib.testing_classes import PublishTest -class TestPublishInMaya(TestCase): +class TestPublishInMaya(PublishTest): """Basic test case for publishing in Maya Uses generic TestCase to prepare fixtures for test data, testing DBs, @@ -64,59 +64,8 @@ class TestPublishInMaya(TestCase): "{};{}".format(original_pythonpath, startup_path)) - @pytest.fixture(scope="module") - def launched_app(self, dbcon, download_test_data, last_workfile_path, - startup_scripts): - """Get sync_server_module from ModulesManager""" - root_key = "config.roots.work.{}".format("windows") # TEMP - dbcon.update_one( - {"type": "project"}, - {"$set": - { - root_key: download_test_data - }} - ) - - from openpype import PACKAGE_DIR - - # Path to OpenPype's schema - schema_path = os.path.join( - os.path.dirname(PACKAGE_DIR), - "schema" - ) - os.environ["AVALON_SCHEMA"] = schema_path # TEMP - - import openpype - openpype.install() - os.environ["OPENPYPE_EXECUTABLE"] = sys.executable - from openpype.lib import ApplicationManager - - application_manager = ApplicationManager() - data = { - "last_workfile_path": last_workfile_path, - "start_last_workfile": True, - "project_name": self.PROJECT, - "asset_name": self.ASSET, - "task_name": self.TASK - } - - yield application_manager.launch(self.APP_NAME, **data) - - @pytest.fixture(scope="module") - def publish_finished(self, dbcon, launched_app): - """Dummy fixture waiting for publish to finish""" - import time - time_start = time.time() - while launched_app.poll() is None: - time.sleep(0.5) - if time.time() - time_start > self.TIMEOUT: - raise ValueError("Timeout reached") - - # some clean exit test possible? - print("Publish finished") - yield True - def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" print("test_db_asserts") assert 5 == dbcon.find({"type": "version"}).count(), \ "Not expected no of versions" @@ -146,34 +95,6 @@ class TestPublishInMaya(TestCase): "context.ext": "ma"}).count(), \ "Not expected no of representations with ext 'abc'" - def test_folder_structure_same(self, dbcon, publish_finished, - download_test_data): - """Check if expected and published subfolders contain same files. - - Compares only presence, not size nor content! - """ - published_dir_base = download_test_data - published_dir = os.path.join(published_dir_base, - self.PROJECT, - self.TASK, - "**") - expected_dir_base = os.path.join(published_dir_base, - "expected") - expected_dir = os.path.join(expected_dir_base, - self.PROJECT, - self.TASK, - "**") - - published = set(f.replace(published_dir_base, '') for f in - glob.glob(published_dir, recursive=True) if - f != published_dir_base and os.path.exists(f)) - expected = set(f.replace(expected_dir_base, '') for f in - glob.glob(expected_dir, recursive=True) if - f != expected_dir_base and os.path.exists(f)) - - not_matched = expected.difference(published) - assert not not_matched, "Missing {} files".format(not_matched) - if __name__ == "__main__": test_case = TestPublishInMaya() diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_classes.py similarity index 54% rename from tests/lib/testing_wrapper.py rename to tests/lib/testing_classes.py index 0a7c9a382f..6c7bebd469 100644 --- a/tests/lib/testing_wrapper.py +++ b/tests/lib/testing_classes.py @@ -1,3 +1,4 @@ +"""Testing classes for module testing and publishing in hosts.""" import os import sys import six @@ -5,13 +6,18 @@ import json import pytest import tempfile import shutil +import glob from tests.lib.db_handler import DBHandler from tests.lib.file_handler import RemoteFileHandler -class TestCase: - """Generic test class for testing +class BaseTest: + """Empty base test class""" + + +class ModuleUnitTest(BaseTest): + """Generic test class for testing modules Implemented fixtures: monkeypatch_session - fixture for env vars with session scope @@ -22,17 +28,12 @@ class TestCase: dbcon - returns DBConnection to AvalonDB dbcon_openpype - returns DBConnection for OpenpypeMongoDB - Not implemented: - last_workfile_path - returns path to testing workfile - """ TEST_OPENPYPE_MONGO = "mongodb://localhost:27017" TEST_DB_NAME = "test_db" TEST_PROJECT_NAME = "test_project" TEST_OPENPYPE_NAME = "test_openpype" - REPRESENTATION_ID = "60e578d0c987036c6a7b741d" - TEST_FILES = [] PROJECT = "test_project" @@ -85,7 +86,7 @@ class TestCase: for key, value in env_dict.items(): all_vars = globals() - all_vars.update(vars(TestCase)) # TODO check + all_vars.update(vars(ModuleUnitTest)) # TODO check value = value.format(**all_vars) print("Setting {}:{}".format(key, value)) monkeypatch_session.setenv(key, str(value)) @@ -135,6 +136,35 @@ class TestCase: mongo_client = OpenPypeMongoConnection.get_mongo_client() yield mongo_client[self.TEST_OPENPYPE_NAME]["settings"] + +class PublishTest(ModuleUnitTest): + """Test class for publishing in hosts. + + Implemented fixtures: + launched_app - launches APP with last_workfile_path + publish_finished - waits until publish is finished, host must + kill its process when finished publishing. Includes timeout + which raises ValueError + + Not implemented: + last_workfile_path - returns path to testing workfile + startup_scripts - provide script for setup in host + + Implemented tests: + test_folder_structure_same - compares published and expected + subfolders if they contain same files. Compares only on file + presence + + TODO: implement test on file size, file content + """ + + APP = "" + APP_VARIANT = "" + + APP_NAME = "{}/{}".format(APP, APP_VARIANT) + + TIMEOUT = 120 # publish timeout + @pytest.fixture(scope="module") def last_workfile_path(self, download_test_data): raise NotImplementedError @@ -142,3 +172,84 @@ class TestCase: @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): raise NotImplementedError + + @pytest.fixture(scope="module") + def launched_app(self, dbcon, download_test_data, last_workfile_path, + startup_scripts): + """Launch host app""" + # set publishing folders + root_key = "config.roots.work.{}".format("windows") # TEMP + dbcon.update_one( + {"type": "project"}, + {"$set": + { + root_key: download_test_data + }} + ) + + # set schema - for integrate_new + from openpype import PACKAGE_DIR + # Path to OpenPype's schema + schema_path = os.path.join( + os.path.dirname(PACKAGE_DIR), + "schema" + ) + os.environ["AVALON_SCHEMA"] = schema_path + + import openpype + openpype.install() + os.environ["OPENPYPE_EXECUTABLE"] = sys.executable + from openpype.lib import ApplicationManager + + application_manager = ApplicationManager() + data = { + "last_workfile_path": last_workfile_path, + "start_last_workfile": True, + "project_name": self.PROJECT, + "asset_name": self.ASSET, + "task_name": self.TASK + } + + yield application_manager.launch(self.APP_NAME, **data) + + @pytest.fixture(scope="module") + def publish_finished(self, dbcon, launched_app): + """Dummy fixture waiting for publish to finish""" + import time + time_start = time.time() + while launched_app.poll() is None: + time.sleep(0.5) + if time.time() - time_start > self.TIMEOUT: + raise ValueError("Timeout reached") + + # some clean exit test possible? + print("Publish finished") + yield True + + def test_folder_structure_same(self, dbcon, publish_finished, + download_test_data): + """Check if expected and published subfolders contain same files. + + Compares only presence, not size nor content! + """ + published_dir_base = download_test_data + published_dir = os.path.join(published_dir_base, + self.PROJECT, + self.TASK, + "**") + expected_dir_base = os.path.join(published_dir_base, + "expected") + expected_dir = os.path.join(expected_dir_base, + self.PROJECT, + self.TASK, + "**") + + published = set(f.replace(published_dir_base, '') for f in + glob.glob(published_dir, recursive=True) if + f != published_dir_base and os.path.exists(f)) + expected = set(f.replace(expected_dir_base, '') for f in + glob.glob(expected_dir, recursive=True) if + f != expected_dir_base and os.path.exists(f)) + + not_matched = expected.difference(published) + assert not not_matched, "Missing {} files".format(not_matched) \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index 029f9a9f05..ab15025399 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -13,11 +13,13 @@ """ import pytest -from tests.lib.testing_wrapper import TestCase +from tests.lib.testing_classes import ModuleUnitTest from bson.objectid import ObjectId -class TestSiteOperation(TestCase): +class TestSiteOperation(ModuleUnitTest): + + REPRESENTATION_ID = "60e578d0c987036c6a7b741d" @pytest.fixture(scope="module") def setup_sync_server_module(self, dbcon): From d0a4293b9d12e59b59bac0c9845f45fc84839ff3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 6 Sep 2021 18:42:34 +0200 Subject: [PATCH 054/450] #1784 - added howto create new publishing test --- tests/integration/README.md | 31 ++++++++++++++++++ .../hosts/maya/test_publish_in_maya.py | 4 ++- tests/lib/testing_classes.py | 20 +++++++---- tests/resources/test_data.zip | Bin 0 -> 7350 bytes 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 tests/resources/test_data.zip diff --git a/tests/integration/README.md b/tests/integration/README.md index 00d8a4c10d..81c07ec50c 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -4,3 +4,34 @@ Contains end-to-end tests for automatic testing of OP. Should run headless publish on all hosts to check basic publish use cases automatically to limit regression issues. + +How to create test for publishing from host +------------------------------------------ +- Extend PublishTest +- Use `resources\test_data.zip` skeleton file as a template for testing input data +- Put workfile into `test_data.zip/input/workfile` +- If you require other than base DB dumps provide them to `test_data.zip/input/dumps` +-- (Check commented code in `db_handler.py` how to dump specific DB. Currently all collections will be dumped.) +- Implement `last_workfile_path` +- `startup_scripts` - must contain pointing host to startup script saved into `test_data.zip/input/startup` + -- Script must contain something like +``` +import openpype +from avalon import api, HOST + +api.install(HOST) +pyblish.util.publish() + +EXIT_APP (command to exit host) +``` +(Install and publish methods must be triggered only AFTER host app is fully initialized!) +- Zip `test_data.zip`, named it with descriptive name, upload it to Google Drive, right click - `Get link`, copy hash id +- Put this hash id and zip file name into TEST_FILES [(HASH_ID, FILE_NAME, MD5_OPTIONAL)]. If you want to check MD5 of downloaded +file, provide md5 value of zipped file. +- Implement any assert checks you need in extended class +- Run test class manually (via Pycharm or pytest runner (TODO)) +- If you want test to compare expected files to published one, set PERSIST to True, run test manually + -- Locate temporary `publish` subfolder of temporary folder (found in debugging console log) + -- Copy whole folder content into .zip file into `expected` subfolder + -- By default tests are comparing only structure of `expected` and published format (eg. if you want to save space, replace published files with empty files, but with expected names!) + -- Zip and upload again, change PERSIST to False \ No newline at end of file diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index 86b26ba5e5..b9c63651f1 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -20,6 +20,8 @@ class TestPublishInMaya(PublishTest): Checks tmp folder if all expected files were published. """ + PERSIST = True + TEST_FILES = [ ("1pOwjA_VVBc6ooTZyFxtAwLS2KZHaBlkY", "test_maya_publish.zip", "") ] @@ -39,7 +41,7 @@ class TestPublishInMaya(PublishTest): """ src_path = os.path.join(download_test_data, "input", - "data", + "workfile", "test_project_test_asset_TestTask_v001.mb") dest_folder = os.path.join(download_test_data, self.PROJECT, diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 6c7bebd469..6cd3c10d3e 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -19,6 +19,9 @@ class BaseTest: class ModuleUnitTest(BaseTest): """Generic test class for testing modules + Use PERSIST==True to keep temporary folder and DB prepared for + debugging or preparation of test files. + Implemented fixtures: monkeypatch_session - fixture for env vars with session scope download_test_data - tmp folder with extracted data from GDrive @@ -29,6 +32,8 @@ class ModuleUnitTest(BaseTest): dbcon_openpype - returns DBConnection for OpenpypeMongoDB """ + PERSIST = False # True to not purge temporary folder nor test DB + TEST_OPENPYPE_MONGO = "mongodb://localhost:27017" TEST_DB_NAME = "test_db" TEST_PROJECT_NAME = "test_project" @@ -62,10 +67,12 @@ class ModuleUnitTest(BaseTest): if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) - + print("Temporary folder created:: {}".format(tmpdir)) yield tmpdir - print("Removing {}".format(tmpdir)) - shutil.rmtree(tmpdir) + + if not self.PERSIST: + print("Removing {}".format(tmpdir)) + shutil.rmtree(tmpdir) @pytest.fixture(scope="module") def env_var(self, monkeypatch_session, download_test_data): @@ -112,8 +119,9 @@ class ModuleUnitTest(BaseTest): yield db_handler - db_handler.teardown(self.TEST_DB_NAME) - db_handler.teardown(self.TEST_OPENPYPE_NAME) + if not self.PERSIST: + db_handler.teardown(self.TEST_DB_NAME) + db_handler.teardown(self.TEST_OPENPYPE_NAME) @pytest.fixture(scope="module") def dbcon(self, db_setup): @@ -213,7 +221,7 @@ class PublishTest(ModuleUnitTest): yield application_manager.launch(self.APP_NAME, **data) @pytest.fixture(scope="module") - def publish_finished(self, dbcon, launched_app): + def publish_finished(self, dbcon, launched_app, download_test_data): """Dummy fixture waiting for publish to finish""" import time time_start = time.time() diff --git a/tests/resources/test_data.zip b/tests/resources/test_data.zip new file mode 100644 index 0000000000000000000000000000000000000000..0faab86b37d5c7d1224e8a92cca766ed80536718 GIT binary patch literal 7350 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 z Date: Tue, 7 Sep 2021 11:47:23 +0200 Subject: [PATCH 055/450] added tine addon --- openpype/modules/example_addons/tiny_addon.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 openpype/modules/example_addons/tiny_addon.py diff --git a/openpype/modules/example_addons/tiny_addon.py b/openpype/modules/example_addons/tiny_addon.py new file mode 100644 index 0000000000..62962954f5 --- /dev/null +++ b/openpype/modules/example_addons/tiny_addon.py @@ -0,0 +1,9 @@ +from openpype.modules import OpenPypeAddOn + + +class TinyAddon(OpenPypeAddOn): + """This is tiniest possible addon. + + This addon won't do much but will exist in OpenPype modules environment. + """ + name = "tiniest_addon_ever" From 3dd69032510e8087a889c3bf38fbde96ce1204d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 11:53:59 +0200 Subject: [PATCH 056/450] added base of example addon --- .../example_addons/example_addon/__init__.py | 13 ++ .../example_addons/example_addon/addon.py | 124 ++++++++++++++++++ .../example_addon/interfaces.py | 28 ++++ .../plugins/publish/example_plugin.py | 10 ++ .../settings/defaults/project_settings.json | 1 + .../project_dynamic_schemas.json | 6 + .../system_dynamic_schemas.json | 6 + .../schemas/project_schemas/main.json | 29 ++++ .../schemas/project_schemas/the_template.json | 30 +++++ .../settings/schemas/system_schemas/main.json | 14 ++ .../example_addons/example_addon/widgets.py | 30 +++++ 11 files changed, 291 insertions(+) create mode 100644 openpype/modules/example_addons/example_addon/__init__.py create mode 100644 openpype/modules/example_addons/example_addon/addon.py create mode 100644 openpype/modules/example_addons/example_addon/interfaces.py create mode 100644 openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py create mode 100644 openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json create mode 100644 openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json create mode 100644 openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json create mode 100644 openpype/modules/example_addons/example_addon/widgets.py diff --git a/openpype/modules/example_addons/example_addon/__init__.py b/openpype/modules/example_addons/example_addon/__init__.py new file mode 100644 index 0000000000..df4d61650b --- /dev/null +++ b/openpype/modules/example_addons/example_addon/__init__.py @@ -0,0 +1,13 @@ +""" Addon class definition and Settings definition must be imported here. + +If addon class or settings definition won't be here their definition won't +be found by OpenPype discovery. +""" + +from .addon import ( + AddonSettingsDef, +) + +__all__ = ( + "AddonSettingsDef", +) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py new file mode 100644 index 0000000000..64504be756 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -0,0 +1,124 @@ +"""Addon definition is located here. + +Import of python packages that may not be available should not be imported +in global space here until are required or used. +- Qt related imports +- imports of Python 3 packages + - we still support Python 2 hosts where addon definition should available +""" + +import os + +from openpype.modules import ( + JsonFilesSettingsDef, + OpenPypeAddOn +) +# Import interface defined by this addon to be able find other addons using it +from openpype_interfaces import ( + IExampleInterface, + IPluginPaths, + ITrayAction +) + + +# Settings definiton of this addon using `JsonFilesSettingsDef` +# - JsonFilesSettingsDef is prepared settings definiton using json files +# to define settings and store defaul values +class AddonSettingsDef(JsonFilesSettingsDef): + # This will add prefix to every schema and template from `schemas` + # subfolder. + # - it is not required to fill the prefix but it is highly + # recommended as schemas and templates may have name clashes across + # multiple addons + # - it is also recommended that prefix has addon name in it + schema_prefix = "addon_with_settings" + + def get_settings_root_path(self): + """Implemented abstract class of JsonFilesSettingsDef. + + Return directory path where json files defying addon settings are + located. + """ + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "settings" + ) + + +class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): + """This Addon has defined it's settings and interface. + + This example has system settings with enabled option. And use + few other interfaces: + - `IPluginPaths` to define custom plugin paths + - `ITrayAction` to be shown in tray tool + """ + label = "Example Addon" + name = "example_addon" + + def initialize(self, settings): + """Initialization of addon.""" + module_settings = settings[self.name] + # Enabled by settings + self.enabled = module_settings.get("enabled", False) + + # Prepare variables that can be used or set afterwards + self._connected_modules = None + # UI which must not be created at this time + self._dialog = None + + def connect_with_modules(self, enabled_modules): + """Method where you should find connected modules. + + It is triggered by OpenPype modules manager at the best possible time. + Some addons and modules may required to connect with other modules + before their main logic is executed so changes would require to restart + whole process. + """ + self._connected_modules = [] + for module in enabled_modules: + if isinstance(module, IExampleInterface): + self._connected_modules.append(module) + + def _create_dialog(self): + # Don't recreate dialog if already exists + if self._dialog is not None: + return + + from .widgets import MyExampleDialog + + self._dialog = MyExampleDialog() + + def show_dialog(self): + """Show dialog with connected modules. + + This can be called from anywhere but can also crash in headless mode. + There is not way how to prevent addon to do invalid operations if he's + not handling them. + """ + # Make sure dialog is created + self._create_dialog() + # Change value of dialog by current state + self._dialog.set_connected_modules(self.get_connected_modules()) + # Show dialog + self._dialog.open() + + def get_connected_modules(self): + """Custom implementation of addon.""" + names = set() + if self._connected_modules is not None: + for module in self._connected_modules: + names.add(module.name) + return names + + def on_action_trigger(self): + """Implementation of abstract method for `ITrayAction`.""" + self.show_dialog() + + def get_plugin_paths(self): + """Implementation of abstract method for `IPluginPaths`.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + + return { + "publish": [os.path.join(current_dir, "plugins", "publish")] + } diff --git a/openpype/modules/example_addons/example_addon/interfaces.py b/openpype/modules/example_addons/example_addon/interfaces.py new file mode 100644 index 0000000000..371536efc7 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/interfaces.py @@ -0,0 +1,28 @@ +""" Using interfaces is one way of connecting multiple OpenPype Addons/Modules. + +Interfaces must be in `interfaces.py` file (or folder). Interfaces should not +import module logic or other module in global namespace. That is because +all of them must be imported before all OpenPype AddOns and Modules. + +Ideally they should just define abstract and helper methods. If interface +require any logic or connection it should be defined in module. + +Keep in mind that attributes and methods will be added to other addon +attributes and methods so they should be unique and ideally contain +addon name in it's name. +""" + +from abc import abstractmethod +from openpype.modules import OpenPypeInterface + + +class IExampleInterface(OpenPypeInterface): + """Example interface of addon.""" + _example_module = None + + def get_example_module(self): + return self._example_module + + @abstractmethod + def example_method_of_example_interface(self): + pass diff --git a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py new file mode 100644 index 0000000000..8e7fb410bd --- /dev/null +++ b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py @@ -0,0 +1,10 @@ +import os +import pyblish.api + + +class CollectExampleAddon(pyblish.api.ContextPlugin): + order = pyblish.api.CollectorOrder + 0.4 + label = "Collect Example Addon" + + def process(self, context): + self.log.info("I'm in example addon's plugin!") diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json @@ -0,0 +1 @@ +{} diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json new file mode 100644 index 0000000000..f6b7d5d146 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json @@ -0,0 +1,6 @@ +{ + "project_settings/global": { + "type": "schema", + "name": "addon_with_settings/main" + } +} diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json new file mode 100644 index 0000000000..6895fb8f6d --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json @@ -0,0 +1,6 @@ +{ + "system_settings/modules": { + "type": "schema", + "name": "addon_with_settings/main" + } +} diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json new file mode 100644 index 0000000000..80e53ace7f --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "key": "exmaple_addon", + "collapsible": true, + "children": [ + { + "type": "number", + "key": "number", + "label": "This is your lucky number:", + "minimum": 7, + "maximum": 7, + "decimals": 0 + }, + { + "type": "template", + "name": "example_addon/the_template", + "template_data": [ + { + "name": "color_1", + "lable": "Color 1" + }, + { + "name": "color_2", + "lable": "Color 2" + } + ] + } + ] +} diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json new file mode 100644 index 0000000000..af8fd9dae4 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json @@ -0,0 +1,30 @@ +[ + { + "type": "list-strict", + "key": "{name}", + "label": "{label}", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + } + ] + } +] diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json new file mode 100644 index 0000000000..0fb0a7c1be --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json @@ -0,0 +1,14 @@ +{ + "type": "dict", + "key": "example_addon", + "label": "Example addon", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] +} diff --git a/openpype/modules/example_addons/example_addon/widgets.py b/openpype/modules/example_addons/example_addon/widgets.py new file mode 100644 index 0000000000..8a74ad859f --- /dev/null +++ b/openpype/modules/example_addons/example_addon/widgets.py @@ -0,0 +1,30 @@ +from Qt import QtWidgets + + +class MyExampleDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super(MyExampleDialog, self).__init__(parent) + + self.setWindowTitle("Connected modules") + + label_widget = QtWidgets.QLabel(self) + + ok_btn = QtWidgets.QPushButton("OK", self) + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(label_widget) + layout.addLayout(btns_layout) + + self._label_widget = label_widget + + def set_connected_modules(self, connected_modules): + if connected_modules: + message = "\n".join(connected_modules) + else: + message = ( + "Other enabled modules/addons are not using my interface." + ) + self._label_widget.setText(message) From 8c37b2b1419d1b5e3065a4ebfe42e305165c1273 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:11:18 +0200 Subject: [PATCH 057/450] fixed settings and widget of example addon --- .../example_addons/example_addon/__init__.py | 2 ++ .../example_addons/example_addon/addon.py | 10 +++++++++- .../settings/defaults/project_settings.json | 16 +++++++++++++++- .../settings/defaults/system_settings.json | 5 +++++ .../dynamic_schemas/project_dynamic_schemas.json | 2 +- .../dynamic_schemas/system_dynamic_schemas.json | 2 +- .../settings/schemas/project_schemas/main.json | 7 ++++--- .../example_addons/example_addon/widgets.py | 9 +++++++++ 8 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json diff --git a/openpype/modules/example_addons/example_addon/__init__.py b/openpype/modules/example_addons/example_addon/__init__.py index df4d61650b..721d924436 100644 --- a/openpype/modules/example_addons/example_addon/__init__.py +++ b/openpype/modules/example_addons/example_addon/__init__.py @@ -6,8 +6,10 @@ be found by OpenPype discovery. from .addon import ( AddonSettingsDef, + ExampleAddon ) __all__ = ( "AddonSettingsDef", + "ExampleAddon" ) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 64504be756..5a25b80616 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -31,7 +31,7 @@ class AddonSettingsDef(JsonFilesSettingsDef): # recommended as schemas and templates may have name clashes across # multiple addons # - it is also recommended that prefix has addon name in it - schema_prefix = "addon_with_settings" + schema_prefix = "example_addon" def get_settings_root_path(self): """Implemented abstract class of JsonFilesSettingsDef. @@ -67,6 +67,14 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): # UI which must not be created at this time self._dialog = None + def tray_init(self): + """Implementation of abstract method for `ITrayAction`. + + We're definetely in trat tool so we can precreate dialog. + """ + + self._create_dialog() + def connect_with_modules(self, enabled_modules): """Method where you should find connected modules. diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json index 0967ef424b..0a01fa8977 100644 --- a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json +++ b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json @@ -1 +1,15 @@ -{} +{ + "project_settings/example_addon": { + "number": 0, + "color_1": [ + 0.0, + 0.0, + 0.0 + ], + "color_2": [ + 0.0, + 0.0, + 0.0 + ] + } +} \ No newline at end of file diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json new file mode 100644 index 0000000000..1e77356373 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json @@ -0,0 +1,5 @@ +{ + "modules/example_addon": { + "enabled": true + } +} \ No newline at end of file diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json index f6b7d5d146..1f3da7b37f 100644 --- a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json @@ -1,6 +1,6 @@ { "project_settings/global": { "type": "schema", - "name": "addon_with_settings/main" + "name": "example_addon/main" } } diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json index 6895fb8f6d..6faa48ba74 100644 --- a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json @@ -1,6 +1,6 @@ { "system_settings/modules": { "type": "schema", - "name": "addon_with_settings/main" + "name": "example_addon/main" } } diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json index 80e53ace7f..ba692d860e 100644 --- a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json @@ -1,6 +1,7 @@ { "type": "dict", - "key": "exmaple_addon", + "key": "example_addon", + "label": "Example addon", "collapsible": true, "children": [ { @@ -17,11 +18,11 @@ "template_data": [ { "name": "color_1", - "lable": "Color 1" + "label": "Color 1" }, { "name": "color_2", - "lable": "Color 2" + "label": "Color 2" } ] } diff --git a/openpype/modules/example_addons/example_addon/widgets.py b/openpype/modules/example_addons/example_addon/widgets.py index 8a74ad859f..0acf238409 100644 --- a/openpype/modules/example_addons/example_addon/widgets.py +++ b/openpype/modules/example_addons/example_addon/widgets.py @@ -1,5 +1,7 @@ from Qt import QtWidgets +from openpype.style import load_stylesheet + class MyExampleDialog(QtWidgets.QDialog): def __init__(self, parent=None): @@ -18,8 +20,15 @@ class MyExampleDialog(QtWidgets.QDialog): layout.addWidget(label_widget) layout.addLayout(btns_layout) + ok_btn.clicked.connect(self._on_ok_clicked) + self._label_widget = label_widget + self.setStyleSheet(load_stylesheet()) + + def _on_ok_clicked(self): + self.done(1) + def set_connected_modules(self, connected_modules): if connected_modules: message = "\n".join(connected_modules) From 4b0c8abcd52cd0254e4df73c314b98a2ec49f2fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:11:36 +0200 Subject: [PATCH 058/450] added new dynamic schema with name `system_settings/modules` --- .../entities/schemas/system_schema/schema_modules.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..4287dd7905 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -242,6 +242,10 @@ "label": "Enabled" } ] + }, + { + "type": "dynamic_schema", + "name": "system_settings/modules" } ] } From fa8383859cc0c3d01520a0e6213ff3caf3d65da5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:12:10 +0200 Subject: [PATCH 059/450] added ability to use schema as first children of dynamic schema definition --- openpype/settings/entities/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index f207322dee..bf3868c08d 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -168,9 +168,13 @@ class SchemasHub: if isinstance(def_schema, dict): def_schema = [def_schema] + all_def_schema = [] for item in def_schema: - item["_dynamic_schema_id"] = def_id - output.extend(def_schema) + items = self.resolve_schema_data(item) + for _item in items: + _item["_dynamic_schema_id"] = def_id + all_def_schema.extend(items) + output.extend(all_def_schema) return output def get_template_name(self, item_def, default=None): From 5bd1fa8a56b41913932df4aeb61a101109892c88 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:12:22 +0200 Subject: [PATCH 060/450] skip OpenPypeAddOn items too --- openpype/modules/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 01c3cebe60..2cd11e5b94 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -495,6 +495,7 @@ class ModulesManager: if ( not inspect.isclass(modules_item) or modules_item is OpenPypeModule + or modules_item is OpenPypeAddOn or not issubclass(modules_item, OpenPypeModule) ): continue From 7f7c7e00620e4a20143b6d1a2d14a51958a5fc9e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:19:42 +0200 Subject: [PATCH 061/450] added few more information about addons settings to readme --- openpype/modules/README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index a3733518ac..a6857b2c51 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -10,8 +10,6 @@ OpenPype modules should contain separated logic of specific kind of implementati - add module/addon manifest - definition of module (not 100% defined content e.g. minimum require OpenPype version etc.) - defying that folder is content of a module or an addon -- module/addon have it's settings schemas and default values outside OpenPype -- add general setting of paths to modules ## Base class `OpenPypeModule` - abstract class as base for each module @@ -25,6 +23,26 @@ OpenPype modules should contain separated logic of specific kind of implementati - also keep in mind that they may be initialized in headless mode - connection with other modules is made with help of interfaces +## Addon class `OpenPypeAddOn` +- inherit from `OpenPypeModule` but is enabled by default and don't have to implement `initialize` and `connect_with_modules` methods + - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) + +## How to add addons/modules +- in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder +- for openpype example addons use `{OPENPYPE_REPOS_ROOT}/openpype/modules/example_addons` + +## Addon/module settings +- addons/modules may have defined custom settings definitions with default values +- it is based on settings type `dynamic_schema` which has `name` + - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults + - they can't be added to any schema hierarchy + - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) + - addons may define it's dynamic schema items +- they can be defined with class which inherit from `BaseModuleSettingsDef` + - it is recommended to use preimplemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values + - check it's docstring and check for `example_addon` in example addons +- settings definition returns schemas by dynamic schemas names + # Interfaces - interface is class that has defined abstract methods to implement and may contain preimplemented helper methods - module that inherit from an interface must implement those abstract methods otherwise won't be initialized From 0932ea8aba1ec9af50fcbafa87b7022a7ac6dca0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 13:25:13 +0200 Subject: [PATCH 062/450] removed unsused import --- .../example_addon/plugins/publish/example_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py index 8e7fb410bd..695120e93b 100644 --- a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py +++ b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py @@ -1,4 +1,3 @@ -import os import pyblish.api From c6d781d7df7ab6e847328fc8188d77e29fedc941 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 13:34:56 +0200 Subject: [PATCH 063/450] #1976 - added methods to get configurable items for providers without use of Settings --- .../sync_server/sync_server_module.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index e65a410551..d2c70ec75a 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -403,6 +403,59 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """Wrapper for Local settings - all projects incl. Default""" return self.get_configurable_items(EditableScopes.LOCAL) + def get_system_configurable_items_for_provider(self, provider_name): + """ Gets system level configurable items without use of Setting + + Used for Setting UI to provide forms. + """ + scope = EditableScopes.SYSTEM + return self._get_configurable_items_for_provider(provider_name, scope) + + def get_project_configurable_items_for_provider(self, provider_name): + """ Gets project level configurable items without use of Setting + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.PROJECT + return self._get_configurable_items_for_provider(provider_name, scope) + + def get_system_configurable_items_for_providers(self): + """ Gets system level configurable items for all providers. + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.SYSTEM + ret_dict = {} + for provider_name in lib.factory.providers: + ret_dict[provider_name] = \ + self._get_configurable_items_for_provider(provider_name, scope) + + return ret_dict + + def get_project_configurable_items_for_providers(self): + """ Gets project level configurable items for all providers. + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.PROJECT + ret_dict = {} + for provider_name in lib.factory.providers: + ret_dict[provider_name] = \ + self._get_configurable_items_for_provider(provider_name, scope) + + return ret_dict + + def _get_configurable_items_for_provider(self, provider_name, scope): + items = lib.factory.get_provider_configurable_items(provider_name) + ret_dict = {} + + for item_key, item in items.items(): + if scope in item["scope"]: + item.pop("scope") + ret_dict[item_key] = item + + return ret_dict + def get_configurable_items(self, scope=None): """ Returns list of sites that could be configurable for all projects. From a4706062d3d345757a4248c2d650b19647553fe4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 14:50:55 +0200 Subject: [PATCH 064/450] #1976 - made new methods class methods --- .../sync_server/sync_server_module.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index d2c70ec75a..3e10ddac1d 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -403,23 +403,28 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """Wrapper for Local settings - all projects incl. Default""" return self.get_configurable_items(EditableScopes.LOCAL) - def get_system_configurable_items_for_provider(self, provider_name): + @classmethod + def get_system_configurable_items_for_provider(cls, provider_name): """ Gets system level configurable items without use of Setting Used for Setting UI to provide forms. """ scope = EditableScopes.SYSTEM - return self._get_configurable_items_for_provider(provider_name, scope) + return SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) - def get_project_configurable_items_for_provider(self, provider_name): + @classmethod + def get_project_configurable_items_for_provider(cls, provider_name): """ Gets project level configurable items without use of Setting It is not using Setting! Used for Setting UI to provide forms. """ scope = EditableScopes.PROJECT - return self._get_configurable_items_for_provider(provider_name, scope) + return SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) - def get_system_configurable_items_for_providers(self): + @classmethod + def get_system_configurable_items_for_providers(cls): """ Gets system level configurable items for all providers. It is not using Setting! Used for Setting UI to provide forms. @@ -428,11 +433,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - self._get_configurable_items_for_provider(provider_name, scope) + SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) return ret_dict - def get_project_configurable_items_for_providers(self): + @classmethod + def get_project_configurable_items_for_providers(cls): """ Gets project level configurable items for all providers. It is not using Setting! Used for Setting UI to provide forms. @@ -441,11 +448,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - self._get_configurable_items_for_provider(provider_name, scope) + SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) return ret_dict - def _get_configurable_items_for_provider(self, provider_name, scope): + @classmethod + def _get_configurable_items_for_provider(cls, provider_name, scope): items = lib.factory.get_provider_configurable_items(provider_name) ret_dict = {} From eef59e6fffb67671f752c6aef890c0c5d0ae5255 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 15:25:54 +0200 Subject: [PATCH 065/450] #1976 - standardize return to list --- .../sync_server/sync_server_module.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 3e10ddac1d..eeff5c499d 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -455,15 +455,24 @@ class SyncServerModule(OpenPypeModule, ITrayModule): @classmethod def _get_configurable_items_for_provider(cls, provider_name, scope): + """ + Args: + provider_name (str) + scope (EditableScopes) + Returns + (list) of (dict) + """ items = lib.factory.get_provider_configurable_items(provider_name) - ret_dict = {} - for item_key, item in items.items(): + ret = [] + for key in sorted(items.keys()): + item = items[key] if scope in item["scope"]: - item.pop("scope") - ret_dict[item_key] = item + item.pop("scope") # unneeded by UI + item.pop("namespace", None) # unneeded by UI + ret.append(item) - return ret_dict + return ret def get_configurable_items(self, scope=None): """ From 39341b1c2bb40caf038b5cb5595efcb1561ee047 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 15:32:14 +0200 Subject: [PATCH 066/450] #1976 - explicitly remove namespace in some cases Used cls instead of class name --- .../sync_server/sync_server_module.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index eeff5c499d..f0ae64d3fd 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -410,8 +410,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Used for Setting UI to provide forms. """ scope = EditableScopes.SYSTEM - return SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + return cls._get_configurable_items_for_provider(provider_name, scope) @classmethod def get_project_configurable_items_for_provider(cls, provider_name): @@ -420,8 +419,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): It is not using Setting! Used for Setting UI to provide forms. """ scope = EditableScopes.PROJECT - return SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + return cls._get_configurable_items_for_provider(provider_name, scope) @classmethod def get_system_configurable_items_for_providers(cls): @@ -433,8 +431,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + cls._get_configurable_items_for_provider(provider_name, scope) return ret_dict @@ -448,8 +445,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + cls._get_configurable_items_for_provider(provider_name, scope) return ret_dict @@ -469,7 +465,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): item = items[key] if scope in item["scope"]: item.pop("scope") # unneeded by UI - item.pop("namespace", None) # unneeded by UI + if scope in [EditableScopes.SYSTEM, EditableScopes.PROJECT]: + item.pop("namespace", None) # unneeded by UI ret.append(item) return ret From 7eed3da7c3c2d525a5dc5e5784749c05b8e7a4bd Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 8 Sep 2021 05:57:12 +0800 Subject: [PATCH 067/450] refactor, publishing render sets as model metadata Instead of loading whole render(augmented) model from Loader, *import* those render sets from scene inventory as model's metadata which allow lookDev artist to modify it and optionally publish a look from there. --- openpype/hosts/maya/api/__init__.py | 2 + .../plugins/inventory/import_modelrender.py | 85 +++++++++++ .../hosts/maya/plugins/load/load_reference.py | 12 -- .../maya/plugins/publish/collect_look.py | 24 +--- .../maya/plugins/publish/extract_look.py | 133 +++--------------- 5 files changed, 106 insertions(+), 150 deletions(-) create mode 100644 openpype/hosts/maya/plugins/inventory/import_modelrender.py diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 9219da407f..b48027ddba 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -35,6 +35,7 @@ def install(): pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) log.info(PUBLISH_PATH) menu.install() @@ -97,6 +98,7 @@ def uninstall(): pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) menu.uninstall() diff --git a/openpype/hosts/maya/plugins/inventory/import_modelrender.py b/openpype/hosts/maya/plugins/inventory/import_modelrender.py new file mode 100644 index 0000000000..2737901b51 --- /dev/null +++ b/openpype/hosts/maya/plugins/inventory/import_modelrender.py @@ -0,0 +1,85 @@ +from avalon import api, io + + +class ImportModelRender(api.InventoryAction): + + label = "Import Model Render Sets" + icon = "industry" + color = "#55DDAA" + + scene_type = "meta.render.ma" + look_data_type = "meta.render.json" + + @staticmethod + def is_compatible(container): + return container.get("loader") == "ReferenceLoader" \ + and container.get("name", "").startswith("model") + + def process(self, containers): + from maya import cmds + + for container in containers: + container_name = container["objectName"] + nodes = [] + for n in cmds.sets(container_name, query=True, nodesOnly=True) or []: + if cmds.nodeType(n) == "reference": + nodes += cmds.referenceQuery(n, nodes=True) + else: + nodes.append(n) + + repr_doc = io.find_one({"_id": io.ObjectId(container["representation"])}) + version_id = repr_doc["parent"] + + print("Importing render sets for model %r" % container_name) + self.assign_model_render_by_version(nodes, version_id) + + def assign_model_render_by_version(self, nodes, version_id): + """Assign nodes a specific published model render data version by id. + + This assumes the nodes correspond with the asset. + + Args: + nodes(list): nodes to assign render data to + version_id (bson.ObjectId): database id of the version of model + + Returns: + None + """ + import json + from maya import cmds + from avalon import maya, io, pipeline + from openpype.hosts.maya.api import lib + + # Get representations of shader file and relationships + look_representation = io.find_one({"type": "representation", + "parent": version_id, + "name": self.scene_type}) + + if not look_representation: + print("No model render sets for this model version..") + return + + json_representation = io.find_one({"type": "representation", + "parent": version_id, + "name": self.look_data_type}) + + context = pipeline.get_representation_context(look_representation['_id']) + maya_file = pipeline.get_representation_path_from_context(context) + + context = pipeline.get_representation_context(json_representation['_id']) + json_file = pipeline.get_representation_path_from_context(context) + + # Import the look file + with maya.maintained_selection(): + shader_nodes = cmds.file(maya_file, + i=True, # import + returnNewNodes=True) + # imprint context data + + # Load relationships + shader_relation = json_file + with open(shader_relation, "r") as f: + relationships = json.load(f) + + # Assign relationships + lib.apply_shaders(relationships, shader_nodes, nodes) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 77c9f28d10..96269f2771 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -152,15 +152,3 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): options={"useSelection": True}, data={"dependencies": dependency} ) - - -class AugmentedModelLoader(ReferenceLoader): - """Load augmented model via Maya referencing""" - - families = ["model"] - representations = ["fried.ma", "fried.mb"] - - label = "Fried Model" - order = -9 - icon = "code-fork" - color = "yellow" diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index ecc89e9032..712c7f19ff 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -223,8 +223,8 @@ class CollectLook(pyblish.api.InstancePlugin): def process(self, instance): """Collect the Look in the instance with the correct layer settings""" - - with lib.renderlayer(instance.data["renderlayer"]): + renderlayer = instance.data.get("renderlayer", "defaultRenderLayer") + with lib.renderlayer(renderlayer): self.collect(instance) def collect(self, instance): @@ -579,26 +579,6 @@ class CollectModelRenderSets(CollectLook): hosts = ["maya"] maketx = True - def process(self, instance): - """Collect the Look in the instance with the correct layer settings""" - model_nodes = instance[:] - renderlayer = instance.data.get("renderlayer", "defaultRenderLayer") - - with lib.renderlayer(renderlayer): - self.collect(instance) - - set_nodes = [m for m in instance if m not in model_nodes] - instance[:] = model_nodes - - if set_nodes: - instance.data["modelRenderSets"] = set_nodes - instance.data["modelRenderSetsHistory"] = \ - cmds.listHistory(set_nodes, future=False, pruneDagObjects=True) - - self.log.info("Model render sets collected.") - else: - self.log.info("No model render sets.") - def collect_sets(self, instance): """Collect all related objectSets except shadingEngines diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 121c99fc47..62b58623e7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -135,6 +135,7 @@ class ExtractLook(openpype.api.Extractor): families = ["look"] order = pyblish.api.ExtractorOrder + 0.2 scene_type = "ma" + look_data_type = "json" @staticmethod def get_renderer_name(): @@ -186,7 +187,7 @@ class ExtractLook(openpype.api.Extractor): # Define extract output file path dir_path = self.staging_dir(instance) maya_fname = "{0}.{1}".format(instance.name, self.scene_type) - json_fname = "{0}.json".format(instance.name) + json_fname = "{0}.{1}".format(instance.name, self.look_data_type) # Make texture dump folder maya_path = os.path.join(dir_path, maya_fname) @@ -252,19 +253,21 @@ class ExtractLook(openpype.api.Extractor): instance.data["files"].append(maya_fname) instance.data["files"].append(json_fname) - instance.data["representations"] = [] + if instance.data.get("representations") is None: + instance.data["representations"] = [] + instance.data["representations"].append( { - "name": "ma", - "ext": "ma", + "name": self.scene_type, + "ext": self.scene_type, "files": os.path.basename(maya_fname), "stagingDir": os.path.dirname(maya_fname), } ) instance.data["representations"].append( { - "name": "json", - "ext": "json", + "name": self.look_data_type, + "ext": self.look_data_type, "files": os.path.basename(json_fname), "stagingDir": os.path.dirname(json_fname), } @@ -483,119 +486,17 @@ class ExtractLook(openpype.api.Extractor): return filepath, COPY, texture_hash -class ExtractAugmentedModel(ExtractLook): - """Extract as Augmented Model (Maya Scene). +class ExtractModelRenderSets(ExtractLook): + """Extract model render attribute sets as model metadata - Rendering attrs augmented model. - - Only extracts contents based on the original "setMembers" data to ensure - publishing the least amount of required shapes. From that it only takes - the shapes that are not intermediateObjects - - During export it sets a temporary context to perform a clean extraction. - The context ensures: - - Smooth preview is turned off for the geometry - - Default shader is assigned (no materials are exported) - - Remove display layers + Only extracts the render attrib sets (NO shadingEngines) alongside a .json file + that stores it relationships for the sets and "attribute" data for the + instance members. """ - label = "Augmented Model (Maya Scene)" + label = "Model Render Sets" hosts = ["maya"] families = ["model"] - scene_type = "ma" - augmented = "fried" - - def process(self, instance): - """Plugin entry point. - - Args: - instance: Instance to process. - - """ - render_sets = instance.data.get("modelRenderSetsHistory") - if not render_sets: - self.log.info("Model is not render augmented, skip extraction.") - return - - self.get_maya_scene_type(instance) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - # Define extract output file path - stagingdir = self.staging_dir(instance) - ext = "{0}.{1}".format(self.augmented, self.scene_type) - filename = "{0}.{1}".format(instance.name, ext) - path = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction ...") - - results = self.process_resources(instance, staging_dir=stagingdir) - transfers = results["fileTransfers"] - hardlinks = results["fileHardlinks"] - hashes = results["fileHashes"] - remap = results["attrRemap"] - - self.log.info(remap) - - # Get only the shape contents we need in such a way that we avoid - # taking along intermediateObjects - members = instance.data("setMembers") - members = cmds.ls(members, - dag=True, - shapes=True, - type=("mesh", "nurbsCurve"), - noIntermediate=True, - long=True) - members += instance.data.get("modelRenderSetsHistory") - - with lib.no_display_layers(instance): - with lib.displaySmoothness(members, - divisionsU=0, - divisionsV=0, - pointsWire=4, - pointsShaded=1, - polygonObject=1): - with lib.shader(members, - shadingEngine="initialShadingGroup"): - # To avoid Maya trying to automatically remap the file - # textures relative to the `workspace -directory` we force - # it to a fake temporary workspace. This fixes textures - # getting incorrectly remapped. (LKD-17, PLN-101) - with no_workspace_dir(): - with lib.attribute_values(remap): - with avalon.maya.maintained_selection(): - - cmds.select(members, noExpand=True) - cmds.file(path, - force=True, - typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 - exportSelected=True, - preserveReferences=False, - channels=False, - constraints=False, - expressions=False, - constructionHistory=False) - - if "hardlinks" not in instance.data: - instance.data["hardlinks"] = [] - if "transfers" not in instance.data: - instance.data["transfers"] = [] - - # Set up the resources transfers/links for the integrator - instance.data["transfers"].extend(transfers) - instance.data["hardlinks"].extend(hardlinks) - - # Source hash for the textures - instance.data["sourceHashes"] = hashes - - instance.data["representations"].append({ - 'name': ext, - 'ext': ext, - 'files': filename, - "stagingDir": stagingdir, - }) - - self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) + scene_type = "meta.render.ma" + look_data_type = "meta.render.json" From 94b3a182ef37d8d58f21026463fa0a00f50442e8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 11:50:20 +0200 Subject: [PATCH 068/450] #1976 - refactored, created simplified methods Commented out currently unneeded methods, not used anywhere, but logic could be salvaged after Settings will be modified --- .../providers/abstract_provider.py | 28 +- .../sync_server/providers/gdrive.py | 62 ++- .../sync_server/providers/lib.py | 8 + .../sync_server/providers/local_drive.py | 55 ++- .../sync_server/sync_server_module.py | 461 ++++++++---------- 5 files changed, 342 insertions(+), 272 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py index 2e9632134c..7fd25b9852 100644 --- a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py +++ b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py @@ -29,13 +29,35 @@ class AbstractProvider: @classmethod @abc.abstractmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties + Returns dict for editable properties on system settings level Returns: - (dict) + (list) of dict + """ + + @classmethod + @abc.abstractmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + + @classmethod + @abc.abstractmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level + + + Returns: + (list) of dict """ @abc.abstractmethod diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 18d679b833..5db728f2de 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -96,30 +96,62 @@ class GDriveHandler(AbstractProvider): return self.service is not None @classmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties. + Returns dict for editable properties on system settings level + + + Returns: + (list) of dict + """ + return [] + + @classmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + # {platform} tells that value is multiplatform and only specific OS + # should be returned + editable = [ + # credentials could be override on Project or User level + { + 'label': "Credentials url", + 'type': 'text', + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + }, + # roots could be override only on Project leve, User cannot + # + { + 'label': "Roots", + 'type': 'dict' + } + ] + return editable + + @classmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level Returns: (dict) """ - # {platform} tells that value is multiplatform and only specific OS - # should be returned - editable = { + editable = [ # credentials could be override on Project or User level - 'credentials_url': { - 'scope': [EditableScopes.PROJECT, - EditableScopes.LOCAL], + { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 - }, - # roots could be override only on Project leve, User cannot - 'root': {'scope': [EditableScopes.PROJECT], - 'label': "Roots", - 'type': 'dict'} - } + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + } + ] return editable def get_roots_config(self, anatomy=None): diff --git a/openpype/modules/default_modules/sync_server/providers/lib.py b/openpype/modules/default_modules/sync_server/providers/lib.py index 816ccca981..463e49dd4d 100644 --- a/openpype/modules/default_modules/sync_server/providers/lib.py +++ b/openpype/modules/default_modules/sync_server/providers/lib.py @@ -76,6 +76,14 @@ class ProviderFactory: return provider_info[0].get_configurable_items() + def get_provider_cls(self, provider_code): + """ + Returns class object for 'provider_code' to run class methods on. + """ + provider_info = self._get_creator_info(provider_code) + + return provider_info[0] + def _get_creator_info(self, provider): """ Collect all necessary info for provider. Currently only creator diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 4b80ed44f2..b3482ac1d8 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -30,18 +30,59 @@ class LocalDriveHandler(AbstractProvider): return True @classmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties + Returns dict for editable properties on system settings level + + + Returns: + (list) of dict + """ + return [] + + @classmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + # {platform} tells that value is multiplatform and only specific OS + # should be returned + editable = [ + # credentials could be override on Project or User level + { + 'label': "Credentials url", + 'type': 'text', + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + }, + # roots could be override only on Project leve, User cannot + # + { + 'label': "Roots", + 'type': 'dict' + } + ] + return editable + + @classmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level + Returns: (dict) """ - editable = { - 'root': {'scope': [EditableScopes.LOCAL], - 'label': "Roots", - 'type': 'dict'} - } + editable = [ + { + 'label': "Roots", + 'type': 'dict' + } + ] return editable def upload_file(self, source_path, target_path, diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index f0ae64d3fd..2eb749801e 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -8,7 +8,7 @@ import copy from avalon.api import AvalonMongoDB from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayModule +from openpype.modules.default_modules.interfaces import ITrayModule from openpype.api import ( Anatomy, get_project_settings, @@ -399,272 +399,239 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return remote_site - def get_local_settings_schema(self): - """Wrapper for Local settings - all projects incl. Default""" - return self.get_configurable_items(EditableScopes.LOCAL) - + # Methods for Settings UI to draw appropriate forms @classmethod - def get_system_configurable_items_for_provider(cls, provider_name): - """ Gets system level configurable items without use of Setting + def get_system_settings_schema(cls): + """ Gets system level schema of configurable items Used for Setting UI to provide forms. """ - scope = EditableScopes.SYSTEM - return cls._get_configurable_items_for_provider(provider_name, scope) - - @classmethod - def get_project_configurable_items_for_provider(cls, provider_name): - """ Gets project level configurable items without use of Setting - - It is not using Setting! Used for Setting UI to provide forms. - """ - scope = EditableScopes.PROJECT - return cls._get_configurable_items_for_provider(provider_name, scope) - - @classmethod - def get_system_configurable_items_for_providers(cls): - """ Gets system level configurable items for all providers. - - It is not using Setting! Used for Setting UI to provide forms. - """ - scope = EditableScopes.SYSTEM ret_dict = {} - for provider_name in lib.factory.providers: - ret_dict[provider_name] = \ - cls._get_configurable_items_for_provider(provider_name, scope) + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_system_settings_schema() return ret_dict @classmethod - def get_project_configurable_items_for_providers(cls): - """ Gets project level configurable items for all providers. + def get_project_settings_schema(cls): + """ Gets project level schema of configurable items. It is not using Setting! Used for Setting UI to provide forms. """ - scope = EditableScopes.PROJECT ret_dict = {} - for provider_name in lib.factory.providers: - ret_dict[provider_name] = \ - cls._get_configurable_items_for_provider(provider_name, scope) + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_project_settings_schema() return ret_dict @classmethod - def _get_configurable_items_for_provider(cls, provider_name, scope): + def get_local_settings_schema(cls): + """ Gets local level schema of configurable items. + + It is not using Setting! Used for Setting UI to provide forms. """ - Args: - provider_name (str) - scope (EditableScopes) - Returns - (list) of (dict) - """ - items = lib.factory.get_provider_configurable_items(provider_name) + ret_dict = {} + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_local_settings_schema() - ret = [] - for key in sorted(items.keys()): - item = items[key] - if scope in item["scope"]: - item.pop("scope") # unneeded by UI - if scope in [EditableScopes.SYSTEM, EditableScopes.PROJECT]: - item.pop("namespace", None) # unneeded by UI - ret.append(item) + return ret_dict - return ret - - def get_configurable_items(self, scope=None): - """ - Returns list of sites that could be configurable for all projects. - - Could be filtered by 'scope' argument (list) - - Args: - scope (list of utils.EditableScope) - - Returns: - (dict of list of dict) - { - siteA : [ - { - key:"root", label:"root", - "value":"{'work': 'c:/projects'}", - "type": "dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, - { - key:"credentials_url", label:"Credentials url", - "value":"'c:/projects/cred.json'", "type": "text", - "namespace": "{project_setting}/global/sync_server/ - sites" - } - ] - } - """ - editable = {} - applicable_projects = list(self.connection.projects()) - applicable_projects.append(None) - for project in applicable_projects: - project_name = None - if project: - project_name = project["name"] - - items = self.get_configurable_items_for_project(project_name, - scope) - editable.update(items) - - return editable - - def get_local_settings_schema_for_project(self, project_name): - """Wrapper for Local settings - for specific 'project_name'""" - return self.get_configurable_items_for_project(project_name, - EditableScopes.LOCAL) - - def get_configurable_items_for_project(self, project_name=None, - scope=None): - """ - Returns list of items that could be configurable for specific - 'project_name' - - Args: - project_name (str) - None > default project, - scope (list of utils.EditableScope) - (optional, None is all scopes, default is LOCAL) - - Returns: - (dict of list of dict) - { - siteA : [ - { - key:"root", label:"root", - "type": "dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, - { - key:"credentials_url", label:"Credentials url", - "value":"'c:/projects/cred.json'", "type": "text", - "namespace": "{project_setting}/global/sync_server/ - sites" - } - ] - } - """ - allowed_sites = set() - sites = self.get_all_site_configs(project_name) - if project_name: - # Local Settings can select only from allowed sites for project - allowed_sites.update(set(self.get_active_sites(project_name))) - allowed_sites.update(set(self.get_remote_sites(project_name))) - - editable = {} - for site_name in sites.keys(): - if allowed_sites and site_name not in allowed_sites: - continue - - items = self.get_configurable_items_for_site(project_name, - site_name, - scope) - # Local Settings need 'local' instead of real value - site_name = site_name.replace(get_local_site_id(), 'local') - editable[site_name] = items - - return editable - - def get_local_settings_schema_for_site(self, project_name, site_name): - """Wrapper for Local settings - for particular 'site_name and proj.""" - return self.get_configurable_items_for_site(project_name, - site_name, - EditableScopes.LOCAL) - - def get_configurable_items_for_site(self, project_name=None, - site_name=None, - scope=None): - """ - Returns list of items that could be configurable. - - Args: - project_name (str) - None > default project - site_name (str) - scope (list of utils.EditableScope) - (optional, None is all scopes) - - Returns: - (list) - [ - { - key:"root", label:"root", type:"dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, ... - ] - """ - provider_name = self.get_provider_for_site(site=site_name) - items = lib.factory.get_provider_configurable_items(provider_name) - - if project_name: - sync_s = self.get_sync_project_setting(project_name, - exclude_locals=True, - cached=False) - else: - sync_s = get_default_project_settings(exclude_locals=True) - sync_s = sync_s["global"]["sync_server"] - sync_s["sites"].update( - self._get_default_site_configs(self.enabled)) - - editable = [] - if type(scope) is not list: - scope = [scope] - scope = set(scope) - for key, properties in items.items(): - if scope is None or scope.intersection(set(properties["scope"])): - val = sync_s.get("sites", {}).get(site_name, {}).get(key) - - item = { - "key": key, - "label": properties["label"], - "type": properties["type"] - } - - if properties.get("namespace"): - item["namespace"] = properties.get("namespace") - if "platform" in item["namespace"]: - try: - if val: - val = val[platform.system().lower()] - except KeyError: - st = "{}'s field value {} should be".format(key, val) # noqa: E501 - log.error(st + " multiplatform dict") - - item["namespace"] = item["namespace"].replace('{site}', - site_name) - children = [] - if properties["type"] == "dict": - if val: - for val_key, val_val in val.items(): - child = { - "type": "text", - "key": val_key, - "value": val_val - } - children.append(child) - - if properties["type"] == "dict": - item["children"] = children - else: - item["value"] = val - - editable.append(item) - - return editable + # Needs to be refactored after Settings are updated + # # Methods for Settings to get appriate values to fill forms + # def get_configurable_items(self, scope=None): + # """ + # Returns list of sites that could be configurable for all projects. + # + # Could be filtered by 'scope' argument (list) + # + # Args: + # scope (list of utils.EditableScope) + # + # Returns: + # (dict of list of dict) + # { + # siteA : [ + # { + # key:"root", label:"root", + # "value":"{'work': 'c:/projects'}", + # "type": "dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, + # { + # key:"credentials_url", label:"Credentials url", + # "value":"'c:/projects/cred.json'", "type": "text", + # "namespace": "{project_setting}/global/sync_server/ + # sites" + # } + # ] + # } + # """ + # editable = {} + # applicable_projects = list(self.connection.projects()) + # applicable_projects.append(None) + # for project in applicable_projects: + # project_name = None + # if project: + # project_name = project["name"] + # + # items = self.get_configurable_items_for_project(project_name, + # scope) + # editable.update(items) + # + # return editable + # + # def get_local_settings_schema_for_project(self, project_name): + # """Wrapper for Local settings - for specific 'project_name'""" + # return self.get_configurable_items_for_project(project_name, + # EditableScopes.LOCAL) + # + # def get_configurable_items_for_project(self, project_name=None, + # scope=None): + # """ + # Returns list of items that could be configurable for specific + # 'project_name' + # + # Args: + # project_name (str) - None > default project, + # scope (list of utils.EditableScope) + # (optional, None is all scopes, default is LOCAL) + # + # Returns: + # (dict of list of dict) + # { + # siteA : [ + # { + # key:"root", label:"root", + # "type": "dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, + # { + # key:"credentials_url", label:"Credentials url", + # "value":"'c:/projects/cred.json'", "type": "text", + # "namespace": "{project_setting}/global/sync_server/ + # sites" + # } + # ] + # } + # """ + # allowed_sites = set() + # sites = self.get_all_site_configs(project_name) + # if project_name: + # # Local Settings can select only from allowed sites for project + # allowed_sites.update(set(self.get_active_sites(project_name))) + # allowed_sites.update(set(self.get_remote_sites(project_name))) + # + # editable = {} + # for site_name in sites.keys(): + # if allowed_sites and site_name not in allowed_sites: + # continue + # + # items = self.get_configurable_items_for_site(project_name, + # site_name, + # scope) + # # Local Settings need 'local' instead of real value + # site_name = site_name.replace(get_local_site_id(), 'local') + # editable[site_name] = items + # + # return editable + # + # def get_configurable_items_for_site(self, project_name=None, + # site_name=None, + # scope=None): + # """ + # Returns list of items that could be configurable. + # + # Args: + # project_name (str) - None > default project + # site_name (str) + # scope (list of utils.EditableScope) + # (optional, None is all scopes) + # + # Returns: + # (list) + # [ + # { + # key:"root", label:"root", type:"dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, ... + # ] + # """ + # provider_name = self.get_provider_for_site(site=site_name) + # items = lib.factory.get_provider_configurable_items(provider_name) + # + # if project_name: + # sync_s = self.get_sync_project_setting(project_name, + # exclude_locals=True, + # cached=False) + # else: + # sync_s = get_default_project_settings(exclude_locals=True) + # sync_s = sync_s["global"]["sync_server"] + # sync_s["sites"].update( + # self._get_default_site_configs(self.enabled)) + # + # editable = [] + # if type(scope) is not list: + # scope = [scope] + # scope = set(scope) + # for key, properties in items.items(): + # if scope is None or scope.intersection(set(properties["scope"])): + # val = sync_s.get("sites", {}).get(site_name, {}).get(key) + # + # item = { + # "key": key, + # "label": properties["label"], + # "type": properties["type"] + # } + # + # if properties.get("namespace"): + # item["namespace"] = properties.get("namespace") + # if "platform" in item["namespace"]: + # try: + # if val: + # val = val[platform.system().lower()] + # except KeyError: + # st = "{}'s field value {} should be".format(key, val) # noqa: E501 + # log.error(st + " multiplatform dict") + # + # item["namespace"] = item["namespace"].replace('{site}', + # site_name) + # children = [] + # if properties["type"] == "dict": + # if val: + # for val_key, val_val in val.items(): + # child = { + # "type": "text", + # "key": val_key, + # "value": val_val + # } + # children.append(child) + # + # if properties["type"] == "dict": + # item["children"] = children + # else: + # item["value"] = val + # + # editable.append(item) + # + # return editable def reset_timer(self): """ From 3f2a4d5a5aa5f8d037d55551a127e86990b60c70 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 11:59:40 +0200 Subject: [PATCH 069/450] Hound --- .../default_modules/sync_server/sync_server_module.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 2eb749801e..39b5c9314e 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -8,7 +8,7 @@ import copy from avalon.api import AvalonMongoDB from openpype.modules import OpenPypeModule -from openpype.modules.default_modules.interfaces import ITrayModule +from openpype_interfaces import ITrayModule from openpype.api import ( Anatomy, get_project_settings, @@ -16,14 +16,13 @@ from openpype.api import ( get_local_site_id) from openpype.lib import PypeLogger from openpype.settings.lib import ( - get_default_project_settings, get_default_anatomy_settings, get_anatomy_settings) from .providers.local_drive import LocalDriveHandler from .providers import lib -from .utils import time_function, SyncStatus, EditableScopes +from .utils import time_function, SyncStatus log = PypeLogger().get_logger("SyncServer") @@ -646,7 +645,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): enabled_projects = [] if self.enabled: - for project in self.connection.projects(): + for project in self.connection.projects(projection={"name": 1}): project_name = project["name"] project_settings = self.get_sync_project_setting(project_name) if project_settings and project_settings.get("enabled"): From 7d37971d64f57408b22fc582e1a388a99cc28c3a Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 15:47:38 +0200 Subject: [PATCH 070/450] get last version string from path This changes get_version_from_path() to produce the same result as version_up --- openpype/lib/path_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index e1dd1e7f10..88bb1a216a 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -77,7 +77,7 @@ def get_version_from_path(file): """ pattern = re.compile(r"[\._]v([0-9]+)", re.IGNORECASE) try: - return pattern.findall(file)[0] + return pattern.findall(file)[-1] except IndexError: log.error( "templates:get_version_from_workfile:" From 7a88a4ac1326796b6224e5547a3656b2a5b9032c Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 16:11:30 +0200 Subject: [PATCH 071/450] Makes thumbnail from the middle of the clip If read node frame range is 1001-1010, thumbnail is now made from frame 1005, not 505. --- .../nuke/plugins/publish/extract_thumbnail.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index da30dcc632..6921d6e9b3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -112,12 +112,12 @@ class ExtractThumbnail(openpype.api.Extractor): # create write node write_node = nuke.createNode("Write") - file = fhead + "jpeg" + file = fhead + "jpg" name = "thumbnail" path = os.path.join(staging_dir, file).replace("\\", "/") instance.data["thumbnail"] = path write_node["file"].setValue(path) - write_node["file_type"].setValue("jpeg") + write_node["file_type"].setValue("jpg") write_node["raw"].setValue(1) write_node.setInput(0, previous_node) temporary_nodes.append(write_node) @@ -126,10 +126,11 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 + mid_frame = int((int(last_frame) - int(first_frame) ) / 2) + int(first_frame) repre = { 'name': name, - 'ext': "jpeg", + 'ext': "jpg", "outputName": "thumb", 'files': file, "stagingDir": staging_dir, @@ -140,7 +141,7 @@ class ExtractThumbnail(openpype.api.Extractor): instance.data["representations"].append(repre) # Render frames - nuke.execute(write_node.name(), int(first_frame), int(last_frame)) + nuke.execute(write_node.name(), int(mid_frame), int(mid_frame)) self.log.debug( "representations: {}".format(instance.data["representations"])) @@ -157,12 +158,12 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: - ip = v['input_process'].getValue() - ipn = v['input_process_node'].getValue() - if "VIEWER_INPUT" not in ipn and ip: - ipn_orig = nuke.toNode(ipn) - ipn_orig.setSelected(True) + if "Viewer" == n.Class()]: + ip = v['input_process'].getValue() + ipn = v['input_process_node'].getValue() + if "VIEWER_INPUT" not in ipn and ip: + ipn_orig = nuke.toNode(ipn) + ipn_orig.setSelected(True) if ipn_orig: nuke.nodeCopy('%clipboard%') From 84ca6a591c5aa1d15cb16cde3e05318eb5b71915 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 8 Sep 2021 23:19:23 +0800 Subject: [PATCH 072/450] fix linter --- .../plugins/inventory/import_modelrender.py | 33 +++++++++++-------- .../maya/plugins/publish/extract_look.py | 6 ++-- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/maya/plugins/inventory/import_modelrender.py b/openpype/hosts/maya/plugins/inventory/import_modelrender.py index 2737901b51..c13b4d6a1c 100644 --- a/openpype/hosts/maya/plugins/inventory/import_modelrender.py +++ b/openpype/hosts/maya/plugins/inventory/import_modelrender.py @@ -19,18 +19,20 @@ class ImportModelRender(api.InventoryAction): from maya import cmds for container in containers: - container_name = container["objectName"] + con_name = container["objectName"] nodes = [] - for n in cmds.sets(container_name, query=True, nodesOnly=True) or []: + for n in cmds.sets(con_name, query=True, nodesOnly=True) or []: if cmds.nodeType(n) == "reference": nodes += cmds.referenceQuery(n, nodes=True) else: nodes.append(n) - repr_doc = io.find_one({"_id": io.ObjectId(container["representation"])}) + repr_doc = io.find_one({ + "_id": io.ObjectId(container["representation"]), + }) version_id = repr_doc["parent"] - print("Importing render sets for model %r" % container_name) + print("Importing render sets for model %r" % con_name) self.assign_model_render_by_version(nodes, version_id) def assign_model_render_by_version(self, nodes, version_id): @@ -51,22 +53,25 @@ class ImportModelRender(api.InventoryAction): from openpype.hosts.maya.api import lib # Get representations of shader file and relationships - look_representation = io.find_one({"type": "representation", - "parent": version_id, - "name": self.scene_type}) - - if not look_representation: + look_repr = io.find_one({ + "type": "representation", + "parent": version_id, + "name": self.scene_type, + }) + if not look_repr: print("No model render sets for this model version..") return - json_representation = io.find_one({"type": "representation", - "parent": version_id, - "name": self.look_data_type}) + json_repr = io.find_one({ + "type": "representation", + "parent": version_id, + "name": self.look_data_type, + }) - context = pipeline.get_representation_context(look_representation['_id']) + context = pipeline.get_representation_context(look_repr["_id"]) maya_file = pipeline.get_representation_path_from_context(context) - context = pipeline.get_representation_context(json_representation['_id']) + context = pipeline.get_representation_context(json_repr["_id"]) json_file = pipeline.get_representation_path_from_context(context) # Import the look file diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 62b58623e7..0a3a8d2e79 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -489,9 +489,9 @@ class ExtractLook(openpype.api.Extractor): class ExtractModelRenderSets(ExtractLook): """Extract model render attribute sets as model metadata - Only extracts the render attrib sets (NO shadingEngines) alongside a .json file - that stores it relationships for the sets and "attribute" data for the - instance members. + Only extracts the render attrib sets (NO shadingEngines) alongside + a .json file that stores it relationships for the sets and "attribute" + data for the instance members. """ From 316ee85f7b8635a2cd4266376eee88836d164b44 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 8 Sep 2021 23:21:35 +0800 Subject: [PATCH 073/450] fix linter --- openpype/hosts/maya/plugins/inventory/import_modelrender.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/inventory/import_modelrender.py b/openpype/hosts/maya/plugins/inventory/import_modelrender.py index c13b4d6a1c..3675b757ea 100644 --- a/openpype/hosts/maya/plugins/inventory/import_modelrender.py +++ b/openpype/hosts/maya/plugins/inventory/import_modelrender.py @@ -12,8 +12,10 @@ class ImportModelRender(api.InventoryAction): @staticmethod def is_compatible(container): - return container.get("loader") == "ReferenceLoader" \ - and container.get("name", "").startswith("model") + return ( + container.get("loader") == "ReferenceLoader" + and container.get("name", "").startswith("model") + ) def process(self, containers): from maya import cmds From 0518064e96ef6b3a714988dfe88c6061173795f2 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:30:17 +0200 Subject: [PATCH 074/450] indent change --- .../hosts/nuke/plugins/publish/extract_thumbnail.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 6921d6e9b3..93a5c9b51d 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,11 +159,11 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() if "Viewer" == n.Class()]: - ip = v['input_process'].getValue() - ipn = v['input_process_node'].getValue() - if "VIEWER_INPUT" not in ipn and ip: - ipn_orig = nuke.toNode(ipn) - ipn_orig.setSelected(True) + ip = v['input_process'].getValue() + ipn = v['input_process_node'].getValue() + if "VIEWER_INPUT" not in ipn and ip: + ipn_orig = nuke.toNode(ipn) + ipn_orig.setSelected(True) if ipn_orig: nuke.nodeCopy('%clipboard%') From 81432101164a3731c65e6d60b9880029a28e8202 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:33:32 +0200 Subject: [PATCH 075/450] long line fix --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 93a5c9b51d..dfb26aab80 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -126,7 +126,7 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 - mid_frame = int((int(last_frame) - int(first_frame) ) / 2) + int(first_frame) + mid_frame = int((int(last_frame)-int(first_frame))/2)+int(first_frame) repre = { 'name': name, From 25a7c5d0f0d15525d1fe58ffdc8f120558eac462 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:38:29 +0200 Subject: [PATCH 076/450] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index dfb26aab80..4c21891f48 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -126,7 +126,8 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 - mid_frame = int((int(last_frame)-int(first_frame))/2)+int(first_frame) + mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ + + int(first_frame) repre = { 'name': name, From 17fd666a49a374a895a4c6913cb8f326c411de56 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:40:58 +0200 Subject: [PATCH 077/450] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 4c21891f48..c0dc1417c3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -127,7 +127,7 @@ class ExtractThumbnail(openpype.api.Extractor): first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ - + int(first_frame) + + int(first_frame) repre = { 'name': name, From 1ab40ddd244b27db31fe639cc63e15096301e1e9 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:43:31 +0200 Subject: [PATCH 078/450] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index c0dc1417c3..7ffeec2db9 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -124,10 +124,10 @@ class ExtractThumbnail(openpype.api.Extractor): tags = ["thumbnail", "publish_on_farm"] # retime for - first_frame = int(last_frame) / 2 - last_frame = int(last_frame) / 2 mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ + int(first_frame) + first_frame = int(last_frame) / 2 + last_frame = int(last_frame) / 2 repre = { 'name': name, From ca3034f2725fb1be97b37850f8a7d1ab0e017be7 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:46:09 +0200 Subject: [PATCH 079/450] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 7ffeec2db9..b9d6762880 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,7 +159,7 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From e3b319ffc66fc24668480fc3405bb05aa51089c2 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:58:31 +0200 Subject: [PATCH 080/450] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index b9d6762880..55f7b746fc 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,7 +159,7 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From 777facc69c792356ace880d5c19a7f8f9cf1da51 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 18:46:56 +0200 Subject: [PATCH 081/450] #1784 - init commit of pype command Pype command is better than run_tests.ps1 as OP is initialized by start.py. This is more similar to standard running of OP --- openpype/cli.py | 15 +++++++++++++++ openpype/pype_commands.py | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/openpype/cli.py b/openpype/cli.py index 18cc1c63cd..c69407e295 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -283,3 +283,18 @@ def run(script): args_string = " ".join(args[1:]) print(f"... running: {script} {args_string}") runpy.run_path(script, run_name="__main__", ) + + +@main.command() +@click.argument("folder", nargs=-1) +@click.option("-m", + "--mark", + help="Run tests marked by", + default=None) +@click.option("-p", + "--pyargs", + help="Run tests from package", + default=None) +def runtests(folder, mark, pyargs): + """Run all automatic tests after proper initialization via start.py""" + PypeCommands().run_tests(folder, mark, pyargs) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index c18fe36667..c309ee8c09 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -257,3 +257,24 @@ class PypeCommands: def validate_jsons(self): pass + def run_tests(self, folder, mark, pyargs): + """ + Runs tests from 'folder' + + Args: + folder (str): relative path to folder with tests + mark (str): label to run tests marked by it (slow etc) + pyargs (str): package path to test + """ + print("run_tests") + import subprocess + folder = folder or "../tests" + mark_str = pyargs_str = '' + if mark: + mark_str = "- m {}".format(mark) + + if pyargs: + pyargs_str = "--pyargs {}".format(pyargs) + + subprocess.run("pytest {} {} {}".format(folder, mark_str, pyargs_str)) + From 5a425a5269c57496dfbd02adbaedbc969a812e22 Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Wed, 8 Sep 2021 16:09:40 -0700 Subject: [PATCH 082/450] Moving project folder structure creation out of ftrack module #1989 --- openpype/api.py | 11 ++- openpype/lib/__init__.py | 6 +- openpype/lib/path_tools.py | 43 ++++++++++ .../action_create_project_structure.py | 79 +++---------------- openpype/settings/__init__.py | 5 +- openpype/settings/lib.py | 29 +++++++ 6 files changed, 98 insertions(+), 75 deletions(-) diff --git a/openpype/api.py b/openpype/api.py index ce18097eca..dcff127e9f 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -4,6 +4,7 @@ from .settings import ( get_current_project_settings, get_anatomy_settings, get_environments, + get_project_basic_paths, SystemSettings, ProjectSettings @@ -24,7 +25,8 @@ from .lib import ( get_latest_version, get_global_environments, get_local_site_id, - change_openpype_mongo_url + change_openpype_mongo_url, + create_project_folders ) from .lib.mongo import ( @@ -72,6 +74,7 @@ __all__ = [ "get_current_project_settings", "get_anatomy_settings", "get_environments", + "get_project_basic_paths", "SystemSettings", @@ -120,5 +123,9 @@ __all__ = [ "get_global_environments", "get_local_site_id", - "change_openpype_mongo_url" + "change_openpype_mongo_url", + + "get_project_basic_paths", + "create_project_folders" + ] diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 12c04a4236..886d61fb39 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -139,7 +139,8 @@ from .plugin_tools import ( from .path_tools import ( version_up, get_version_from_path, - get_last_version_from_path + get_last_version_from_path, + create_project_folders ) from .editorial import ( @@ -268,5 +269,6 @@ __all__ = [ "range_from_frames", "frames_to_secons", "frames_to_timecode", - "make_sequence_collection" + "make_sequence_collection", + "create_project_folders" ] diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index e1dd1e7f10..fab0879759 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -2,8 +2,12 @@ import os import re import logging +from openpype.api import Anatomy + log = logging.getLogger(__name__) +pattern_array = re.compile(r"\[.*\]") +project_root_key = "__project_root__" def _rreplace(s, a, b, n=1): """Replace a with b in string s from right side n times.""" @@ -119,3 +123,42 @@ def get_last_version_from_path(path_dir, filter): return filtred_files[-1] return None + + +def compute_paths(basic_paths_items, project_root): + output = [] + for path_items in basic_paths_items: + clean_items = [] + for path_item in path_items: + matches = re.findall(pattern_array, path_item) + if len(matches) > 0: + path_item = path_item.replace(matches[0], "") + if path_item == project_root_key: + path_item = project_root + clean_items.append(path_item) + output.append(os.path.normpath(os.path.sep.join(clean_items))) + return output + + +def create_project_folders(basic_paths, project_name): + anatomy = Anatomy(project_name) + roots_paths = [] + if isinstance(anatomy.roots, dict): + for root in anatomy.roots.values(): + roots_paths.append(root.value) + else: + roots_paths.append(anatomy.roots.value) + + for root_path in roots_paths: + project_root = os.path.join(root_path, project_name) + full_paths = compute_paths(basic_paths, project_root) + # Create folders + for path in full_paths: + full_path = path.format(project_root=project_root) + if os.path.exists(full_path): + log.debug( + "Folder already exists: {}".format(full_path) + ) + else: + log.debug("Creating folder: {}".format(full_path)) + os.makedirs(full_path) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py index 035a1c60de..b0de792473 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py @@ -3,7 +3,7 @@ import re import json from openpype.modules.ftrack.lib import BaseAction, statics_icon -from openpype.api import Anatomy, get_project_settings +from openpype.api import get_project_basic_paths, create_project_folders class CreateProjectFolders(BaseAction): @@ -72,25 +72,18 @@ class CreateProjectFolders(BaseAction): def launch(self, session, entities, event): # Get project entity project_entity = self.get_project_from_entity(entities[0]) - # Load settings for project project_name = project_entity["full_name"] - project_settings = get_project_settings(project_name) - project_folder_structure = ( - project_settings["global"]["project_folder_structure"] - ) - if not project_folder_structure: - return { - "success": False, - "message": "Project structure is not set." - } - try: - if isinstance(project_folder_structure, str): - project_folder_structure = json.loads(project_folder_structure) - # Get paths based on presets - basic_paths = self.get_path_items(project_folder_structure) - self.create_folders(basic_paths, project_entity) + basic_paths = get_project_basic_paths(project_name) + if not basic_paths: + return { + "success": False, + "message": "Project structure is not set." + } + + # Invoking OpenPype API to create the project folders + create_project_folders(basic_paths, project_name) self.create_ftrack_entities(basic_paths, project_entity) except Exception as exc: @@ -195,58 +188,6 @@ class CreateProjectFolders(BaseAction): self.session.commit() return new_ent - def get_path_items(self, in_dict): - output = [] - for key, value in in_dict.items(): - if not value: - output.append(key) - else: - paths = self.get_path_items(value) - for path in paths: - if not isinstance(path, (list, tuple)): - path = [path] - - output.append([key, *path]) - - return output - - def compute_paths(self, basic_paths_items, project_root): - output = [] - for path_items in basic_paths_items: - clean_items = [] - for path_item in path_items: - matches = re.findall(self.pattern_array, path_item) - if len(matches) > 0: - path_item = path_item.replace(matches[0], "") - if path_item == self.project_root_key: - path_item = project_root - clean_items.append(path_item) - output.append(os.path.normpath(os.path.sep.join(clean_items))) - return output - - def create_folders(self, basic_paths, project): - anatomy = Anatomy(project["full_name"]) - roots_paths = [] - if isinstance(anatomy.roots, dict): - for root in anatomy.roots.values(): - roots_paths.append(root.value) - else: - roots_paths.append(anatomy.roots.value) - - for root_path in roots_paths: - project_root = os.path.join(root_path, project["full_name"]) - full_paths = self.compute_paths(basic_paths, project_root) - # Create folders - for path in full_paths: - full_path = path.format(project_root=project_root) - if os.path.exists(full_path): - self.log.debug( - "Folder already exists: {}".format(full_path) - ) - else: - self.log.debug("Creating folder: {}".format(full_path)) - os.makedirs(full_path) - def register(session): CreateProjectFolders(session).register() diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index b5810deef4..1905b6dc73 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -7,7 +7,8 @@ from .lib import ( get_current_project_settings, get_anatomy_settings, get_environments, - get_local_settings + get_local_settings, + get_project_basic_paths ) from .entities import ( SystemSettings, @@ -24,7 +25,7 @@ __all__ = ( "get_anatomy_settings", "get_environments", "get_local_settings", - + "get_project_basic_paths", "SystemSettings", "ProjectSettings" ) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 5c2c0dcd94..6d8ece1b53 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -901,6 +901,35 @@ def get_general_environments(): return environments +def _list_path_items(folder_structure): + output = [] + for key, value in folder_structure.items(): + if not value: + output.append(key) + else: + paths = _list_path_items(value) + for path in paths: + if not isinstance(path, (list, tuple)): + path = [path] + + output.append([key, *path]) + + return output + + +def get_project_basic_paths(project_name): + project_settings = get_project_settings(project_name) + folder_structure = ( + project_settings["global"]["project_folder_structure"] + ) + if not folder_structure: + return [] + + if isinstance(folder_structure, str): + folder_structure = json.loads(folder_structure) + return _list_path_items(folder_structure) + + def clear_metadata_from_settings(values): """Remove all metadata keys from loaded settings.""" if isinstance(values, dict): From 46f38b1e02888769f93727ae77baedc6182c5c26 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:18:02 +0200 Subject: [PATCH 083/450] Fix typos --- openpype/modules/README.md | 168 ++++++++++++++++++------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index a6857b2c51..abc7ed3961 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -1,31 +1,31 @@ # OpenPype modules/addons -OpenPype modules should contain separated logic of specific kind of implementation, like Ftrack connection and usage code or Deadline farm rendering or may contain only special plugins. Addons work the same way currently there is no difference in module and addon. +OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its usage code, Deadline farm rendering or may contain only special plugins. Addons work the same way currently, there is no difference between module and addon functionality. ## Modules concept -- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the modulo located -- modules or addons should never be imported directly even if you know possible full import path - - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts +- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the module located +- modules or addons should never be imported directly, even if you know possible full import path + - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts ### TODOs - add module/addon manifest - - definition of module (not 100% defined content e.g. minimum require OpenPype version etc.) - - defying that folder is content of a module or an addon + - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.) + - defying that folder is content of a module or an addon ## Base class `OpenPypeModule` - abstract class as base for each module -- implementation should be module's api withou GUI parts -- may implement `get_global_environments` method which should return dictionary of environments that are globally appliable and value is the same for whole studio if launched at any workstation (except os specific paths) +- implementation should contain module's api without GUI parts +- may implement `get_global_environments` method which should return dictionary of environments that are globally applicable and value is the same for whole studio if launched at any workstation (except os specific paths) - abstract parts: - - `name` attribute - name of a module - - `initialize` method - method for own initialization of a module (should not override `__init__`) - - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules -- `__init__` should not be overriden and `initialize` should not do time consuming part but only prepare base data about module - - also keep in mind that they may be initialized in headless mode + - `name` attribute - name of a module + - `initialize` method - method for own initialization of a module (should not override `__init__`) + - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules +- `__init__` should not be overridden and `initialize` should not do time consuming part but only prepare base data about module + - also keep in mind that they may be initialized in headless mode - connection with other modules is made with help of interfaces ## Addon class `OpenPypeAddOn` -- inherit from `OpenPypeModule` but is enabled by default and don't have to implement `initialize` and `connect_with_modules` methods - - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) +- inherits from `OpenPypeModule` but is enabled by default and doesn't have to implement `initialize` and `connect_with_modules` methods + - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) ## How to add addons/modules - in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder @@ -34,110 +34,110 @@ OpenPype modules should contain separated logic of specific kind of implementati ## Addon/module settings - addons/modules may have defined custom settings definitions with default values - it is based on settings type `dynamic_schema` which has `name` - - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults - - they can't be added to any schema hierarchy - - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) - - addons may define it's dynamic schema items -- they can be defined with class which inherit from `BaseModuleSettingsDef` - - it is recommended to use preimplemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values - - check it's docstring and check for `example_addon` in example addons + - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults + - they can't be added to any schema hierarchy + - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) + - addons may define it's dynamic schema items +- they can be defined with class which inherits from `BaseModuleSettingsDef` + - it is recommended to use pre implemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values + - check it's docstring and check for `example_addon` in example addons - settings definition returns schemas by dynamic schemas names # Interfaces -- interface is class that has defined abstract methods to implement and may contain preimplemented helper methods +- interface is class that has defined abstract methods to implement and may contain pre implemented helper methods - module that inherit from an interface must implement those abstract methods otherwise won't be initialized -- it is easy to find which module object inherited from which interfaces withh 100% chance they have implemented required methods +- it is easy to find which module object inherited from which interfaces with 100% chance they have implemented required methods - interfaces can be defined in `interfaces.py` inside module directory - - the file can't use relative imports or import anything from other parts - of module itself at the header of file - - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation + - the file can't use relative imports or import anything from other parts + of module itself at the header of file + - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation ## Base class `OpenPypeInterface` - has nothing implemented - has ABCMeta as metaclass - is defined to be able find out classes which inherit from this base to be - able tell this is an Interface + able tell this is an Interface ## Global interfaces - few interfaces are implemented for global usage ### IPluginPaths -- module want to add directory path/s to avalon or publish plugins +- module wants to add directory path/s to avalon or publish plugins - module must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"` - - each key may contain list or string with path to directory with plugins + - each key may contain list or string with a path to directory with plugins ### ITrayModule -- module has more logic when used in tray - - it is possible that module can be used only in tray +- module has more logic when used in a tray + - it is possible that module can be used only in the tray - abstract methods - - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules` - - `tray_menu` - add actions to tray widget's menu that represent the module - - `tray_start` - start of module's login in tray - - module is initialized and connected with other modules - - `tray_exit` - module's cleanup like stop and join threads etc. - - order of calling is based on implementation this order is how it works with `TrayModulesManager` - - it is recommended to import and use GUI implementaion only in these methods + - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules` + - `tray_menu` - add actions to tray widget's menu that represent the module + - `tray_start` - start of module's login in tray + - module is initialized and connected with other modules + - `tray_exit` - module's cleanup like stop and join threads etc. + - order of calling is based on implementation this order is how it works with `TrayModulesManager` + - it is recommended to import and use GUI implementation only in these methods - has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init` - - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations + - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations ### ITrayService -- inherit from `ITrayModule` and implement `tray_menu` method for you - - add action to submenu "Services" in tray widget menu with icon and label -- abstract atttribute `label` - - label shown in menu -- interface has preimplemented methods to change icon color - - `set_service_running` - green icon - - `set_service_failed` - red icon - - `set_service_idle` - orange icon - - these states must be set by module itself `set_service_running` is default state on initialization +- inherits from `ITrayModule` and implements `tray_menu` method for you + - adds action to submenu "Services" in tray widget menu with icon and label +- abstract attribute `label` + - label shown in menu +- interface has pre implemented methods to change icon color + - `set_service_running` - green icon + - `set_service_failed` - red icon + - `set_service_idle` - orange icon + - these states must be set by module itself `set_service_running` is default state on initialization ### ITrayAction -- inherit from `ITrayModule` and implement `tray_menu` method for you - - add action to tray widget menu with label -- abstract atttribute `label` - - label shown in menu +- inherits from `ITrayModule` and implements `tray_menu` method for you + - adds action to tray widget menu with label +- abstract attribute `label` + - label shown in menu - abstract method `on_action_trigger` - - what should happen when action is triggered -- NOTE: It is good idea to implement logic in `on_action_trigger` to api method and trigger that methods on callbacks this gives ability to trigger that method outside tray + - what should happen when an action is triggered +- NOTE: It is a good idea to implement logic in `on_action_trigger` to the api method and trigger that method on callbacks. This gives ability to trigger that method outside tray ## Modules interfaces -- modules may have defined their interfaces to be able recognize other modules that would want to use their features -- -### Example: -- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which of other modules want to add paths to server/user event handlers - - Clockify module use `IFtrackEventHandlerPaths` and return paths to clockify ftrack synchronizers +- modules may have defined their own interfaces to be able to recognize other modules that would want to use their features -- Clockify has more inharitance it's class definition looks like +### Example: +- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which other modules want to add paths to server/user event handlers + - Clockify module use `IFtrackEventHandlerPaths` and returns paths to clockify ftrack synchronizers + +- Clockify inherits from more interfaces. It's class definition looks like: ``` class ClockifyModule( - OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize. - ITrayModule, # Says has special implementation when used in tray. - IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher). - IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server. - ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module. + OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize. + ITrayModule, # Says has special implementation when used in tray. + IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher). + IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server. + ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module. ): ``` ### ModulesManager -- collect module classes and tries to initialize them +- collects module classes and tries to initialize them - important attributes - - `modules` - list of available attributes - - `modules_by_id` - dictionary of modules mapped by their ids - - `modules_by_name` - dictionary of modules mapped by their names - - all these attributes contain all found modules even if are not enabled + - `modules` - list of available attributes + - `modules_by_id` - dictionary of modules mapped by their ids + - `modules_by_name` - dictionary of modules mapped by their names + - all these attributes contain all found modules even if are not enabled - helper methods - - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them - - `collect_plugin_paths` collect plugin paths from all enabled modules - - output is always dictionary with all keys and values as list - ``` - { - "publish": [], - "create": [], - "load": [], - "actions": [] - } - ``` + - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them + - `collect_plugin_paths` collects plugin paths from all enabled modules + - output is always dictionary with all keys and values as an list + ``` + { + "publish": [], + "create": [], + "load": [], + "actions": [] + } + ``` ### TrayModulesManager -- inherit from `ModulesManager` -- has specific implementations for Pype Tray tool and handle `ITrayModule` methods +- inherits from `ModulesManager` +- has specific implementation for Pype Tray tool and handle `ITrayModule` methods \ No newline at end of file From a7932ff6d536fb00415158b617d59a348afaa455 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:38:15 +0200 Subject: [PATCH 084/450] Fix typos --- .../modules/example_addons/example_addon/addon.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 5a25b80616..5573e33cc1 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -21,11 +21,11 @@ from openpype_interfaces import ( ) -# Settings definiton of this addon using `JsonFilesSettingsDef` -# - JsonFilesSettingsDef is prepared settings definiton using json files -# to define settings and store defaul values +# Settings definition of this addon using `JsonFilesSettingsDef` +# - JsonFilesSettingsDef is prepared settings definition using json files +# to define settings and store default values class AddonSettingsDef(JsonFilesSettingsDef): - # This will add prefix to every schema and template from `schemas` + # This will add prefixes to every schema and template from `schemas` # subfolder. # - it is not required to fill the prefix but it is highly # recommended as schemas and templates may have name clashes across @@ -48,7 +48,7 @@ class AddonSettingsDef(JsonFilesSettingsDef): class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): """This Addon has defined it's settings and interface. - This example has system settings with enabled option. And use + This example has system settings with an enabled option. And use few other interfaces: - `IPluginPaths` to define custom plugin paths - `ITrayAction` to be shown in tray tool @@ -70,7 +70,7 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): def tray_init(self): """Implementation of abstract method for `ITrayAction`. - We're definetely in trat tool so we can precreate dialog. + We're definitely in tray tool so we can pre create dialog. """ self._create_dialog() @@ -101,7 +101,7 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): """Show dialog with connected modules. This can be called from anywhere but can also crash in headless mode. - There is not way how to prevent addon to do invalid operations if he's + There is no way to prevent addon to do invalid operations if he's not handling them. """ # Make sure dialog is created From 3643b8e1bd7936dd840b343f9b70946600e249d7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:43:43 +0200 Subject: [PATCH 085/450] Hound --- .../modules/default_modules/sync_server/providers/gdrive.py | 3 +-- .../default_modules/sync_server/providers/local_drive.py | 3 +-- .../default_modules/sync_server/sync_server_module.py | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 5db728f2de..da54eecb8e 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -122,8 +122,7 @@ class GDriveHandler(AbstractProvider): { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index b3482ac1d8..9678d38ed8 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -56,8 +56,7 @@ class LocalDriveHandler(AbstractProvider): { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 39b5c9314e..976a349bfa 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -445,7 +445,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # # Methods for Settings to get appriate values to fill forms # def get_configurable_items(self, scope=None): # """ - # Returns list of sites that could be configurable for all projects. + # Returns list of sites that could be configurable for all projects # # Could be filtered by 'scope' argument (list) # @@ -468,8 +468,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # }, # { # key:"credentials_url", label:"Credentials url", - # "value":"'c:/projects/cred.json'", "type": "text", - # "namespace": "{project_setting}/global/sync_server/ + # "value":"'c:/projects/cred.json'", "type": "text", # noqa: E501 + # "namespace": "{project_setting}/global/sync_server/ # noqa: E501 # sites" # } # ] From 2230d60ff407f033ab7127d5f03c3b62e0309e80 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:48:28 +0200 Subject: [PATCH 086/450] #1976 - added 'key' --- .../default_modules/sync_server/providers/gdrive.py | 3 +++ .../sync_server/providers/local_drive.py | 13 +++---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index da54eecb8e..3bfd6f4854 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -120,6 +120,7 @@ class GDriveHandler(AbstractProvider): editable = [ # credentials could be override on Project or User level { + 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 @@ -127,6 +128,7 @@ class GDriveHandler(AbstractProvider): # roots could be override only on Project leve, User cannot # { + 'key': "roots", 'label': "Roots", 'type': 'dict' } @@ -145,6 +147,7 @@ class GDriveHandler(AbstractProvider): editable = [ # credentials could be override on Project or User level { + 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 9678d38ed8..4b703267d5 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -49,18 +49,10 @@ class LocalDriveHandler(AbstractProvider): Returns: (list) of dict """ - # {platform} tells that value is multiplatform and only specific OS - # should be returned + # for non 'studio' sites, 'studio' is configured in Anatomy editable = [ - # credentials could be override on Project or User level - { - 'label': "Credentials url", - 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 - }, - # roots could be override only on Project leve, User cannot - # { + 'key': "roots", 'label': "Roots", 'type': 'dict' } @@ -78,6 +70,7 @@ class LocalDriveHandler(AbstractProvider): """ editable = [ { + 'key': "roots", 'label': "Roots", 'type': 'dict' } From fc0872a99750e0c648f7b5c77bb1de65c753a924 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:52:25 +0200 Subject: [PATCH 087/450] #1976 - small fixes --- .../modules/default_modules/sync_server/providers/gdrive.py | 5 ++--- .../default_modules/sync_server/providers/local_drive.py | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 3bfd6f4854..8c93f41d67 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -8,7 +8,7 @@ import platform from openpype.api import Logger from openpype.api import get_system_settings from .abstract_provider import AbstractProvider -from ..utils import time_function, ResumableError, EditableScopes +from ..utils import time_function, ResumableError log = Logger().get_logger("SyncServer") @@ -122,8 +122,7 @@ class GDriveHandler(AbstractProvider): { 'key': "credentials_url", 'label': "Credentials url", - 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 + 'type': 'text' }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 4b703267d5..8e5f170bc9 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -7,8 +7,6 @@ import time from openpype.api import Logger, Anatomy from .abstract_provider import AbstractProvider -from ..utils import EditableScopes - log = Logger().get_logger("SyncServer") From a5bbe779fbec6fb061234072017aeb212630a594 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:56:43 +0200 Subject: [PATCH 088/450] Hound --- .../default_modules/sync_server/providers/gdrive.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 8c93f41d67..f1ec0b6a0d 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -118,14 +118,13 @@ class GDriveHandler(AbstractProvider): # {platform} tells that value is multiplatform and only specific OS # should be returned editable = [ - # credentials could be override on Project or User level - { + # credentials could be overriden on Project or User level + { 'key': "credentials_url", 'label': "Credentials url", 'type': 'text' }, - # roots could be override only on Project leve, User cannot - # + # roots could be overriden only on Project leve, User cannot { 'key': "roots", 'label': "Roots", @@ -149,8 +148,7 @@ class GDriveHandler(AbstractProvider): 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 } ] return editable From 2167671d8e8adf8f2ea4eeaf2745266899494f2b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 14:18:13 +0200 Subject: [PATCH 089/450] #1784 - fixing Hound, typos, run_tests command Added documentation --- openpype/pype_commands.py | 14 ++++++++++---- tests/README.md | 15 ++++++++++++++- .../hosts/maya/test_publish_in_maya.py | 2 -- tests/lib/file_handler.py | 15 +++++++-------- tests/lib/testing_classes.py | 2 +- .../modules/sync_server/test_site_operations.py | 5 ++++- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index c309ee8c09..5288749e8b 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -268,13 +268,19 @@ class PypeCommands: """ print("run_tests") import subprocess - folder = folder or "../tests" + + if folder: + folder = " ".join(list(folder)) + else: + folder = "../tests" + mark_str = pyargs_str = '' if mark: - mark_str = "- m {}".format(mark) + mark_str = "-m {}".format(mark) if pyargs: pyargs_str = "--pyargs {}".format(pyargs) - subprocess.run("pytest {} {} {}".format(folder, mark_str, pyargs_str)) - + cmd = "pytest {} {} {}".format(folder, mark_str, pyargs_str) + print("Running {}".format(cmd)) + subprocess.run(cmd) diff --git a/tests/README.md b/tests/README.md index 727b89a86e..6317b2ab3c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,7 @@ Automatic tests for OpenPype ============================ Structure: -- integration - end to end tests, slow +- integration - end to end tests, slow (see README.md in the integration folder for more info) - openpype/modules/MODULE_NAME - structure follow directory structure in code base - fixture - sample data `(MongoDB dumps, test files etc.)` - `tests.py` - single or more pytest files for MODULE_NAME @@ -10,3 +10,16 @@ Structure: - fixture - `tests.py` +How to run: +---------- +- single test class could be run by PyCharm and its pytest runner directly +- OR +- use Openpype command 'runtests' from command line +-- `${OPENPYPE_ROOT}/start.py runtests` + +By default, this command will run all tests in ${OPENPYPE_ROOT}/tests. + +Specific location could be provided to this command as an argument, either as absolute path, or relative path to ${OPENPYPE_ROOT}. +(eg. `${OPENPYPE_ROOT}/start.py runtests ../tests/integration`) will trigger only tests in `integration` folder. + +See `${OPENPYPE_ROOT}/cli.py:runtests` for other arguments. diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index b9c63651f1..c178a6687e 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -1,8 +1,6 @@ import pytest -import sys import os import shutil -import glob from tests.lib.testing_classes import PublishTest diff --git a/tests/lib/file_handler.py b/tests/lib/file_handler.py index 98e14b0541..ee3abc6ecb 100644 --- a/tests/lib/file_handler.py +++ b/tests/lib/file_handler.py @@ -193,11 +193,10 @@ class RemoteFileHandler: urllib.request.Request(url, headers={"User-Agent": USER_AGENT})) \ as response: - for chunk in iter(lambda: response.read(chunk_size), - ""): - if not chunk: - break - fh.write(chunk) + for chunk in iter(lambda: response.read(chunk_size), ""): + if not chunk: + break + fh.write(chunk) @staticmethod def _get_redirect_url(url, max_hops): @@ -218,7 +217,7 @@ class RemoteFileHandler: ) @staticmethod - def _get_confirm_token(response): # type: ignore[name-defined] + def _get_confirm_token(response): for key, value in response.cookies.items(): if key.startswith('download_warning'): return value @@ -227,7 +226,7 @@ class RemoteFileHandler: @staticmethod def _save_response_content( - response_gen, destination, # type: ignore[name-defined] + response_gen, destination, ): with open(destination, "wb") as f: pbar = enlighten.Counter( @@ -241,7 +240,7 @@ class RemoteFileHandler: pbar.close() @staticmethod - def _quota_exceeded(first_chunk): # type: ignore[name-defined] + def _quota_exceeded(first_chunk): try: return "Google Drive - Quota exceeded" in first_chunk.decode() except UnicodeDecodeError: diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 6cd3c10d3e..1832efb7ed 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -260,4 +260,4 @@ class PublishTest(ModuleUnitTest): f != expected_dir_base and os.path.exists(f)) not_matched = expected.difference(published) - assert not not_matched, "Missing {} files".format(not_matched) \ No newline at end of file + assert not not_matched, "Missing {} files".format(not_matched) diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index ab15025399..6a861100a4 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -21,6 +21,9 @@ class TestSiteOperation(ModuleUnitTest): REPRESENTATION_ID = "60e578d0c987036c6a7b741d" + TEST_FILES = [("1eCwPljuJeOI8A3aisfOIBKKjcmIycTEt", + "test_site_operations.zip", '')] + @pytest.fixture(scope="module") def setup_sync_server_module(self, dbcon): """Get sync_server_module from ModulesManager""" @@ -109,7 +112,7 @@ class TestSiteOperation(ModuleUnitTest): site_names = [site["name"] for site in ret["files"][0]["sites"]] assert 'test_site' not in site_names, "Site name wasn't removed" - + @pytest.mark.usefixtures("setup_sync_server_module") def test_remove_site_again(self, dbcon, setup_sync_server_module): """Depends on test_add_site, must trow exception""" From 72b2f44fa9e829925858e70d621d8622f60d1b5b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:25:58 +0200 Subject: [PATCH 090/450] added loading of steps from schema --- openpype/settings/entities/input_entities.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index ebc70b840d..128625619a 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,6 +379,10 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) + steps = self.schema_data.get("steps", None) + if steps is None: + steps = 1 / (10 ** self.decimal) + self.steps = steps def _convert_to_valid_type(self, value): if isinstance(value, str): From 7e973a5de1fba402a04d1e780a4a6a35e78c8afc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:27:32 +0200 Subject: [PATCH 091/450] added steps to Number widget --- openpype/tools/settings/settings/widgets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index b821c3bb2c..2caf8c33ba 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -92,11 +92,15 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox): min_value = kwargs.pop("minimum", -99999) max_value = kwargs.pop("maximum", 99999) decimals = kwargs.pop("decimal", 0) + steps = kwargs.pop("steps", None) + super(NumberSpinBox, self).__init__(*args, **kwargs) self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setDecimals(decimals) self.setMinimum(min_value) self.setMaximum(max_value) + if steps is not None: + self.setSingleStep(steps) def focusInEvent(self, event): super(NumberSpinBox, self).focusInEvent(event) From 7af864a6e0eeb256177525785707d66cc385495c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:28:17 +0200 Subject: [PATCH 092/450] steps can not be set --- openpype/settings/entities/input_entities.py | 5 +---- openpype/tools/settings/settings/item_widgets.py | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 128625619a..4afa0d9484 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,10 +379,7 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) - steps = self.schema_data.get("steps", None) - if steps is None: - steps = 1 / (10 ** self.decimal) - self.steps = steps + self.steps = self.schema_data.get("steps", None) def _convert_to_valid_type(self, value): if isinstance(value, str): diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 736ba77652..da74c2adc5 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -411,7 +411,8 @@ class NumberWidget(InputWidget): kwargs = { "minimum": self.entity.minimum, "maximum": self.entity.maximum, - "decimal": self.entity.decimal + "decimal": self.entity.decimal, + "steps": self.entity.steps } self.input_field = NumberSpinBox(self.content_widget, **kwargs) input_field_stretch = 1 @@ -426,6 +427,10 @@ class NumberWidget(InputWidget): int(self.entity.minimum * slider_multiplier), int(self.entity.maximum * slider_multiplier) ) + if self.entity.steps is not None: + slider_widget.setSingleStep( + self.entity.steps * slider_multiplier + ) self.content_layout.addWidget(slider_widget, 1) From e3b8e25a1a15cbdc24519e49f0067126bb071610 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:33:09 +0200 Subject: [PATCH 093/450] added steps to readme --- openpype/settings/entities/schemas/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 9b53e89dd7..c8432f0f2e 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -316,6 +316,7 @@ How output of the schema could look like on save: - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +- key `"steps"` will change single step value of UI inputs (using arrows and wheel scroll) - for UI it is possible to show slider to enable this option set `show_slider` to `true` ``` { From e75e9a6465c59751ffb62ac143532255eef9a837 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:33:19 +0200 Subject: [PATCH 094/450] make sure that steps are not `0` --- openpype/settings/entities/input_entities.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 4afa0d9484..0ded3ab7e5 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,7 +379,11 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) - self.steps = self.schema_data.get("steps", None) + steps = self.schema_data.get("steps", None) + # Make sure that steps are not set to `0` + if steps == 0: + steps = None + self.steps = steps def _convert_to_valid_type(self, value): if isinstance(value, str): From 40a6712384e5fec86bd0ab1ccdae2ec8d8317fad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:35:19 +0200 Subject: [PATCH 095/450] added steps to avalon mongo timeout --- .../entities/schemas/system_schema/schema_modules.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..b52a646954 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -28,7 +28,8 @@ "type": "number", "key": "AVALON_TIMEOUT", "minimum": 0, - "label": "Avalon Mongo Timeout (ms)" + "label": "Avalon Mongo Timeout (ms)", + "steps": 100 }, { "type": "path", From d961e0a26209fa13da5c184d68be765bfca7c956 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 15:17:23 +0200 Subject: [PATCH 096/450] replaced `providers-enum` with `sync-server-providers` to be able set system settings for provider --- openpype/settings/entities/__init__.py | 8 ++-- .../settings/entities/dict_conditional.py | 46 +++++++++++++++++++ openpype/settings/entities/enum_entity.py | 38 --------------- .../schemas/system_schema/schema_modules.json | 9 +--- 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 8c30d5044c..aae2d1fa89 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -105,7 +105,6 @@ from .enum_entity import ( AppsEnumEntity, ToolsEnumEntity, TaskTypeEnumEntity, - ProvidersEnum, DeadlineUrlEnumEntity, AnatomyTemplatesEnumEntity ) @@ -113,7 +112,10 @@ from .enum_entity import ( from .list_entity import ListEntity from .dict_immutable_keys_entity import DictImmutableKeysEntity from .dict_mutable_keys_entity import DictMutableKeysEntity -from .dict_conditional import DictConditionalEntity +from .dict_conditional import ( + DictConditionalEntity, + SyncServerProviders +) from .anatomy_entities import AnatomyEntity @@ -161,7 +163,6 @@ __all__ = ( "AppsEnumEntity", "ToolsEnumEntity", "TaskTypeEnumEntity", - "ProvidersEnum", "DeadlineUrlEnumEntity", "AnatomyTemplatesEnumEntity", @@ -172,6 +173,7 @@ __all__ = ( "DictMutableKeysEntity", "DictConditionalEntity", + "SyncServerProviders", "AnatomyEntity" ) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d7b416921c..6f27760570 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -724,3 +724,49 @@ class DictConditionalEntity(ItemEntity): for children in self.children.values(): for child_entity in children: child_entity.reset_callbacks() + + +class SyncServerProviders(DictConditionalEntity): + schema_types = ["sync-server-providers"] + + def _add_children(self): + self.enum_key = "provider" + self.enum_label = "Provider" + + enum_children = self._get_enum_children() + if not enum_children: + enum_children.append({ + "key": None, + "label": "< Nothing >" + }) + self.enum_children = enum_children + + super(SyncServerProviders, self)._add_children() + + def _get_enum_children(self): + from openpype_modules import sync_server + + from openpype_modules.sync_server.providers import lib as lib_providers + + provider_code_to_label = {} + providers = lib_providers.factory.providers + for provider_code, provider_info in providers.items(): + provider, _ = provider_info + provider_code_to_label[provider_code] = provider.LABEL + + system_settings_schema = ( + sync_server + .SyncServerModule + .get_system_settings_schema() + ) + + enum_children = [] + for provider_code, configurables in system_settings_schema.items(): + label = provider_code_to_label.get(provider_code) or provider_code + + enum_children.append({ + "key": provider_code, + "label": label, + "children": configurables + }) + return enum_children diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index cb532c5ae0..66279f529d 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -407,44 +407,6 @@ class TaskTypeEnumEntity(BaseEnumEntity): self._current_value = new_value -class ProvidersEnum(BaseEnumEntity): - schema_types = ["providers-enum"] - - def _item_initalization(self): - self.multiselection = False - self.value_on_not_set = "" - self.enum_items = [] - self.valid_keys = set() - self.valid_value_types = (str, ) - self.placeholder = None - - def _get_enum_values(self): - from openpype_modules.sync_server.providers import lib as lib_providers - - providers = lib_providers.factory.providers - - valid_keys = set() - valid_keys.add('') - enum_items = [{'': 'Choose Provider'}] - for provider_code, provider_info in providers.items(): - provider, _ = provider_info - enum_items.append({provider_code: provider.LABEL}) - valid_keys.add(provider_code) - - return enum_items, valid_keys - - def set_override_state(self, *args, **kwargs): - super(ProvidersEnum, self).set_override_state(*args, **kwargs) - - self.enum_items, self.valid_keys = self._get_enum_values() - - value_on_not_set = list(self.valid_keys)[0] - if self._current_value is NOT_SET: - self._current_value = value_on_not_set - - self.value_on_not_set = value_on_not_set - - class DeadlineUrlEnumEntity(BaseEnumEntity): schema_types = ["deadline_url-enum"] diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..9961341ba5 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -121,14 +121,7 @@ "collapsible_key": false, "object_type": { - "type": "dict", - "children": [ - { - "type": "providers-enum", - "key": "provider", - "label": "Provider" - } - ] + "type": "sync-server-providers" } } ] From 971844ed867e71a87f146a78f8be411bf6ba2a17 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Sep 2021 16:25:20 +0200 Subject: [PATCH 097/450] nuke: python3 compatibility wip --- openpype/hosts/nuke/api/lib.py | 4 +++- openpype/hosts/nuke/plugins/publish/extract_ouput_node.py | 5 +++-- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 5 +++++ openpype/hosts/nuke/plugins/publish/precollect_workfile.py | 1 - openpype/hosts/nuke/startup/write_to_read.py | 3 ++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 7e7cd27f90..257bf8d64e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -727,7 +727,7 @@ class WorkfileSettings(object): log.error(msg) nuke.message(msg) - log.warning(">> root_dict: {}".format(root_dict)) + log.debug(">> root_dict: {}".format(root_dict)) # first set OCIO if self._root_node["colorManagement"].value() \ @@ -1277,6 +1277,7 @@ class ExporterReview: def clean_nodes(self): for node in self._temp_nodes: nuke.delete(node) + self._temp_nodes = [] self.log.info("Deleted nodes...") @@ -1301,6 +1302,7 @@ class ExporterReviewLut(ExporterReview): lut_style=None): # initialize parent class ExporterReview.__init__(self, klass, instance) + self._temp_nodes = [] # deal with now lut defined in viewer lut if hasattr(klass, "viewer_lut_raw"): diff --git a/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py b/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py index a144761e5f..c3a6a3b167 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py +++ b/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py @@ -2,6 +2,7 @@ import nuke import pyblish.api from avalon.nuke import maintained_selection + class CreateOutputNode(pyblish.api.ContextPlugin): """Adding output node for each ouput write node So when latly user will want to Load .nk as LifeGroup or Precomp @@ -15,8 +16,8 @@ class CreateOutputNode(pyblish.api.ContextPlugin): def process(self, context): # capture selection state with maintained_selection(): - active_node = [node for inst in context[:] - for node in inst[:] + active_node = [node for inst in context + for node in inst if "ak:family" in node.knobs()] if active_node: diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 55f7b746fc..0c9af66435 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -1,3 +1,4 @@ +import sys import os import nuke from avalon.nuke import lib as anlib @@ -5,6 +6,10 @@ import pyblish.api import openpype +if sys.version_info[0] >= 3: + unicode = str + + class ExtractThumbnail(openpype.api.Extractor): """Extracts movie and thumbnail with baked in luts diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 5d3eb5f609..e10cfe7b47 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -3,7 +3,6 @@ import pyblish.api import os import openpype.api as pype from avalon.nuke import lib as anlib -reload(anlib) class CollectWorkfile(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/nuke/startup/write_to_read.py b/openpype/hosts/nuke/startup/write_to_read.py index deb5ce1b82..295a6e3c85 100644 --- a/openpype/hosts/nuke/startup/write_to_read.py +++ b/openpype/hosts/nuke/startup/write_to_read.py @@ -69,7 +69,8 @@ def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame): frames = sorted(frames) firstframe = frames[0] lastframe = frames[len(frames) - 1] - if lastframe < 0: + + if int(lastframe) < 0: lastframe = firstframe return filepath, firstframe, lastframe From 42bb2a866c1a56131119eff6562e3c9590ef5f94 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Sep 2021 10:37:04 +0200 Subject: [PATCH 098/450] nuke: fixing issue with shared python object --- .../hosts/nuke/plugins/publish/extract_review_data_lut.py | 6 ++++++ .../hosts/nuke/plugins/publish/extract_review_data_mov.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py index b0d3ec6241..a0f1c9a087 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py @@ -3,6 +3,12 @@ import pyblish.api from avalon.nuke import lib as anlib from openpype.hosts.nuke.api import lib as pnlib import openpype + +try: + from __builtin__ import reload +except ImportError: + from importlib import reload + reload(pnlib) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index cea7d86c26..f4fbc2d0e4 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -4,6 +4,13 @@ from avalon.nuke import lib as anlib from openpype.hosts.nuke.api import lib as pnlib import openpype +try: + from __builtin__ import reload +except ImportError: + from importlib import reload + +reload(pnlib) + class ExtractReviewDataMov(openpype.api.Extractor): """Extracts movie and thumbnail with baked in luts From a16924d0c4a3845722eba9146c4379a58b4f58f7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Sep 2021 10:46:28 +0200 Subject: [PATCH 099/450] nuke: removing (Testing only) from nuke and nukex --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 842c294599..cfdeca4b87 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -195,7 +195,7 @@ "environment": {} }, "__dynamic_keys_labels__": { - "13-0": "13.0 (Testing only)", + "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", @@ -331,7 +331,7 @@ "environment": {} }, "__dynamic_keys_labels__": { - "13-0": "13.0 (Testing only)", + "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", From e6144f42b1a134e316f397952e1085b500a98034 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Sep 2021 10:58:50 +0200 Subject: [PATCH 100/450] Fix - explicitly capitalize task type Settings expect task type capitalized --- .../webpublisher/plugins/publish/collect_published_files.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 6584120d97..6a4f83b0fa 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -174,6 +174,8 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): (family, [families], subset_template_name, tags) tuple AssertionError if not matching family found """ + if task_type: + task_type = task_type.capitalize() task_obj = settings.get(task_type) assert task_obj, "No family configuration for '{}'".format(task_type) From fd6bd1dbc15122145a29ea89946e6e3e45c658a5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Sep 2021 15:28:59 +0200 Subject: [PATCH 101/450] Fix - explicitly add frameStart, frameEnd for single frame Temporary files produced to temp folder --- .../webpublisher/plugins/publish/collect_published_files.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 6a4f83b0fa..434f82d3ea 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -10,6 +10,7 @@ Provides: import os import json import clique +import tempfile import pyblish.api from avalon import io @@ -94,7 +95,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): instance.data["families"] = families instance.data["version"] = \ self._get_last_version(asset, subset) + 1 - instance.data["stagingDir"] = task_dir + instance.data["stagingDir"] = tempfile.mkdtemp() instance.data["source"] = "webpublisher" # to store logging info into DB openpype.webpublishes @@ -113,6 +114,8 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): instance.data["frameEnd"] = \ instance.data["representations"][0]["frameEnd"] else: + instance.data["frameStart"] = 0 + instance.data["frameEnd"] = 1 instance.data["representations"] = self._get_single_repre( task_dir, task_data["files"], tags ) From b2b248f818f5e1d2cd411fd14cf3e43d23875cda Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Fri, 10 Sep 2021 14:43:09 -0700 Subject: [PATCH 102/450] addressing comments PR#1996 --- openpype/api.py | 4 +- openpype/lib/__init__.py | 6 ++- openpype/lib/path_tools.py | 40 +++++++++++++++++-- .../action_create_project_structure.py | 2 +- openpype/settings/__init__.py | 4 +- openpype/settings/lib.py | 29 -------------- 6 files changed, 44 insertions(+), 41 deletions(-) diff --git a/openpype/api.py b/openpype/api.py index dcff127e9f..e4bbb104a3 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -4,7 +4,6 @@ from .settings import ( get_current_project_settings, get_anatomy_settings, get_environments, - get_project_basic_paths, SystemSettings, ProjectSettings @@ -26,7 +25,8 @@ from .lib import ( get_global_environments, get_local_site_id, change_openpype_mongo_url, - create_project_folders + create_project_folders, + get_project_basic_paths ) from .lib.mongo import ( diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 4abfd69175..9bc68c9558 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -144,7 +144,8 @@ from .path_tools import ( version_up, get_version_from_path, get_last_version_from_path, - create_project_folders + create_project_folders, + get_project_basic_paths ) from .editorial import ( @@ -278,5 +279,6 @@ __all__ = [ "frames_to_secons", "frames_to_timecode", "make_sequence_collection", - "create_project_folders" + "create_project_folders", + "get_project_basic_paths" ] diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index fab0879759..42b5db9e25 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -1,13 +1,14 @@ +import json +import logging import os import re -import logging -from openpype.api import Anatomy + +from .anatomy import Anatomy +from openpype.settings import get_project_settings log = logging.getLogger(__name__) -pattern_array = re.compile(r"\[.*\]") -project_root_key = "__project_root__" def _rreplace(s, a, b, n=1): """Replace a with b in string s from right side n times.""" @@ -126,6 +127,8 @@ def get_last_version_from_path(path_dir, filter): def compute_paths(basic_paths_items, project_root): + pattern_array = re.compile(r"\[.*\]") + project_root_key = "__project_root__" output = [] for path_items in basic_paths_items: clean_items = [] @@ -162,3 +165,32 @@ def create_project_folders(basic_paths, project_name): else: log.debug("Creating folder: {}".format(full_path)) os.makedirs(full_path) + + +def _list_path_items(folder_structure): + output = [] + for key, value in folder_structure.items(): + if not value: + output.append(key) + else: + paths = _list_path_items(value) + for path in paths: + if not isinstance(path, (list, tuple)): + path = [path] + + output.append([key, *path]) + + return output + + +def get_project_basic_paths(project_name): + project_settings = get_project_settings(project_name) + folder_structure = ( + project_settings["global"]["project_folder_structure"] + ) + if not folder_structure: + return [] + + if isinstance(folder_structure, str): + folder_structure = json.loads(folder_structure) + return _list_path_items(folder_structure) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py index b0de792473..94f359c317 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py @@ -2,7 +2,7 @@ import os import re import json -from openpype.modules.ftrack.lib import BaseAction, statics_icon +from openpype_modules.ftrack.lib import BaseAction, statics_icon from openpype.api import get_project_basic_paths, create_project_folders diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index 0d6be51253..74f2684b2a 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -21,8 +21,7 @@ from .lib import ( get_current_project_settings, get_anatomy_settings, get_environments, - get_local_settings, - get_project_basic_paths + get_local_settings ) from .entities import ( SystemSettings, @@ -52,7 +51,6 @@ __all__ = ( "get_anatomy_settings", "get_environments", "get_local_settings", - "get_project_basic_paths", "SystemSettings", "ProjectSettings" ) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 749b337df7..60ed54bd4a 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -941,35 +941,6 @@ def get_general_environments(): return environments -def _list_path_items(folder_structure): - output = [] - for key, value in folder_structure.items(): - if not value: - output.append(key) - else: - paths = _list_path_items(value) - for path in paths: - if not isinstance(path, (list, tuple)): - path = [path] - - output.append([key, *path]) - - return output - - -def get_project_basic_paths(project_name): - project_settings = get_project_settings(project_name) - folder_structure = ( - project_settings["global"]["project_folder_structure"] - ) - if not folder_structure: - return [] - - if isinstance(folder_structure, str): - folder_structure = json.loads(folder_structure) - return _list_path_items(folder_structure) - - def clear_metadata_from_settings(values): """Remove all metadata keys from loaded settings.""" if isinstance(values, dict): From 181c1faf002b8b8953b3fa76585c95d84a4a206e Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 11 Sep 2021 17:26:38 +0800 Subject: [PATCH 103/450] add project archive confirm setting attribute --- openpype/settings/defaults/project_anatomy/attributes.json | 3 ++- .../projects_schema/schemas/schema_anatomy_attributes.json | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_anatomy/attributes.json b/openpype/settings/defaults/project_anatomy/attributes.json index 387e12bcea..77e6d5e07a 100644 --- a/openpype/settings/defaults/project_anatomy/attributes.json +++ b/openpype/settings/defaults/project_anatomy/attributes.json @@ -22,5 +22,6 @@ "aftereffects/2021", "unreal/4-26" ], - "tools_env": [] + "tools_env": [], + "archive_confirm": "" } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index 7391108a02..dc54fa598e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -69,6 +69,12 @@ "type": "tools-enum", "key": "tools_env", "label": "Tools" + }, + { + "type": "text", + "key": "archive_confirm", + "label": "Archive Project", + "placeholder": "Input project name to confirm archiving." } ] } From 394b714496e27aff5b7effd28a8950e749a9a0e8 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 11 Sep 2021 17:44:01 +0800 Subject: [PATCH 104/450] Launcher ignore archived project Based on avalon-core bfce450f --- openpype/tools/launcher/models.py | 16 +--------------- openpype/tools/launcher/window.py | 1 - 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 4988829c11..bbd419df1c 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -326,8 +326,6 @@ class ProjectModel(QtGui.QStandardItemModel): super(ProjectModel, self).__init__(parent=parent) self.dbcon = dbcon - - self.hide_invisible = False self.project_icon = qtawesome.icon("fa.map", color="white") self._project_names = set() @@ -380,16 +378,4 @@ class ProjectModel(QtGui.QStandardItemModel): self.invisibleRootItem().insertRows(row, items) def get_projects(self): - project_docs = [] - - for project_doc in sorted( - self.dbcon.projects(), key=lambda x: x["name"] - ): - if ( - self.hide_invisible - and not project_doc["data"].get("visible", True) - ): - continue - project_docs.append(project_doc) - - return project_docs + return sorted(self.dbcon.projects(no_archived=True), key=lambda x: x["name"]) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index bd37a9b89c..4331892e94 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -271,7 +271,6 @@ class LauncherWindow(QtWidgets.QDialog): ) project_model = ProjectModel(self.dbcon) - project_model.hide_invisible = True project_handler = ProjectHandler(self.dbcon, project_model) project_panel = ProjectsPanel(project_handler) From 902334d384a9c6fe2fb4a1076298a5358028420d Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 11 Sep 2021 17:44:22 +0800 Subject: [PATCH 105/450] ProjectManager ignore archived project Based on avalon-core bfce450f --- .../project_manager/project_manager/model.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 7ee43a6b61..e31dd2ccfe 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -43,18 +43,14 @@ class ProjectModel(QtGui.QStandardItemModel): none_project.setData(None) project_items.append(none_project) - database = self.dbcon.database project_names = set() - for project_name in database.collection_names(): - # Each collection will have exactly one project document - project_doc = database[project_name].find_one( - {"type": "project"}, - {"name": 1} - ) - if not project_doc: - continue - project_name = project_doc.get("name") + for doc in sorted( + self.dbcon.projects(projection={"name": 1}, no_archived=True), + key=lambda x: x["name"] + ): + + project_name = doc.get("name") if project_name: project_names.add(project_name) project_items.append(QtGui.QStandardItem(project_name)) From 07e248da8912092779b1ac9a96a3c2d7bb9a6a79 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 11 Sep 2021 17:44:57 +0800 Subject: [PATCH 106/450] StandalonePublisher ignore archived project Based on avalon-core bfce450f --- openpype/tools/standalonepublish/widgets/widget_asset.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index c39d71b055..e4fed1b9a7 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -273,8 +273,10 @@ class AssetWidget(QtWidgets.QWidget): def _set_projects(self): project_names = list() - for project in self.dbcon.projects(): - project_name = project.get("name") + + for doc in self.dbcon.projects(projection={"name": 1}, no_archived=True): + + project_name = doc.get("name") if project_name: project_names.append(project_name) @@ -299,7 +301,8 @@ class AssetWidget(QtWidgets.QWidget): def on_project_change(self): projects = list() - for project in self.dbcon.projects(): + + for project in self.dbcon.projects(projection={"name": 1}, no_archived=True): projects.append(project['name']) project_name = self.combo_projects.currentText() if project_name in projects: From 1ec69c37ceb0467947c79fc5218a8d422e93d629 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 11 Sep 2021 17:45:32 +0800 Subject: [PATCH 107/450] Settings ignore archived project Based on avalon-core bfce450f --- .../settings/local_settings/projects_widget.py | 2 +- openpype/tools/settings/settings/widgets.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/tools/settings/local_settings/projects_widget.py b/openpype/tools/settings/local_settings/projects_widget.py index a48c504d59..7d19c37bdf 100644 --- a/openpype/tools/settings/local_settings/projects_widget.py +++ b/openpype/tools/settings/local_settings/projects_widget.py @@ -809,7 +809,7 @@ class ProjectSettingsWidget(QtWidgets.QWidget): self.modules_manager = modules_manager - projects_widget = _ProjectListWidget(self) + projects_widget = _ProjectListWidget(self, no_archived=True) roos_site_widget = RootSiteWidget( modules_manager, project_settings, self ) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 2caf8c33ba..dd95eeb100 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -616,7 +616,7 @@ class ProjectListWidget(QtWidgets.QWidget): default = "< Default >" project_changed = QtCore.Signal() - def __init__(self, parent): + def __init__(self, parent, no_archived=False): self._parent = parent self.current_project = None @@ -645,6 +645,7 @@ class ProjectListWidget(QtWidgets.QWidget): self.project_list = project_list self.dbcon = None + self._no_archived = no_archived def on_item_clicked(self, new_index): new_project_name = new_index.data(QtCore.Qt.DisplayRole) @@ -731,14 +732,13 @@ class ProjectListWidget(QtWidgets.QWidget): self.current_project = None if self.dbcon: - database = self.dbcon.database - for project_name in database.collection_names(): - project_doc = database[project_name].find_one( - {"type": "project"}, - {"name": 1} - ) - if project_doc: - items.append(project_doc["name"]) + for doc in sorted( + self.dbcon.projects(projection={"name": 1}, + no_archived=self._no_archived), + key=lambda x: x["name"] + ): + items.append(doc["name"]) + for item in items: model.appendRow(QtGui.QStandardItem(item)) From 04d4afa9579d6e14e98087832213598439fe146e Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 19:09:11 +0800 Subject: [PATCH 108/450] evaluate archive flag on saving anatomy --- openpype/settings/entities/root_entities.py | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 05d20ee60b..e0a9ecdb35 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -755,6 +755,31 @@ class ProjectSettings(RootEntity): """ return DEFAULTS_DIR + def settings_value(self): + output = super(ProjectSettings, self).settings_value() + + anatomy = output.get(PROJECT_ANATOMY_KEY) or {} + + # Evaluate project archiving flag + # + archive_confirm = anatomy.get("attributes", {}).get("archive_confirm") + if archive_confirm: + # set flag + if archive_confirm == self.project_name: + self.log.debug( + "Project archiving." + ) + anatomy["attributes"]["archived"] = True + + else: + self.log.debug( + "Project archiving confirmation string not matched." + ) + anatomy["attributes"]["archive_confirm"] = "" + anatomy["attributes"]["archived"] = False + + return output + def _save_studio_values(self): settings_value = self.settings_value() From f817740a92c43df0c43f4d4d5c5d8e301b97c2ea Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 19:13:27 +0800 Subject: [PATCH 109/450] add archived sorting/filtering in Studio Settings --- openpype/tools/settings/settings/widgets.py | 103 +++++++++++++++++--- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index dd95eeb100..6dee90daa9 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -612,10 +612,34 @@ class ProjectListView(QtWidgets.QListView): super(ProjectListView, self).mouseReleaseEvent(event) +class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): + + def __init__(self, *args, **kwargs): + super(ProjectListSortFilterProxy, self).__init__(*args, **kwargs) + self._enable_filter = True + + def filterAcceptsRow(self, source_row, source_parent): + if not self._enable_filter: + return True + + index = self.sourceModel().index(source_row, 0, source_parent) + return bool(index.data(self.filterRole())) + + def is_filter_enabled(self): + return self._enable_filter + + def set_filter_enabled(self, value): + self._enable_filter = value + self.invalidateFilter() + + class ProjectListWidget(QtWidgets.QWidget): default = "< Default >" project_changed = QtCore.Signal() + ProjectSortRole = QtCore.Qt.UserRole + 10 + ProjectFilterRole = QtCore.Qt.UserRole + 11 + def __init__(self, parent, no_archived=False): self._parent = parent @@ -625,8 +649,17 @@ class ProjectListWidget(QtWidgets.QWidget): self.setObjectName("ProjectListWidget") label_widget = QtWidgets.QLabel("Projects") + project_list = ProjectListView(self) - project_list.setModel(QtGui.QStandardItemModel()) + project_model = QtGui.QStandardItemModel() + project_proxy = ProjectListSortFilterProxy() + + project_proxy.setFilterRole(self.ProjectFilterRole) + project_proxy.setSortRole(self.ProjectSortRole) + project_proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + + project_proxy.setSourceModel(project_model) + project_list.setModel(project_proxy) # Do not allow editing project_list.setEditTriggers( @@ -640,9 +673,24 @@ class ProjectListWidget(QtWidgets.QWidget): layout.addWidget(label_widget, 0) layout.addWidget(project_list, 1) + if no_archived: + archived_chk = None + else: + archived_chk = QtWidgets.QCheckBox(" Show Archived Project ") + archived_chk.setChecked(not project_proxy.is_filter_enabled()) + + layout.addSpacing(5) + layout.addWidget(archived_chk, 0) + layout.addSpacing(5) + + archived_chk.stateChanged.connect(self.on_archive_vis_changed) + project_list.left_mouse_released_at.connect(self.on_item_clicked) self.project_list = project_list + self.project_proxy = project_proxy + self.project_model = project_model + self.archived_chk = archived_chk self.dbcon = None self._no_archived = no_archived @@ -680,6 +728,14 @@ class ProjectListWidget(QtWidgets.QWidget): else: self.select_project(self.current_project) + def on_archive_vis_changed(self): + if self.archived_chk is None: + # should not happen. + return + + enable_filter = not self.archived_chk.isChecked() + self.project_proxy.set_filter_enabled(enable_filter) + def validate_context_change(self): return not self._parent.entity.has_unsaved_changes @@ -692,12 +748,16 @@ class ProjectListWidget(QtWidgets.QWidget): self.select_project(self.default) def select_project(self, project_name): - model = self.project_list.model() + model = self.project_model + proxy = self.project_proxy + found_items = model.findItems(project_name) if not found_items: found_items = model.findItems(self.default) index = model.indexFromItem(found_items[0]) + index = proxy.mapFromSource(index) + self.project_list.selectionModel().clear() self.project_list.selectionModel().setCurrentIndex( index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent @@ -709,10 +769,10 @@ class ProjectListWidget(QtWidgets.QWidget): selected_project = index.data(QtCore.Qt.DisplayRole) break - model = self.project_list.model() + model = self.project_model model.clear() - items = [self.default] + items = [(self.default, None)] mongo_url = os.environ["OPENPYPE_MONGO"] @@ -732,15 +792,34 @@ class ProjectListWidget(QtWidgets.QWidget): self.current_project = None if self.dbcon: - for doc in sorted( - self.dbcon.projects(projection={"name": 1}, - no_archived=self._no_archived), - key=lambda x: x["name"] - ): - items.append(doc["name"]) - for item in items: - model.appendRow(QtGui.QStandardItem(item)) + for doc in self.dbcon.projects( + projection={"name": 1, "data.archived": 1}, + no_archived=self._no_archived + ): + items.append( + (doc["name"], doc.get("data", {}).get("archived")) + ) + + for project_name, is_archived in items: + visible = not is_archived + + row = QtGui.QStandardItem(project_name) + row.setData(visible, self.ProjectFilterRole) + + if is_archived: + row.setData("~" + project_name, self.ProjectSortRole) + + font = row.font() + font.setItalic(True) + row.setFont(font) + + else: + row.setData(project_name, self.ProjectSortRole) + + model.appendRow(row) + + self.project_proxy.sort(0) self.select_project(selected_project) From 02937e308f8346193df16637f84d0bd3a8ad60ce Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 19:33:43 +0800 Subject: [PATCH 110/450] SyncServer ignore archived project --- .../modules/default_modules/sync_server/tray/widgets.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index c9160733a0..1737b2e0c6 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -34,7 +34,7 @@ class SyncProjectListWidget(ProjectListWidget): """ def __init__(self, sync_server, parent): - super(SyncProjectListWidget, self).__init__(parent) + super(SyncProjectListWidget, self).__init__(parent, no_archived=True) self.sync_server = sync_server self.project_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.project_list.customContextMenuRequested.connect( @@ -49,7 +49,7 @@ class SyncProjectListWidget(ProjectListWidget): return True def refresh(self): - model = self.project_list.model() + model = self.project_model model.clear() project_name = None @@ -70,8 +70,7 @@ class SyncProjectListWidget(ProjectListWidget): QtCore.Qt.DisplayRole ) if not self.current_project: - self.current_project = self.project_list.model().item(0). \ - data(QtCore.Qt.DisplayRole) + self.current_project = model.item(0).data(QtCore.Qt.DisplayRole) if project_name: self.local_site = self.sync_server.get_active_site(project_name) From c234cb907993282c00bff9a31083a7d7b1b5ab64 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 23:21:10 +0800 Subject: [PATCH 111/450] register 'archived' to project anatomy schema So the settings can recognize and won't pop warnings --- openpype/settings/defaults/project_anatomy/attributes.json | 3 ++- .../projects_schema/schemas/schema_anatomy_attributes.json | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_anatomy/attributes.json b/openpype/settings/defaults/project_anatomy/attributes.json index 77e6d5e07a..ac91622726 100644 --- a/openpype/settings/defaults/project_anatomy/attributes.json +++ b/openpype/settings/defaults/project_anatomy/attributes.json @@ -23,5 +23,6 @@ "unreal/4-26" ], "tools_env": [], - "archive_confirm": "" + "archive_confirm": "", + "archived": false } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index dc54fa598e..f70ab2fc5f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -75,6 +75,11 @@ "key": "archive_confirm", "label": "Archive Project", "placeholder": "Input project name to confirm archiving." + }, + { + "type": "boolean", + "key": "archived", + "label": "Is Archived" } ] } From 662e74816b16e108d80b80d55bc726c0859df056 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 23:22:42 +0800 Subject: [PATCH 112/450] reset 'archived' to False if no confirmation string --- openpype/settings/entities/root_entities.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index e0a9ecdb35..5701b60098 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -778,6 +778,10 @@ class ProjectSettings(RootEntity): anatomy["attributes"]["archive_confirm"] = "" anatomy["attributes"]["archived"] = False + else: + if anatomy and "attributes" in anatomy: + anatomy["attributes"]["archived"] = False + return output def _save_studio_values(self): From 921fc00e8f8471d00bb14d571ea77c0d232492b4 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 23:26:53 +0800 Subject: [PATCH 113/450] keep selected project in view when being archived --- openpype/tools/settings/settings/widgets.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 6dee90daa9..d1a8bd8958 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -623,7 +623,10 @@ class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): return True index = self.sourceModel().index(source_row, 0, source_parent) - return bool(index.data(self.filterRole())) + is_active = bool(index.data(self.filterRole())) + is_selected = bool(index.data(ProjectListWidget.ProjectSelectedRole)) + + return is_active or is_selected def is_filter_enabled(self): return self._enable_filter @@ -639,6 +642,7 @@ class ProjectListWidget(QtWidgets.QWidget): ProjectSortRole = QtCore.Qt.UserRole + 10 ProjectFilterRole = QtCore.Qt.UserRole + 11 + ProjectSelectedRole = QtCore.Qt.UserRole + 12 def __init__(self, parent, no_archived=False): self._parent = parent @@ -756,6 +760,8 @@ class ProjectListWidget(QtWidgets.QWidget): found_items = model.findItems(self.default) index = model.indexFromItem(found_items[0]) + model.setData(index, True, self.ProjectSelectedRole) + index = proxy.mapFromSource(index) self.project_list.selectionModel().clear() @@ -806,6 +812,7 @@ class ProjectListWidget(QtWidgets.QWidget): row = QtGui.QStandardItem(project_name) row.setData(visible, self.ProjectFilterRole) + row.setData(False, self.ProjectSelectedRole) if is_archived: row.setData("~" + project_name, self.ProjectSortRole) From 453a813ec1ab2067927fc9ddc1eb4acb37f0b607 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 13 Sep 2021 00:03:12 +0800 Subject: [PATCH 114/450] fix linter --- openpype/tools/launcher/models.py | 3 ++- openpype/tools/standalonepublish/widgets/widget_asset.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index bbd419df1c..53e2c19a3d 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -378,4 +378,5 @@ class ProjectModel(QtGui.QStandardItemModel): self.invisibleRootItem().insertRows(row, items) def get_projects(self): - return sorted(self.dbcon.projects(no_archived=True), key=lambda x: x["name"]) + return sorted(self.dbcon.projects(no_archived=True), + key=lambda x: x["name"]) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index e4fed1b9a7..8ee09d2435 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -274,7 +274,8 @@ class AssetWidget(QtWidgets.QWidget): def _set_projects(self): project_names = list() - for doc in self.dbcon.projects(projection={"name": 1}, no_archived=True): + for doc in self.dbcon.projects(projection={"name": 1}, + no_archived=True): project_name = doc.get("name") if project_name: @@ -302,7 +303,8 @@ class AssetWidget(QtWidgets.QWidget): def on_project_change(self): projects = list() - for project in self.dbcon.projects(projection={"name": 1}, no_archived=True): + for project in self.dbcon.projects(projection={"name": 1}, + no_archived=True): projects.append(project['name']) project_name = self.combo_projects.currentText() if project_name in projects: From a0338cb548b94fccd9ed0d1b799f10d80eb4a9d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 13 Sep 2021 10:40:29 +0200 Subject: [PATCH 115/450] avalon-core update --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index f48fce09c0..b3e4959778 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit f48fce09c0986c1fd7f6731de33907be46b436c5 +Subproject commit b3e49597786c931c13bca207769727d5fc56d5f6 From 500a6d53df5e1023f175d91e1cde8ce5656c0e5e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Sep 2021 11:20:17 +0200 Subject: [PATCH 116/450] connect_with_modules is not abstract method --- openpype/modules/base.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 01c3cebe60..c2b40b7c4a 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -417,7 +417,6 @@ class OpenPypeModule: """ pass - @abstractmethod def connect_with_modules(self, enabled_modules): """Connect with other enabled modules.""" pass @@ -438,10 +437,6 @@ class OpenPypeAddOn(OpenPypeModule): """Initialization is not be required for most of addons.""" pass - def connect_with_modules(self, enabled_modules): - """Do not require to implement connection with modules for addon.""" - pass - class ModulesManager: """Manager of Pype modules helps to load and prepare them to work. From d12509bd8edcd7f6d3c12660fa0ea62e2045a29f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Sep 2021 11:20:43 +0200 Subject: [PATCH 117/450] removed empty implementations of connect_with_modules from modules --- openpype/modules/default_modules/avalon_apps/avalon_app.py | 3 --- .../modules/default_modules/clockify/clockify_module.py | 3 --- .../modules/default_modules/deadline/deadline_module.py | 3 --- .../modules/default_modules/log_viewer/log_view_module.py | 4 ---- openpype/modules/default_modules/muster/muster.py | 3 --- openpype/modules/default_modules/project_manager_action.py | 3 --- .../default_modules/python_console_interpreter/module.py | 3 --- .../default_modules/settings_module/settings_action.py | 6 ------ openpype/modules/default_modules/slack/slack_module.py | 4 ---- .../default_modules/sync_server/sync_server_module.py | 3 --- 10 files changed, 35 deletions(-) diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index 53e06ec90a..eae013c060 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -71,9 +71,6 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes): exc_info=True ) - def connect_with_modules(self, _enabled_modules): - return - def webserver_initialization(self, server_manager): """Implementation of IWebServerRoutes interface.""" diff --git a/openpype/modules/default_modules/clockify/clockify_module.py b/openpype/modules/default_modules/clockify/clockify_module.py index a9e989f4ec..f82ae0d55d 100644 --- a/openpype/modules/default_modules/clockify/clockify_module.py +++ b/openpype/modules/default_modules/clockify/clockify_module.py @@ -94,9 +94,6 @@ class ClockifyModule( "server": [CLOCKIFY_FTRACK_SERVER_PATH] } - def connect_with_modules(self, *_a, **_kw): - return - def clockify_timer_stopped(self): self.bool_timer_run = False # Call `ITimersManager` method diff --git a/openpype/modules/default_modules/deadline/deadline_module.py b/openpype/modules/default_modules/deadline/deadline_module.py index ada5e8225a..1a179e9aaf 100644 --- a/openpype/modules/default_modules/deadline/deadline_module.py +++ b/openpype/modules/default_modules/deadline/deadline_module.py @@ -26,9 +26,6 @@ class DeadlineModule(OpenPypeModule, IPluginPaths): "not specified. Disabling module.")) return - def connect_with_modules(self, *_a, **_kw): - return - def get_plugin_paths(self): """Deadline plugin paths.""" current_dir = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/modules/default_modules/log_viewer/log_view_module.py b/openpype/modules/default_modules/log_viewer/log_view_module.py index bc1a98f4ad..14be6b392e 100644 --- a/openpype/modules/default_modules/log_viewer/log_view_module.py +++ b/openpype/modules/default_modules/log_viewer/log_view_module.py @@ -40,10 +40,6 @@ class LogViewModule(OpenPypeModule, ITrayModule): def tray_exit(self): return - def connect_with_modules(self, _enabled_modules): - """Nothing special.""" - return - def _show_logs_gui(self): if self.window: self.window.show() diff --git a/openpype/modules/default_modules/muster/muster.py b/openpype/modules/default_modules/muster/muster.py index a0e72006af..76d6cbb8f6 100644 --- a/openpype/modules/default_modules/muster/muster.py +++ b/openpype/modules/default_modules/muster/muster.py @@ -54,9 +54,6 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes): """Nothing special for Muster.""" return - def connect_with_modules(self, *_a, **_kw): - return - # Definition of Tray menu def tray_menu(self, parent): """Add **change credentials** option to tray menu.""" diff --git a/openpype/modules/default_modules/project_manager_action.py b/openpype/modules/default_modules/project_manager_action.py index c1f984a8cb..251964a059 100644 --- a/openpype/modules/default_modules/project_manager_action.py +++ b/openpype/modules/default_modules/project_manager_action.py @@ -17,9 +17,6 @@ class ProjectManagerAction(OpenPypeModule, ITrayAction): # Tray attributes self.project_manager_window = None - def connect_with_modules(self, *_a, **_kw): - return - def tray_init(self): """Initialization in tray implementation of ITrayAction.""" self.create_project_manager_window() diff --git a/openpype/modules/default_modules/python_console_interpreter/module.py b/openpype/modules/default_modules/python_console_interpreter/module.py index f4df3fb6d8..8c4a2fba73 100644 --- a/openpype/modules/default_modules/python_console_interpreter/module.py +++ b/openpype/modules/default_modules/python_console_interpreter/module.py @@ -18,9 +18,6 @@ class PythonInterpreterAction(OpenPypeModule, ITrayAction): if self._interpreter_window is not None: self._interpreter_window.save_registry() - def connect_with_modules(self, *args, **kwargs): - pass - def create_interpreter_window(self): """Initializa Settings Qt window.""" if self._interpreter_window: diff --git a/openpype/modules/default_modules/settings_module/settings_action.py b/openpype/modules/default_modules/settings_module/settings_action.py index 7140c57bab..2b4b51e3ad 100644 --- a/openpype/modules/default_modules/settings_module/settings_action.py +++ b/openpype/modules/default_modules/settings_module/settings_action.py @@ -19,9 +19,6 @@ class SettingsAction(OpenPypeModule, ITrayAction): # Tray attributes self.settings_window = None - def connect_with_modules(self, *_a, **_kw): - return - def tray_init(self): """Initialization in tray implementation of ITrayAction.""" self.create_settings_window() @@ -84,9 +81,6 @@ class LocalSettingsAction(OpenPypeModule, ITrayAction): self.settings_window = None self._first_trigger = True - def connect_with_modules(self, *_a, **_kw): - return - def tray_init(self): """Initialization in tray implementation of ITrayAction.""" self.create_settings_window() diff --git a/openpype/modules/default_modules/slack/slack_module.py b/openpype/modules/default_modules/slack/slack_module.py index e3f7b4ad19..9b2976d766 100644 --- a/openpype/modules/default_modules/slack/slack_module.py +++ b/openpype/modules/default_modules/slack/slack_module.py @@ -17,10 +17,6 @@ class SlackIntegrationModule(OpenPypeModule, IPluginPaths, ILaunchHookPaths): slack_settings = modules_settings[self.name] self.enabled = slack_settings["enabled"] - def connect_with_modules(self, _enabled_modules): - """Nothing special.""" - return - def get_launch_hook_paths(self): """Implementation of `ILaunchHookPaths`.""" return os.path.join(SLACK_MODULE_DIR, "launch_hooks") diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 976a349bfa..4c54f25c02 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -680,9 +680,6 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return sites - def connect_with_modules(self, *_a, **kw): - return - def tray_init(self): """ Actual initialization of Sync Server. From 0cb2cbb14f39a52befde9a6e7d7f4635594618fc Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 13 Sep 2021 12:27:31 +0200 Subject: [PATCH 118/450] Update openpype/modules/README.md --- openpype/modules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index abc7ed3961..5716324365 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -9,7 +9,7 @@ OpenPype modules should contain separated logic of specific kind of implementati ### TODOs - add module/addon manifest - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.) - - defying that folder is content of a module or an addon + - defining a folder as a content of a module or an addon ## Base class `OpenPypeModule` - abstract class as base for each module From 0adcf3334ad5a119341132c777540a22a5f51f30 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 13 Sep 2021 13:35:24 +0200 Subject: [PATCH 119/450] Added new InventoryAction to import (localize) reference in Maya PYPE-1399 --- openpype/hosts/maya/api/__init__.py | 1 + .../plugins/inventory/import_reference.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 openpype/hosts/maya/plugins/inventory/import_reference.py diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 9219da407f..1c8534d9a5 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -35,6 +35,7 @@ def install(): pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) log.info(PUBLISH_PATH) menu.install() diff --git a/openpype/hosts/maya/plugins/inventory/import_reference.py b/openpype/hosts/maya/plugins/inventory/import_reference.py new file mode 100644 index 0000000000..d389c8733e --- /dev/null +++ b/openpype/hosts/maya/plugins/inventory/import_reference.py @@ -0,0 +1,27 @@ +from maya import cmds + +from avalon import api + + +class ImportReference(api.InventoryAction): + """Imports selected reference inside the file.""" + + label = "Import Reference" + icon = "mouse-pointer" + color = "#d8d8d8" + + def process(self, containers): + references = cmds.ls(type="reference") + + for container in containers: + if container["loader"] != "ReferenceLoader": + print("Not a reference, skipping") + continue + + reference_name = container["namespace"] + "RN" + if reference_name in references: + print("Importing {}".format(reference_name)) + + ref_file = cmds.referenceQuery(reference_name, f=True) + + cmds.file(ref_file, importReference=True) From dfa3f76d2c71975ebcd26335f6d6077968ae0be1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 13 Sep 2021 13:46:37 +0200 Subject: [PATCH 120/450] Added return to force refresh of SceneInventory window --- openpype/hosts/maya/plugins/inventory/import_reference.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/inventory/import_reference.py b/openpype/hosts/maya/plugins/inventory/import_reference.py index d389c8733e..ac97096ee7 100644 --- a/openpype/hosts/maya/plugins/inventory/import_reference.py +++ b/openpype/hosts/maya/plugins/inventory/import_reference.py @@ -25,3 +25,5 @@ class ImportReference(api.InventoryAction): ref_file = cmds.referenceQuery(reference_name, f=True) cmds.file(ref_file, importReference=True) + + return "refresh" From 65b9bcf6e1c43746c9f7038db19602e21dcca925 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 09:45:07 +0200 Subject: [PATCH 121/450] removed arguments for storing credentials and event paths --- website/docs/admin_openpype_commands.md | 7 ++----- website/docs/module_ftrack.md | 5 ----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index d6ccc883b0..7a46ee7906 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -55,7 +55,7 @@ openpype_console tray --debug --- ### `launch` arguments {#eventserver-arguments} You have to set either proper environment variables to provide URL and credentials or use -option to specify them. If you use `--store_credentials` provided credentials will be stored for later use. +option to specify them. | Argument | Description | | --- | --- | @@ -63,16 +63,13 @@ option to specify them. If you use `--store_credentials` provided credentials wi | `--ftrack-url` | URL to ftrack server (can be set with `FTRACK_SERVER`) | | `--ftrack-user` |user name to log in to ftrack (can be set with `FTRACK_API_USER`) | | `--ftrack-api-key` | ftrack api key (can be set with `FTRACK_API_KEY`) | -| `--ftrack-events-path` | path to event server plugins (can be set with `FTRACK_EVENTS_PATH`) | -| `--no-stored-credentials` | will use credential specified with options above | -| `--store-credentials` | will store credentials to file for later use | | `--legacy` | run event server without mongo storing | | `--clockify-api-key` | Clockify API key (can be set with `CLOCKIFY_API_KEY`) | | `--clockify-workspace` | Clockify workspace (can be set with `CLOCKIFY_WORKSPACE`) | To run ftrack event server: ```shell -openpype_console eventserver --ftrack-url= --ftrack-user= --ftrack-api-key= --ftrack-events-path= --no-stored-credentials --store-credentials +openpype_console eventserver --ftrack-url= --ftrack-user= --ftrack-api-key= ``` --- diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index 005270b3b9..cafee628c1 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -51,10 +51,7 @@ There are specific launch arguments for event server. With `openpype_console eve - **`--ftrack-user "your.username"`** : Ftrack Username - **`--ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee"`** : User's API key -- **`--store-crededentials`** : Entered credentials will be stored for next launch with this argument _(It is not needed to enter **ftrackuser** and **ftrackapikey** args on next launch)_ -- **`--no-stored-credentials`** : Stored credentials are loaded first so if you want to change credentials use this argument - `--ftrack-url "https://yourdomain.ftrackapp.com/"` : Ftrack server URL _(it is not needed to enter if you have set `FTRACK_SERVER` in OpenPype' environments)_ -- `--ftrack-events-path "//Paths/To/Events/"` : Paths to events folder. May contain multiple paths separated by `;`. _(it is not needed to enter if you have set `FTRACK_EVENTS_PATH` in OpenPype' environments)_ So if you want to use OpenPype's environments then you can launch event server for first time with these arguments `openpype_console.exe eventserver --ftrack-user "my.username" --ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `openpype_console.exe eventserver`. @@ -64,8 +61,6 @@ So if you want to use OpenPype's environments then you can launch event server f - `FTRACK_API_USER` - Username _("your.username")_ - `FTRACK_API_KEY` - User's API key _("00000aaa-11bb-22cc-33dd-444444eeeee")_ - `FTRACK_SERVER` - Ftrack server url _(")_ -- `FTRACK_EVENTS_PATH` - Paths to events _("//Paths/To/Events/")_ - We do not recommend you this way. From 2e5800cf8c2bb355175d189ec3468b42aeb835d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 09:48:16 +0200 Subject: [PATCH 122/450] updated how to prepare shell scripts --- website/docs/module_ftrack.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index cafee628c1..8e3806828d 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -98,10 +98,12 @@ Event server should **not** run more than once! It may cause major issues. `sudo vi /opt/openpype/run_event_server.sh` - add content to the file: ```sh -#!/usr/bin/env -export OPENPYPE_DEBUG=3 -pushd /mnt/pipeline/prod/openpype-setup -. openpype_console eventserver --ftrack-user --ftrack-api-key +#!/usr/bin/env bash +export OPENPYPE_DEBUG=1 +export OPENPYPE_MONGO= + +pushd /mnt/path/to/openpype +./openpype_console eventserver --ftrack-user --ftrack-api-key ``` - change file permission: `sudo chmod 0755 /opt/openpype/run_event_server.sh` @@ -141,9 +143,11 @@ WantedBy=multi-user.target - add content to the service file: ```sh @echo off -set OPENPYPE_DEBUG=3 -pushd \\path\to\file\ -openpype_console.exe eventserver --ftrack-user --ftrack-api-key +set OPENPYPE_DEBUG=1 +set OPENPYPE_MONGO= + +pushd \\path\to\openpype +openpype_console.exe eventserver --ftrack-user --ftrack-api-key ``` - download and install `nssm.cc` - create Windows service according to nssm.cc manual From e7046b09d7f7373bbe9e10931afde8dbe1646974 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:30:37 +0200 Subject: [PATCH 123/450] synchronization logic is encapsulated in `synchronization` method --- .../action_sync_to_avalon.py | 22 ++++++++++------- .../action_sync_to_avalon.py | 24 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py index d449c4b7df..aa5b95b207 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -52,17 +52,21 @@ class SyncToAvalonServer(ServerAction): return False def launch(self, session, in_entities, event): + project_entity = self.get_project_from_entity(in_entities[0]) + project_name = project_entity["full_name"] + result = self.synchronization( + session, in_entities, event, project_name + ) + + return result + + def synchronization(self, session, in_entities, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) - # Get ftrack project - if in_entities[0].entity_type.lower() == "project": - ft_project_name = in_entities[0]["full_name"] - else: - ft_project_name = in_entities[0]["project"]["full_name"] try: - output = self.entities_factory.launch_setup(ft_project_name) + output = self.entities_factory.launch_setup(project_name) if output is not None: return output @@ -72,7 +76,7 @@ class SyncToAvalonServer(ServerAction): time_2 = time.time() # This must happen before all filtering!!! - self.entities_factory.prepare_avalon_entities(ft_project_name) + self.entities_factory.prepare_avalon_entities(project_name) time_3 = time.time() self.entities_factory.filter_by_ignore_sync() @@ -118,7 +122,7 @@ class SyncToAvalonServer(ServerAction): report = self.entities_factory.report() if report and report.get("items"): default_title = "Synchronization report ({}):".format( - ft_project_name + project_name ) self.show_interface( items=report["items"], @@ -135,7 +139,6 @@ class SyncToAvalonServer(ServerAction): "Synchronization failed due to code error", exc_info=True ) msg = "An error has happened during synchronization" - title = "Synchronization report ({}):".format(ft_project_name) items = [] items.append({ "type": "label", @@ -160,6 +163,7 @@ class SyncToAvalonServer(ServerAction): report = self.entities_factory.report() except Exception: pass + title = "Synchronization report ({}):".format(project_name) _items = report.get("items", []) if _items: diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index d6ca561bbe..a57bb819a4 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -63,17 +63,23 @@ class SyncToAvalonLocal(BaseAction): return is_valid def launch(self, session, in_entities, event): + project_entity = self.get_project_from_entity(in_entities[0]) + project_name = project_entity["full_name"] + + result = self.synchronization( + session, in_entities, event, project_name + ) + + + return result + + def synchronization(self, session, in_entities, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) - # Get ftrack project - if in_entities[0].entity_type.lower() == "project": - ft_project_name = in_entities[0]["full_name"] - else: - ft_project_name = in_entities[0]["project"]["full_name"] try: - output = self.entities_factory.launch_setup(ft_project_name) + output = self.entities_factory.launch_setup(project_name) if output is not None: return output @@ -83,7 +89,7 @@ class SyncToAvalonLocal(BaseAction): time_2 = time.time() # This must happen before all filtering!!! - self.entities_factory.prepare_avalon_entities(ft_project_name) + self.entities_factory.prepare_avalon_entities(project_name) time_3 = time.time() self.entities_factory.filter_by_ignore_sync() @@ -129,7 +135,7 @@ class SyncToAvalonLocal(BaseAction): report = self.entities_factory.report() if report and report.get("items"): default_title = "Synchronization report ({}):".format( - ft_project_name + project_name ) self.show_interface( items=report["items"], @@ -146,7 +152,6 @@ class SyncToAvalonLocal(BaseAction): "Synchronization failed due to code error", exc_info=True ) msg = "An error occurred during synchronization" - title = "Synchronization report ({}):".format(ft_project_name) items = [] items.append({ "type": "label", @@ -181,6 +186,7 @@ class SyncToAvalonLocal(BaseAction): return {"success": True, "message": msg} + title = "Synchronization report ({}):".format(project_name) finally: try: self.entities_factory.dbcon.uninstall() From 2bd9ef0b53d1a95df767057cc2cf79c835b3dccf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:35:10 +0200 Subject: [PATCH 124/450] exception handling moved from 'synchronization' to 'launch' --- .../action_sync_to_avalon.py | 75 ++++++++----------- .../action_sync_to_avalon.py | 75 ++++++++----------- 2 files changed, 61 insertions(+), 89 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py index aa5b95b207..7f9074907a 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -54,13 +54,40 @@ class SyncToAvalonServer(ServerAction): def launch(self, session, in_entities, event): project_entity = self.get_project_from_entity(in_entities[0]) project_name = project_entity["full_name"] - result = self.synchronization( - session, in_entities, event, project_name - ) + + try: + result = self.synchronization(event, project_name) + + except Exception as exc: + self.log.error( + "Synchronization failed due to code error", exc_info=True + ) + msg = "An error has happened during synchronization" + title = "Synchronization report ({}):".format(project_name) + items = [] + items.append({ + "type": "label", + "value": "# {}".format(msg) + }) + + report = {} + try: + report = self.entities_factory.report() + except Exception: + pass + + _items = report.get("items") or [] + if _items: + items.append(self.entities_factory.report_splitter) + items.extend(_items) + + self.show_interface(items, title, event, submit_btn_label="Ok") + + return {"success": True, "message": msg} return result - def synchronization(self, session, in_entities, event, project_name): + def synchronization(self, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) @@ -134,46 +161,6 @@ class SyncToAvalonServer(ServerAction): "message": "Synchronization Finished" } - except Exception: - self.log.error( - "Synchronization failed due to code error", exc_info=True - ) - msg = "An error has happened during synchronization" - items = [] - items.append({ - "type": "label", - "value": "# {}".format(msg) - }) - items.append({ - "type": "label", - "value": "## Traceback of the error" - }) - items.append({ - "type": "label", - "value": "

    {}

    ".format( - str(traceback.format_exc()).replace( - "\n", "
    ").replace( - " ", " " - ) - ) - }) - - report = {"items": []} - try: - report = self.entities_factory.report() - except Exception: - pass - title = "Synchronization report ({}):".format(project_name) - - _items = report.get("items", []) - if _items: - items.append(self.entities_factory.report_splitter) - items.extend(_items) - - self.show_interface(items, title, event) - - return {"success": True, "message": msg} - finally: try: self.entities_factory.dbcon.uninstall() diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index a57bb819a4..4d030d03e8 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -65,15 +65,40 @@ class SyncToAvalonLocal(BaseAction): def launch(self, session, in_entities, event): project_entity = self.get_project_from_entity(in_entities[0]) project_name = project_entity["full_name"] - - result = self.synchronization( - session, in_entities, event, project_name - ) + try: + result = self.synchronization(event, project_name) + + except Exception as exc: + self.log.error( + "Synchronization failed due to code error", exc_info=True + ) + msg = "An error has happened during synchronization" + title = "Synchronization report ({}):".format(project_name) + items = [] + items.append({ + "type": "label", + "value": "# {}".format(msg) + }) + + report = {} + try: + report = self.entities_factory.report() + except Exception: + pass + + _items = report.get("items") or [] + if _items: + items.append(self.entities_factory.report_splitter) + items.extend(_items) + + self.show_interface(items, title, event, submit_btn_label="Ok") + + return {"success": True, "message": msg} return result - def synchronization(self, session, in_entities, event, project_name): + def synchronization(self, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) @@ -147,46 +172,6 @@ class SyncToAvalonLocal(BaseAction): "message": "Synchronization Finished" } - except Exception: - self.log.error( - "Synchronization failed due to code error", exc_info=True - ) - msg = "An error occurred during synchronization" - items = [] - items.append({ - "type": "label", - "value": "# {}".format(msg) - }) - items.append({ - "type": "label", - "value": "## Traceback of the error" - }) - items.append({ - "type": "label", - "value": "

    {}

    ".format( - str(traceback.format_exc()).replace( - "\n", "
    ").replace( - " ", " " - ) - ) - }) - - report = {"items": []} - try: - report = self.entities_factory.report() - except Exception: - pass - - _items = report.get("items", []) - if _items: - items.append(self.entities_factory.report_splitter) - items.extend(_items) - - self.show_interface(items, title, event) - - return {"success": True, "message": msg} - - title = "Synchronization report ({}):".format(project_name) finally: try: self.entities_factory.dbcon.uninstall() From 71a2dd8a677f6daded58d02192e1ccc9791765ac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:35:40 +0200 Subject: [PATCH 125/450] added job of synchronization where can be uploaded traceback --- .../action_sync_to_avalon.py | 34 +++++++++++++++++++ .../action_sync_to_avalon.py | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py index 7f9074907a..9d3dee9e71 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -1,4 +1,6 @@ import time +import sys +import json import traceback from openpype_modules.ftrack.lib import ServerAction @@ -52,6 +54,20 @@ class SyncToAvalonServer(ServerAction): return False def launch(self, session, in_entities, event): + self.log.debug("{}: Creating job".format(self.label)) + + user_entity = session.query( + "User where id is {}".format(event["source"]["user"]["id"]) + ).one() + job_entity = session.create("Job", { + "user": user_entity, + "status": "running", + "data": json.dumps({ + "description": "Sync to avalon is running..." + }) + }) + session.commit() + project_entity = self.get_project_from_entity(in_entities[0]) project_name = project_entity["full_name"] @@ -62,6 +78,12 @@ class SyncToAvalonServer(ServerAction): self.log.error( "Synchronization failed due to code error", exc_info=True ) + + description = "Sync to avalon Crashed (Download traceback)" + self.add_traceback_to_job( + job_entity, session, sys.exc_info(), description + ) + msg = "An error has happened during synchronization" title = "Synchronization report ({}):".format(project_name) items = [] @@ -69,6 +91,12 @@ class SyncToAvalonServer(ServerAction): "type": "label", "value": "# {}".format(msg) }) + items.append({ + "type": "label", + "value": ( + "

    Download report from job for more information.

    " + ) + }) report = {} try: @@ -85,6 +113,12 @@ class SyncToAvalonServer(ServerAction): return {"success": True, "message": msg} + job_entity["status"] = "done" + job_entity["data"] = json.dumps({ + "description": "Sync to avalon finished." + }) + session.commit() + return result def synchronization(self, event, project_name): diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index 4d030d03e8..7d345771e8 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -1,4 +1,6 @@ import time +import sys +import json import traceback from openpype_modules.ftrack.lib import BaseAction, statics_icon @@ -63,6 +65,20 @@ class SyncToAvalonLocal(BaseAction): return is_valid def launch(self, session, in_entities, event): + self.log.debug("{}: Creating job".format(self.label)) + + user_entity = session.query( + "User where id is {}".format(event["source"]["user"]["id"]) + ).one() + job_entity = session.create("Job", { + "user": user_entity, + "status": "running", + "data": json.dumps({ + "description": "Sync to avalon is running..." + }) + }) + session.commit() + project_entity = self.get_project_from_entity(in_entities[0]) project_name = project_entity["full_name"] @@ -73,6 +89,12 @@ class SyncToAvalonLocal(BaseAction): self.log.error( "Synchronization failed due to code error", exc_info=True ) + + description = "Sync to avalon Crashed (Download traceback)" + self.add_traceback_to_job( + job_entity, session, sys.exc_info(), description + ) + msg = "An error has happened during synchronization" title = "Synchronization report ({}):".format(project_name) items = [] @@ -80,6 +102,12 @@ class SyncToAvalonLocal(BaseAction): "type": "label", "value": "# {}".format(msg) }) + items.append({ + "type": "label", + "value": ( + "

    Download report from job for more information.

    " + ) + }) report = {} try: @@ -96,6 +124,12 @@ class SyncToAvalonLocal(BaseAction): return {"success": True, "message": msg} + job_entity["status"] = "done" + job_entity["data"] = json.dumps({ + "description": "Sync to avalon finished." + }) + session.commit() + return result def synchronization(self, event, project_name): From c13f6132d624942b6c5b4a9f3e6fa95cd645cf23 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:36:07 +0200 Subject: [PATCH 126/450] removed irelevant comments --- .../ftrack/event_handlers_user/action_sync_to_avalon.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index 7d345771e8..2cb9ab4610 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -32,17 +32,10 @@ class SyncToAvalonLocal(BaseAction): - or do it manually (Not recommended) """ - #: Action identifier. identifier = "sync.to.avalon.local" - #: Action label. label = "OpenPype Admin" - #: Action variant variant = "- Sync To Avalon (Local)" - #: Action description. - description = "Send data from Ftrack to Avalon" - #: priority priority = 200 - #: roles that are allowed to register this action icon = statics_icon("ftrack", "action_icons", "OpenPypeAdmin.svg") settings_key = "sync_to_avalon_local" From 0749a96492696c67380b3e3dacd8470b4333497c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:36:29 +0200 Subject: [PATCH 127/450] `show_interface` can change submit button label --- .../ftrack/lib/ftrack_base_handler.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/default_modules/ftrack/lib/ftrack_base_handler.py index 7027154d86..a457b886ac 100644 --- a/openpype/modules/default_modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/default_modules/ftrack/lib/ftrack_base_handler.py @@ -384,8 +384,8 @@ class BaseHandler(object): ) def show_interface( - self, items, title='', - event=None, user=None, username=None, user_id=None + self, items, title="", event=None, user=None, + username=None, user_id=None, submit_btn_label=None ): """ Shows interface to user @@ -428,14 +428,18 @@ class BaseHandler(object): 'applicationId=ftrack.client.web and user.id="{0}"' ).format(user_id) + event_data = { + "type": "widget", + "items": items, + "title": title + } + if submit_btn_label: + event_data["submit_button_label"] = submit_btn_label + self.session.event_hub.publish( ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', - data=dict( - type='widget', - items=items, - title=title - ), + data=event_data, target=target ), on_error='ignore' @@ -443,7 +447,7 @@ class BaseHandler(object): def show_interface_from_dict( self, messages, title="", event=None, - user=None, username=None, user_id=None + user=None, username=None, user_id=None, submit_btn_label=None ): if not messages: self.log.debug("No messages to show! (messages dict is empty)") @@ -469,7 +473,9 @@ class BaseHandler(object): message = {'type': 'label', 'value': '

    {}

    '.format(value)} items.append(message) - self.show_interface(items, title, event, user, username, user_id) + self.show_interface( + items, title, event, user, username, user_id, submit_btn_label + ) def trigger_action( self, action_name, event=None, session=None, From 57af5bdf08b920971b19aead1f443c037f907118 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:41:54 +0200 Subject: [PATCH 128/450] removed unused exception variables --- .../ftrack/event_handlers_server/action_sync_to_avalon.py | 2 +- .../ftrack/event_handlers_user/action_sync_to_avalon.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py index 9d3dee9e71..58f79e8a2b 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -74,7 +74,7 @@ class SyncToAvalonServer(ServerAction): try: result = self.synchronization(event, project_name) - except Exception as exc: + except Exception: self.log.error( "Synchronization failed due to code error", exc_info=True ) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index 2cb9ab4610..cd2f371f38 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -78,7 +78,7 @@ class SyncToAvalonLocal(BaseAction): try: result = self.synchronization(event, project_name) - except Exception as exc: + except Exception: self.log.error( "Synchronization failed due to code error", exc_info=True ) From 1b3771c3b3dca919a8895e899426a9f32683ae3c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 13:32:44 +0200 Subject: [PATCH 129/450] fix python 2 host breaking line --- openpype/lib/path_tools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index 9dc14497a4..048bf0eda0 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -178,7 +178,9 @@ def _list_path_items(folder_structure): if not isinstance(path, (list, tuple)): path = [path] - output.append([key, *path]) + item = [key] + item.extend(path) + output.append(item) return output From 279f54c5a849d4e862676c2d89d16f5905e08b77 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Sep 2021 14:07:34 +0200 Subject: [PATCH 130/450] added get_openpype_icon_filepath and get_openpype_splash_filepath functions --- openpype/resources/__init__.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/openpype/resources/__init__.py b/openpype/resources/__init__.py index ef4ed73974..8d4f3fd1fa 100644 --- a/openpype/resources/__init__.py +++ b/openpype/resources/__init__.py @@ -30,23 +30,31 @@ def get_liberation_font_path(bold=False, italic=False): return font_path -def pype_icon_filepath(debug=None): - if debug is None: - debug = bool(os.getenv("OPENPYPE_DEV")) +def get_openpype_icon_filepath(staging=None): + if staging is None: + staging = bool(os.getenv("OPENPYPE_DEV")) - if debug: + if staging: icon_file_name = "openpype_icon_staging.png" else: icon_file_name = "openpype_icon.png" return get_resource("icons", icon_file_name) -def pype_splash_filepath(debug=None): - if debug is None: - debug = bool(os.getenv("OPENPYPE_DEV")) +def get_openpype_splash_filepath(staging=None): + if staging is None: + staging = bool(os.getenv("OPENPYPE_DEV")) - if debug: + if staging: splash_file_name = "openpype_splash_staging.png" else: splash_file_name = "openpype_splash.png" return get_resource("icons", splash_file_name) + + +def pype_icon_filepath(staging=None): + return get_openpype_icon_filepath(staging) + + +def pype_splash_filepath(staging=None): + return get_openpype_splash_filepath(staging) From 1cf8f47c751e703251d750f3fa009c4f2e58143c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:18:52 +0200 Subject: [PATCH 131/450] implemented `is_running_staging` in pype_info --- openpype/lib/pype_info.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index c56782be9e..5ca04e839f 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -16,6 +16,12 @@ def get_pype_version(): return openpype.version.__version__ +def is_running_staging(): + if "staging" in get_pype_version(): + return True + return False + + def get_pype_info(): """Information about currently used Pype process.""" executable_args = get_pype_execute_args() From 1f55ae870121151737cb413e0d3e96e9ec699dc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:20:51 +0200 Subject: [PATCH 132/450] use 'get_openpype_icon_filepath' instead of 'pype_icon_filepath' --- openpype/hosts/maya/api/shader_definition_editor.py | 2 +- openpype/modules/default_modules/avalon_apps/avalon_app.py | 2 +- openpype/modules/default_modules/clockify/widgets.py | 4 ++-- openpype/modules/default_modules/ftrack/tray/login_dialog.py | 2 +- openpype/modules/default_modules/muster/widget_login.py | 2 +- .../python_console_interpreter/window/widgets.py | 2 +- openpype/modules/default_modules/sync_server/tray/app.py | 2 +- .../default_modules/timers_manager/widget_user_idle.py | 2 +- openpype/plugins/load/delivery.py | 2 +- openpype/style/__init__.py | 2 +- openpype/tools/launcher/actions.py | 2 +- openpype/tools/launcher/window.py | 2 +- openpype/tools/project_manager/project_manager/window.py | 2 +- openpype/tools/settings/settings/style/__init__.py | 2 +- openpype/tools/standalonepublish/app.py | 2 +- openpype/tools/tray/pype_info_widget.py | 2 +- openpype/tools/tray/pype_tray.py | 4 ++-- 17 files changed, 19 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/maya/api/shader_definition_editor.py b/openpype/hosts/maya/api/shader_definition_editor.py index 73cc6246ab..ed425f4718 100644 --- a/openpype/hosts/maya/api/shader_definition_editor.py +++ b/openpype/hosts/maya/api/shader_definition_editor.py @@ -31,7 +31,7 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget): self.setObjectName("shaderDefinitionEditor") self.setWindowTitle("OpenPype shader name definition editor") - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags(QtCore.Qt.Window) self.setParent(parent) diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index 53e06ec90a..9232d9bbd3 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -60,7 +60,7 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes): from Qt import QtGui self.libraryloader = app.Window( - icon=QtGui.QIcon(resources.pype_icon_filepath()), + icon=QtGui.QIcon(resources.get_openpype_icon_filepath()), show_projects=True, show_libraries=True ) diff --git a/openpype/modules/default_modules/clockify/widgets.py b/openpype/modules/default_modules/clockify/widgets.py index fc8e7fa8a3..d58df3c067 100644 --- a/openpype/modules/default_modules/clockify/widgets.py +++ b/openpype/modules/default_modules/clockify/widgets.py @@ -13,7 +13,7 @@ class MessageWidget(QtWidgets.QWidget): super(MessageWidget, self).__init__() # Icon - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags( @@ -90,7 +90,7 @@ class ClockifySettings(QtWidgets.QWidget): self.validated = False # Icon - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setWindowTitle("Clockify settings") diff --git a/openpype/modules/default_modules/ftrack/tray/login_dialog.py b/openpype/modules/default_modules/ftrack/tray/login_dialog.py index 6384621c8e..05d9226ca4 100644 --- a/openpype/modules/default_modules/ftrack/tray/login_dialog.py +++ b/openpype/modules/default_modules/ftrack/tray/login_dialog.py @@ -25,7 +25,7 @@ class CredentialsDialog(QtWidgets.QDialog): self._is_logged = False self._in_advance_mode = False - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags( diff --git a/openpype/modules/default_modules/muster/widget_login.py b/openpype/modules/default_modules/muster/widget_login.py index 231b52c6bd..ae838c6cea 100644 --- a/openpype/modules/default_modules/muster/widget_login.py +++ b/openpype/modules/default_modules/muster/widget_login.py @@ -17,7 +17,7 @@ class MusterLogin(QtWidgets.QWidget): self.module = module # Icon - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags( diff --git a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py index 975decf4f4..d7a043a151 100644 --- a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py +++ b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py @@ -331,7 +331,7 @@ class PythonInterpreterWidget(QtWidgets.QWidget): super(PythonInterpreterWidget, self).__init__(parent) self.setWindowTitle("OpenPype Console") - self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath())) + self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath())) self.ansi_escape = re.compile( r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]" diff --git a/openpype/modules/default_modules/sync_server/tray/app.py b/openpype/modules/default_modules/sync_server/tray/app.py index 106076d81c..a5f73db5d5 100644 --- a/openpype/modules/default_modules/sync_server/tray/app.py +++ b/openpype/modules/default_modules/sync_server/tray/app.py @@ -26,7 +26,7 @@ class SyncServerWindow(QtWidgets.QDialog): self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setStyleSheet(style.load_stylesheet()) - self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath())) + self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath())) self.resize(1450, 700) self.timer = QtCore.QTimer() diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index 25b4e56650..cefa6bb4fb 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -16,7 +16,7 @@ class WidgetUserIdle(QtWidgets.QWidget): self.module = module - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags( QtCore.Qt.WindowCloseButtonHint diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 3753f1bfc9..a8cb0070ee 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -71,7 +71,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self._set_representations(contexts) self.setWindowTitle("OpenPype - Deliver versions") - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags( diff --git a/openpype/style/__init__.py b/openpype/style/__init__.py index 87547b1a90..0d7904d133 100644 --- a/openpype/style/__init__.py +++ b/openpype/style/__init__.py @@ -91,4 +91,4 @@ def load_stylesheet(): def app_icon_path(): - return resources.pype_icon_filepath() + return resources.get_openpype_icon_filepath() diff --git a/openpype/tools/launcher/actions.py b/openpype/tools/launcher/actions.py index 14c6aff4ad..4d86970f9c 100644 --- a/openpype/tools/launcher/actions.py +++ b/openpype/tools/launcher/actions.py @@ -84,7 +84,7 @@ class ApplicationAction(api.Action): def _show_message_box(self, title, message, details=None): dialog = QtWidgets.QMessageBox() - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) dialog.setWindowIcon(icon) dialog.setStyleSheet(style.load_stylesheet()) dialog.setWindowTitle(title) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index bd37a9b89c..1a753db16a 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -261,7 +261,7 @@ class LauncherWindow(QtWidgets.QDialog): self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setStyleSheet(style.load_stylesheet()) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 7c71f4b451..4a23649ef3 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -29,7 +29,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): self._user_passed = False self.setWindowTitle("OpenPype Project Manager") - self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath())) + self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath())) # Top part of window top_part_widget = QtWidgets.QWidget(self) diff --git a/openpype/tools/settings/settings/style/__init__.py b/openpype/tools/settings/settings/style/__init__.py index 5a57642ee1..f1d9829a04 100644 --- a/openpype/tools/settings/settings/style/__init__.py +++ b/openpype/tools/settings/settings/style/__init__.py @@ -10,4 +10,4 @@ def load_stylesheet(): def app_icon_path(): - return resources.pype_icon_filepath() + return resources.get_openpype_icon_filepath() diff --git a/openpype/tools/standalonepublish/app.py b/openpype/tools/standalonepublish/app.py index 81a53c52b8..2ce757f773 100644 --- a/openpype/tools/standalonepublish/app.py +++ b/openpype/tools/standalonepublish/app.py @@ -231,7 +231,7 @@ def main(): qt_app = QtWidgets.QApplication([]) # app.setQuitOnLastWindowClosed(False) qt_app.setStyleSheet(style.load_stylesheet()) - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) qt_app.setWindowIcon(icon) def signal_handler(sig, frame): diff --git a/openpype/tools/tray/pype_info_widget.py b/openpype/tools/tray/pype_info_widget.py index 2965463c37..2ca625f307 100644 --- a/openpype/tools/tray/pype_info_widget.py +++ b/openpype/tools/tray/pype_info_widget.py @@ -214,7 +214,7 @@ class PypeInfoWidget(QtWidgets.QWidget): self.setStyleSheet(style.load_stylesheet()) - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setWindowTitle("OpenPype info") diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index ed66f1a80f..35b254513f 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -200,7 +200,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): doubleclick_time_ms = 100 def __init__(self, parent): - icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) super(SystemTrayIcon, self).__init__(icon, parent) @@ -308,7 +308,7 @@ class PypeTrayApplication(QtWidgets.QApplication): splash_widget.hide() def set_splash(self): - splash_pix = QtGui.QPixmap(resources.pype_splash_filepath()) + splash_pix = QtGui.QPixmap(resources.get_openpype_splash_filepath()) splash = QtWidgets.QSplashScreen(splash_pix) splash.setMask(splash_pix.mask()) splash.setEnabled(False) From 541bf817c5827a1611be8a915e07ac96d341b882 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:21:19 +0200 Subject: [PATCH 133/450] use 'is_running_staging' to determine if should use staging icon or not --- openpype/resources/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/resources/__init__.py b/openpype/resources/__init__.py index 8d4f3fd1fa..f463933525 100644 --- a/openpype/resources/__init__.py +++ b/openpype/resources/__init__.py @@ -1,5 +1,5 @@ import os - +from openpype.lib.pype_info import is_running_staging RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -32,7 +32,7 @@ def get_liberation_font_path(bold=False, italic=False): def get_openpype_icon_filepath(staging=None): if staging is None: - staging = bool(os.getenv("OPENPYPE_DEV")) + staging = is_running_staging() if staging: icon_file_name = "openpype_icon_staging.png" @@ -43,7 +43,7 @@ def get_openpype_icon_filepath(staging=None): def get_openpype_splash_filepath(staging=None): if staging is None: - staging = bool(os.getenv("OPENPYPE_DEV")) + staging = is_running_staging() if staging: splash_file_name = "openpype_splash_staging.png" From 2f274e9c8f72b515cdcce6e2782c4839dfe651f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:27:47 +0200 Subject: [PATCH 134/450] removed 'pype_icon_filepath' and 'pype_splash_filepath' --- openpype/resources/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openpype/resources/__init__.py b/openpype/resources/__init__.py index f463933525..c6886fea73 100644 --- a/openpype/resources/__init__.py +++ b/openpype/resources/__init__.py @@ -50,11 +50,3 @@ def get_openpype_splash_filepath(staging=None): else: splash_file_name = "openpype_splash.png" return get_resource("icons", splash_file_name) - - -def pype_icon_filepath(staging=None): - return get_openpype_icon_filepath(staging) - - -def pype_splash_filepath(staging=None): - return get_openpype_splash_filepath(staging) From c4ce2001cb5dc78d74e84964bd37e061206d73f1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:38:47 +0200 Subject: [PATCH 135/450] added short docstring --- openpype/lib/pype_info.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index 5ca04e839f..2479e68e1a 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -17,6 +17,11 @@ def get_pype_version(): def is_running_staging(): + """Currently used OpenPype is staging version. + + Returns: + bool: True if openpype version containt 'staging'. + """ if "staging" in get_pype_version(): return True return False From b71898e1d8020dd2c136e08fe17b9c26dfc7722b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:51:23 +0200 Subject: [PATCH 136/450] replaced 'get_pype_version' with 'get_openpype_version' --- openpype/lib/pype_info.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index c56782be9e..ec04f50532 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -11,11 +11,20 @@ from .execute import get_pype_execute_args from .local_settings import get_local_site_id -def get_pype_version(): +def get_openpype_version(): """Version of pype that is currently used.""" return openpype.version.__version__ +def get_pype_version(): + """Backwards compatibility. Remove when 100% not used.""" + print(( + "Using deprecated function 'openpype.lib.pype_info.get_pype_version'" + " replace with 'openpype.lib.pype_info.get_openpype_version'." + )) + return get_openpype_version() + + def get_pype_info(): """Information about currently used Pype process.""" executable_args = get_pype_execute_args() @@ -25,7 +34,7 @@ def get_pype_info(): version_type = "code" return { - "version": get_pype_version(), + "version": get_openpype_version(), "version_type": version_type, "executable": executable_args[-1], "pype_root": os.environ["OPENPYPE_REPOS_ROOT"], @@ -73,7 +82,7 @@ def extract_pype_info_to_file(dirpath): filepath (str): Full path to file where data were extracted. """ filename = "{}_{}_{}.json".format( - get_pype_version(), + get_openpype_version(), get_local_site_id(), datetime.datetime.now().strftime("%y%m%d%H%M%S") ) From 32d65315a0c9fb3f900fd1c3bae10510074fee8f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Sep 2021 15:54:48 +0200 Subject: [PATCH 137/450] Changed how reference nodes are resolved Added get_reference_node as public api method --- openpype/hosts/maya/api/plugin.py | 93 ++++++++++--------- .../plugins/inventory/import_reference.py | 20 ++-- 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 121f7a08a7..448cb814d9 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -4,6 +4,53 @@ import avalon.maya from openpype.api import PypeCreatorMixin +def get_reference_node(members, log=None): + """Get the reference node from the container members + Args: + members: list of node names + + Returns: + str: Reference node name. + + """ + + from maya import cmds + + # Collect the references without .placeHolderList[] attributes as + # unique entries (objects only) and skipping the sharedReferenceNode. + references = set() + for ref in cmds.ls(members, exactType="reference", objectsOnly=True): + + # Ignore any `:sharedReferenceNode` + if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"): + continue + + # Ignore _UNKNOWN_REF_NODE_ (PLN-160) + if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"): + continue + + references.add(ref) + + assert references, "No reference node found in container" + + # Get highest reference node (least parents) + highest = min(references, + key=lambda x: len(get_reference_node_parents(x))) + + # Warn the user when we're taking the highest reference node + if len(references) > 1: + if not log: + from openpype.lib import PypeLogger + + log = PypeLogger().get_logger(__name__) + + log.warning("More than one reference node found in " + "container, using highest reference node: " + "%s (in: %s)", highest, list(references)) + + return highest + + def get_reference_node_parents(ref): """Return all parent reference nodes of reference node @@ -109,7 +156,7 @@ class ReferenceLoader(api.Loader): loader=self.__class__.__name__ )) else: - ref_node = self._get_reference_node(nodes) + ref_node = get_reference_node(nodes, self.log) loaded_containers.append(containerise( name=name, namespace=namespace, @@ -126,46 +173,6 @@ class ReferenceLoader(api.Loader): """To be implemented by subclass""" raise NotImplementedError("Must be implemented by subclass") - def _get_reference_node(self, members): - """Get the reference node from the container members - Args: - members: list of node names - - Returns: - str: Reference node name. - - """ - - from maya import cmds - - # Collect the references without .placeHolderList[] attributes as - # unique entries (objects only) and skipping the sharedReferenceNode. - references = set() - for ref in cmds.ls(members, exactType="reference", objectsOnly=True): - - # Ignore any `:sharedReferenceNode` - if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"): - continue - - # Ignore _UNKNOWN_REF_NODE_ (PLN-160) - if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"): - continue - - references.add(ref) - - assert references, "No reference node found in container" - - # Get highest reference node (least parents) - highest = min(references, - key=lambda x: len(get_reference_node_parents(x))) - - # Warn the user when we're taking the highest reference node - if len(references) > 1: - self.log.warning("More than one reference node found in " - "container, using highest reference node: " - "%s (in: %s)", highest, list(references)) - - return highest def update(self, container, representation): @@ -178,7 +185,7 @@ class ReferenceLoader(api.Loader): # Get reference node from container members members = cmds.sets(node, query=True, nodesOnly=True) - reference_node = self._get_reference_node(members) + reference_node = get_reference_node(members, self.log) file_type = { "ma": "mayaAscii", @@ -274,7 +281,7 @@ class ReferenceLoader(api.Loader): # Assume asset has been referenced members = cmds.sets(node, query=True) - reference_node = self._get_reference_node(members) + reference_node = get_reference_node(members, self.log) assert reference_node, ("Imported container not supported; " "container must be referenced.") diff --git a/openpype/hosts/maya/plugins/inventory/import_reference.py b/openpype/hosts/maya/plugins/inventory/import_reference.py index ac97096ee7..2fa132a867 100644 --- a/openpype/hosts/maya/plugins/inventory/import_reference.py +++ b/openpype/hosts/maya/plugins/inventory/import_reference.py @@ -2,28 +2,28 @@ from maya import cmds from avalon import api +from openpype.hosts.maya.api.plugin import get_reference_node + class ImportReference(api.InventoryAction): - """Imports selected reference inside the file.""" + """Imports selected reference to inside of the file.""" label = "Import Reference" - icon = "mouse-pointer" + icon = "download" color = "#d8d8d8" def process(self, containers): references = cmds.ls(type="reference") - for container in containers: if container["loader"] != "ReferenceLoader": print("Not a reference, skipping") continue - reference_name = container["namespace"] + "RN" - if reference_name in references: - print("Importing {}".format(reference_name)) + node = container["objectName"] + members = cmds.sets(node, query=True, nodesOnly=True) + ref_node = get_reference_node(members) - ref_file = cmds.referenceQuery(reference_name, f=True) + ref_file = cmds.referenceQuery(ref_node, f=True) + cmds.file(ref_file, importReference=True) - cmds.file(ref_file, importReference=True) - - return "refresh" + return True # return anything to trigger model refresh From 23c32cd86dcd06c8270b0180be97449b9acc6b52 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Tue, 14 Sep 2021 15:55:25 +0200 Subject: [PATCH 138/450] merged with nuke page --- website/docs/artist_hosts_nuke_tut.md | 188 ++++++++++++++++++++++---- website/sidebars.js | 1 - 2 files changed, 165 insertions(+), 24 deletions(-) diff --git a/website/docs/artist_hosts_nuke_tut.md b/website/docs/artist_hosts_nuke_tut.md index 0ec1e104b9..4d116bd958 100644 --- a/website/docs/artist_hosts_nuke_tut.md +++ b/website/docs/artist_hosts_nuke_tut.md @@ -1,12 +1,150 @@ --- id: artist_hosts_nuke_tut -title: Nuke QuickStart -sidebar_label: Nuke QuickStart +title: Nuke +sidebar_label: Nuke --- -This QuickStart is just a small introduction to what OpenPype can do for you. It attempts to make an overview for compositing artists, and simplifies processes that are better described in specific parts of the documentation. +:::note +OpenPype supports Nuke version **`11.0`** and above. +::: -## Launch Nuke - Shot and Task Context +## OpenPype global tools + +- [Set Context](artist_tools.md#set-context) +- [Work Files](artist_tools.md#workfiles) +- [Create](artist_tools.md#creator) +- [Load](artist_tools.md#loader) +- [Manage (Inventory)](artist_tools.md#inventory) +- [Publish](artist_tools.md#publisher) +- [Library Loader](artist_tools.md#library-loader) + +## Nuke specific tools + +
    +
    + +### Set Frame Ranges + +Use this feature in case you are not sure the frame range is correct. + +##### Result + +- setting Frame Range in script settings +- setting Frame Range in viewers (timeline) + +
    +
    + +![Set Frame Ranges](assets/nuke_setFrameRanges.png) + +
    +
    + + +
    + +![Set Frame Ranges Timeline](assets/nuke_setFrameRanges_timeline.png) + +
    + +1. limiting to Frame Range without handles +2. **Input** handle on start +3. **Output** handle on end + +
    +
    + +### Set Resolution + +
    +
    + + +This menu item will set correct resolution format for you defined by your production. + +##### Result + +- creates new item in formats with project name +- sets the new format as used + +
    +
    + +![Set Resolution](assets/nuke_setResolution.png) + +
    +
    + + +### Set Colorspace + +
    +
    + +This menu item will set correct Colorspace definitions for you. All has to be configured by your production (Project coordinator). + +##### Result + +- set Colorspace in your script settings +- set preview LUT to your viewers +- set correct colorspace to all discovered Read nodes (following expression set in settings) + +
    +
    + +![Set Colorspace](assets/nuke_setColorspace.png) + +
    +
    + + +### Apply All Settings + +
    +
    + +It is usually enough if you once per while use this option just to make yourself sure the workfile is having set correct properties. + +##### Result + +- set Frame Ranges +- set Colorspace +- set Resolution + +
    +
    + +![Apply All Settings](assets/nuke_applyAllSettings.png) + +
    +
    + +### Build Workfile + +
    +
    + +This tool will append all available subsets into an actual node graph. It will look into database and get all last [versions](artist_concepts.md#version) of available [subsets](artist_concepts.md#subset). + + +##### Result + +- adds all last versions of subsets (rendered image sequences) as read nodes +- ~~adds publishable write node as `renderMain` subset~~ + +
    +
    + +![Build First Work File](assets/nuke_buildFirstWorkfile.png) + +
    +
    + +## Nuke QuickStart + +This QuickStart is short introduction to what OpenPype can do for you. It attempts to make an overview for compositing artists, and simplifies processes that are better described in specific parts of the documentation. + +### Launch Nuke - Shot and Task Context OpenPype has to know what shot and task you are working on. You need to run Nuke in context of the task, using Ftrack Action or OpenPype Launcher to select the task and run Nuke. ![Run Nuke From Ftrack](assets/nuke_tut/nuke_RunNukeFtrackAction_p3.png) @@ -16,24 +154,27 @@ OpenPype has to know what shot and task you are working on. You need to run Nuke You can [configure](admin_settings_project_anatomy.md#Attributes) which DCC version(s) will be available for current project in **Studio Settings → Project → Anatomy → Attributes → Applications** ::: -## Nuke OpenPype menu shows the current context +### Nuke Initial setup +Nuke OpenPype menu shows the current context ![Context](assets/nuke_tut/nuke_Context.png) Launching Nuke with context stops your timer, and starts the clock on the shot and task you picked. -## Nuke Initial setup Openpype makes initial setup for your Nuke script. It is the same as running [Apply All Settings](artist_hosts_nuke.md#apply-all-settings) from the OpenPype menu. -Reads frame range and resolution from Avalon database, sets it in Nuke Project Settings, +- Reads frame range and resolution from Avalon database, sets it in Nuke Project Settings, Creates Viewer node, sets it’s range and indicates handles by In and Out points. -Reads Color settings from the project configuration, and sets it in Nuke Project Settings and Viewer. +- Reads Color settings from the project configuration, and sets it in Nuke Project Settings and Viewer. -Sets project directory in the Nuke Project Settings to the Nuke Script Directory +- Sets project directory in the Nuke Project Settings to the Nuke Script Directory +:::tip Tip - Project Settings +After Nuke starts it will automatically **Apply All Settings** for you. If you are sure the settings are wrong just contact your supervisor and he will set them correctly for you in project database. +::: -## Save Nuke script – the Work File +### Save Nuke script – the Work File Use OpenPype - Work files menu to create a new Nuke script. Openpype offers you the preconfigured naming. ![Context](assets/nuke_tut/nuke_WorkFileSaveAs.png) @@ -58,7 +199,7 @@ More about [workfiles](artist_tools#workfiles). - [Color setting](project_settings/settings_project_nuke) for Nuke can be found in **Studio Settings → Project → Anatomy → Color Management and Output Formats → Nuke** ::: -## Load plate – Asset Loader +### Load plate Use Load from OpenPype menu to load any plates or renders available. ![Asset Load](assets/nuke_tut/nuke_AssetLoader.png) @@ -71,7 +212,7 @@ Note that the Read node created by OpenPype is green. Green color indicates the More about [Asset loader](artist_tools#loader). -## Create Write Node – Instance Creator +### Create Write Node To create OpenPype managed Write node, select the Read node you just created, from OpenPype menu, pick Create. In the Instance Creator, pick Create Write Render, and Create. @@ -85,7 +226,7 @@ This will create a Group with a Write node inside. You can configure write node parameters in **Studio Settings → Project → Anatomy → Color Management and Output Formats → Nuke → Nodes** ::: -## What Nuke Publish Does +#### What Nuke Publish Does From Artist perspective, Nuke publish gathers all the stuff found in the Nuke script with Publish checkbox set to on, exports stuff and raises the Nuke script (workfile) version. The Pyblish dialog shows the progress of the process. @@ -96,7 +237,7 @@ The left column of the dialog shows what will be published. Typically it is one The right column shows the publish steps -#### Publish steps +##### Publish steps 1. Gathers all the stuff found in the Nuke script with Publish checkbox set to on 2. Collects all the info (from the script, database…) 3. Validates components to be published (checks render range and resolution...) @@ -110,12 +251,12 @@ The right column shows the publish steps Gathering all the info and validating usually takes just a few seconds. Creating reviews for long, high resolution shots can however take significant amount of time when publishing locally. -#### Pyblish Note and Intent +##### Pyblish Note and Intent ![Note and Intent](assets/nuke_tut/nuke_PyblishDialogNukeNoteIntent.png) Artist can add Note and Intent before firing the publish button. The Note and Intent is ment for easy communication between artist and supervisor. After publish, Note and Intent can be seen in Ftrack notes. -#### Pyblish Checkbox +##### Pyblish Checkbox ![Note and Intent](assets/nuke_tut/nuke_PyblishCheckBox.png) @@ -129,7 +270,7 @@ More info about [Using Pyblish](artist_tools#publisher) You can configure Nuke validators like Output Resolution in **Studio Settings → Project → Nuke → Publish plugins** ::: -## Review +### Review ![Write Node Review](assets/nuke_tut/nuke_WriteNodeReview.png) When you turn the review checkbox on in your OpenPype write node, here is what happens: @@ -152,7 +293,7 @@ You can configure reviewsin **Studio Settings → Project → Global → Publish Reviews can be configured separately for each host, task, or family. For example Maya can produce different review to Nuke, animation task can have different burnin then modelling, and plate can have different review then model. ::: -## Render and Publish +### Render and Publish ![OpenPype Create](assets/nuke_tut/nuke_WriteNode.png) @@ -164,17 +305,17 @@ If you want to render and publish on the farm in one go, run publish with On far ![Versionless](assets/nuke_tut/nuke_RenderLocalFarm.png) -## Version-less Render +### Version-less Render ![Versionless](assets/nuke_tut/nuke_versionless.png) -OpenPype is configured so your render file names have no version number. The main advantage is that you can keep the render from the previous version and re-render only part of the shot. With care, this is handy. +OpenPype is configured so your render file names have no version number until the render is fully finished and published. The main advantage is that you can keep the render from the previous version and re-render only part of the shot. With care, this is handy. Main disadvantage of this approach is that you can render only one version of your shot at one time. Otherwise you risk to partially overwrite your shot render before publishing copies and renames the rendered files to the properly versioned publish folder. When making quick farm publishes, like making two versions with different color correction, care must be taken to let the first job (first version) completely finish before the second version starts rendering. -## Managing Versions +### Managing Versions ![Versionless](assets/nuke_tut/nuke_ManageVersion.png) @@ -182,14 +323,15 @@ OpenPype checks all the assets loaded to Nuke on script open. All out of date as Use Manage to switch versions for loaded assets. +## Troubleshooting -## Fixing Validate Containers +### Fixing Validate Containers ![Versionless](assets/nuke_tut/nuke_ValidateContainers.png) If your Pyblish dialog fails on Validate Containers, you might have an old asset loaded. Use OpenPype - Manage... to switch the asset(s) to the latest version. -## Fixing Validate Version +### Fixing Validate Version If your Pyblish dialog fails on Validate Version, you might be trying to publish already published version. Rise your version in the OpenPype WorkFiles SaveAs. Or maybe you accidentaly copied write node from different shot to your current one. Check the write publishes on the left side of the Pyblish dialog. Typically you publish only one write. Locate and delete the stray write from other shot. \ No newline at end of file diff --git a/website/sidebars.js b/website/sidebars.js index f805382518..23de72001b 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -18,7 +18,6 @@ module.exports = { label: "Integrations", items: [ "artist_hosts_hiero", - "artist_hosts_nuke", "artist_hosts_nuke_tut", "artist_hosts_maya", "artist_hosts_blender", From 39233653f893b7ffc547d174dfb52bfd2f61cc9f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:56:09 +0200 Subject: [PATCH 139/450] added 'is_running_from_build' function --- openpype/lib/pype_info.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index ec04f50532..2feeab7cae 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -25,10 +25,23 @@ def get_pype_version(): return get_openpype_version() +def is_running_from_build(): + """Determine if current process is running from build or code. + + Returns: + bool: True if running from build. + """ + executable_path = os.environ["OPENPYPE_EXECUTABLE"] + executable_filename = os.path.basename(executable_path) + if "python" in executable_filename.lower(): + return False + return True + + def get_pype_info(): """Information about currently used Pype process.""" executable_args = get_pype_execute_args() - if len(executable_args) == 1: + if is_running_from_build(): version_type = "build" else: version_type = "code" From 2d826a65ca7c039f44e6b35eafbf2c331db66144 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:56:46 +0200 Subject: [PATCH 140/450] added 'get_build_version' to be able get build version --- openpype/lib/pype_info.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index 2feeab7cae..c50c4db94b 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -9,6 +9,7 @@ import openpype.version from openpype.settings.lib import get_local_settings from .execute import get_pype_execute_args from .local_settings import get_local_site_id +from .python_module_tools import import_filepath def get_openpype_version(): @@ -25,6 +26,25 @@ def get_pype_version(): return get_openpype_version() +def get_build_version(): + """OpenPype version of build.""" + # Return OpenPype version if is running from code + if not is_running_from_build(): + return get_openpype_version() + + # Import `version.py` from build directory + version_filepath = os.path.join( + os.environ["OPENPYPE_ROOT"], + "openpype", + "version.py" + ) + if not os.path.exists(version_filepath): + return None + + module = import_filepath(version_filepath, "openpype_build_version") + return getattr(module, "__version__", None) + + def is_running_from_build(): """Determine if current process is running from build or code. From 12b362a15554d65b405509644fe29ec1eb9670a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 16:02:13 +0200 Subject: [PATCH 141/450] added 'get_openpype_version' and 'get_build_version' to `openpype.lib` scope --- openpype/lib/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 9bc68c9558..e96f1cc99f 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -160,6 +160,11 @@ from .editorial import ( make_sequence_collection ) +from .pype_info import ( + get_openpype_version, + get_build_version +) + terminal = Terminal __all__ = [ @@ -280,5 +285,8 @@ __all__ = [ "frames_to_timecode", "make_sequence_collection", "create_project_folders", - "get_project_basic_paths" + "get_project_basic_paths", + + "get_openpype_version", + "get_build_version", ] From 9e7a44527520fe6300687f693a831853f7e00100 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 16:19:30 +0200 Subject: [PATCH 142/450] Statuser converts status data to OrderedDict --- .../default_modules/ftrack/scripts/sub_event_status.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py b/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py index 8a2733b635..004f61338c 100644 --- a/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py +++ b/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py @@ -2,6 +2,7 @@ import os import sys import json import threading +import collections import signal import socket import datetime @@ -165,7 +166,7 @@ class StatusFactory: return source = event["data"]["source"] - data = event["data"]["status_info"] + data = collections.OrderedDict(event["data"]["status_info"]) self.update_status_info(source, data) @@ -348,7 +349,7 @@ def heartbeat(): def main(args): port = int(args[-1]) - server_info = json.loads(args[-2]) + server_info = collections.OrderedDict(json.loads(args[-2])) # Create a TCP/IP socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) From ce379e3cb42f4a191e35bd6a82c1b1a08fe4465e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 16:26:05 +0200 Subject: [PATCH 143/450] add openpype version information about each process --- .../ftrack/ftrack_server/event_server_cli.py | 19 +++++++++++-------- .../ftrack/scripts/sub_event_processor.py | 13 ++++++++++--- .../ftrack/scripts/sub_event_storer.py | 14 ++++++++++---- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py index d8e4d05580..1eeda1fefd 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py @@ -6,7 +6,6 @@ import subprocess import socket import json import platform -import argparse import getpass import atexit import time @@ -16,7 +15,9 @@ import ftrack_api import pymongo from openpype.lib import ( get_pype_execute_args, - OpenPypeMongoConnection + OpenPypeMongoConnection, + get_openpype_version, + get_build_version ) from openpype_modules.ftrack import FTRACK_MODULE_DIR from openpype_modules.ftrack.lib import credentials @@ -238,12 +239,14 @@ def main_loop(ftrack_url): system_name, pc_name = platform.uname()[:2] host_name = socket.gethostname() - main_info = { - "created_at": datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S"), - "Username": getpass.getuser(), - "Host Name": host_name, - "Host IP": socket.gethostbyname(host_name) - } + main_info = [ + ["created_at", datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S")], + ["Username", getpass.getuser()], + ["Host Name", host_name], + ["Host IP", socket.gethostbyname(host_name)], + ["OpenPype version", get_openpype_version() or "N/A"], + ["OpenPype build version", get_build_version() or "N/A"] + ] main_info_str = json.dumps(main_info) # Main loop while True: diff --git a/openpype/modules/default_modules/ftrack/scripts/sub_event_processor.py b/openpype/modules/default_modules/ftrack/scripts/sub_event_processor.py index 51b45eb93b..d1e2e3aaeb 100644 --- a/openpype/modules/default_modules/ftrack/scripts/sub_event_processor.py +++ b/openpype/modules/default_modules/ftrack/scripts/sub_event_processor.py @@ -13,6 +13,11 @@ from openpype_modules.ftrack.ftrack_server.lib import ( from openpype.modules import ModulesManager from openpype.api import Logger +from openpype.lib import ( + get_openpype_version, + get_build_version +) + import ftrack_api @@ -40,9 +45,11 @@ def send_status(event): new_event_data = { "subprocess_id": subprocess_id, "source": "processor", - "status_info": { - "created_at": subprocess_started.strftime("%Y.%m.%d %H:%M:%S") - } + "status_info": [ + ["created_at", subprocess_started.strftime("%Y.%m.%d %H:%M:%S")], + ["OpenPype version", get_openpype_version() or "N/A"], + ["OpenPype build version", get_build_version() or "N/A"] + ] } new_event = ftrack_api.event.base.Event( diff --git a/openpype/modules/default_modules/ftrack/scripts/sub_event_storer.py b/openpype/modules/default_modules/ftrack/scripts/sub_event_storer.py index a8649e0ccc..5543ed74e2 100644 --- a/openpype/modules/default_modules/ftrack/scripts/sub_event_storer.py +++ b/openpype/modules/default_modules/ftrack/scripts/sub_event_storer.py @@ -14,7 +14,11 @@ from openpype_modules.ftrack.ftrack_server.lib import ( TOPIC_STATUS_SERVER_RESULT ) from openpype_modules.ftrack.lib import get_ftrack_event_mongo_info -from openpype.lib import OpenPypeMongoConnection +from openpype.lib import ( + OpenPypeMongoConnection, + get_openpype_version, + get_build_version +) from openpype.api import Logger log = Logger.get_logger("Event storer") @@ -153,9 +157,11 @@ def send_status(event): new_event_data = { "subprocess_id": os.environ["FTRACK_EVENT_SUB_ID"], "source": "storer", - "status_info": { - "created_at": subprocess_started.strftime("%Y.%m.%d %H:%M:%S") - } + "status_info": [ + ["created_at", subprocess_started.strftime("%Y.%m.%d %H:%M:%S")], + ["OpenPype version", get_openpype_version() or "N/A"], + ["OpenPype build version", get_build_version() or "N/A"] + ] } new_event = ftrack_api.event.base.Event( From 01a3e96b72e1861720be3c4f1cb0099ebaf616cc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 16:26:20 +0200 Subject: [PATCH 144/450] added openpype executable to main process info --- .../default_modules/ftrack/ftrack_server/event_server_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py index 1eeda1fefd..075694d8f6 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py @@ -237,13 +237,13 @@ def main_loop(ftrack_url): statuser_thread=statuser_thread ) - system_name, pc_name = platform.uname()[:2] host_name = socket.gethostname() main_info = [ ["created_at", datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S")], ["Username", getpass.getuser()], ["Host Name", host_name], ["Host IP", socket.gethostbyname(host_name)], + ["OpenPype executable", get_pype_execute_args()[-1]], ["OpenPype version", get_openpype_version() or "N/A"], ["OpenPype build version", get_build_version() or "N/A"] ] From b0d166fe09b5f1a650ed476de43142cda243d9bd Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 15 Sep 2021 10:03:05 +0800 Subject: [PATCH 145/450] reverse project 'archive' to 'active' --- .../sync_server/tray/widgets.py | 2 +- .../defaults/project_anatomy/attributes.json | 2 +- .../schemas/schema_anatomy_attributes.json | 4 +-- openpype/tools/launcher/models.py | 2 +- .../project_manager/project_manager/model.py | 2 +- .../local_settings/projects_widget.py | 2 +- openpype/tools/settings/settings/widgets.py | 29 +++++++++---------- .../standalonepublish/widgets/widget_asset.py | 4 +-- 8 files changed, 23 insertions(+), 24 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index 1737b2e0c6..13389ed36c 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -34,7 +34,7 @@ class SyncProjectListWidget(ProjectListWidget): """ def __init__(self, sync_server, parent): - super(SyncProjectListWidget, self).__init__(parent, no_archived=True) + super(SyncProjectListWidget, self).__init__(parent, only_active=True) self.sync_server = sync_server self.project_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.project_list.customContextMenuRequested.connect( diff --git a/openpype/settings/defaults/project_anatomy/attributes.json b/openpype/settings/defaults/project_anatomy/attributes.json index ac91622726..128d5c14ca 100644 --- a/openpype/settings/defaults/project_anatomy/attributes.json +++ b/openpype/settings/defaults/project_anatomy/attributes.json @@ -24,5 +24,5 @@ ], "tools_env": [], "archive_confirm": "", - "archived": false + "active": true } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index f70ab2fc5f..fdae566655 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -78,8 +78,8 @@ }, { "type": "boolean", - "key": "archived", - "label": "Is Archived" + "key": "active", + "label": "Is Project Active" } ] } diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 53e2c19a3d..f87871409e 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -378,5 +378,5 @@ class ProjectModel(QtGui.QStandardItemModel): self.invisibleRootItem().insertRows(row, items) def get_projects(self): - return sorted(self.dbcon.projects(no_archived=True), + return sorted(self.dbcon.projects(only_active=True), key=lambda x: x["name"]) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index e31dd2ccfe..7036b65f87 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -46,7 +46,7 @@ class ProjectModel(QtGui.QStandardItemModel): project_names = set() for doc in sorted( - self.dbcon.projects(projection={"name": 1}, no_archived=True), + self.dbcon.projects(projection={"name": 1}, only_active=True), key=lambda x: x["name"] ): diff --git a/openpype/tools/settings/local_settings/projects_widget.py b/openpype/tools/settings/local_settings/projects_widget.py index 7d19c37bdf..9cd3b9a38e 100644 --- a/openpype/tools/settings/local_settings/projects_widget.py +++ b/openpype/tools/settings/local_settings/projects_widget.py @@ -809,7 +809,7 @@ class ProjectSettingsWidget(QtWidgets.QWidget): self.modules_manager = modules_manager - projects_widget = _ProjectListWidget(self, no_archived=True) + projects_widget = _ProjectListWidget(self, only_active=True) roos_site_widget = RootSiteWidget( modules_manager, project_settings, self ) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index d1a8bd8958..133f59e862 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -644,7 +644,7 @@ class ProjectListWidget(QtWidgets.QWidget): ProjectFilterRole = QtCore.Qt.UserRole + 11 ProjectSelectedRole = QtCore.Qt.UserRole + 12 - def __init__(self, parent, no_archived=False): + def __init__(self, parent, only_active=False): self._parent = parent self.current_project = None @@ -677,7 +677,7 @@ class ProjectListWidget(QtWidgets.QWidget): layout.addWidget(label_widget, 0) layout.addWidget(project_list, 1) - if no_archived: + if only_active: archived_chk = None else: archived_chk = QtWidgets.QCheckBox(" Show Archived Project ") @@ -697,7 +697,7 @@ class ProjectListWidget(QtWidgets.QWidget): self.archived_chk = archived_chk self.dbcon = None - self._no_archived = no_archived + self._only_active = only_active def on_item_clicked(self, new_index): new_project_name = new_index.data(QtCore.Qt.DisplayRole) @@ -778,8 +778,6 @@ class ProjectListWidget(QtWidgets.QWidget): model = self.project_model model.clear() - items = [(self.default, None)] - mongo_url = os.environ["OPENPYPE_MONGO"] # Force uninstall of whole avalon connection if url does not match @@ -797,33 +795,34 @@ class ProjectListWidget(QtWidgets.QWidget): self.dbcon = None self.current_project = None + items = [(self.default, True)] + if self.dbcon: for doc in self.dbcon.projects( - projection={"name": 1, "data.archived": 1}, - no_archived=self._no_archived + projection={"name": 1, "data.active": 1}, + only_active=self._only_active ): items.append( - (doc["name"], doc.get("data", {}).get("archived")) + (doc["name"], doc.get("data", {}).get("active", True)) ) - for project_name, is_archived in items: - visible = not is_archived + for project_name, is_active in items: row = QtGui.QStandardItem(project_name) - row.setData(visible, self.ProjectFilterRole) + row.setData(is_active, self.ProjectFilterRole) row.setData(False, self.ProjectSelectedRole) - if is_archived: + if is_active: + row.setData(project_name, self.ProjectSortRole) + + else: row.setData("~" + project_name, self.ProjectSortRole) font = row.font() font.setItalic(True) row.setFont(font) - else: - row.setData(project_name, self.ProjectSortRole) - model.appendRow(row) self.project_proxy.sort(0) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 8ee09d2435..eb22883c11 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -275,7 +275,7 @@ class AssetWidget(QtWidgets.QWidget): project_names = list() for doc in self.dbcon.projects(projection={"name": 1}, - no_archived=True): + only_active=True): project_name = doc.get("name") if project_name: @@ -304,7 +304,7 @@ class AssetWidget(QtWidgets.QWidget): projects = list() for project in self.dbcon.projects(projection={"name": 1}, - no_archived=True): + only_active=True): projects.append(project['name']) project_name = self.combo_projects.currentText() if project_name in projects: From 0bf84ad8133732c7e367b85ef3dc3a9278d3ad18 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 15 Sep 2021 11:13:04 +0800 Subject: [PATCH 146/450] refactor data role attrib to model --- openpype/tools/settings/settings/widgets.py | 28 +++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 133f59e862..73076deeee 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -602,6 +602,12 @@ class NiceCheckbox(QtWidgets.QFrame): return super(NiceCheckbox, self).mouseReleaseEvent(event) +class ProjectListModel(QtGui.QStandardItemModel): + sort_role = QtCore.Qt.UserRole + 10 + filter_role = QtCore.Qt.UserRole + 11 + selected_role = QtCore.Qt.UserRole + 12 + + class ProjectListView(QtWidgets.QListView): left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex) @@ -624,7 +630,7 @@ class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): index = self.sourceModel().index(source_row, 0, source_parent) is_active = bool(index.data(self.filterRole())) - is_selected = bool(index.data(ProjectListWidget.ProjectSelectedRole)) + is_selected = bool(index.data(ProjectListModel.selected_role)) return is_active or is_selected @@ -640,10 +646,6 @@ class ProjectListWidget(QtWidgets.QWidget): default = "< Default >" project_changed = QtCore.Signal() - ProjectSortRole = QtCore.Qt.UserRole + 10 - ProjectFilterRole = QtCore.Qt.UserRole + 11 - ProjectSelectedRole = QtCore.Qt.UserRole + 12 - def __init__(self, parent, only_active=False): self._parent = parent @@ -655,11 +657,11 @@ class ProjectListWidget(QtWidgets.QWidget): label_widget = QtWidgets.QLabel("Projects") project_list = ProjectListView(self) - project_model = QtGui.QStandardItemModel() + project_model = ProjectListModel() project_proxy = ProjectListSortFilterProxy() - project_proxy.setFilterRole(self.ProjectFilterRole) - project_proxy.setSortRole(self.ProjectSortRole) + project_proxy.setFilterRole(ProjectListModel.filter_role) + project_proxy.setSortRole(ProjectListModel.sort_role) project_proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) project_proxy.setSourceModel(project_model) @@ -760,7 +762,7 @@ class ProjectListWidget(QtWidgets.QWidget): found_items = model.findItems(self.default) index = model.indexFromItem(found_items[0]) - model.setData(index, True, self.ProjectSelectedRole) + model.setData(index, True, ProjectListModel.selected_role) index = proxy.mapFromSource(index) @@ -810,14 +812,14 @@ class ProjectListWidget(QtWidgets.QWidget): for project_name, is_active in items: row = QtGui.QStandardItem(project_name) - row.setData(is_active, self.ProjectFilterRole) - row.setData(False, self.ProjectSelectedRole) + row.setData(is_active, ProjectListModel.filter_role) + row.setData(False, ProjectListModel.selected_role) if is_active: - row.setData(project_name, self.ProjectSortRole) + row.setData(project_name, ProjectListModel.sort_role) else: - row.setData("~" + project_name, self.ProjectSortRole) + row.setData("~" + project_name, ProjectListModel.sort_role) font = row.font() font.setItalic(True) From 7ac2c04f613a77bceed2ff0e9e30b8e6ad24c6dd Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 15 Sep 2021 11:14:31 +0800 Subject: [PATCH 147/450] remove project archive confirmation --- .../defaults/project_anatomy/attributes.json | 1 - openpype/settings/entities/root_entities.py | 29 ------------------- .../schemas/schema_anatomy_attributes.json | 6 ---- 3 files changed, 36 deletions(-) diff --git a/openpype/settings/defaults/project_anatomy/attributes.json b/openpype/settings/defaults/project_anatomy/attributes.json index 128d5c14ca..983ac603f9 100644 --- a/openpype/settings/defaults/project_anatomy/attributes.json +++ b/openpype/settings/defaults/project_anatomy/attributes.json @@ -23,6 +23,5 @@ "unreal/4-26" ], "tools_env": [], - "archive_confirm": "", "active": true } \ No newline at end of file diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 5701b60098..05d20ee60b 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -755,35 +755,6 @@ class ProjectSettings(RootEntity): """ return DEFAULTS_DIR - def settings_value(self): - output = super(ProjectSettings, self).settings_value() - - anatomy = output.get(PROJECT_ANATOMY_KEY) or {} - - # Evaluate project archiving flag - # - archive_confirm = anatomy.get("attributes", {}).get("archive_confirm") - if archive_confirm: - # set flag - if archive_confirm == self.project_name: - self.log.debug( - "Project archiving." - ) - anatomy["attributes"]["archived"] = True - - else: - self.log.debug( - "Project archiving confirmation string not matched." - ) - anatomy["attributes"]["archive_confirm"] = "" - anatomy["attributes"]["archived"] = False - - else: - if anatomy and "attributes" in anatomy: - anatomy["attributes"]["archived"] = False - - return output - def _save_studio_values(self): settings_value = self.settings_value() diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index fdae566655..530bf70a7c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -70,12 +70,6 @@ "key": "tools_env", "label": "Tools" }, - { - "type": "text", - "key": "archive_confirm", - "label": "Archive Project", - "placeholder": "Input project name to confirm archiving." - }, { "type": "boolean", "key": "active", From b12bc7b6a5bbe6cf899bfdb634687897416d613a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 10:38:50 +0200 Subject: [PATCH 148/450] resize console window only once and make sure it has minimum size --- .../python_console_interpreter/window/widgets.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py index 975decf4f4..f8ff6f56fc 100644 --- a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py +++ b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py @@ -387,8 +387,6 @@ class PythonInterpreterWidget(QtWidgets.QWidget): self.setStyleSheet(load_stylesheet()) - self.resize(self.default_width, self.default_height) - self._init_from_registry() if self._tab_widget.count() < 1: @@ -396,16 +394,23 @@ class PythonInterpreterWidget(QtWidgets.QWidget): def _init_from_registry(self): setting_registry = PythonInterpreterRegistry() - + width = None + height = None try: width = setting_registry.get_item("width") height = setting_registry.get_item("height") - if width is not None and height is not None: - self.resize(width, height) except ValueError: pass + if width is None or width < 200: + width = self.default_width + + if height is None or height < 200: + height = self.default_height + + self.resize(width, height) + try: sizes = setting_registry.get_item("splitter_sizes") if len(sizes) == len(self._widgets_splitter.sizes()): From d600bfb113e647e6f13dfbb9da9b32d43501a497 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:19:51 +0200 Subject: [PATCH 149/450] removed interface of timers manager --- .../timers_manager/interfaces.py | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 openpype/modules/default_modules/timers_manager/interfaces.py diff --git a/openpype/modules/default_modules/timers_manager/interfaces.py b/openpype/modules/default_modules/timers_manager/interfaces.py deleted file mode 100644 index 179013cffe..0000000000 --- a/openpype/modules/default_modules/timers_manager/interfaces.py +++ /dev/null @@ -1,26 +0,0 @@ -from abc import abstractmethod -from openpype.modules import OpenPypeInterface - - -class ITimersManager(OpenPypeInterface): - timer_manager_module = None - - @abstractmethod - def stop_timer(self): - pass - - @abstractmethod - def start_timer(self, data): - pass - - def timer_started(self, data): - if not self.timer_manager_module: - return - - self.timer_manager_module.timer_started(self.id, data) - - def timer_stopped(self): - if not self.timer_manager_module: - return - - self.timer_manager_module.timer_stopped(self.id) From 292655e1d64f2acc32a58bda428b5461c0b34c41 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:30:48 +0200 Subject: [PATCH 150/450] TimersManager has new way of connection definition to it's logic --- .../timers_manager/timers_manager.py | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 80f448095f..7fb52eaef7 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -22,6 +22,11 @@ class TimersManager( name = "timers_manager" label = "Timers Service" + _required_methods = ( + "stop_timer", + "start_timer" + ) + def initialize(self, modules_settings): timers_settings = modules_settings[self.name] @@ -44,7 +49,8 @@ class TimersManager( self.widget_user_idle = None self.signal_handler = None - self.modules = [] + self._connectors_by_module_id = {} + self._modules_by_id = {} def tray_init(self): from .widget_user_idle import WidgetUserIdle, SignalHandler @@ -135,10 +141,36 @@ class TimersManager( def connect_with_modules(self, enabled_modules): for module in enabled_modules: - if not isinstance(module, ITimersManager): + connector = getattr(module, "timers_manager_connector", None) + if connector is None: continue - module.timer_manager_module = self - self.modules.append(module) + + missing_methods = set() + for method_name in self._required_methods: + if not hasattr(connector, method_name): + missing_methods.add(method_name) + + if missing_methods: + joined = ", ".join( + ['"{}"'.format(name for name in missing_methods)] + ) + self.log.info(( + "Module \"{}\" has missing required methods {}." + ).format(module.name, joined)) + continue + + self._connectors_by_module_id[module.id] = connector + self._modules_by_id[module.id] = module + + # Optional method + if hasattr(connector, "register_timers_manager"): + try: + connector.register_timers_manager(self) + except Exception: + self.log.info(( + "Failed to register timers manager" + " for connector of module \"{}\"." + ).format(module.name)) def callbacks_by_idle_time(self): """Implementation of IIdleManager interface.""" From 412d429f6d210f986f653ba053bcd93feab92ec3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:32:13 +0200 Subject: [PATCH 151/450] modified timer stopped/started methods --- .../timers_manager/timers_manager.py | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 7fb52eaef7..b66dfaef94 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -31,6 +31,7 @@ class TimersManager( timers_settings = modules_settings[self.name] self.enabled = timers_settings["enabled"] + auto_stop = timers_settings["auto_stop"] # When timer will stop if idle manager is running (minutes) full_time = int(timers_settings["full_time"] * 60) @@ -68,8 +69,9 @@ class TimersManager( """Implementation of IWebServerRoutes interface.""" if self.tray_initialized: from .rest_api import TimersManagerModuleRestApi - self.rest_api_obj = TimersManagerModuleRestApi(self, - server_manager) + self.rest_api_obj = TimersManagerModuleRestApi( + self, server_manager + ) def start_timer(self, project_name, asset_name, task_name, hierarchy): """ @@ -112,17 +114,35 @@ class TimersManager( self.timer_started(None, data) def timer_started(self, source_id, data): - for module in self.modules: - if module.id != source_id: - module.start_timer(data) + for module_id, connector in self._connectors_by_module_id.items(): + if module_id == source_id: + continue + + try: + connector.start_timer(data) + except Exception: + self.log.info( + "Failed to start timer on connector {}".format( + str(connector) + ) + ) self.last_task = data self.is_running = True def timer_stopped(self, source_id): - for module in self.modules: - if module.id != source_id: - module.stop_timer() + for module_id, connector in self._connectors_by_module_id.items(): + if module_id == source_id: + continue + + try: + connector.stop_timer() + except Exception: + self.log.info( + "Failed to stop timer on connector {}".format( + str(connector) + ) + ) def restart_timers(self): if self.last_task is not None: @@ -136,8 +156,7 @@ class TimersManager( self.widget_user_idle.refresh_context() self.is_running = False - for module in self.modules: - module.stop_timer() + self.timer_stopper(None) def connect_with_modules(self, enabled_modules): for module in enabled_modules: From 2647b3fb1fcf351d7e748889cd4618c8edf763b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:32:34 +0200 Subject: [PATCH 152/450] added example of connector to timers manager --- .../timers_manager/timers_manager.py | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index b66dfaef94..7d83cf0349 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -10,14 +10,78 @@ from openpype_interfaces import ( from avalon.api import AvalonMongoDB +class ExampleTimersManagerConnector: + """Timers manager can handle timers of multiple modules/addons. + + Module must have object under `timers_manager_connector` attribute with + few methods. This is example class of the object that could be stored under + module. + + Required methods are 'stop_timer' and 'start_timer'. + + # TODO pass asset document instead of `hierarchy` + Example of `data` that are passed during changing timer: + ``` + data = { + "project_name": project_name, + "task_name": task_name, + "task_type": task_type, + "hierarchy": hierarchy + } + ``` + """ + # Not needed at all + def __init__(self, module): + # Store timer manager module to be able call it's methods when needed + self._timers_manager_module = None + + # Store module which want to use timers manager to have access + self._module = module + + # Required + def stop_timer(self): + """Called by timers manager when module should stop timer.""" + self._module.stop_timer() + + # Required + def start_timer(self, data): + """Method called by timers manager when should start timer.""" + self._module.start_timer(data) + + # Optional + def register_timers_manager(self, timer_manager_module): + """Method called by timers manager where it's object is passed. + + This is moment when timers manager module can be store to be able + call it's callbacks (e.g. timer started). + """ + self._timers_manager_module = timer_manager_module + + # Custom implementation + def timer_started(self, data): + """This is example of possibility to trigger callbacks on manager.""" + if self._timers_manager_module is not None: + self._timers_manager_module.timer_started(self._module.id, data) + + # Custom implementation + def timer_stopped(self): + if self._timers_manager_module is not None: + self._timers_manager_module.timer_stopped(self._module.id) + + class TimersManager( OpenPypeModule, ITrayService, IIdleManager, IWebServerRoutes ): """ Handles about Timers. Should be able to start/stop all timers at once. - If IdleManager is imported then is able to handle about stop timers - when user idles for a long time (set in presets). + + To be able use this advantage module has to have attribute with name + `timers_manager_connector` which has two methods 'stop_timer' + and 'start_timer'. Optionally may have `register_timers_manager` where + object of TimersManager module is passed to be able call it's callbacks. + + See `ExampleTimersManagerConnector`. """ name = "timers_manager" label = "Timers Service" From 15a8c477d79fa311e1f258835e6a59531579effa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:34:39 +0200 Subject: [PATCH 153/450] modified clickify to not use ITimersManager but defined attribute with methods --- .../clockify/clockify_module.py | 105 +++++++++++------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/openpype/modules/default_modules/clockify/clockify_module.py b/openpype/modules/default_modules/clockify/clockify_module.py index a9e989f4ec..5136b9cbc3 100644 --- a/openpype/modules/default_modules/clockify/clockify_module.py +++ b/openpype/modules/default_modules/clockify/clockify_module.py @@ -11,8 +11,7 @@ from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITrayModule, IPluginPaths, - IFtrackEventHandlerPaths, - ITimersManager + IFtrackEventHandlerPaths ) @@ -20,8 +19,7 @@ class ClockifyModule( OpenPypeModule, ITrayModule, IPluginPaths, - IFtrackEventHandlerPaths, - ITimersManager + IFtrackEventHandlerPaths ): name = "clockify" @@ -39,6 +37,11 @@ class ClockifyModule( self.clockapi = ClockifyAPI(master_parent=self) + # TimersManager attributes + # - set `timers_manager_connector` only in `tray_init` + self.timers_manager_connector = None + self._timers_manager_module = None + def get_global_environments(self): return { "CLOCKIFY_WORKSPACE": self.workspace_name @@ -61,6 +64,9 @@ class ClockifyModule( self.bool_timer_run = False self.bool_api_key_set = self.clockapi.set_api() + # Define itself as TimersManager connector + self.timers_manager_connector = self + def tray_start(self): if self.bool_api_key_set is False: self.show_settings() @@ -165,10 +171,6 @@ class ClockifyModule( self.set_menu_visibility() time.sleep(5) - def stop_timer(self): - """Implementation of ITimersManager.""" - self.clockapi.finish_time_entry() - def signed_in(self): if not self.timer_manager: return @@ -179,8 +181,60 @@ class ClockifyModule( if self.timer_manager.is_running: self.start_timer_manager(self.timer_manager.last_task) + def on_message_widget_close(self): + self.message_widget = None + + # Definition of Tray menu + def tray_menu(self, parent_menu): + # Menu for Tray App + from Qt import QtWidgets + menu = QtWidgets.QMenu("Clockify", parent_menu) + menu.setProperty("submenu", "on") + + # Actions + action_show_settings = QtWidgets.QAction("Settings", menu) + action_stop_timer = QtWidgets.QAction("Stop timer", menu) + + menu.addAction(action_show_settings) + menu.addAction(action_stop_timer) + + action_show_settings.triggered.connect(self.show_settings) + action_stop_timer.triggered.connect(self.stop_timer) + + self.action_stop_timer = action_stop_timer + + self.set_menu_visibility() + + parent_menu.addMenu(menu) + + def show_settings(self): + self.widget_settings.input_api_key.setText(self.clockapi.get_api_key()) + self.widget_settings.show() + + def set_menu_visibility(self): + self.action_stop_timer.setVisible(self.bool_timer_run) + + # --- TimersManager connection methods --- + def register_timers_manager(self, timer_manager_module): + """Store TimersManager for future use.""" + self._timers_manager_module = timer_manager_module + + def timer_started(self, data): + """Tell TimersManager that timer started.""" + if self._timers_manager_module is not None: + self._timers_manager_module.timer_started(self._module.id, data) + + def timer_stopped(self): + """Tell TimersManager that timer stopped.""" + if self._timers_manager_module is not None: + self._timers_manager_module.timer_stopped(self._module.id) + + def stop_timer(self): + """Called from TimersManager to stop timer.""" + self.clockapi.finish_time_entry() + def start_timer(self, input_data): - """Implementation of ITimersManager.""" + """Called from TimersManager to start timer.""" # If not api key is not entered then skip if not self.clockapi.get_api_key(): return @@ -237,36 +291,3 @@ class ClockifyModule( self.clockapi.start_time_entry( description, project_id, tag_ids=tag_ids ) - - def on_message_widget_close(self): - self.message_widget = None - - # Definition of Tray menu - def tray_menu(self, parent_menu): - # Menu for Tray App - from Qt import QtWidgets - menu = QtWidgets.QMenu("Clockify", parent_menu) - menu.setProperty("submenu", "on") - - # Actions - action_show_settings = QtWidgets.QAction("Settings", menu) - action_stop_timer = QtWidgets.QAction("Stop timer", menu) - - menu.addAction(action_show_settings) - menu.addAction(action_stop_timer) - - action_show_settings.triggered.connect(self.show_settings) - action_stop_timer.triggered.connect(self.stop_timer) - - self.action_stop_timer = action_stop_timer - - self.set_menu_visibility() - - parent_menu.addMenu(menu) - - def show_settings(self): - self.widget_settings.input_api_key.setText(self.clockapi.get_api_key()) - self.widget_settings.show() - - def set_menu_visibility(self): - self.action_stop_timer.setVisible(self.bool_timer_run) From 3704b0c4cf022a33d0582c5242f396f1e5de9ff8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:35:11 +0200 Subject: [PATCH 154/450] modified ftrack to not use ITimersManager but defined attributes with predefined methods --- .../default_modules/ftrack/ftrack_module.py | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 1de152535c..3732e762b4 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -7,7 +7,6 @@ from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITrayModule, IPluginPaths, - ITimersManager, ILaunchHookPaths, ISettingsChangeListener, IFtrackEventHandlerPaths @@ -21,7 +20,6 @@ class FtrackModule( OpenPypeModule, ITrayModule, IPluginPaths, - ITimersManager, ILaunchHookPaths, ISettingsChangeListener ): @@ -61,6 +59,10 @@ class FtrackModule( self.user_event_handlers_paths = user_event_handlers_paths self.tray_module = None + # TimersManager connection + self.timers_manager_connector = None + self._timers_manager_module = None + def get_global_environments(self): """Ftrack's global environments.""" return { @@ -102,16 +104,6 @@ class FtrackModule( elif key == "user": self.user_event_handlers_paths.extend(value) - def start_timer(self, data): - """Implementation of ITimersManager interface.""" - if self.tray_module: - self.tray_module.start_timer_manager(data) - - def stop_timer(self): - """Implementation of ITimersManager interface.""" - if self.tray_module: - self.tray_module.stop_timer_manager() - def on_system_settings_save( self, old_value, new_value, changes, new_value_metadata ): @@ -343,7 +335,10 @@ class FtrackModule( def tray_init(self): from .tray import FtrackTrayWrapper + self.tray_module = FtrackTrayWrapper(self) + # Module is it's own connector to TimersManager + self.timers_manager_connector = self def tray_menu(self, parent_menu): return self.tray_module.tray_menu(parent_menu) @@ -357,3 +352,23 @@ class FtrackModule( def set_credentials_to_env(self, username, api_key): os.environ["FTRACK_API_USER"] = username or "" os.environ["FTRACK_API_KEY"] = api_key or "" + + # --- TimersManager connection methods --- + def start_timer(self, data): + if self.tray_module: + self.tray_module.start_timer_manager(data) + + def stop_timer(self): + if self.tray_module: + self.tray_module.stop_timer_manager() + + def register_timers_manager(self, timer_manager_module): + self._timers_manager_module = timer_manager_module + + def timer_started(self, data): + if self._timers_manager_module is not None: + self._timers_manager_module.timer_started(self.id, data) + + def timer_stopped(self): + if self._timers_manager_module is not None: + self._timers_manager_module.timer_stopped(self.id) From 9a8fa8311e5843bfefb702887c05490f83a72d74 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 15 Sep 2021 13:28:17 +0200 Subject: [PATCH 155/450] update avalon core submodule --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index b3e4959778..1e94241ffe 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit b3e49597786c931c13bca207769727d5fc56d5f6 +Subproject commit 1e94241ffe2dd7ce65ca66b08e452ffc03180235 From b1db37c4c45550c2cf9a952911ebfef901e540f8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 15:44:58 +0200 Subject: [PATCH 156/450] module just must have implemented `webserver_initialization` method to be able use webserver module --- .../default_modules/webserver/webserver_module.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/webserver/webserver_module.py b/openpype/modules/default_modules/webserver/webserver_module.py index 5bfb2d6390..8374eecf8c 100644 --- a/openpype/modules/default_modules/webserver/webserver_module.py +++ b/openpype/modules/default_modules/webserver/webserver_module.py @@ -28,8 +28,15 @@ class WebServerModule(OpenPypeModule, ITrayService): return for module in enabled_modules: - if isinstance(module, IWebServerRoutes): + if not hasattr(module, "webserver_initialization"): + continue + + try: module.webserver_initialization(self.server_manager) + except Exception: + self.log.warning(( + "Failed to connect module \"{}\" to webserver." + ).format(module.name)) def tray_init(self): self.create_server_manager() From 3f13a75af3be9dfba1d680c18c22fdd056f0ca3b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 15:45:59 +0200 Subject: [PATCH 157/450] removed IWebServerRoutes --- .../default_modules/avalon_apps/avalon_app.py | 21 ++++++++--------- .../modules/default_modules/muster/muster.py | 22 ++++++++---------- .../timers_manager/timers_manager.py | 23 +++++++++---------- .../default_modules/webserver/interfaces.py | 9 -------- .../webserver/webserver_module.py | 5 +--- 5 files changed, 31 insertions(+), 49 deletions(-) delete mode 100644 openpype/modules/default_modules/webserver/interfaces.py diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index 53e06ec90a..df8e9aca99 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -2,13 +2,10 @@ import os import openpype from openpype import resources from openpype.modules import OpenPypeModule -from openpype_interfaces import ( - ITrayModule, - IWebServerRoutes -) +from openpype_interfaces import ITrayModule -class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes): +class AvalonModule(OpenPypeModule, ITrayModule): name = "avalon" def initialize(self, modules_settings): @@ -74,13 +71,6 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes): def connect_with_modules(self, _enabled_modules): return - def webserver_initialization(self, server_manager): - """Implementation of IWebServerRoutes interface.""" - - if self.tray_initialized: - from .rest_api import AvalonRestApiResource - self.rest_api_obj = AvalonRestApiResource(self, server_manager) - # Definition of Tray menu def tray_menu(self, tray_menu): from Qt import QtWidgets @@ -108,3 +98,10 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes): # for Windows self.libraryloader.activateWindow() self.libraryloader.refresh() + + # Webserver module implementation + def webserver_initialization(self, server_manager): + """Add routes for webserver.""" + if self.tray_initialized: + from .rest_api import AvalonRestApiResource + self.rest_api_obj = AvalonRestApiResource(self, server_manager) diff --git a/openpype/modules/default_modules/muster/muster.py b/openpype/modules/default_modules/muster/muster.py index a0e72006af..1acde0805e 100644 --- a/openpype/modules/default_modules/muster/muster.py +++ b/openpype/modules/default_modules/muster/muster.py @@ -3,13 +3,10 @@ import json import appdirs import requests from openpype.modules import OpenPypeModule -from openpype_interfaces import ( - ITrayModule, - IWebServerRoutes -) +from openpype_interfaces import ITrayModule -class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes): +class MusterModule(OpenPypeModule, ITrayModule): """ Module handling Muster Render credentials. This will display dialog asking for user credentials for Muster if not already specified. @@ -76,13 +73,6 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes): parent.addMenu(menu) - def webserver_initialization(self, server_manager): - """Implementation of IWebServerRoutes interface.""" - if self.tray_initialized: - from .rest_api import MusterModuleRestApi - - self.rest_api_obj = MusterModuleRestApi(self, server_manager) - def load_credentials(self): """ Get credentials from JSON file @@ -142,6 +132,14 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes): if self.widget_login: self.widget_login.show() + # Webserver module implementation + def webserver_initialization(self, server_manager): + """Add routes for Muster login.""" + if self.tray_initialized: + from .rest_api import MusterModuleRestApi + + self.rest_api_obj = MusterModuleRestApi(self, server_manager) + def _requests_post(self, *args, **kwargs): """ Wrapper for requests, disabling SSL certificate validation if DONT_VERIFY_SSL environment variable is found. This is useful when diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 80f448095f..90c8a9c7db 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -4,15 +4,12 @@ from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITimersManager, ITrayService, - IIdleManager, - IWebServerRoutes + IIdleManager ) from avalon.api import AvalonMongoDB -class TimersManager( - OpenPypeModule, ITrayService, IIdleManager, IWebServerRoutes -): +class TimersManager(OpenPypeModule, ITrayService, IIdleManager): """ Handles about Timers. Should be able to start/stop all timers at once. @@ -58,13 +55,6 @@ class TimersManager( """Nothing special for TimersManager.""" return - def webserver_initialization(self, server_manager): - """Implementation of IWebServerRoutes interface.""" - if self.tray_initialized: - from .rest_api import TimersManagerModuleRestApi - self.rest_api_obj = TimersManagerModuleRestApi(self, - server_manager) - def start_timer(self, project_name, asset_name, task_name, hierarchy): """ Start timer for 'project_name', 'asset_name' and 'task_name' @@ -205,6 +195,15 @@ class TimersManager( if self.widget_user_idle.bool_is_showed is False: self.widget_user_idle.show() + # Webserver module implementation + def webserver_initialization(self, server_manager): + """Add routes for timers to be able start/stop with rest api.""" + if self.tray_initialized: + from .rest_api import TimersManagerModuleRestApi + self.rest_api_obj = TimersManagerModuleRestApi( + self, server_manager + ) + def change_timer_from_host(self, project_name, asset_name, task_name): """Prepared method for calling change timers on REST api""" webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") diff --git a/openpype/modules/default_modules/webserver/interfaces.py b/openpype/modules/default_modules/webserver/interfaces.py deleted file mode 100644 index 779361a9ec..0000000000 --- a/openpype/modules/default_modules/webserver/interfaces.py +++ /dev/null @@ -1,9 +0,0 @@ -from abc import abstractmethod -from openpype.modules import OpenPypeInterface - - -class IWebServerRoutes(OpenPypeInterface): - """Other modules interface to register their routes.""" - @abstractmethod - def webserver_initialization(self, server_manager): - pass diff --git a/openpype/modules/default_modules/webserver/webserver_module.py b/openpype/modules/default_modules/webserver/webserver_module.py index 8374eecf8c..871461ab25 100644 --- a/openpype/modules/default_modules/webserver/webserver_module.py +++ b/openpype/modules/default_modules/webserver/webserver_module.py @@ -3,10 +3,7 @@ import socket from openpype import resources from openpype.modules import OpenPypeModule -from openpype_interfaces import ( - ITrayService, - IWebServerRoutes -) +from openpype_interfaces import ITrayService class WebServerModule(OpenPypeModule, ITrayService): From 375d626de32ddc19a94309f695496938f61975d8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 15:47:34 +0200 Subject: [PATCH 158/450] added short docstring to webserver_module.py file --- .../webserver/webserver_module.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/openpype/modules/default_modules/webserver/webserver_module.py b/openpype/modules/default_modules/webserver/webserver_module.py index 871461ab25..686bd27bfd 100644 --- a/openpype/modules/default_modules/webserver/webserver_module.py +++ b/openpype/modules/default_modules/webserver/webserver_module.py @@ -1,3 +1,25 @@ +"""WebServerModule spawns aiohttp server in asyncio loop. + +Main usage of the module is in OpenPype tray where make sense to add ability +of other modules to add theirs routes. Module which would want use that +option must have implemented method `webserver_initialization` which must +expect `WebServerManager` object where is possible to add routes or paths +with handlers. + +WebServerManager is by default created only in tray. + +It is possible to create server manager without using module logic at all +using `create_new_server_manager`. That can be handy for standalone scripts +with predefined host and port and separated routes and logic. + +Running multiple servers in one process is not recommended and probably won't +work as expected. It is because of few limitations connected to asyncio module. + +When module's `create_server_manager` is called it is also set environment +variable "OPENPYPE_WEBSERVER_URL". Which should lead to root access point +of server. +""" + import os import socket From db9e5fa1df8d2c740cbe143c2179fad4a5a03b73 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 15 Sep 2021 23:30:00 +0800 Subject: [PATCH 159/450] Rephrase 'archived' to 'inactive' --- openpype/tools/settings/settings/widgets.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 73076deeee..a461f3e675 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -680,23 +680,23 @@ class ProjectListWidget(QtWidgets.QWidget): layout.addWidget(project_list, 1) if only_active: - archived_chk = None + inactive_chk = None else: - archived_chk = QtWidgets.QCheckBox(" Show Archived Project ") - archived_chk.setChecked(not project_proxy.is_filter_enabled()) + inactive_chk = QtWidgets.QCheckBox(" Show Inactive Projects ") + inactive_chk.setChecked(not project_proxy.is_filter_enabled()) layout.addSpacing(5) - layout.addWidget(archived_chk, 0) + layout.addWidget(inactive_chk, 0) layout.addSpacing(5) - archived_chk.stateChanged.connect(self.on_archive_vis_changed) + inactive_chk.stateChanged.connect(self.on_inactive_vis_changed) project_list.left_mouse_released_at.connect(self.on_item_clicked) self.project_list = project_list self.project_proxy = project_proxy self.project_model = project_model - self.archived_chk = archived_chk + self.inactive_chk = inactive_chk self.dbcon = None self._only_active = only_active @@ -734,12 +734,12 @@ class ProjectListWidget(QtWidgets.QWidget): else: self.select_project(self.current_project) - def on_archive_vis_changed(self): - if self.archived_chk is None: + def on_inactive_vis_changed(self): + if self.inactive_chk is None: # should not happen. return - enable_filter = not self.archived_chk.isChecked() + enable_filter = not self.inactive_chk.isChecked() self.project_proxy.set_filter_enabled(enable_filter) def validate_context_change(self): From 4650669782a7bc8e904214bbb3b88632d597e5b7 Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Wed, 15 Sep 2021 14:14:13 -0700 Subject: [PATCH 160/450] Adding predefined project folders creation in PM #1989. --- .../project_manager/project_manager/window.py | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 7c71f4b451..caea6f46ab 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -1,3 +1,4 @@ +import logging from Qt import QtWidgets, QtCore, QtGui from . import ( @@ -15,8 +16,11 @@ from openpype.lib import is_admin_password_required from openpype.widgets import PasswordDialog from openpype import resources +from openpype.api import get_project_basic_paths, create_project_folders from avalon.api import AvalonMongoDB +log = logging.getLogger(__name__) + class ProjectManagerWindow(QtWidgets.QWidget): """Main widget of Project Manager tool.""" @@ -28,6 +32,9 @@ class ProjectManagerWindow(QtWidgets.QWidget): self._password_dialog = None self._user_passed = False + # keep track of the current project PM is viewing + self._current_project = None + self.setWindowTitle("OpenPype Project Manager") self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath())) @@ -82,11 +89,20 @@ class ProjectManagerWindow(QtWidgets.QWidget): add_asset_btn.setObjectName("IconBtn") add_task_btn.setObjectName("IconBtn") + add_misc_folders_label = QtWidgets.QLabel("Create misc. folders:", helper_btns_widget) + add_misc_folders_btn = QtWidgets.QPushButton( + ResourceCache.get_icon("asset", "default"), + "Create Misc. Folders", + helper_btns_widget + ) + helper_btns_layout = QtWidgets.QHBoxLayout(helper_btns_widget) helper_btns_layout.setContentsMargins(0, 0, 0, 0) helper_btns_layout.addWidget(helper_label) helper_btns_layout.addWidget(add_asset_btn) helper_btns_layout.addWidget(add_task_btn) + helper_btns_layout.addWidget(add_misc_folders_label) + helper_btns_layout.addWidget(add_misc_folders_btn) helper_btns_layout.addStretch(1) # Add widgets to top widget layout @@ -128,6 +144,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): save_btn.clicked.connect(self._on_save_click) add_asset_btn.clicked.connect(self._on_add_asset) add_task_btn.clicked.connect(self._on_add_task) + add_misc_folders_btn.clicked.connect(self._on_add_misc_folders) self._project_model = project_model @@ -142,6 +159,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): self._add_asset_btn = add_asset_btn self._add_task_btn = add_task_btn + self._add_misc_folders_btn = add_misc_folders_btn self.resize(1200, 600) self.setStyleSheet(load_stylesheet()) @@ -179,7 +197,9 @@ class ProjectManagerWindow(QtWidgets.QWidget): self._set_project(self._project_combobox.currentText()) def _on_project_change(self): - self._set_project(self._project_combobox.currentText()) + if self._project_combobox.currentIndex() != 0: + self._current_project = self._project_combobox.currentText() + self._set_project(self._current_project) def _on_project_refresh(self): self.refresh_projects() @@ -193,6 +213,23 @@ class ProjectManagerWindow(QtWidgets.QWidget): def _on_add_task(self): self.hierarchy_view.add_task() + def _on_add_misc_folders(self): + if not self._current_project: + return + + qm = QtWidgets.QMessageBox + ans = qm.question(self, '', "Confirm to create misc. project folders?", qm.Yes | qm.No) + if ans == qm.Yes: + try: + # Get paths based on presets + basic_paths = get_project_basic_paths(self._current_project) + if not basic_paths: + pass + # Invoking OpenPype API to create the project folders + create_project_folders(basic_paths, self._current_project) + except Exception as exc: + log.warning("Error creating.", exc_info=True) + def show_message(self, message): # TODO add nicer message pop self.message_label.setText(message) @@ -203,9 +240,9 @@ class ProjectManagerWindow(QtWidgets.QWidget): if dialog.result() != 1: return - project_name = dialog.project_name - self.show_message("Created project \"{}\"".format(project_name)) - self.refresh_projects(project_name) + self._current_project = dialog.project_name + self.show_message("Created project \"{}\"".format(self._current_project)) + self.refresh_projects(self._current_project) def _show_password_dialog(self): if self._password_dialog: From 1e6b82bf1137364aa115ad63b1e5ead14c91ad57 Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Wed, 15 Sep 2021 14:41:36 -0700 Subject: [PATCH 161/450] Make hound happy. --- .../project_manager/project_manager/window.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index caea6f46ab..f8fbe2f288 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -89,7 +89,10 @@ class ProjectManagerWindow(QtWidgets.QWidget): add_asset_btn.setObjectName("IconBtn") add_task_btn.setObjectName("IconBtn") - add_misc_folders_label = QtWidgets.QLabel("Create misc. folders:", helper_btns_widget) + add_misc_folders_label = QtWidgets.QLabel( + "Create misc. folders:", + helper_btns_widget + ) add_misc_folders_btn = QtWidgets.QPushButton( ResourceCache.get_icon("asset", "default"), "Create Misc. Folders", @@ -218,7 +221,10 @@ class ProjectManagerWindow(QtWidgets.QWidget): return qm = QtWidgets.QMessageBox - ans = qm.question(self, '', "Confirm to create misc. project folders?", qm.Yes | qm.No) + ans = qm.question(self, + "", + "Confirm to create misc. project folders?", + qm.Yes | qm.No) if ans == qm.Yes: try: # Get paths based on presets @@ -228,7 +234,8 @@ class ProjectManagerWindow(QtWidgets.QWidget): # Invoking OpenPype API to create the project folders create_project_folders(basic_paths, self._current_project) except Exception as exc: - log.warning("Error creating.", exc_info=True) + log.warning("Failed to create misc folders: {}".format(exc), + exc_info=True) def show_message(self, message): # TODO add nicer message pop @@ -241,7 +248,9 @@ class ProjectManagerWindow(QtWidgets.QWidget): return self._current_project = dialog.project_name - self.show_message("Created project \"{}\"".format(self._current_project)) + self.show_message( + "Created project \"{}\"".format(self._current_project) + ) self.refresh_projects(self._current_project) def _show_password_dialog(self): From d0e0bcb67cbb9eb3ab2032651a0f1a8723ef809b Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 16 Sep 2021 16:07:24 +0800 Subject: [PATCH 162/450] Update openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json Co-authored-by: Milan Kolar --- .../projects_schema/schemas/schema_anatomy_attributes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index 530bf70a7c..a2a566da0e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -73,7 +73,7 @@ { "type": "boolean", "key": "active", - "label": "Is Project Active" + "label": "Active Project" } ] } From 0631a7d9b921225b00a3fc36dd8b67c4d5e3e788 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 11:12:14 +0200 Subject: [PATCH 163/450] added task types to setting schemas --- .../defaults/project_settings/ftrack.json | 8 ++++++++ .../defaults/project_settings/global.json | 10 ++++++++++ .../defaults/project_settings/maya.json | 1 + .../defaults/project_settings/nuke.json | 1 + .../defaults/project_settings/slack.json | 3 ++- .../projects_schema/schema_project_ftrack.json | 5 +++++ .../projects_schema/schema_project_slack.json | 17 +++++++++++------ .../schemas/schema_global_publish.json | 10 ++++++++++ .../schemas/schema_global_tools.json | 16 ++++++++++++++++ .../schemas/schema_workfile_build.json | 7 ++++++- .../schemas/template_workfile_options.json | 5 +++++ 11 files changed, 75 insertions(+), 8 deletions(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 692176a585..b3ea77a584 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -209,6 +209,7 @@ "standalonepublisher" ], "families": [], + "task_types": [], "tasks": [], "add_ftrack_family": true, "advanced_filtering": [] @@ -221,6 +222,7 @@ "matchmove", "shot" ], + "task_types": [], "tasks": [], "add_ftrack_family": false, "advanced_filtering": [] @@ -232,6 +234,7 @@ "families": [ "plate" ], + "task_types": [], "tasks": [], "add_ftrack_family": false, "advanced_filtering": [ @@ -256,6 +259,7 @@ "rig", "camera" ], + "task_types": [], "tasks": [], "add_ftrack_family": true, "advanced_filtering": [] @@ -267,6 +271,7 @@ "families": [ "renderPass" ], + "task_types": [], "tasks": [], "add_ftrack_family": false, "advanced_filtering": [] @@ -276,6 +281,7 @@ "tvpaint" ], "families": [], + "task_types": [], "tasks": [], "add_ftrack_family": true, "advanced_filtering": [] @@ -288,6 +294,7 @@ "write", "render" ], + "task_types": [], "tasks": [], "add_ftrack_family": false, "advanced_filtering": [ @@ -307,6 +314,7 @@ "render", "workfile" ], + "task_types": [], "tasks": [], "add_ftrack_family": true, "advanced_filtering": [] diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index a53ae14914..05c3e871c0 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -152,6 +152,7 @@ { "families": [], "hosts": [], + "task_types": [], "tasks": [], "template_name": "publish" }, @@ -162,6 +163,7 @@ "prerender" ], "hosts": [], + "task_types": [], "tasks": [], "template_name": "render" } @@ -170,6 +172,7 @@ { "families": [], "hosts": [], + "task_types": [], "tasks": [], "template": "" } @@ -205,6 +208,7 @@ { "families": [], "hosts": [], + "task_types": [], "tasks": [], "template": "{family}{Variant}" }, @@ -213,6 +217,7 @@ "render" ], "hosts": [], + "task_types": [], "tasks": [], "template": "{family}{Task}{Variant}" }, @@ -224,6 +229,7 @@ "hosts": [ "tvpaint" ], + "task_types": [], "tasks": [], "template": "{family}{Task}_{Render_layer}_{Render_pass}" }, @@ -235,6 +241,7 @@ "hosts": [ "tvpaint" ], + "task_types": [], "tasks": [], "template": "{family}{Task}" }, @@ -245,6 +252,7 @@ "hosts": [ "aftereffects" ], + "task_types": [], "tasks": [], "template": "render{Task}{Variant}" } @@ -261,6 +269,7 @@ "last_workfile_on_startup": [ { "hosts": [], + "task_types": [], "tasks": [], "enabled": true } @@ -268,6 +277,7 @@ "open_workfile_tool_on_startup": [ { "hosts": [], + "task_types": [], "tasks": [], "enabled": false } diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index f9911897d7..3540c3eb29 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -520,6 +520,7 @@ "workfile_build": { "profiles": [ { + "task_types": [], "tasks": [ "Lighting" ], diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 136f1d6b42..86b94823c1 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -163,6 +163,7 @@ "builder_on_start": false, "profiles": [ { + "task_types": [], "tasks": [], "current_context": [ { diff --git a/openpype/settings/defaults/project_settings/slack.json b/openpype/settings/defaults/project_settings/slack.json index e70ef77fd2..2d10bd173d 100644 --- a/openpype/settings/defaults/project_settings/slack.json +++ b/openpype/settings/defaults/project_settings/slack.json @@ -7,8 +7,9 @@ "profiles": [ { "families": [], - "tasks": [], "hosts": [], + "task_types": [], + "tasks": [], "channel_messages": [] } ] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index 1cc08b96f8..e50e269695 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -650,6 +650,11 @@ "type": "list", "object_type": "text" }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, { "key": "tasks", "label": "Task names", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json index 170de7c8a2..9ca4e443bd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json @@ -52,18 +52,23 @@ "type": "list", "object_type": "text" }, - { - "key": "tasks", - "label": "Task names", - "type": "list", - "object_type": "text" - }, { "type": "hosts-enum", "key": "hosts", "label": "Host names", "multiselection": true }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, { "type": "separator" }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 4b91072eb6..e59d22aa89 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -502,6 +502,11 @@ "label": "Hosts", "multiselection": true }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, { "key": "tasks", "label": "Task names", @@ -543,6 +548,11 @@ "label": "Hosts", "multiselection": true }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, { "key": "tasks", "label": "Task names", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 245560f115..33a8883b73 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -40,6 +40,11 @@ "label": "Hosts", "multiselection": true }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, { "key": "tasks", "label": "Task names", @@ -126,6 +131,11 @@ "unreal" ] }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, { "key": "tasks", "label": "Tasks", @@ -161,6 +171,12 @@ "nuke" ] }, + { + "key": "task_types", + "label": "Task types", + "type": "list", + "object_type": "task-types-enum" + }, { "key": "tasks", "label": "Tasks", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json index 078bb81bba..09da5b70e3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json @@ -11,6 +11,11 @@ "object_type": { "type": "dict", "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, { "key": "tasks", "label": "Tasks", @@ -94,4 +99,4 @@ } } ] -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json index 815df85879..4d5a8d56ab 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json @@ -55,6 +55,11 @@ "object_type": { "type": "dict", "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, { "key": "tasks", "label": "Tasks", From 95424cdfc7835a9cb2b86554d14034ae60a5da81 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 11:49:37 +0200 Subject: [PATCH 164/450] moved compile_list_of_regexes to profiles_filtering and moved it's import in openpype.lib earlier --- openpype/lib/__init__.py | 11 ++++++----- openpype/lib/applications.py | 20 +------------------- openpype/lib/profiles_filtering.py | 20 +++++++++++++++++++- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index e96f1cc99f..0ead28289b 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -59,6 +59,11 @@ from .python_module_tools import ( import_module_from_dirpath ) +from .profiles_filtering import ( + compile_list_of_regexes, + filter_profiles +) + from .avalon_context import ( CURRENT_DOC_SCHEMAS, PROJECT_NAME_ALLOWED_SYMBOLS, @@ -118,13 +123,9 @@ from .applications import ( prepare_host_environments, prepare_context_environments, get_app_environments_for_context, - apply_project_environments_value, - - compile_list_of_regexes + apply_project_environments_value ) -from .profiles_filtering import filter_profiles - from .plugin_tools import ( TaskNotSetError, get_subset_name, diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 45b8e6468d..0206f80fed 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -25,6 +25,7 @@ from . import ( PypeLogger, Anatomy ) +from .profiles_filtering import compile_list_of_regexes from .local_settings import get_openpype_username from .avalon_context import ( get_workdir_data, @@ -1495,22 +1496,3 @@ def should_workfile_tool_start( return get_option_from_settings( startup_presets, host_name, task_name, default_output) - - -def compile_list_of_regexes(in_list): - """Convert strings in entered list to compiled regex objects.""" - regexes = list() - if not in_list: - return regexes - - for item in in_list: - if not item: - continue - try: - regexes.append(re.compile(item)) - except TypeError: - print(( - "Invalid type \"{}\" value \"{}\"." - " Expected string based object. Skipping." - ).format(str(type(item)), str(item))) - return regexes diff --git a/openpype/lib/profiles_filtering.py b/openpype/lib/profiles_filtering.py index 992d757059..0bb901aff8 100644 --- a/openpype/lib/profiles_filtering.py +++ b/openpype/lib/profiles_filtering.py @@ -1,10 +1,28 @@ import re import logging -from .applications import compile_list_of_regexes log = logging.getLogger(__name__) +def compile_list_of_regexes(in_list): + """Convert strings in entered list to compiled regex objects.""" + regexes = list() + if not in_list: + return regexes + + for item in in_list: + if not item: + continue + try: + regexes.append(re.compile(item)) + except TypeError: + print(( + "Invalid type \"{}\" value \"{}\"." + " Expected string based object. Skipping." + ).format(str(type(item)), str(item))) + return regexes + + def _profile_exclusion(matching_profiles, logger): """Find out most matching profile byt host, task and family match. From 456cca8d74f287e0bdaf7f0132969dba7d3b1884 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 12:16:08 +0200 Subject: [PATCH 165/450] store task_type to prepare context data --- openpype/lib/applications.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 0206f80fed..44b9744719 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1245,6 +1245,9 @@ def prepare_context_environments(data): asset_tasks = asset_doc.get("data", {}).get("tasks") or {} task_info = asset_tasks.get(task_name) or {} task_type = task_info.get("type") + # Temp solution how to pass task type to `_prepare_last_workfile` + data["task_type"] = task_type + workfile_template_key = get_workfile_template_key( task_type, app.host_name, @@ -1321,6 +1324,7 @@ def _prepare_last_workfile(data, workdir, workfile_template_key): workdir_data = copy.deepcopy(_workdir_data) project_name = data["project_name"] task_name = data["task_name"] + task_type = data["task_type"] start_last_workfile = should_start_last_workfile( project_name, app.host_name, task_name ) From 32688694c8f9fa6878b689023d45f374e316f0fa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 12:17:08 +0200 Subject: [PATCH 166/450] use task type for filtering in workfile builder --- openpype/lib/avalon_context.py | 48 ++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 497348af33..b043cbfdb4 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -10,6 +10,7 @@ import functools from openpype.settings import get_project_settings from .anatomy import Anatomy +from .profiles_filtering import filter_profiles # avalon module is not imported at the top # - may not be in path at the time of pype.lib initialization @@ -453,8 +454,6 @@ def get_workfile_template_key( if not profiles: return default - from .profiles_filtering import filter_profiles - profile_filter = { "task_types": task_type, "hosts": host_name @@ -791,7 +790,9 @@ class BuildWorkfile: current_task_name = avalon.io.Session["AVALON_TASK"] # Load workfile presets for task - self.build_presets = self.get_build_presets(current_task_name) + self.build_presets = self.get_build_presets( + current_task_name, current_asset_entity + ) # Skip if there are any presets for task if not self.build_presets: @@ -875,7 +876,7 @@ class BuildWorkfile: return loaded_containers @with_avalon - def get_build_presets(self, task_name): + def get_build_presets(self, task_name, asset_doc): """ Returns presets to build workfile for task name. Presets are loaded for current project set in @@ -889,30 +890,33 @@ class BuildWorkfile: (dict): preset per entered task name """ host_name = avalon.api.registered_host().__name__.rsplit(".", 1)[-1] - presets = get_project_settings(avalon.io.Session["AVALON_PROJECT"]) + project_settings = get_project_settings( + avalon.io.Session["AVALON_PROJECT"] + ) + host_settings = project_settings.get(host_name) or {} # Get presets for host - wb_settings = presets.get(host_name, {}).get("workfile_builder") - + wb_settings = host_settings.get("workfile_builder") if not wb_settings: # backward compatibility - wb_settings = presets.get(host_name, {}).get("workfile_build") + wb_settings = host_settings.get("workfile_build") or {} - builder_presets = wb_settings.get("profiles") + builder_profiles = wb_settings.get("profiles") + if not builder_profiles: + return None - if not builder_presets: - return - - task_name_low = task_name.lower() - per_task_preset = None - for preset in builder_presets: - preset_tasks = preset.get("tasks") or [] - preset_tasks_low = [task.lower() for task in preset_tasks] - if task_name_low in preset_tasks_low: - per_task_preset = preset - break - - return per_task_preset + task_type = ( + asset_doc + .get("data", {}) + .get("tasks", {}) + .get(task_name, {}) + .get("type") + ) + filter_data = { + "task_types": task_type, + "tasks": task_name + } + return filter_profiles(builder_profiles, filter_data) def _filter_build_profiles(self, build_profiles, loaders_by_name): """ Filter build profiles by loaders and prepare process data. From 6d59b6e3eb53675e58b81bbe7559feca9eebc1ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 12:17:27 +0200 Subject: [PATCH 167/450] use task type filreting in should start last workfile and open workfile tool --- openpype/lib/applications.py | 96 ++++++++++++++---------------------- 1 file changed, 37 insertions(+), 59 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 44b9744719..245f2ee9a2 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -25,7 +25,7 @@ from . import ( PypeLogger, Anatomy ) -from .profiles_filtering import compile_list_of_regexes +from .profiles_filtering import filter_profiles from .local_settings import get_openpype_username from .avalon_context import ( get_workdir_data, @@ -1326,12 +1326,12 @@ def _prepare_last_workfile(data, workdir, workfile_template_key): task_name = data["task_name"] task_type = data["task_type"] start_last_workfile = should_start_last_workfile( - project_name, app.host_name, task_name + project_name, app.host_name, task_name, task_type ) data["start_last_workfile"] = start_last_workfile workfile_startup = should_workfile_tool_start( - project_name, app.host_name, task_name + project_name, app.host_name, task_name, task_type ) data["workfile_startup"] = workfile_startup @@ -1380,54 +1380,8 @@ def _prepare_last_workfile(data, workdir, workfile_template_key): data["last_workfile_path"] = last_workfile_path -def get_option_from_settings( - startup_presets, host_name, task_name, default_output -): - host_name_lowered = host_name.lower() - task_name_lowered = task_name.lower() - - max_points = 2 - matching_points = -1 - matching_item = None - for item in startup_presets: - hosts = item.get("hosts") or tuple() - tasks = item.get("tasks") or tuple() - - hosts_lowered = tuple(_host_name.lower() for _host_name in hosts) - # Skip item if has set hosts and current host is not in - if hosts_lowered and host_name_lowered not in hosts_lowered: - continue - - tasks_lowered = tuple(_task_name.lower() for _task_name in tasks) - # Skip item if has set tasks and current task is not in - if tasks_lowered: - task_match = False - for task_regex in compile_list_of_regexes(tasks_lowered): - if re.match(task_regex, task_name_lowered): - task_match = True - break - - if not task_match: - continue - - points = int(bool(hosts_lowered)) + int(bool(tasks_lowered)) - if points > matching_points: - matching_item = item - matching_points = points - - if matching_points == max_points: - break - - if matching_item is not None: - output = matching_item.get("enabled") - if output is None: - output = default_output - return output - return default_output - - def should_start_last_workfile( - project_name, host_name, task_name, default_output=False + project_name, host_name, task_name, task_type, default_output=False ): """Define if host should start last version workfile if possible. @@ -1449,7 +1403,7 @@ def should_start_last_workfile( """ project_settings = get_project_settings(project_name) - startup_presets = ( + profiles = ( project_settings ["global"] ["tools"] @@ -1457,15 +1411,27 @@ def should_start_last_workfile( ["last_workfile_on_startup"] ) - if not startup_presets: + if not profiles: return default_output - return get_option_from_settings( - startup_presets, host_name, task_name, default_output) + filter_data = { + "tasks": task_name, + "task_types": task_type, + "hosts": host_name + } + matching_item = filter_profiles(profiles, filter_data) + + output = None + if matching_item: + output = matching_item.get("enabled") + + if output is None: + return default_output + return output def should_workfile_tool_start( - project_name, host_name, task_name, default_output=False + project_name, host_name, task_name, task_type, default_output=False ): """Define if host should start workfile tool at host launch. @@ -1487,7 +1453,7 @@ def should_workfile_tool_start( """ project_settings = get_project_settings(project_name) - startup_presets = ( + profiles = ( project_settings ["global"] ["tools"] @@ -1495,8 +1461,20 @@ def should_workfile_tool_start( ["open_workfile_tool_on_startup"] ) - if not startup_presets: + if not profiles: return default_output - return get_option_from_settings( - startup_presets, host_name, task_name, default_output) + filter_data = { + "tasks": task_name, + "task_types": task_type, + "hosts": host_name + } + matching_item = filter_profiles(profiles, filter_data) + + output = None + if matching_item: + output = matching_item.get("enabled") + + if output is None: + return default_output + return output From 983a2fff2541afa9375e86d7e792e2742c79e1ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:16:24 +0200 Subject: [PATCH 168/450] added task type usage in get_subset_name --- openpype/lib/plugin_tools.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 1f2fb7a46e..9dccadc44e 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -35,7 +35,8 @@ def get_subset_name( project_name=None, host_name=None, default_template=None, - dynamic_data=None + dynamic_data=None, + dbcon=None ): if not family: return "" @@ -46,13 +47,42 @@ def get_subset_name( # Use only last part of class family value split by dot (`.`) family = family.rsplit(".", 1)[-1] + if project_name is None: + import avalon.api + + project_name = avalon.api.Session["AVALON_PROJECT"] + + # Function should expect asset document instead of asset id + # - that way `dbcon` is not needed + if dbcon is None: + from avalon.api import AvalonMongoDB + + dbcon = AvalonMongoDB() + dbcon.Session["AVALON_PROJECT"] = project_name + + dbcon.install() + + asset_doc = dbcon.find_one( + { + "type": "asset", + "_id": asset_id + }, + { + "data.tasks": True + } + ) + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + # Get settings tools_settings = get_project_settings(project_name)["global"]["tools"] profiles = tools_settings["creator"]["subset_name_profiles"] filtering_criteria = { "families": family, "hosts": host_name, - "tasks": task_name + "tasks": task_name, + "task_types": task_type } matching_profile = filter_profiles(profiles, filtering_criteria) From f5f736307b6070a0a9d1acaeb623679f50141ba0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:18:08 +0200 Subject: [PATCH 169/450] changed "Tasks" label to "Task names" --- .../schemas/projects_schema/schemas/schema_global_tools.json | 4 ++-- .../projects_schema/schemas/schema_workfile_build.json | 2 +- .../projects_schema/schemas/template_workfile_options.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 33a8883b73..e6f9bc41a5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -138,7 +138,7 @@ }, { "key": "tasks", - "label": "Tasks", + "label": "Task names", "type": "list", "object_type": "text" }, @@ -179,7 +179,7 @@ }, { "key": "tasks", - "label": "Tasks", + "label": "Task names", "type": "list", "object_type": "text" }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json index 09da5b70e3..2a3f0ae136 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_workfile_build.json @@ -18,7 +18,7 @@ }, { "key": "tasks", - "label": "Tasks", + "label": "Task names", "type": "list", "object_type": "text" }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json index 4d5a8d56ab..90fc4fbdd0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_workfile_options.json @@ -62,7 +62,7 @@ }, { "key": "tasks", - "label": "Tasks", + "label": "Task names", "type": "list", "object_type": "text" }, From 70872c240fe4730b6e35877e73b02b9868a4a336 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:25:14 +0200 Subject: [PATCH 170/450] use list or arguments in extract trimming video where all arguments are known --- .../plugins/publish/extract_otio_trimming_video.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index fdb7c4b096..3e2d39c99c 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -75,7 +75,7 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): output_path = self._get_ffmpeg_output(input_file_path) # start command list - command = ['"{}"'.format(ffmpeg_path)] + command = [ffmpeg_path] video_path = input_file_path frame_start = otio_range.start_time.value @@ -86,17 +86,17 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): # form command for rendering gap files command.extend([ - "-ss {}".format(sec_start), - "-t {}".format(sec_duration), - "-i \"{}\"".format(video_path), - "-c copy", + "-ss", str(sec_start), + "-t", str(sec_duration), + "-i", video_path, + "-c", "copy", output_path ]) # execute self.log.debug("Executing: {}".format(" ".join(command))) output = openpype.api.run_subprocess( - " ".join(command), logger=self.log + command, logger=self.log ) self.log.debug("Output: {}".format(output)) From 1f5f7ac25d67fb54084bd7e5d0b8ee34c50a0531 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:32:02 +0200 Subject: [PATCH 171/450] added function 'split_command_to_list' --- openpype/lib/__init__.py | 3 +++ openpype/lib/execute.py | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index e96f1cc99f..5725e1c0e3 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -27,6 +27,7 @@ from .execute import ( get_pype_execute_args, execute, run_subprocess, + split_command_to_list, CREATE_NO_WINDOW ) from .log import PypeLogger, timeit @@ -171,6 +172,8 @@ __all__ = [ "get_pype_execute_args", "execute", "run_subprocess", + "split_command_to_list", + "CREATE_NO_WINDOW", "env_value_to_bool", "get_paths_from_environ", diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 12fba23e82..d41db19a78 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -1,11 +1,10 @@ -import logging import os +import shlex import subprocess +import platform from .log import PypeLogger as Logger -log = logging.getLogger(__name__) - # MSDN process creation flag (Windows only) CREATE_NO_WINDOW = 0x08000000 @@ -100,7 +99,9 @@ def run_subprocess(*args, **kwargs): filtered_env = {str(k): str(v) for k, v in env.items()} # Use lib's logger if was not passed with kwargs. - logger = kwargs.pop("logger", log) + logger = kwargs.pop("logger", None) + if logger is None: + logger = Logger.get_logger("run_subprocess") # set overrides kwargs['stdout'] = kwargs.get('stdout', subprocess.PIPE) @@ -138,6 +139,14 @@ def run_subprocess(*args, **kwargs): return full_output +def split_command_to_list(string_command): + """Split string subprocess command to list.""" + posix = True + if platform.system().lower() == "windows": + posix = False + return shlex.split(string_command, posix=posix) + + def get_pype_execute_args(*args): """Arguments to run pype command. From 27d3a954ab0968273fc9da6689a7b097929438c7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:34:40 +0200 Subject: [PATCH 172/450] use split_command_to_list in extract jpeg --- openpype/plugins/publish/extract_jpeg_exr.py | 25 +++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index ae691285b5..464c190762 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -1,10 +1,16 @@ import os import pyblish.api -import openpype.api -import openpype.lib -from openpype.lib import should_decompress, \ - get_decompress_dir, decompress +from openpype.lib import ( + get_ffmpeg_tool_path, + + run_subprocess, + split_command_to_list, + + should_decompress, + get_decompress_dir, + decompress +) import shutil @@ -85,7 +91,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.info("output {}".format(full_output_path)) - ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") + ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") ffmpeg_args = self.ffmpeg_args or {} jpeg_items = [] @@ -106,13 +112,14 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): # output file jpeg_items.append("\"{}\"".format(full_output_path)) - subprocess_jpeg = " ".join(jpeg_items) + subprocess_command = " ".join(jpeg_items) + subprocess_args = split_command_to_list(subprocess_command) # run subprocess - self.log.debug("{}".format(subprocess_jpeg)) + self.log.debug("{}".format(subprocess_command)) try: # temporary until oiiotool is supported cross platform - openpype.api.run_subprocess( - subprocess_jpeg, shell=True, logger=self.log + run_subprocess( + subprocess_args, shell=True, logger=self.log ) except RuntimeError as exp: if "Compression" in str(exp): From 1a588ede020d4ff60f08b270b7926849c4c12536 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:40:25 +0200 Subject: [PATCH 173/450] use list where all arguments are known and can be sent as list --- .../publish/extract_otio_audio_tracks.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 2dc822fb0e..e340a17609 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -99,16 +99,16 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # temp audio file audio_fpath = self.create_temp_file(name) - cmd = " ".join([ - '"{}"'.format(self.ffmpeg_path), - "-ss {}".format(start_sec), - "-t {}".format(duration_sec), - "-i \"{}\"".format(audio_file), + cmd = [ + self.ffmpeg_path, + "-ss", str(start_sec), + "-t", str(duration_sec), + "-i", audio_file, audio_fpath - ]) + ] # run subprocess - self.log.debug("Executing: {}".format(cmd)) + self.log.debug("Executing: {}".format(" ".join(cmd))) openpype.api.run_subprocess( cmd, logger=self.log ) @@ -220,17 +220,17 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): max_duration_sec = max(end_secs) # create empty cmd - cmd = " ".join([ - '"{}"'.format(self.ffmpeg_path), - "-f lavfi", - "-i anullsrc=channel_layout=stereo:sample_rate=48000", - "-t {}".format(max_duration_sec), - "\"{}\"".format(empty_fpath) - ]) + cmd = [ + self.ffmpeg_path, + "-f", "lavfi", + "-i", "anullsrc=channel_layout=stereo:sample_rate=48000", + "-t", str(max_duration_sec), + empty_fpath + ] # generate empty with ffmpeg # run subprocess - self.log.debug("Executing: {}".format(cmd)) + self.log.debug("Executing: {}".format(" ".join(cmd))) openpype.api.run_subprocess( cmd, logger=self.log From 01bca81ea13c85de0ee1f0e75be45537b0c5a68b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:41:29 +0200 Subject: [PATCH 174/450] use split_command_to_list for audio inputs --- openpype/plugins/publish/extract_otio_audio_tracks.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index e340a17609..3fc5a6740d 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -2,7 +2,8 @@ import os import pyblish import openpype.api from openpype.lib import ( - get_ffmpeg_tool_path + get_ffmpeg_tool_path, + split_command_to_list ) import tempfile import opentimelineio as otio @@ -60,10 +61,13 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): cmd += self.create_cmd(audio_inputs) cmd += "\"{}\"".format(audio_temp_fpath) + # Split command to list for subprocess + cmd_list = split_command_to_list(cmd) + # run subprocess self.log.debug("Executing: {}".format(cmd)) openpype.api.run_subprocess( - cmd, logger=self.log + cmd_list, logger=self.log ) # remove empty From 6c0e71a7d7cc45c9afecc7111b8d9c0d7e3c4e94 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:45:13 +0200 Subject: [PATCH 175/450] use list of arguments where are all known --- .../plugins/publish/extract_otio_review.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/openpype/plugins/publish/extract_otio_review.py b/openpype/plugins/publish/extract_otio_review.py index 818903b54b..ed2ba017d5 100644 --- a/openpype/plugins/publish/extract_otio_review.py +++ b/openpype/plugins/publish/extract_otio_review.py @@ -312,7 +312,7 @@ class ExtractOTIOReview(openpype.api.Extractor): out_frame_start += end_offset # start command list - command = ['"{}"'.format(ffmpeg_path)] + command = [ffmpeg_path] if sequence: input_dir, collection = sequence @@ -324,8 +324,8 @@ class ExtractOTIOReview(openpype.api.Extractor): # form command for rendering gap files command.extend([ - "-start_number {}".format(in_frame_start), - "-i \"{}\"".format(input_path) + "-start_number", str(in_frame_start), + "-i", input_path ]) elif video: @@ -334,13 +334,15 @@ class ExtractOTIOReview(openpype.api.Extractor): input_fps = otio_range.start_time.rate frame_duration = otio_range.duration.value sec_start = openpype.lib.frames_to_secons(frame_start, input_fps) - sec_duration = openpype.lib.frames_to_secons(frame_duration, input_fps) + sec_duration = openpype.lib.frames_to_secons( + frame_duration, input_fps + ) # form command for rendering gap files command.extend([ - "-ss {}".format(sec_start), - "-t {}".format(sec_duration), - "-i \"{}\"".format(video_path) + "-ss", str(sec_start), + "-t", str(sec_duration), + "-i", video_path ]) elif gap: @@ -349,22 +351,24 @@ class ExtractOTIOReview(openpype.api.Extractor): # form command for rendering gap files command.extend([ - "-t {} -r {}".format(sec_duration, self.actual_fps), - "-f lavfi", - "-i color=c=black:s={}x{}".format(self.to_width, - self.to_height), - "-tune stillimage" + "-t", str(sec_duration), + "-r", str(self.actual_fps), + "-f", "lavfi", + "-i", "color=c=black:s={}x{}".format( + self.to_width, self.to_height + ), + "-tune", "stillimage" ]) # add output attributes command.extend([ - "-start_number {}".format(out_frame_start), - "\"{}\"".format(output_path) + "-start_number", str(out_frame_start), + output_path ]) # execute self.log.debug("Executing: {}".format(" ".join(command))) output = openpype.api.run_subprocess( - " ".join(command), logger=self.log + command, logger=self.log ) self.log.debug("Output: {}".format(output)) From bb69545050b949a15b746521f7afe6b10917b12d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:49:24 +0200 Subject: [PATCH 176/450] split arguments using split_command_to_list in extract review slate --- openpype/plugins/publish/extract_review_slate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 2b07d7db74..ca917f3c3b 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -197,11 +197,13 @@ class ExtractReviewSlate(openpype.api.Extractor): " ".join(output_args) ] slate_subprcs_cmd = " ".join(slate_args) - + slate_subprocess_args = openpype.lib.split_command_to_list( + slate_subprcs_cmd + ) # run slate generation subprocess self.log.debug("Slate Executing: {}".format(slate_subprcs_cmd)) openpype.api.run_subprocess( - slate_subprcs_cmd, shell=True, logger=self.log + slate_subprocess_args, shell=True, logger=self.log ) # create ffmpeg concat text file path From 85728d5f96260ab944521c761c709bc417fa7e28 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:49:41 +0200 Subject: [PATCH 177/450] use list of arguments directly where are known --- .../plugins/publish/extract_review_slate.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index ca917f3c3b..38c9b15844 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -223,23 +223,22 @@ class ExtractReviewSlate(openpype.api.Extractor): ]) # concat slate and videos together - conc_input_args = ["-y", "-f concat", "-safe 0"] - conc_input_args.append("-i {}".format(conc_text_path)) - - conc_output_args = ["-c copy"] - conc_output_args.append(output_path) - concat_args = [ ffmpeg_path, - " ".join(conc_input_args), - " ".join(conc_output_args) + "-y", + "-f", "concat", + "-safe", "0", + "-i", conc_text_path, + "-c", "copy", + output_path ] - concat_subprcs_cmd = " ".join(concat_args) # ffmpeg concat subprocess - self.log.debug("Executing concat: {}".format(concat_subprcs_cmd)) + self.log.debug( + "Executing concat: {}".format(" ".join(concat_args)) + ) openpype.api.run_subprocess( - concat_subprcs_cmd, shell=True, logger=self.log + concat_args, shell=True, logger=self.log ) self.log.debug("__ repre[tags]: {}".format(repre["tags"])) From bd3d770acc2f206312bbe0927c31c898f3c354b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 14:51:51 +0200 Subject: [PATCH 178/450] extract review use split_command_to_list --- openpype/plugins/publish/extract_review.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 78cbea10be..6d254d7366 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -13,6 +13,9 @@ import openpype.api from openpype.lib import ( get_ffmpeg_tool_path, ffprobe_streams, + + split_command_to_list, + should_decompress, get_decompress_dir, decompress @@ -216,12 +219,15 @@ class ExtractReview(pyblish.api.InstancePlugin): raise NotImplementedError subprcs_cmd = " ".join(ffmpeg_args) + subprocess_args = split_command_to_list(subprcs_cmd) # run subprocess - self.log.debug("Executing: {}".format(subprcs_cmd)) + self.log.debug( + "Executing: {}".format(" ".join(subprocess_args)) + ) openpype.api.run_subprocess( - subprcs_cmd, shell=True, logger=self.log + subprocess_args, shell=True, logger=self.log ) # delete files added to fill gaps From 08f43824093f6ef7b15e6f5debcd264a5378c452 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 15:01:21 +0200 Subject: [PATCH 179/450] use args as list where possible --- .../plugins/publish/extract_review.py | 6 +++-- .../publish/extract_trim_video_audio.py | 25 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index b52078fd5f..1c53c3a2ef 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -60,7 +60,8 @@ class ExtractReview(openpype.api.Extractor): # Generate thumbnail. thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") args = [ - "{}".format(ffmpeg_path), "-y", + ffmpeg_path, + "-y", "-i", output_image_path, "-vf", "scale=300:-1", "-vframes", "1", @@ -78,7 +79,8 @@ class ExtractReview(openpype.api.Extractor): # Generate mov. mov_path = os.path.join(staging_dir, "review.mov") args = [ - ffmpeg_path, "-y", + ffmpeg_path, + "-y", "-i", output_image_path, "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-vframes", "1", diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py index 059ac9603c..4d482825bc 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py @@ -59,27 +59,30 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): if "trimming" not in fml ] - args = [ - f"\"{ffmpeg_path}\"", + ffmpeg_args = [ + ffmpeg_path, "-ss", str(start / fps), - "-i", f"\"{video_file_path}\"", + "-i", video_file_path, "-t", str(dur / fps) ] if ext in [".mov", ".mp4"]: - args.extend([ + ffmpeg_args.extend([ "-crf", "18", - "-pix_fmt", "yuv420p"]) + "-pix_fmt", "yuv420p" + ]) elif ext in ".wav": - args.extend([ - "-vn -acodec pcm_s16le", - "-ar 48000 -ac 2" + ffmpeg_args.extend([ + "-vn", + "-acodec", "pcm_s16le", + "-ar", "48000", + "-ac", "2" ]) # add output path - args.append(f"\"{clip_trimed_path}\"") + ffmpeg_args.append(clip_trimed_path) - self.log.info(f"Processing: {args}") - ffmpeg_args = " ".join(args) + joined_args = " ".join(ffmpeg_args) + self.log.info(f"Processing: {joined_args}") openpype.api.run_subprocess( ffmpeg_args, shell=True, logger=self.log ) From 58cff296c03deb7fb125d47fe749da63ce8a5de0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 15:01:45 +0200 Subject: [PATCH 180/450] changed variable name from 'repr' to 'repre' ('repr' builtin function) --- .../plugins/publish/extract_trim_video_audio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py index 4d482825bc..1cbf186a6c 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py @@ -87,7 +87,7 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): ffmpeg_args, shell=True, logger=self.log ) - repr = { + repre = { "name": ext[1:], "ext": ext[1:], "files": os.path.basename(clip_trimed_path), @@ -100,10 +100,10 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): } if ext in [".mov", ".mp4"]: - repr.update({ + repre.update({ "thumbnail": True, "tags": ["review", "ftrackreview", "delete"]}) - instance.data["representations"].append(repr) + instance.data["representations"].append(repre) self.log.debug(f"Instance data: {pformat(instance.data)}") From 443f79d1d2ae29215c674846ee9f78c6a22585f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 15:02:08 +0200 Subject: [PATCH 181/450] log arguments that are going to be executed --- .../plugins/publish/extract_thumbnail.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 0792254716..cdbfe942f0 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -101,11 +101,14 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): jpeg_items.append("\"{}\"".format(full_thumbnail_path)) subprocess_jpeg = " ".join(jpeg_items) + subprocess_args = openpype.lib.split_command_to_list( + subprocess_jpeg + ) # run subprocess - self.log.debug("Executing: {}".format(subprocess_jpeg)) + self.log.debug("Executing: {}".format(" ".join(subprocess_args))) openpype.api.run_subprocess( - subprocess_jpeg, shell=True, logger=self.log + subprocess_args, shell=True, logger=self.log ) # remove thumbnail key from origin repre From 9720dac97b7d82e9b7ce5bae897040a9bec6d298 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 15:04:08 +0200 Subject: [PATCH 182/450] removed unnecessary formatting --- openpype/hosts/harmony/plugins/publish/extract_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/harmony/plugins/publish/extract_render.py b/openpype/hosts/harmony/plugins/publish/extract_render.py index 8374a9427a..827b03443c 100644 --- a/openpype/hosts/harmony/plugins/publish/extract_render.py +++ b/openpype/hosts/harmony/plugins/publish/extract_render.py @@ -91,7 +91,8 @@ class ExtractRender(pyblish.api.InstancePlugin): thumbnail_path = os.path.join(path, "thumbnail.png") ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") args = [ - "{}".format(ffmpeg_path), "-y", + ffmpeg_path, + "-y", "-i", os.path.join(path, list(collections[0])[0]), "-vf", "scale=300:-1", "-vframes", "1", From 7f47367fbfc2c3b2656566d9e1c5902c56dc1305 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 15:12:35 +0200 Subject: [PATCH 183/450] added docstring for split_command_to_list and modified --- openpype/lib/execute.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index d41db19a78..e93fc3efdd 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -140,8 +140,25 @@ def run_subprocess(*args, **kwargs): def split_command_to_list(string_command): - """Split string subprocess command to list.""" - posix = True + """Split string subprocess command to list. + + Should be able to split complex subprocess command to separated arguments: + `"C:\\ffmpeg folder\\ffmpeg.exe" -i \"D:\\input.mp4\\" \"D:\\output.mp4\"` + + Should result into list: + `["C:\ffmpeg folder\ffmpeg.exe", "-i", "D:\input.mp4", "D:\output.mp4"]` + + This may be required on few versions of python where subprocess can handle + only list of arguments. + + To be able do that is using `shlex` python module. + + Args: + string_command(str): Full subprocess command. + + Returns: + list: Command separated into individual arguments. + """ if platform.system().lower() == "windows": posix = False return shlex.split(string_command, posix=posix) From 004b2c5a5e78632d686eb345cd42d35883621bdf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 15:13:36 +0200 Subject: [PATCH 184/450] use posix argument only on windows --- openpype/lib/execute.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index e93fc3efdd..2dbee4e674 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -159,9 +159,11 @@ def split_command_to_list(string_command): Returns: list: Command separated into individual arguments. """ + kwargs = {} + # Use 'posix' argument only on windows if platform.system().lower() == "windows": - posix = False - return shlex.split(string_command, posix=posix) + kwargs["posix"] = False + return shlex.split(string_command, **kwargs) def get_pype_execute_args(*args): From 87e4900a5934f4e154881c8bc8b543513d1acfa5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 16 Sep 2021 15:30:33 +0200 Subject: [PATCH 185/450] hiero: fixing tag creation --- openpype/hosts/hiero/api/tags.py | 80 +++++++++++++------------------- 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/openpype/hosts/hiero/api/tags.py b/openpype/hosts/hiero/api/tags.py index d2502f3c71..5015f3dfea 100644 --- a/openpype/hosts/hiero/api/tags.py +++ b/openpype/hosts/hiero/api/tags.py @@ -78,8 +78,7 @@ def update_tag(tag, data): # set icon if any available in input data if data.get("icon"): tag.setIcon(str(data["icon"])) - # set note description of tag - tag.setNote(data["note"]) + # get metadata of tag mtd = tag.metadata() # get metadata key from data @@ -97,6 +96,9 @@ def update_tag(tag, data): "tag.{}".format(str(k)), str(v) ) + + # set note description of tag + tag.setNote(str(data["note"])) return tag @@ -106,6 +108,26 @@ def add_tags_to_workfile(): """ from .lib import get_current_project + def add_tag_to_bin(root_bin, name, data): + # for Tags to be created in root level Bin + # at first check if any of input data tag is not already created + done_tag = next((t for t in root_bin.items() + if str(name) in t.name()), None) + + if not done_tag: + # create Tag + tag = create_tag(name, data) + tag.setName(str(name)) + + log.debug("__ creating tag: {}".format(tag)) + # adding Tag to Root Bin + root_bin.addItem(tag) + else: + # update only non hierarchy tags + update_tag(done_tag, data) + done_tag.setName(str(name)) + log.debug("__ updating tag: {}".format(done_tag)) + # get project and root bin object project = get_current_project() root_bin = project.tagsBin() @@ -125,10 +147,8 @@ def add_tags_to_workfile(): for task_type in tasks.keys(): nks_pres_tags["[Tasks]"][task_type.lower()] = { "editable": "1", - "note": "", - "icon": { - "path": "icons:TagGood.png" - }, + "note": task_type, + "icon": "icons:TagGood.png", "metadata": { "family": "task", "type": task_type @@ -157,10 +177,10 @@ def add_tags_to_workfile(): # check if key is not decorated with [] so it is defined as bin bin_find = None pattern = re.compile(r"\[(.*)\]") - bin_finds = pattern.findall(_k) + _bin_finds = pattern.findall(_k) # if there is available any then pop it to string - if bin_finds: - bin_find = bin_finds.pop() + if _bin_finds: + bin_find = _bin_finds.pop() # if bin was found then create or update if bin_find: @@ -168,7 +188,6 @@ def add_tags_to_workfile(): # first check if in root lever is not already created bins bins = [b for b in root_bin.items() if b.name() in str(bin_find)] - log.debug(">>> bins: {}".format(bins)) if bins: bin = bins.pop() @@ -178,49 +197,14 @@ def add_tags_to_workfile(): bin = hiero.core.Bin(str(bin_find)) # update or create tags in the bin - for k, v in _val.items(): - tags = [t for t in bin.items() - if str(k) in t.name() - if len(str(k)) == len(t.name())] - if not tags: - # create Tag obj - tag = create_tag(k, v) - - # adding Tag to Bin - bin.addItem(tag) - else: - update_tag(tags.pop(), v) + for __k, __v in _val.items(): + add_tag_to_bin(bin, __k, __v) # finally add the Bin object to the root level Bin if root_add: # adding Tag to Root Bin root_bin.addItem(bin) else: - # for Tags to be created in root level Bin - # at first check if any of input data tag is not already created - tags = None - tags = [t for t in root_bin.items() - if str(_k) in t.name()] - - if not tags: - # create Tag - tag = create_tag(_k, _val) - - # adding Tag to Root Bin - root_bin.addItem(tag) - else: - # update Tags if they already exists - for _t in tags: - # skip bin objects - if isinstance(_t, hiero.core.Bin): - continue - - # check if Hierarchy in name and skip it - # because hierarchy could be edited - if "hierarchy" in _t.name().lower(): - continue - - # update only non hierarchy tags - update_tag(_t, _val) + add_tag_to_bin(root_bin, _k, _val) log.info("Default Tags were set...") From bdea9df14b273027efa566c5bf2ccce5bb2e267d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 16 Sep 2021 15:35:50 +0200 Subject: [PATCH 186/450] hiero: tags: removing unused tags --- openpype/hosts/hiero/api/tags.py | 38 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/hiero/api/tags.py b/openpype/hosts/hiero/api/tags.py index 5015f3dfea..68f8d35106 100644 --- a/openpype/hosts/hiero/api/tags.py +++ b/openpype/hosts/hiero/api/tags.py @@ -10,16 +10,16 @@ log = Logger().get_logger(__name__) def tag_data(): return { - "Retiming": { - "editable": "1", - "note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", # noqa - "icon": "retiming.png", - "metadata": { - "family": "retiming", - "marginIn": 1, - "marginOut": 1 - } - }, + # "Retiming": { + # "editable": "1", + # "note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", # noqa + # "icon": "retiming.png", + # "metadata": { + # "family": "retiming", + # "marginIn": 1, + # "marginOut": 1 + # } + # }, "[Lenses]": { "Set lense here": { "editable": "1", @@ -31,15 +31,15 @@ def tag_data(): } } }, - "NukeScript": { - "editable": "1", - "note": "Collecting track items to Nuke scripts.", - "icon": "icons:TagNuke.png", - "metadata": { - "family": "nukescript", - "subset": "main" - } - }, + # "NukeScript": { + # "editable": "1", + # "note": "Collecting track items to Nuke scripts.", + # "icon": "icons:TagNuke.png", + # "metadata": { + # "family": "nukescript", + # "subset": "main" + # } + # }, "Comment": { "editable": "1", "note": "Comment on a shot.", From 3d9970aa4fef902fff102267b76e77a3f0dc58d3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 15:57:25 +0200 Subject: [PATCH 187/450] added function 'path_to_subprocess_arg' which decide if path should be wrapped in quotes --- openpype/lib/__init__.py | 2 ++ openpype/lib/execute.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 5725e1c0e3..1acd07c0ba 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -28,6 +28,7 @@ from .execute import ( execute, run_subprocess, split_command_to_list, + path_to_subprocess_arg, CREATE_NO_WINDOW ) from .log import PypeLogger, timeit @@ -173,6 +174,7 @@ __all__ = [ "execute", "run_subprocess", "split_command_to_list", + "path_to_subprocess_arg", "CREATE_NO_WINDOW", "env_value_to_bool", diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 2dbee4e674..3e5b6d3853 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -139,6 +139,14 @@ def run_subprocess(*args, **kwargs): return full_output +def path_to_subprocess_arg(path): + """Prepare path for subprocess arguments. + + Returned path can be wrapped with quotes or kept as is. + """ + return subprocess.list2cmdline([path]) + + def split_command_to_list(string_command): """Split string subprocess command to list. @@ -159,6 +167,9 @@ def split_command_to_list(string_command): Returns: list: Command separated into individual arguments. """ + if not string_command: + return [] + kwargs = {} # Use 'posix' argument only on windows if platform.system().lower() == "windows": From 628e9df78467f2992a91e46daa7376fe800f1c01 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 15:58:35 +0200 Subject: [PATCH 188/450] use 'path_to_subprocess_arg' in subprocess paths definitions --- openpype/plugins/publish/extract_jpeg_exr.py | 9 +++++--- .../publish/extract_otio_audio_tracks.py | 13 +++++++---- openpype/plugins/publish/extract_review.py | 13 +++++++---- .../plugins/publish/extract_review_slate.py | 22 ++++++++++++------- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 464c190762..31e58025d5 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -6,6 +6,7 @@ from openpype.lib import ( run_subprocess, split_command_to_list, + path_to_subprocess_arg, should_decompress, get_decompress_dir, @@ -95,13 +96,15 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): ffmpeg_args = self.ffmpeg_args or {} jpeg_items = [] - jpeg_items.append("\"{}\"".format(ffmpeg_path)) + jpeg_items.append(path_to_subprocess_arg(ffmpeg_path)) # override file if already exists jpeg_items.append("-y") # use same input args like with mov jpeg_items.extend(ffmpeg_args.get("input") or []) # input file - jpeg_items.append("-i \"{}\"".format(full_input_path)) + jpeg_items.append("-i {}".format( + path_to_subprocess_arg(full_input_path) + )) # output arguments from presets jpeg_items.extend(ffmpeg_args.get("output") or []) @@ -110,7 +113,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): jpeg_items.append("-vframes 1") # output file - jpeg_items.append("\"{}\"".format(full_output_path)) + jpeg_items.append(path_to_subprocess_arg(full_output_path)) subprocess_command = " ".join(jpeg_items) subprocess_args = split_command_to_list(subprocess_command) diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 3fc5a6740d..2cdc072ffd 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -3,7 +3,8 @@ import pyblish import openpype.api from openpype.lib import ( get_ffmpeg_tool_path, - split_command_to_list + split_command_to_list, + path_to_subprocess_arg ) import tempfile import opentimelineio as otio @@ -57,9 +58,9 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): audio_inputs.insert(0, empty) # create cmd - cmd = '"{}"'.format(self.ffmpeg_path) + " " + cmd = path_to_subprocess_arg(self.ffmpeg_path) + " " cmd += self.create_cmd(audio_inputs) - cmd += "\"{}\"".format(audio_temp_fpath) + cmd += path_to_subprocess_arg(audio_temp_fpath) # Split command to list for subprocess cmd_list = split_command_to_list(cmd) @@ -265,10 +266,14 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): for index, input in enumerate(inputs): input_format = input.copy() input_format.update({"i": index}) + input_format["mediaPath"] = path_to_subprocess_arg( + input_format["mediaPath"] + ) + _inputs += ( "-ss {startSec} " "-t {durationSec} " - "-i \"{mediaPath}\" " + "-i {mediaPath} " ).format(**input_format) _filters += "[{i}]adelay={delayMilSec}:all=1[r{i}]; ".format( diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 6d254d7366..ecc49a8da6 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -15,6 +15,7 @@ from openpype.lib import ( ffprobe_streams, split_command_to_list, + path_to_subprocess_arg, should_decompress, get_decompress_dir, @@ -486,7 +487,9 @@ class ExtractReview(pyblish.api.InstancePlugin): # Add video/image input path ffmpeg_input_args.append( - "-i \"{}\"".format(temp_data["full_input_path"]) + "-i {}".format( + path_to_subprocess_arg(temp_data["full_input_path"]) + ) ) # Add audio arguments if there are any. Skipped when output are images. @@ -544,7 +547,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # NOTE This must be latest added item to output arguments. ffmpeg_output_args.append( - "\"{}\"".format(temp_data["full_output_path"]) + path_to_subprocess_arg(temp_data["full_output_path"]) ) return self.ffmpeg_full_args( @@ -613,7 +616,7 @@ class ExtractReview(pyblish.api.InstancePlugin): audio_filters.append(arg) all_args = [] - all_args.append("\"{}\"".format(self.ffmpeg_path)) + all_args.append(path_to_subprocess_arg(self.ffmpeg_path)) all_args.extend(input_args) if video_filters: all_args.append("-filter:v") @@ -860,7 +863,9 @@ class ExtractReview(pyblish.api.InstancePlugin): audio_in_args.append("-to {:0.10f}".format(audio_duration)) # Add audio input path - audio_in_args.append("-i \"{}\"".format(audio["filename"])) + audio_in_args.append("-i {}".format( + path_to_subprocess_arg(audio["filename"]) + )) # NOTE: These were changed from input to output arguments. # NOTE: value in "-ac" was hardcoded to 2, changed to audio inputs len. diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 38c9b15844..4d26fd1ebc 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -117,11 +117,13 @@ class ExtractReviewSlate(openpype.api.Extractor): input_args.extend(repre["_profile"].get('input', [])) else: input_args.extend(repre["outputDef"].get('input', [])) - input_args.append("-loop 1 -i {}".format(slate_path)) + input_args.append("-loop 1 -i {}".format( + openpype.lib.path_to_subprocess_arg(slate_path) + )) input_args.extend([ "-r {}".format(fps), - "-t 0.04"] - ) + "-t 0.04" + ]) if use_legacy_code: codec_args = repre["_profile"].get('codec', []) @@ -188,20 +190,24 @@ class ExtractReviewSlate(openpype.api.Extractor): output_args.append("-y") slate_v_path = slate_path.replace(".png", ext) - output_args.append(slate_v_path) + output_args.append( + openpype.lib.path_to_subprocess_arg(slate_v_path) + ) _remove_at_end.append(slate_v_path) slate_args = [ - "\"{}\"".format(ffmpeg_path), + openpype.lib.path_to_subprocess_arg(ffmpeg_path), " ".join(input_args), " ".join(output_args) ] - slate_subprcs_cmd = " ".join(slate_args) slate_subprocess_args = openpype.lib.split_command_to_list( - slate_subprcs_cmd + " ".join(slate_args) ) + # run slate generation subprocess - self.log.debug("Slate Executing: {}".format(slate_subprcs_cmd)) + self.log.debug( + "Slate Executing: {}".format(" ".join(slate_subprocess_args)) + ) openpype.api.run_subprocess( slate_subprocess_args, shell=True, logger=self.log ) From a35950e7cd032e77f43f76cd331fba840bbdc519 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 16 Sep 2021 16:18:23 +0200 Subject: [PATCH 189/450] nuke: fix typo --- openpype/hosts/nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 257bf8d64e..4c82b8348b 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -287,7 +287,7 @@ def script_name(): def add_button_write_to_read(node): name = "createReadNode" - label = "Cread Read From Rendered" + label = "Create Read From Rendered" value = "import write_to_read;write_to_read.write_to_read(nuke.thisNode())" knob = nuke.PyScript_Knob(name, label, value) knob.clearFlag(nuke.STARTLINE) From 4fbf5dd8dbc43d2467d4ec3c10a0d644dca70566 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 17:01:08 +0200 Subject: [PATCH 190/450] removed unused ProjectModel --- .../sync_server/tray/models.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/models.py b/openpype/modules/default_modules/sync_server/tray/models.py index 8c86d3b98f..96d09b8786 100644 --- a/openpype/modules/default_modules/sync_server/tray/models.py +++ b/openpype/modules/default_modules/sync_server/tray/models.py @@ -17,25 +17,6 @@ from . import lib log = PypeLogger().get_logger("SyncServer") -class ProjectModel(QtCore.QAbstractListModel): - def __init__(self, *args, projects=None, **kwargs): - super(ProjectModel, self).__init__(*args, **kwargs) - self.projects = projects or [] - - def data(self, index, role): - if role == Qt.DisplayRole: - # See below for the data structure. - status, text = self.projects[index.row()] - # Return the todo text only. - return text - - def rowCount(self, _index): - return len(self.todos) - - def columnCount(self, _index): - return len(self._header) - - class _SyncRepresentationModel(QtCore.QAbstractTableModel): COLUMN_LABELS = [] From a67cfcd5f54a1f8ba04bac0122078565235413bb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 17:01:28 +0200 Subject: [PATCH 191/450] delegate signal handler to method --- .../modules/default_modules/sync_server/tray/app.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/app.py b/openpype/modules/default_modules/sync_server/tray/app.py index 106076d81c..0299edb2eb 100644 --- a/openpype/modules/default_modules/sync_server/tray/app.py +++ b/openpype/modules/default_modules/sync_server/tray/app.py @@ -77,8 +77,8 @@ class SyncServerWindow(QtWidgets.QDialog): self.setWindowTitle("Sync Queue") self.projects.project_changed.connect( - lambda: repres.table_view.model().set_project( - self.projects.current_project)) + self._on_project_change + ) self.pause_btn.clicked.connect(self._pause) self.pause_btn.setAutoDefault(False) @@ -87,6 +87,13 @@ class SyncServerWindow(QtWidgets.QDialog): self.representationWidget = repres + def _on_project_change(self): + if self.projects.current_project is None: + return + self.representationWidget.table_view.model().set_project( + self.projects.current_project + ) + def showEvent(self, event): self.representationWidget.model.set_project( self.projects.current_project) From c5cc3fcf99c5eb2564476b4074eccff3f86c3369 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 17:23:49 +0200 Subject: [PATCH 192/450] Transfer logic from settings project list widget to sync server --- .../sync_server/tray/widgets.py | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index 13389ed36c..e2009bd219 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -6,10 +6,7 @@ from functools import partial from Qt import QtWidgets, QtCore, QtGui from Qt.QtCore import Qt -from openpype.tools.settings import ( - ProjectListWidget, - style -) +from openpype.tools.settings import style from openpype.api import get_local_site_id from openpype.lib import PypeLogger @@ -28,25 +25,53 @@ from . import delegates log = PypeLogger().get_logger("SyncServer") -class SyncProjectListWidget(ProjectListWidget): +class SyncProjectListWidget(QtWidgets.QWidget): """ Lists all projects that are synchronized to choose from """ + project_changed = QtCore.Signal() def __init__(self, sync_server, parent): - super(SyncProjectListWidget, self).__init__(parent, only_active=True) + super(SyncProjectListWidget, self).__init__(parent) + self.setObjectName("ProjectListWidget") + + self._parent = parent + + label_widget = QtWidgets.QLabel("Projects", self) + project_list = QtWidgets.QListView(self) + project_model = QtGui.QStandardItemModel() + project_list.setModel(project_model) + project_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + # Do not allow editing + project_list.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(3) + layout.addWidget(label_widget, 0) + layout.addWidget(project_list, 1) + + project_list.customContextMenuRequested.connect(self._on_context_menu) + project_list.selectionModel().currentChanged.connect( + self._on_index_change + ) + + self.project_model = project_model + self.project_list = project_list self.sync_server = sync_server - self.project_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.project_list.customContextMenuRequested.connect( - self._on_context_menu) + self.current_project = None self.project_name = None self.local_site = None self.icons = {} - self.layout().setContentsMargins(0, 0, 0, 0) + def _on_index_change(self, new_idx, _old_idx): + project_name = new_idx.data(QtCore.Qt.DisplayRole) - def validate_context_change(self): - return True + self.current_project = project_name + self.project_changed.emit() def refresh(self): model = self.project_model From cca201729c821a0fa3bc55c2922d76ca4c8c9ddd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 17:24:22 +0200 Subject: [PATCH 193/450] modified how projects are queried for sync server --- .../sync_server/sync_server_module.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 976a349bfa..5e6a9d1823 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -815,17 +815,22 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def _prepare_sync_project_settings(self, exclude_locals): sync_project_settings = {} system_sites = self.get_all_site_configs() - for collection in self.connection.database.collection_names(False): + project_docs = self.connection.projects( + projection={"name": 1}, + only_active=True + ) + for project_doc in project_docs: + project_name = project_doc["name"] sites = copy.deepcopy(system_sites) # get all configured sites proj_settings = self._parse_sync_settings_from_settings( - get_project_settings(collection, + get_project_settings(project_name, exclude_locals=exclude_locals)) sites.update(self._get_default_site_configs( - proj_settings["enabled"], collection)) + proj_settings["enabled"], project_name)) sites.update(proj_settings['sites']) proj_settings["sites"] = sites - sync_project_settings[collection] = proj_settings + sync_project_settings[project_name] = proj_settings if not sync_project_settings: log.info("No enabled and configured projects for sync.") return sync_project_settings From cfff637cda0501e75e34ca73946ce4f8317cb127 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 18:12:20 +0200 Subject: [PATCH 194/450] use task name from instance data instead of AVALON_TASK --- openpype/plugins/publish/integrate_new.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index f9e9b43f08..3a79cc6ecc 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -165,10 +165,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): hierarchy = "/".join(parents) anatomy_data["hierarchy"] = hierarchy + # Make sure task name in anatomy data is same as on instance.data task_name = instance.data.get("task") if task_name: anatomy_data["task"] = task_name + else: + # Just set 'task_name' variable to context task + task_name = anatomy_data["task"] + # Fill family in anatomy data anatomy_data["family"] = instance.data.get("family") stagingdir = instance.data.get("stagingDir") @@ -298,7 +303,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: orig_transfers = list(instance.data['transfers']) - task_name = io.Session.get("AVALON_TASK") family = self.main_family_from_instance(instance) key_values = {"families": family, From 24a910a88141ceaebbc771b3f7355959562df957 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 18:12:42 +0200 Subject: [PATCH 195/450] added subset_grouping_profiles attribute which can be set by settings --- openpype/plugins/publish/integrate_new.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 3a79cc6ecc..7598ada8fb 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -112,6 +112,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): integrated_file_sizes = {} TMP_FILE_EXT = 'tmp' # suffix to denote temporary files, use without '.' + subset_grouping_profiles = None def process(self, instance): self.integrated_file_sizes = {} From e92053f46d4942d18ebbc4cfa3db61ef5149c64a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 18:12:52 +0200 Subject: [PATCH 196/450] reorganized class attributes --- openpype/plugins/publish/integrate_new.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 7598ada8fb..176777a249 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -106,12 +106,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "family", "hierarchy", "task", "username" ] default_template_name = "publish" - template_name_profiles = None + + # suffix to denote temporary files, use without '.' + TMP_FILE_EXT = 'tmp' # file_url : file_size of all published and uploaded files integrated_file_sizes = {} - TMP_FILE_EXT = 'tmp' # suffix to denote temporary files, use without '.' + # Attributes set by settings + template_name_profiles = None subset_grouping_profiles = None def process(self, instance): From ef4d459a53095feb70f8d842a6560f198edb569a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 18:14:55 +0200 Subject: [PATCH 197/450] separated setting of subset group into 2 methods --- openpype/plugins/publish/integrate_new.py | 104 +++++++++++++--------- 1 file changed, 62 insertions(+), 42 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 176777a249..ac25fa47d3 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -738,6 +738,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): subset = io.find_one({"_id": _id}) + # QUESTION Why is changing of group and updating it's + # families in 'get_subset'? self._set_subset_group(instance, subset["_id"]) # Update families on subset. @@ -761,54 +763,72 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): subset_id (str): DB's subset _id """ - # add group if available - integrate_new_sett = (instance.context.data["project_settings"] - ["global"] - ["publish"] - ["IntegrateAssetNew"]) - - profiles = integrate_new_sett["subset_grouping_profiles"] - - filtering_criteria = { - "families": instance.data["family"], - "hosts": instance.data["anatomyData"]["app"], - "tasks": instance.data["anatomyData"]["task"] or - io.Session["AVALON_TASK"] - } - matching_profile = filter_profiles(profiles, filtering_criteria) - - filled_template = None - if matching_profile: - template = matching_profile["template"] - fill_pairs = ( - ("family", filtering_criteria["families"]), - ("task", filtering_criteria["tasks"]), - ("host", filtering_criteria["hosts"]), - ("subset", instance.data["subset"]), - ("renderlayer", instance.data.get("renderlayer")) - ) - fill_pairs = prepare_template_data(fill_pairs) - - try: - filled_template = \ - format_template_with_optional_keys(fill_pairs, template) - except KeyError: - keys = [] - if fill_pairs: - keys = fill_pairs.keys() - - msg = "Subset grouping failed. " \ - "Only {} are expected in Settings".format(','.join(keys)) - self.log.warning(msg) - - if instance.data.get("subsetGroup") or filled_template: - subset_group = instance.data.get('subsetGroup') or filled_template + # Fist look into instance data + subset_group = instance.data.get("subsetGroup") + if not subset_group: + subset_group = self._get_subset_group(instance) + if subset_group: io.update_many({ 'type': 'subset', '_id': io.ObjectId(subset_id) }, {'$set': {'data.subsetGroup': subset_group}}) + def _get_subset_group(self, instance): + """Look into subset group profiles set by settings. + + Attribute 'subset_grouping_profiles' is defined by OpenPype settings. + """ + # Skip if 'subset_grouping_profiles' is empty + if not self.subset_grouping_profiles: + return None + + # QUESTION + # - is there a chance that task name is not filled in anatomy + # data? + # - should we use context task in that case? + task_name = ( + instance.data["anatomyData"]["task"] + or io.Session["AVALON_TASK"] + ) + filtering_criteria = { + "families": instance.data["family"], + "hosts": instance.context.data["hostName"], + "tasks": task_name + } + matching_profile = filter_profiles( + self.subset_grouping_profiles, + filtering_criteria + ) + # Skip if there is not matchin profile + if not matching_profile: + return None + + filled_template = None + template = matching_profile["template"] + fill_pairs = ( + ("family", filtering_criteria["families"]), + ("task", filtering_criteria["tasks"]), + ("host", filtering_criteria["hosts"]), + ("subset", instance.data["subset"]), + ("renderlayer", instance.data.get("renderlayer")) + ) + fill_pairs = prepare_template_data(fill_pairs) + + try: + filled_template = \ + format_template_with_optional_keys(fill_pairs, template) + except KeyError: + keys = [] + if fill_pairs: + keys = fill_pairs.keys() + + msg = "Subset grouping failed. " \ + "Only {} are expected in Settings".format(','.join(keys)) + self.log.warning(msg) + + return filled_template + def create_version(self, subset, version_number, data=None): """ Copy given source to destination From 8cae291abb29f082dca2a96e45e81a646ed2d733 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 18:15:28 +0200 Subject: [PATCH 198/450] get task type from asset document --- openpype/plugins/publish/integrate_new.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index ac25fa47d3..6ec860e9ba 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -177,6 +177,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Just set 'task_name' variable to context task task_name = anatomy_data["task"] + # Find task type for current task name + # - this should be already prepared on instance + asset_tasks = ( + asset_entity.get("data", {}).get("tasks") + ) or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + # Fill family in anatomy data anatomy_data["family"] = instance.data.get("family") From 5b2f00be72f1af1f9858c061186ae2e93775e84a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 18:15:43 +0200 Subject: [PATCH 199/450] use it for template name profiles --- openpype/plugins/publish/integrate_new.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6ec860e9ba..d8b824a09e 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -317,11 +317,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): family = self.main_family_from_instance(instance) - key_values = {"families": family, - "tasks": task_name, - "hosts": instance.data["anatomyData"]["app"]} - profile = filter_profiles(self.template_name_profiles, key_values, - logger=self.log) + key_values = { + "families": family, + "tasks": task_name, + "hosts": instance.context.data["hostName"], + "task_types": task_type + } + profile = filter_profiles( + self.template_name_profiles, + key_values, + logger=self.log + ) template_name = "publish" if profile: From 1d8a2e2c0c3e66196683d6776db7786fb6d3e1a1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Sep 2021 18:17:03 +0200 Subject: [PATCH 200/450] store taskt types to instance data and use if for subset group profiles --- openpype/plugins/publish/integrate_new.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index d8b824a09e..3bff3ff79c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -184,6 +184,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) or {} task_info = asset_tasks.get(task_name) or {} task_type = task_info.get("type") + instance.data["task_type"] = task_type # Fill family in anatomy data anatomy_data["family"] = instance.data.get("family") @@ -805,10 +806,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): instance.data["anatomyData"]["task"] or io.Session["AVALON_TASK"] ) + task_type = instance.data["task_type"] filtering_criteria = { "families": instance.data["family"], "hosts": instance.context.data["hostName"], - "tasks": task_name + "tasks": task_name, + "task_types": task_type } matching_profile = filter_profiles( self.subset_grouping_profiles, From f96555d9e95d34d9e7f3160f674a11e3276d4955 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 17 Sep 2021 02:27:52 +0800 Subject: [PATCH 201/450] modelrender-sets add vray light-mesh, obj-properties --- openpype/hosts/maya/plugins/publish/collect_look.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 712c7f19ff..8ebdfa1b67 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -360,6 +360,8 @@ class CollectLook(pyblish.api.InstancePlugin): # handling render attribute sets render_set_types = [ "VRayDisplacement", + "VRayLightMesh", + "VRayObjectProperties", ] render_sets = cmds.ls(look_sets, type=render_set_types) if render_sets: From da3d4d02039084658a618084d5ce1ba9374b27ca Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 17 Sep 2021 02:28:50 +0800 Subject: [PATCH 202/450] modelrender-sets add redshift object-id, mesh-parameters --- openpype/hosts/maya/plugins/publish/collect_look.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 8ebdfa1b67..9c047b252f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -362,6 +362,8 @@ class CollectLook(pyblish.api.InstancePlugin): "VRayDisplacement", "VRayLightMesh", "VRayObjectProperties", + "RedshiftObjectId", + "RedshiftMeshParameters", ] render_sets = cmds.ls(look_sets, type=render_set_types) if render_sets: From e9f9c387fb62e65d028c72a7f2d608be165e3912 Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Thu, 16 Sep 2021 15:09:28 -0700 Subject: [PATCH 203/450] addressing PR comments. --- .../project_manager/project_manager/window.py | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index f8fbe2f288..57e373086f 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -16,11 +16,9 @@ from openpype.lib import is_admin_password_required from openpype.widgets import PasswordDialog from openpype import resources -from openpype.api import get_project_basic_paths, create_project_folders +from openpype.api import get_project_basic_paths, create_project_folders, Logger from avalon.api import AvalonMongoDB -log = logging.getLogger(__name__) - class ProjectManagerWindow(QtWidgets.QWidget): """Main widget of Project Manager tool.""" @@ -28,6 +26,8 @@ class ProjectManagerWindow(QtWidgets.QWidget): def __init__(self, parent=None): super(ProjectManagerWindow, self).__init__(parent) + self.log = Logger.get_logger(self.__class__.__name__) + self._initial_reset = False self._password_dialog = None self._user_passed = False @@ -64,12 +64,18 @@ class ProjectManagerWindow(QtWidgets.QWidget): create_project_btn = QtWidgets.QPushButton( "Create project...", project_widget ) + create_folders_btn = QtWidgets.QPushButton( + ResourceCache.get_icon("asset", "default"), + "Create Starting Folders", + project_widget + ) project_layout = QtWidgets.QHBoxLayout(project_widget) project_layout.setContentsMargins(0, 0, 0, 0) project_layout.addWidget(project_combobox, 0) project_layout.addWidget(refresh_projects_btn, 0) project_layout.addWidget(create_project_btn, 0) + project_layout.addWidget(create_folders_btn) project_layout.addStretch(1) # Helper buttons @@ -89,23 +95,11 @@ class ProjectManagerWindow(QtWidgets.QWidget): add_asset_btn.setObjectName("IconBtn") add_task_btn.setObjectName("IconBtn") - add_misc_folders_label = QtWidgets.QLabel( - "Create misc. folders:", - helper_btns_widget - ) - add_misc_folders_btn = QtWidgets.QPushButton( - ResourceCache.get_icon("asset", "default"), - "Create Misc. Folders", - helper_btns_widget - ) - helper_btns_layout = QtWidgets.QHBoxLayout(helper_btns_widget) helper_btns_layout.setContentsMargins(0, 0, 0, 0) helper_btns_layout.addWidget(helper_label) helper_btns_layout.addWidget(add_asset_btn) helper_btns_layout.addWidget(add_task_btn) - helper_btns_layout.addWidget(add_misc_folders_label) - helper_btns_layout.addWidget(add_misc_folders_btn) helper_btns_layout.addStretch(1) # Add widgets to top widget layout @@ -143,11 +137,11 @@ class ProjectManagerWindow(QtWidgets.QWidget): refresh_projects_btn.clicked.connect(self._on_project_refresh) create_project_btn.clicked.connect(self._on_project_create) + create_folders_btn.clicked.connect(self._on_add_misc_folders) project_combobox.currentIndexChanged.connect(self._on_project_change) save_btn.clicked.connect(self._on_save_click) add_asset_btn.clicked.connect(self._on_add_asset) add_task_btn.clicked.connect(self._on_add_task) - add_misc_folders_btn.clicked.connect(self._on_add_misc_folders) self._project_model = project_model @@ -159,10 +153,10 @@ class ProjectManagerWindow(QtWidgets.QWidget): self._refresh_projects_btn = refresh_projects_btn self._project_combobox = project_combobox self._create_project_btn = create_project_btn + self._create_folders_btn = create_folders_btn self._add_asset_btn = add_asset_btn self._add_task_btn = add_task_btn - self._add_misc_folders_btn = add_misc_folders_btn self.resize(1200, 600) self.setStyleSheet(load_stylesheet()) @@ -222,8 +216,8 @@ class ProjectManagerWindow(QtWidgets.QWidget): qm = QtWidgets.QMessageBox ans = qm.question(self, - "", - "Confirm to create misc. project folders?", + "OpenPype Project Manager", + "Confirm to create starting project folders?", qm.Yes | qm.No) if ans == qm.Yes: try: @@ -234,8 +228,8 @@ class ProjectManagerWindow(QtWidgets.QWidget): # Invoking OpenPype API to create the project folders create_project_folders(basic_paths, self._current_project) except Exception as exc: - log.warning("Failed to create misc folders: {}".format(exc), - exc_info=True) + self.log.warning("Cannot create starting folders: {}".format(exc), + exc_info=True) def show_message(self, message): # TODO add nicer message pop From bc05de97d554f136ded0a6d9c2be3dc8621fee34 Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Thu, 16 Sep 2021 15:17:15 -0700 Subject: [PATCH 204/450] Hound fixes. --- .../tools/project_manager/project_manager/window.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 57e373086f..21367287cd 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -1,4 +1,3 @@ -import logging from Qt import QtWidgets, QtCore, QtGui from . import ( @@ -16,7 +15,11 @@ from openpype.lib import is_admin_password_required from openpype.widgets import PasswordDialog from openpype import resources -from openpype.api import get_project_basic_paths, create_project_folders, Logger +from openpype.api import ( + get_project_basic_paths, + create_project_folders, + Logger +) from avalon.api import AvalonMongoDB @@ -228,8 +231,10 @@ class ProjectManagerWindow(QtWidgets.QWidget): # Invoking OpenPype API to create the project folders create_project_folders(basic_paths, self._current_project) except Exception as exc: - self.log.warning("Cannot create starting folders: {}".format(exc), - exc_info=True) + self.log.warning( + "Cannot create starting folders: {}".format(exc), + exc_info=True + ) def show_message(self, message): # TODO add nicer message pop From 284e2cca1850385077c0f16888eb51cb71ee50df Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Thu, 16 Sep 2021 16:30:16 -0700 Subject: [PATCH 205/450] Fix function name to be consistent. --- openpype/tools/project_manager/project_manager/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 21367287cd..a89aff1168 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -140,7 +140,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): refresh_projects_btn.clicked.connect(self._on_project_refresh) create_project_btn.clicked.connect(self._on_project_create) - create_folders_btn.clicked.connect(self._on_add_misc_folders) + create_folders_btn.clicked.connect(self._on_create_folders) project_combobox.currentIndexChanged.connect(self._on_project_change) save_btn.clicked.connect(self._on_save_click) add_asset_btn.clicked.connect(self._on_add_asset) @@ -213,7 +213,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): def _on_add_task(self): self.hierarchy_view.add_task() - def _on_add_misc_folders(self): + def _on_create_folders(self): if not self._current_project: return From c8ee3484225842476d56f993573022cc539cc822 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:07:19 +0200 Subject: [PATCH 206/450] moved loader, library loader and related widgets to openpype tools --- openpype/tools/libraryloader/__init__.py | 11 + openpype/tools/libraryloader/__main__.py | 5 + openpype/tools/libraryloader/app.py | 591 +++++++++ openpype/tools/libraryloader/lib.py | 33 + openpype/tools/libraryloader/widgets.py | 18 + openpype/tools/loader/__init__.py | 11 + openpype/tools/loader/__main__.py | 35 + openpype/tools/loader/app.py | 674 ++++++++++ openpype/tools/loader/lib.py | 190 +++ openpype/tools/loader/model.py | 1191 ++++++++++++++++++ openpype/tools/loader/widgets.py | 1457 ++++++++++++++++++++++ openpype/tools/utils/__init__.py | 0 openpype/tools/utils/delegates.py | 448 +++++++ openpype/tools/utils/lib.py | 622 +++++++++ openpype/tools/utils/models.py | 626 ++++++++++ openpype/tools/utils/views.py | 86 ++ openpype/tools/utils/widgets.py | 499 ++++++++ 17 files changed, 6497 insertions(+) create mode 100644 openpype/tools/libraryloader/__init__.py create mode 100644 openpype/tools/libraryloader/__main__.py create mode 100644 openpype/tools/libraryloader/app.py create mode 100644 openpype/tools/libraryloader/lib.py create mode 100644 openpype/tools/libraryloader/widgets.py create mode 100644 openpype/tools/loader/__init__.py create mode 100644 openpype/tools/loader/__main__.py create mode 100644 openpype/tools/loader/app.py create mode 100644 openpype/tools/loader/lib.py create mode 100644 openpype/tools/loader/model.py create mode 100644 openpype/tools/loader/widgets.py create mode 100644 openpype/tools/utils/__init__.py create mode 100644 openpype/tools/utils/delegates.py create mode 100644 openpype/tools/utils/lib.py create mode 100644 openpype/tools/utils/models.py create mode 100644 openpype/tools/utils/views.py create mode 100644 openpype/tools/utils/widgets.py diff --git a/openpype/tools/libraryloader/__init__.py b/openpype/tools/libraryloader/__init__.py new file mode 100644 index 0000000000..bbf4a1087d --- /dev/null +++ b/openpype/tools/libraryloader/__init__.py @@ -0,0 +1,11 @@ +from .app import ( + LibraryLoaderWindow, + show, + cli +) + +__all__ = [ + "LibraryLoaderWindow", + "show", + "cli", +] diff --git a/openpype/tools/libraryloader/__main__.py b/openpype/tools/libraryloader/__main__.py new file mode 100644 index 0000000000..d77bc585c5 --- /dev/null +++ b/openpype/tools/libraryloader/__main__.py @@ -0,0 +1,5 @@ +from . import cli + +if __name__ == '__main__': + import sys + sys.exit(cli(sys.argv[1:])) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py new file mode 100644 index 0000000000..362d05cce6 --- /dev/null +++ b/openpype/tools/libraryloader/app.py @@ -0,0 +1,591 @@ +import sys +import time + +from Qt import QtWidgets, QtCore, QtGui + +from avalon import style +from avalon.api import AvalonMongoDB +from openpype.tools.utils import lib as tools_lib +from openpype.tools.loader.widgets import ( + ThumbnailWidget, + VersionWidget, + FamilyListWidget, + RepresentationWidget +) +from openpype.tools.utils.widgets import AssetWidget + +from openpype.modules import ModulesManager + +from . import lib +from .widgets import LibrarySubsetWidget + +module = sys.modules[__name__] +module.window = None + + +class LibraryLoaderWindow(QtWidgets.QDialog): + """Asset library loader interface""" + + tool_title = "Library Loader 0.5" + tool_name = "library_loader" + + def __init__( + self, parent=None, icon=None, show_projects=False, show_libraries=True + ): + super(LibraryLoaderWindow, self).__init__(parent) + + self._initial_refresh = False + self._ignore_project_change = False + + # Enable minimize and maximize for app + self.setWindowTitle(self.tool_title) + self.setWindowFlags(QtCore.Qt.Window) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + if icon is not None: + self.setWindowIcon(icon) + # self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + body = QtWidgets.QWidget() + footer = QtWidgets.QWidget() + footer.setFixedHeight(20) + + container = QtWidgets.QWidget() + + self.dbcon = AvalonMongoDB() + self.dbcon.install() + self.dbcon.Session["AVALON_PROJECT"] = None + + self.show_projects = show_projects + self.show_libraries = show_libraries + + # Groups config + self.groups_config = tools_lib.GroupsConfig(self.dbcon) + self.family_config_cache = tools_lib.FamilyConfigCache(self.dbcon) + + assets = AssetWidget( + self.dbcon, multiselection=True, parent=self + ) + families = FamilyListWidget( + self.dbcon, self.family_config_cache, parent=self + ) + subsets = LibrarySubsetWidget( + self.dbcon, + self.groups_config, + self.family_config_cache, + tool_name=self.tool_name, + parent=self + ) + + version = VersionWidget(self.dbcon) + thumbnail = ThumbnailWidget(self.dbcon) + + # Project + self.combo_projects = QtWidgets.QComboBox() + + # Create splitter to show / hide family filters + asset_filter_splitter = QtWidgets.QSplitter() + asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) + asset_filter_splitter.addWidget(self.combo_projects) + asset_filter_splitter.addWidget(assets) + asset_filter_splitter.addWidget(families) + asset_filter_splitter.setStretchFactor(1, 65) + asset_filter_splitter.setStretchFactor(2, 35) + + manager = ModulesManager() + sync_server = manager.modules_by_name["sync_server"] + + representations = RepresentationWidget(self.dbcon) + thumb_ver_splitter = QtWidgets.QSplitter() + thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) + thumb_ver_splitter.addWidget(thumbnail) + thumb_ver_splitter.addWidget(version) + if sync_server.enabled: + thumb_ver_splitter.addWidget(representations) + thumb_ver_splitter.setStretchFactor(0, 30) + thumb_ver_splitter.setStretchFactor(1, 35) + + container_layout = QtWidgets.QHBoxLayout(container) + container_layout.setContentsMargins(0, 0, 0, 0) + split = QtWidgets.QSplitter() + split.addWidget(asset_filter_splitter) + split.addWidget(subsets) + split.addWidget(thumb_ver_splitter) + split.setSizes([180, 950, 200]) + container_layout.addWidget(split) + + body_layout = QtWidgets.QHBoxLayout(body) + body_layout.addWidget(container) + body_layout.setContentsMargins(0, 0, 0, 0) + + message = QtWidgets.QLabel() + message.hide() + + footer_layout = QtWidgets.QVBoxLayout(footer) + footer_layout.addWidget(message) + footer_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + layout.addWidget(footer) + + self.data = { + "widgets": { + "families": families, + "assets": assets, + "subsets": subsets, + "version": version, + "thumbnail": thumbnail, + "representations": representations + }, + "label": { + "message": message, + }, + "state": { + "assetIds": None + } + } + + families.active_changed.connect(subsets.set_family_filters) + assets.selection_changed.connect(self.on_assetschanged) + assets.refresh_triggered.connect(self.on_assetschanged) + assets.view.clicked.connect(self.on_assetview_click) + subsets.active_changed.connect(self.on_subsetschanged) + subsets.version_changed.connect(self.on_versionschanged) + self.combo_projects.currentTextChanged.connect(self.on_project_change) + + self.sync_server = sync_server + + # Set default thumbnail on start + thumbnail.set_thumbnail(None) + + # Defaults + if sync_server.enabled: + split.setSizes([250, 1000, 550]) + self.resize(1800, 900) + else: + split.setSizes([250, 850, 200]) + self.resize(1300, 700) + + def showEvent(self, event): + super(LibraryLoaderWindow, self).showEvent(event) + if not self._initial_refresh: + self.refresh() + + def on_assetview_click(self, *args): + subsets_widget = self.data["widgets"]["subsets"] + selection_model = subsets_widget.view.selectionModel() + if selection_model.selectedIndexes(): + selection_model.clearSelection() + + def _set_projects(self): + # Store current project + old_project_name = self.current_project + + self._ignore_project_change = True + + # Cleanup + self.combo_projects.clear() + + # Fill combobox with projects + select_project_item = QtGui.QStandardItem("< Select project >") + select_project_item.setData(None, QtCore.Qt.UserRole + 1) + + combobox_items = [select_project_item] + + project_names = self.get_filtered_projects() + + for project_name in sorted(project_names): + item = QtGui.QStandardItem(project_name) + item.setData(project_name, QtCore.Qt.UserRole + 1) + combobox_items.append(item) + + root_item = self.combo_projects.model().invisibleRootItem() + root_item.appendRows(combobox_items) + + index = 0 + self._ignore_project_change = False + + if old_project_name: + index = self.combo_projects.findText( + old_project_name, QtCore.Qt.MatchFixedString + ) + + self.combo_projects.setCurrentIndex(index) + + def get_filtered_projects(self): + projects = list() + for project in self.dbcon.projects(): + is_library = project.get("data", {}).get("library_project", False) + if ( + (is_library and self.show_libraries) or + (not is_library and self.show_projects) + ): + projects.append(project["name"]) + + return projects + + def on_project_change(self): + if self._ignore_project_change: + return + + row = self.combo_projects.currentIndex() + index = self.combo_projects.model().index(row, 0) + project_name = index.data(QtCore.Qt.UserRole + 1) + + self.dbcon.Session["AVALON_PROJECT"] = project_name + + _config = lib.find_config() + if hasattr(_config, "install"): + _config.install() + else: + print( + "Config `%s` has no function `install`" % _config.__name__ + ) + + self.family_config_cache.refresh() + self.groups_config.refresh() + + self._refresh_assets() + self._assetschanged() + + project_name = self.dbcon.active_project() or "No project selected" + title = "{} - {}".format(self.tool_title, project_name) + self.setWindowTitle(title) + + subsets = self.data["widgets"]["subsets"] + subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) + + representations = self.data["widgets"]["representations"] + representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) + + @property + def current_project(self): + if ( + not self.dbcon.active_project() or + self.dbcon.active_project() == "" + ): + return None + + return self.dbcon.active_project() + + # ------------------------------- + # Delay calling blocking methods + # ------------------------------- + + def refresh(self): + self.echo("Fetching results..") + tools_lib.schedule(self._refresh, 50, channel="mongo") + + def on_assetschanged(self, *args): + self.echo("Fetching asset..") + tools_lib.schedule(self._assetschanged, 50, channel="mongo") + + def on_subsetschanged(self, *args): + self.echo("Fetching subset..") + tools_lib.schedule(self._subsetschanged, 50, channel="mongo") + + def on_versionschanged(self, *args): + self.echo("Fetching version..") + tools_lib.schedule(self._versionschanged, 150, channel="mongo") + + def set_context(self, context, refresh=True): + self.echo("Setting context: {}".format(context)) + lib.schedule( + lambda: self._set_context(context, refresh=refresh), + 50, channel="mongo" + ) + + # ------------------------------ + def _refresh(self): + if not self._initial_refresh: + self._initial_refresh = True + self._set_projects() + + def _refresh_assets(self): + """Load assets from database""" + if self.current_project is not None: + # Ensure a project is loaded + project_doc = self.dbcon.find_one( + {"type": "project"}, + {"type": 1} + ) + assert project_doc, "This is a bug" + + assets_widget = self.data["widgets"]["assets"] + assets_widget.model.stop_fetch_thread() + assets_widget.refresh() + assets_widget.setFocus() + + families = self.data["widgets"]["families"] + families.refresh() + + def clear_assets_underlines(self): + last_asset_ids = self.data["state"]["assetIds"] + if not last_asset_ids: + return + + assets_widget = self.data["widgets"]["assets"] + id_role = assets_widget.model.ObjectIdRole + + for index in tools_lib.iter_model_rows(assets_widget.model, 0): + if index.data(id_role) not in last_asset_ids: + continue + + assets_widget.model.setData( + index, [], assets_widget.model.subsetColorsRole + ) + + def _assetschanged(self): + """Selected assets have changed""" + t1 = time.time() + + assets_widget = self.data["widgets"]["assets"] + subsets_widget = self.data["widgets"]["subsets"] + subsets_model = subsets_widget.model + + subsets_model.clear() + self.clear_assets_underlines() + + if not self.dbcon.Session.get("AVALON_PROJECT"): + subsets_widget.set_loading_state( + loading=False, + empty=True + ) + return + + # filter None docs they are silo + asset_docs = assets_widget.get_selected_assets() + if len(asset_docs) == 0: + return + + asset_ids = [asset_doc["_id"] for asset_doc in asset_docs] + # Start loading + subsets_widget.set_loading_state( + loading=bool(asset_ids), + empty=True + ) + + def on_refreshed(has_item): + empty = not has_item + subsets_widget.set_loading_state(loading=False, empty=empty) + subsets_model.refreshed.disconnect() + self.echo("Duration: %.3fs" % (time.time() - t1)) + + subsets_model.refreshed.connect(on_refreshed) + + subsets_model.set_assets(asset_ids) + subsets_widget.view.setColumnHidden( + subsets_model.Columns.index("asset"), + len(asset_ids) < 2 + ) + + # Clear the version information on asset change + self.data["widgets"]["version"].set_version(None) + self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs) + + self.data["state"]["assetIds"] = asset_ids + + representations = self.data["widgets"]["representations"] + representations.set_version_ids([]) # reset repre list + + self.echo("Duration: %.3fs" % (time.time() - t1)) + + def _subsetschanged(self): + asset_ids = self.data["state"]["assetIds"] + # Skip setting colors if not asset multiselection + if not asset_ids or len(asset_ids) < 2: + self._versionschanged() + return + + subsets = self.data["widgets"]["subsets"] + selected_subsets = subsets.selected_subsets(_merged=True, _other=False) + + asset_models = {} + asset_ids = [] + for subset_node in selected_subsets: + asset_ids.extend(subset_node.get("assetIds", [])) + asset_ids = set(asset_ids) + + for subset_node in selected_subsets: + for asset_id in asset_ids: + if asset_id not in asset_models: + asset_models[asset_id] = [] + + color = None + if asset_id in subset_node.get("assetIds", []): + color = subset_node["subsetColor"] + + asset_models[asset_id].append(color) + + self.clear_assets_underlines() + + assets_widget = self.data["widgets"]["assets"] + indexes = assets_widget.view.selectionModel().selectedRows() + + for index in indexes: + id = index.data(assets_widget.model.ObjectIdRole) + if id not in asset_models: + continue + + assets_widget.model.setData( + index, asset_models[id], assets_widget.model.subsetColorsRole + ) + # Trigger repaint + assets_widget.view.updateGeometries() + # Set version in Version Widget + self._versionschanged() + + def _versionschanged(self): + + subsets = self.data["widgets"]["subsets"] + selection = subsets.view.selectionModel() + + # Active must be in the selected rows otherwise we + # assume it's not actually an "active" current index. + version_docs = None + version_doc = None + active = selection.currentIndex() + rows = selection.selectedRows(column=active.column()) + if active and active in rows: + item = active.data(subsets.model.ItemRole) + if ( + item is not None + and not (item.get("isGroup") or item.get("isMerged")) + ): + version_doc = item["version_document"] + + if rows: + version_docs = [] + for index in rows: + if not index or not index.isValid(): + continue + item = index.data(subsets.model.ItemRole) + if ( + item is None + or item.get("isGroup") + or item.get("isMerged") + ): + continue + version_docs.append(item["version_document"]) + + self.data["widgets"]["version"].set_version(version_doc) + + thumbnail_docs = version_docs + if not thumbnail_docs: + assets_widget = self.data["widgets"]["assets"] + asset_docs = assets_widget.get_selected_assets() + if len(asset_docs) > 0: + thumbnail_docs = asset_docs + + self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs) + + representations = self.data["widgets"]["representations"] + version_ids = [doc["_id"] for doc in version_docs or []] + representations.set_version_ids(version_ids) + + def _set_context(self, context, refresh=True): + """Set the selection in the interface using a context. + The context must contain `asset` data by name. + Note: Prior to setting context ensure `refresh` is triggered so that + the "silos" are listed correctly, aside from that setting the + context will force a refresh further down because it changes + the active silo and asset. + Args: + context (dict): The context to apply. + Returns: + None + """ + + asset = context.get("asset", None) + if asset is None: + return + + if refresh: + # Workaround: + # Force a direct (non-scheduled) refresh prior to setting the + # asset widget's silo and asset selection to ensure it's correctly + # displaying the silo tabs. Calling `window.refresh()` and directly + # `window.set_context()` the `set_context()` seems to override the + # scheduled refresh and the silo tabs are not shown. + self._refresh_assets() + + asset_widget = self.data["widgets"]["assets"] + asset_widget.select_assets(asset) + + def echo(self, message): + widget = self.data["label"]["message"] + widget.setText(str(message)) + widget.show() + print(message) + + tools_lib.schedule(widget.hide, 5000, channel="message") + + def closeEvent(self, event): + # Kill on holding SHIFT + modifiers = QtWidgets.QApplication.queryKeyboardModifiers() + shift_pressed = QtCore.Qt.ShiftModifier & modifiers + + if shift_pressed: + print("Force quitted..") + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + print("Good bye") + return super(LibraryLoaderWindow, self).closeEvent(event) + + +def show( + debug=False, parent=None, icon=None, + show_projects=False, show_libraries=True +): + """Display Loader GUI + + Arguments: + debug (bool, optional): Run loader in debug-mode, + defaults to False + parent (QtCore.QObject, optional): The Qt object to parent to. + use_context (bool): Whether to apply the current context upon launch + + """ + # Remember window + if module.window is not None: + try: + module.window.show() + + # If the window is minimized then unminimize it. + if module.window.windowState() & QtCore.Qt.WindowMinimized: + module.window.setWindowState(QtCore.Qt.WindowActive) + + # Raise and activate the window + module.window.raise_() # for MacOS + module.window.activateWindow() # for Windows + module.window.refresh() + return + except RuntimeError as e: + if not e.message.rstrip().endswith("already deleted."): + raise + + # Garbage collected + module.window = None + + if debug: + import traceback + sys.excepthook = lambda typ, val, tb: traceback.print_last() + + with tools_lib.application(): + window = LibraryLoaderWindow( + parent, icon, show_projects, show_libraries + ) + window.setStyleSheet(style.load_stylesheet()) + window.show() + + module.window = window + + +def cli(args): + + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("project") + + show(show_projects=True, show_libraries=True) diff --git a/openpype/tools/libraryloader/lib.py b/openpype/tools/libraryloader/lib.py new file mode 100644 index 0000000000..6a497a6a16 --- /dev/null +++ b/openpype/tools/libraryloader/lib.py @@ -0,0 +1,33 @@ +import os +import importlib +import logging +from openpype.api import Anatomy + +log = logging.getLogger(__name__) + + +# `find_config` from `pipeline` +def find_config(): + log.info("Finding configuration for project..") + + config = os.environ["AVALON_CONFIG"] + + if not config: + raise EnvironmentError( + "No configuration found in " + "the project nor environment" + ) + + log.info("Found %s, loading.." % config) + return importlib.import_module(config) + + +class RegisteredRoots: + roots_per_project = {} + + @classmethod + def registered_root(cls, project_name): + if project_name not in cls.roots_per_project: + cls.roots_per_project[project_name] = Anatomy(project_name).roots + + return cls.roots_per_project[project_name] diff --git a/openpype/tools/libraryloader/widgets.py b/openpype/tools/libraryloader/widgets.py new file mode 100644 index 0000000000..45f9ea2048 --- /dev/null +++ b/openpype/tools/libraryloader/widgets.py @@ -0,0 +1,18 @@ +from Qt import QtWidgets + +from .lib import RegisteredRoots +from openpype.tools.loader.widgets import SubsetWidget + + +class LibrarySubsetWidget(SubsetWidget): + def on_copy_source(self): + """Copy formatted source path to clipboard""" + source = self.data.get("source", None) + if not source: + return + + project_name = self.dbcon.Session["AVALON_PROJECT"] + root = RegisteredRoots.registered_root(project_name) + path = source.format(root=root) + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(path) diff --git a/openpype/tools/loader/__init__.py b/openpype/tools/loader/__init__.py new file mode 100644 index 0000000000..c7bd6148a7 --- /dev/null +++ b/openpype/tools/loader/__init__.py @@ -0,0 +1,11 @@ +from .app import ( + LoaderWidow, + show, + cli, +) + +__all__ = ( + "LoaderWidow", + "show", + "cli", +) diff --git a/openpype/tools/loader/__main__.py b/openpype/tools/loader/__main__.py new file mode 100644 index 0000000000..27794b9bc5 --- /dev/null +++ b/openpype/tools/loader/__main__.py @@ -0,0 +1,35 @@ +"""Main entrypoint for standalone debugging""" +""" + Used for running 'avalon.tool.loader.__main__' as a module (-m), useful for + debugging without need to start host. + + Modify AVALON_MONGO accordingly +""" +from . import cli + + +def my_exception_hook(exctype, value, traceback): + # Print the error and traceback + print(exctype, value, traceback) + # Call the normal Exception hook after + sys._excepthook(exctype, value, traceback) + sys.exit(1) + + +if __name__ == '__main__': + import os + os.environ["AVALON_MONGO"] = "mongodb://localhost:27017" + os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017" + os.environ["AVALON_DB"] = "avalon" + os.environ["AVALON_TIMEOUT"] = "1000" + os.environ["OPENPYPE_DEBUG"] = "1" + os.environ["AVALON_CONFIG"] = "pype" + os.environ["AVALON_ASSET"] = "Jungle" + + + import sys + + # Set the exception hook to our wrapping function + sys.excepthook = my_exception_hook + + sys.exit(cli(sys.argv[1:])) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py new file mode 100644 index 0000000000..381d6b25d8 --- /dev/null +++ b/openpype/tools/loader/app.py @@ -0,0 +1,674 @@ +import sys +import time + +from Qt import QtWidgets, QtCore +from avalon import api, io, style, pipeline + +from openpype.tools.utils.widgets import AssetWidget + +from openpype.tools.utils import lib + +from .widgets import ( + SubsetWidget, + VersionWidget, + FamilyListWidget, + ThumbnailWidget, + RepresentationWidget, + OverlayFrame +) + +from openpype.modules import ModulesManager + +module = sys.modules[__name__] +module.window = None + + +# Register callback on task change +# - callback can't be defined in Window as it is weak reference callback +# so `WeakSet` will remove it immidiatelly +def on_context_task_change(*args, **kwargs): + if module.window: + module.window.on_context_task_change(*args, **kwargs) + + +pipeline.on("taskChanged", on_context_task_change) + + +class LoaderWidow(QtWidgets.QDialog): + """Asset loader interface""" + + tool_name = "loader" + + def __init__(self, parent=None): + super(LoaderWidow, self).__init__(parent) + title = "Asset Loader 2.1" + project_name = api.Session.get("AVALON_PROJECT") + if project_name: + title += " - {}".format(project_name) + self.setWindowTitle(title) + + # Groups config + self.groups_config = lib.GroupsConfig(io) + self.family_config_cache = lib.FamilyConfigCache(io) + + # Enable minimize and maximize for app + self.setWindowFlags(QtCore.Qt.Window) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + body = QtWidgets.QWidget() + footer = QtWidgets.QWidget() + footer.setFixedHeight(20) + + container = QtWidgets.QWidget() + + assets = AssetWidget(io, multiselection=True, parent=self) + assets.set_current_asset_btn_visibility(True) + + families = FamilyListWidget(io, self.family_config_cache, self) + subsets = SubsetWidget( + io, + self.groups_config, + self.family_config_cache, + tool_name=self.tool_name, + parent=self + ) + version = VersionWidget(io) + thumbnail = ThumbnailWidget(io) + representations = RepresentationWidget(io, self.tool_name) + + manager = ModulesManager() + sync_server = manager.modules_by_name["sync_server"] + + thumb_ver_splitter = QtWidgets.QSplitter() + thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) + thumb_ver_splitter.addWidget(thumbnail) + thumb_ver_splitter.addWidget(version) + if sync_server.enabled: + thumb_ver_splitter.addWidget(representations) + thumb_ver_splitter.setStretchFactor(0, 30) + thumb_ver_splitter.setStretchFactor(1, 35) + + # Create splitter to show / hide family filters + asset_filter_splitter = QtWidgets.QSplitter() + asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) + asset_filter_splitter.addWidget(assets) + asset_filter_splitter.addWidget(families) + asset_filter_splitter.setStretchFactor(0, 65) + asset_filter_splitter.setStretchFactor(1, 35) + + container_layout = QtWidgets.QHBoxLayout(container) + container_layout.setContentsMargins(0, 0, 0, 0) + split = QtWidgets.QSplitter() + split.addWidget(asset_filter_splitter) + split.addWidget(subsets) + split.addWidget(thumb_ver_splitter) + + container_layout.addWidget(split) + + body_layout = QtWidgets.QHBoxLayout(body) + body_layout.addWidget(container) + body_layout.setContentsMargins(0, 0, 0, 0) + + message = QtWidgets.QLabel() + message.hide() + + footer_layout = QtWidgets.QVBoxLayout(footer) + footer_layout.addWidget(message) + footer_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + layout.addWidget(footer) + + self.data = { + "widgets": { + "families": families, + "assets": assets, + "subsets": subsets, + "version": version, + "thumbnail": thumbnail, + "representations": representations + }, + "label": { + "message": message, + }, + "state": { + "assetIds": None + } + } + + overlay_frame = OverlayFrame("Loading...", self) + overlay_frame.setVisible(False) + + families.active_changed.connect(subsets.set_family_filters) + assets.selection_changed.connect(self.on_assetschanged) + assets.refresh_triggered.connect(self.on_assetschanged) + assets.view.clicked.connect(self.on_assetview_click) + subsets.active_changed.connect(self.on_subsetschanged) + subsets.version_changed.connect(self.on_versionschanged) + + subsets.load_started.connect(self._on_load_start) + subsets.load_ended.connect(self._on_load_end) + representations.load_started.connect(self._on_load_start) + representations.load_ended.connect(self._on_load_end) + + self._overlay_frame = overlay_frame + + self.family_config_cache.refresh() + self.groups_config.refresh() + + self._refresh() + self._assetschanged() + + # Defaults + if sync_server.enabled: + split.setSizes([250, 1000, 550]) + self.resize(1800, 900) + else: + split.setSizes([250, 850, 200]) + self.resize(1300, 700) + + def resizeEvent(self, event): + super(LoaderWidow, self).resizeEvent(event) + self._overlay_frame.resize(self.size()) + + def moveEvent(self, event): + super(LoaderWidow, self).moveEvent(event) + self._overlay_frame.move(0, 0) + + # ------------------------------- + # Delay calling blocking methods + # ------------------------------- + + def on_assetview_click(self, *args): + subsets_widget = self.data["widgets"]["subsets"] + selection_model = subsets_widget.view.selectionModel() + if selection_model.selectedIndexes(): + selection_model.clearSelection() + + def refresh(self): + self.echo("Fetching results..") + lib.schedule(self._refresh, 50, channel="mongo") + + def on_assetschanged(self, *args): + self.echo("Fetching asset..") + lib.schedule(self._assetschanged, 50, channel="mongo") + + def on_subsetschanged(self, *args): + self.echo("Fetching subset..") + lib.schedule(self._subsetschanged, 50, channel="mongo") + + def on_versionschanged(self, *args): + self.echo("Fetching version..") + lib.schedule(self._versionschanged, 150, channel="mongo") + + def set_context(self, context, refresh=True): + self.echo("Setting context: {}".format(context)) + lib.schedule(lambda: self._set_context(context, refresh=refresh), + 50, channel="mongo") + + def _on_load_start(self): + # Show overlay and process events so it's repainted + self._overlay_frame.setVisible(True) + QtWidgets.QApplication.processEvents() + + def _hide_overlay(self): + self._overlay_frame.setVisible(False) + + def _on_load_end(self): + # Delay hiding as click events happened during loading should be + # blocked + QtCore.QTimer.singleShot(100, self._hide_overlay) + + # ------------------------------ + + def on_context_task_change(self, *args, **kwargs): + # Change to context asset on context change + assets_widget = self.data["widgets"]["assets"] + assets_widget.select_assets(io.Session["AVALON_ASSET"]) + + def _refresh(self): + """Load assets from database""" + + # Ensure a project is loaded + project = io.find_one({"type": "project"}, {"type": 1}) + assert project, "Project was not found! This is a bug" + + assets_widget = self.data["widgets"]["assets"] + assets_widget.refresh() + assets_widget.setFocus() + + families = self.data["widgets"]["families"] + families.refresh() + + def clear_assets_underlines(self): + """Clear colors from asset data to remove colored underlines + When multiple assets are selected colored underlines mark which asset + own selected subsets. These colors must be cleared from asset data + on selection change so they match current selection. + """ + last_asset_ids = self.data["state"]["assetIds"] + if not last_asset_ids: + return + + assets_widget = self.data["widgets"]["assets"] + id_role = assets_widget.model.ObjectIdRole + + for index in lib.iter_model_rows(assets_widget.model, 0): + if index.data(id_role) not in last_asset_ids: + continue + + assets_widget.model.setData( + index, [], assets_widget.model.subsetColorsRole + ) + + def _assetschanged(self): + """Selected assets have changed""" + t1 = time.time() + + assets_widget = self.data["widgets"]["assets"] + subsets_widget = self.data["widgets"]["subsets"] + subsets_model = subsets_widget.model + + subsets_model.clear() + self.clear_assets_underlines() + + # filter None docs they are silo + asset_docs = assets_widget.get_selected_assets() + + asset_ids = [asset_doc["_id"] for asset_doc in asset_docs] + # Start loading + subsets_widget.set_loading_state( + loading=bool(asset_ids), + empty=True + ) + + def on_refreshed(has_item): + empty = not has_item + subsets_widget.set_loading_state(loading=False, empty=empty) + subsets_model.refreshed.disconnect() + self.echo("Duration: %.3fs" % (time.time() - t1)) + + subsets_model.refreshed.connect(on_refreshed) + + subsets_model.set_assets(asset_ids) + subsets_widget.view.setColumnHidden( + subsets_model.Columns.index("asset"), + len(asset_ids) < 2 + ) + + # Clear the version information on asset change + self.data["widgets"]["version"].set_version(None) + self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs) + + self.data["state"]["assetIds"] = asset_ids + + representations = self.data["widgets"]["representations"] + representations.set_version_ids([]) # reset repre list + + def _subsetschanged(self): + asset_ids = self.data["state"]["assetIds"] + # Skip setting colors if not asset multiselection + if not asset_ids or len(asset_ids) < 2: + self._versionschanged() + return + + subsets = self.data["widgets"]["subsets"] + selected_subsets = subsets.selected_subsets(_merged=True, _other=False) + + asset_models = {} + asset_ids = [] + for subset_node in selected_subsets: + asset_ids.extend(subset_node.get("assetIds", [])) + asset_ids = set(asset_ids) + + for subset_node in selected_subsets: + for asset_id in asset_ids: + if asset_id not in asset_models: + asset_models[asset_id] = [] + + color = None + if asset_id in subset_node.get("assetIds", []): + color = subset_node["subsetColor"] + + asset_models[asset_id].append(color) + + self.clear_assets_underlines() + + assets_widget = self.data["widgets"]["assets"] + indexes = assets_widget.view.selectionModel().selectedRows() + + for index in indexes: + id = index.data(assets_widget.model.ObjectIdRole) + if id not in asset_models: + continue + + assets_widget.model.setData( + index, asset_models[id], assets_widget.model.subsetColorsRole + ) + # Trigger repaint + assets_widget.view.updateGeometries() + # Set version in Version Widget + self._versionschanged() + + def _versionschanged(self): + subsets = self.data["widgets"]["subsets"] + selection = subsets.view.selectionModel() + + # Active must be in the selected rows otherwise we + # assume it's not actually an "active" current index. + version_docs = None + version_doc = None + active = selection.currentIndex() + rows = selection.selectedRows(column=active.column()) + if active: + if active in rows: + item = active.data(subsets.model.ItemRole) + if ( + item is not None and + not (item.get("isGroup") or item.get("isMerged")) + ): + version_doc = item["version_document"] + + if rows: + version_docs = [] + for index in rows: + if not index or not index.isValid(): + continue + item = index.data(subsets.model.ItemRole) + if item is None: + continue + if item.get("isGroup") or item.get("isMerged"): + for child in item.children(): + version_docs.append(child["version_document"]) + else: + version_docs.append(item["version_document"]) + + self.data["widgets"]["version"].set_version(version_doc) + + thumbnail_docs = version_docs + assets_widget = self.data["widgets"]["assets"] + asset_docs = assets_widget.get_selected_assets() + if not thumbnail_docs: + if len(asset_docs) > 0: + thumbnail_docs = asset_docs + + self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs) + + representations = self.data["widgets"]["representations"] + version_ids = [doc["_id"] for doc in version_docs or []] + representations.set_version_ids(version_ids) + + # representations.change_visibility("subset", len(rows) > 1) + # representations.change_visibility("asset", len(asset_docs) > 1) + + def _set_context(self, context, refresh=True): + """Set the selection in the interface using a context. + + The context must contain `asset` data by name. + + Note: Prior to setting context ensure `refresh` is triggered so that + the "silos" are listed correctly, aside from that setting the + context will force a refresh further down because it changes + the active silo and asset. + + Args: + context (dict): The context to apply. + + Returns: + None + + """ + + asset = context.get("asset", None) + if asset is None: + return + + if refresh: + # Workaround: + # Force a direct (non-scheduled) refresh prior to setting the + # asset widget's silo and asset selection to ensure it's correctly + # displaying the silo tabs. Calling `window.refresh()` and directly + # `window.set_context()` the `set_context()` seems to override the + # scheduled refresh and the silo tabs are not shown. + self._refresh() + + asset_widget = self.data["widgets"]["assets"] + asset_widget.select_assets(asset) + + def echo(self, message): + widget = self.data["label"]["message"] + widget.setText(str(message)) + widget.show() + print(message) + + lib.schedule(widget.hide, 5000, channel="message") + + def closeEvent(self, event): + # Kill on holding SHIFT + modifiers = QtWidgets.QApplication.queryKeyboardModifiers() + shift_pressed = QtCore.Qt.ShiftModifier & modifiers + + if shift_pressed: + print("Force quitted..") + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + print("Good bye") + return super(LoaderWidow, self).closeEvent(event) + + def keyPressEvent(self, event): + modifiers = event.modifiers() + ctrl_pressed = QtCore.Qt.ControlModifier & modifiers + + # Grouping subsets on pressing Ctrl + G + if (ctrl_pressed and event.key() == QtCore.Qt.Key_G and + not event.isAutoRepeat()): + self.show_grouping_dialog() + return + + super(LoaderWidow, self).keyPressEvent(event) + event.setAccepted(True) # Avoid interfering other widgets + + def show_grouping_dialog(self): + subsets = self.data["widgets"]["subsets"] + if not subsets.is_groupable(): + self.echo("Grouping not enabled.") + return + + selected = [] + merged_items = [] + for item in subsets.selected_subsets(_merged=True): + if item.get("isMerged"): + merged_items.append(item) + else: + selected.append(item) + + for merged_item in merged_items: + for child_item in merged_item.children(): + selected.append(child_item) + + if not selected: + self.echo("No selected subset.") + return + + dialog = SubsetGroupingDialog( + items=selected, groups_config=self.groups_config, parent=self + ) + dialog.grouped.connect(self._assetschanged) + dialog.show() + + +class SubsetGroupingDialog(QtWidgets.QDialog): + grouped = QtCore.Signal() + + def __init__(self, items, groups_config, parent=None): + super(SubsetGroupingDialog, self).__init__(parent=parent) + self.setWindowTitle("Grouping Subsets") + self.setMinimumWidth(250) + self.setModal(True) + + self.items = items + self.groups_config = groups_config + self.subsets = parent.data["widgets"]["subsets"] + self.asset_ids = parent.data["state"]["assetIds"] + + name = QtWidgets.QLineEdit() + name.setPlaceholderText("Remain blank to ungroup..") + + # Menu for pre-defined subset groups + name_button = QtWidgets.QPushButton() + name_button.setFixedWidth(18) + name_button.setFixedHeight(20) + name_menu = QtWidgets.QMenu(name_button) + name_button.setMenu(name_menu) + + name_layout = QtWidgets.QHBoxLayout() + name_layout.addWidget(name) + name_layout.addWidget(name_button) + name_layout.setContentsMargins(0, 0, 0, 0) + + group_btn = QtWidgets.QPushButton("Apply") + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(QtWidgets.QLabel("Group Name")) + layout.addLayout(name_layout) + layout.addWidget(group_btn) + + group_btn.clicked.connect(self.on_group) + group_btn.setAutoDefault(True) + group_btn.setDefault(True) + + self.name = name + self.name_menu = name_menu + + self._build_menu() + + def _build_menu(self): + menu = self.name_menu + button = menu.parent() + # Get and destroy the action group + group = button.findChild(QtWidgets.QActionGroup) + if group: + group.deleteLater() + + active_groups = self.groups_config.active_groups(self.asset_ids) + + # Build new action group + group = QtWidgets.QActionGroup(button) + group_names = list() + for data in sorted(active_groups, key=lambda x: x["order"]): + name = data["name"] + if name in group_names: + continue + group_names.append(name) + icon = data["icon"] + + action = group.addAction(name) + action.setIcon(icon) + menu.addAction(action) + + group.triggered.connect(self._on_action_clicked) + button.setEnabled(not menu.isEmpty()) + + def _on_action_clicked(self, action): + self.name.setText(action.text()) + + def on_group(self): + name = self.name.text().strip() + self.subsets.group_subsets(name, self.asset_ids, self.items) + + with lib.preserve_selection(tree_view=self.subsets.view, + current_index=False): + self.grouped.emit() + self.close() + + +def show(debug=False, parent=None, use_context=False): + """Display Loader GUI + + Arguments: + debug (bool, optional): Run loader in debug-mode, + defaults to False + parent (QtCore.QObject, optional): The Qt object to parent to. + use_context (bool): Whether to apply the current context upon launch + + """ + + # Remember window + if module.window is not None: + try: + module.window.show() + + # If the window is minimized then unminimize it. + if module.window.windowState() & QtCore.Qt.WindowMinimized: + module.window.setWindowState(QtCore.Qt.WindowActive) + + # Raise and activate the window + module.window.raise_() # for MacOS + module.window.activateWindow() # for Windows + module.window.refresh() + return + except (AttributeError, RuntimeError): + # Garbage collected + module.window = None + + if debug: + import traceback + sys.excepthook = lambda typ, val, tb: traceback.print_last() + + io.install() + + any_project = next( + project for project in io.projects() + if project.get("active", True) is not False + ) + + api.Session["AVALON_PROJECT"] = any_project["name"] + module.project = any_project["name"] + + with lib.application(): + window = LoaderWidow(parent) + window.setStyleSheet(style.load_stylesheet()) + window.show() + + if use_context: + context = {"asset": api.Session["AVALON_ASSET"]} + window.set_context(context, refresh=True) + else: + window.refresh() + + module.window = window + + # Pull window to the front. + module.window.raise_() + module.window.activateWindow() + + +def cli(args): + + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("project") + + args = parser.parse_args(args) + project = args.project + + print("Entering Project: %s" % project) + + io.install() + + # Store settings + api.Session["AVALON_PROJECT"] = project + + from avalon import pipeline + + # Find the set config + _config = pipeline.find_config() + if hasattr(_config, "install"): + _config.install() + else: + print("Config `%s` has no function `install`" % + _config.__name__) + + show() diff --git a/openpype/tools/loader/lib.py b/openpype/tools/loader/lib.py new file mode 100644 index 0000000000..14ebab6c85 --- /dev/null +++ b/openpype/tools/loader/lib.py @@ -0,0 +1,190 @@ +import inspect +from Qt import QtGui + +from avalon.vendor import qtawesome +from openpype.tools.utils.widgets import ( + OptionalAction, + OptionDialog +) + + +def change_visibility(model, view, column_name, visible): + """ + Hides or shows particular 'column_name'. + + "asset" and "subset" columns should be visible only in multiselect + """ + index = model.Columns.index(column_name) + view.setColumnHidden(index, not visible) + + +def get_selected_items(rows, item_role): + items = [] + for row_index in rows: + item = row_index.data(item_role) + if item.get("isGroup"): + continue + + elif item.get("isMerged"): + for idx in range(row_index.model().rowCount(row_index)): + child_index = row_index.child(idx, 0) + item = child_index.data(item_role) + if item not in items: + items.append(item) + + else: + if item not in items: + items.append(item) + return items + + +def get_options(action, loader, parent, repre_contexts): + """Provides dialog to select value from loader provided options. + + Loader can provide static or dynamically created options based on + qargparse variants. + + Args: + action (OptionalAction) - action in menu + loader (cls of api.Loader) - not initilized yet + parent (Qt element to parent dialog to) + repre_contexts (list) of dict with full info about selected repres + Returns: + (dict) - selected value from OptionDialog + None when dialog was closed or cancelled, in all other cases {} + if no options + """ + # Pop option dialog + options = {} + loader_options = loader.get_options(repre_contexts) + if getattr(action, "optioned", False) and loader_options: + dialog = OptionDialog(parent) + dialog.setWindowTitle(action.label + " Options") + dialog.create(loader_options) + + if not dialog.exec_(): + return None + + # Get option + options = dialog.parse() + + return options + + +def add_representation_loaders_to_menu(loaders, menu, repre_contexts): + """ + Loops through provider loaders and adds them to 'menu'. + + Expects loaders sorted in requested order. + Expects loaders de-duplicated if wanted. + + Args: + loaders(tuple): representation - loader + menu (OptionalMenu): + repre_contexts (dict): full info about representations (contains + their repre_doc, asset_doc, subset_doc, version_doc), + keys are repre_ids + + Returns: + menu (OptionalMenu): with new items + """ + # List the available loaders + for representation, loader in loaders: + label = None + repre_context = None + if representation: + label = representation.get("custom_label") + repre_context = repre_contexts[representation["_id"]] + + if not label: + label = get_label_from_loader(loader, representation) + + icon = get_icon_from_loader(loader) + + loader_options = loader.get_options([repre_context]) + + use_option = bool(loader_options) + action = OptionalAction(label, icon, use_option, menu) + if use_option: + # Add option box tip + action.set_option_tip(loader_options) + + action.setData((representation, loader)) + + # Add tooltip and statustip from Loader docstring + tip = inspect.getdoc(loader) + if tip: + action.setToolTip(tip) + action.setStatusTip(tip) + + menu.addAction(action) + + return menu + + +def remove_tool_name_from_loaders(available_loaders, tool_name): + if not tool_name: + return available_loaders + filtered_loaders = [] + for loader in available_loaders: + if hasattr(loader, "tool_names"): + if not ("*" in loader.tool_names or + tool_name in loader.tool_names): + continue + filtered_loaders.append(loader) + return filtered_loaders + + +def get_icon_from_loader(loader): + """Pull icon info from loader class""" + # Support font-awesome icons using the `.icon` and `.color` + # attributes on plug-ins. + icon = getattr(loader, "icon", None) + if icon is not None: + try: + key = "fa.{0}".format(icon) + color = getattr(loader, "color", "white") + icon = qtawesome.icon(key, color=color) + except Exception as e: + print("Unable to set icon for loader " + "{}: {}".format(loader, e)) + icon = None + return icon + + +def get_label_from_loader(loader, representation=None): + """Pull label info from loader class""" + label = getattr(loader, "label", None) + if label is None: + label = loader.__name__ + if representation: + # Add the representation as suffix + label = "{0} ({1})".format(label, representation['name']) + return label + + +def get_no_loader_action(menu, one_item_selected=False): + """Creates dummy no loader option in 'menu'""" + submsg = "your selection." + if one_item_selected: + submsg = "this version." + msg = "No compatible loaders for {}".format(submsg) + print(msg) + icon = qtawesome.icon( + "fa.exclamation", + color=QtGui.QColor(255, 51, 0) + ) + action = OptionalAction(("*" + msg), icon, False, menu) + return action + + +def sort_loaders(loaders, custom_sorter=None): + def sorter(value): + """Sort the Loaders by their order and then their name""" + Plugin = value[1] + return Plugin.order, Plugin.__name__ + + if not custom_sorter: + custom_sorter = sorter + + return sorted(loaders, key=custom_sorter) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py new file mode 100644 index 0000000000..253341f70d --- /dev/null +++ b/openpype/tools/loader/model.py @@ -0,0 +1,1191 @@ +import copy +import re +import math + +from avalon import ( + style, + schema +) +from Qt import QtCore, QtGui + +from avalon.vendor import qtawesome +from avalon.lib import HeroVersionType + +from openpype.tools.utils.models import TreeModel, Item +from openpype.tools.utils import lib + +from openpype.modules import ModulesManager + + +def is_filtering_recursible(): + """Does Qt binding support recursive filtering for QSortFilterProxyModel? + + (NOTE) Recursive filtering was introduced in Qt 5.10. + + """ + return hasattr(QtCore.QSortFilterProxyModel, + "setRecursiveFilteringEnabled") + + +class BaseRepresentationModel(object): + """Methods for SyncServer useful in multiple models""" + + def reset_sync_server(self, project_name=None): + """Sets/Resets sync server vars after every change (refresh.)""" + repre_icons = {} + sync_server = None + active_site = active_provider = None + remote_site = remote_provider = None + + if not project_name: + project_name = self.dbcon.Session["AVALON_PROJECT"] + else: + self.dbcon.Session["AVALON_PROJECT"] = project_name + + if project_name: + manager = ModulesManager() + sync_server = manager.modules_by_name["sync_server"] + + if project_name in sync_server.get_enabled_projects(): + active_site = sync_server.get_active_site(project_name) + active_provider = sync_server.get_provider_for_site( + project_name, active_site) + if active_site == 'studio': # for studio use explicit icon + active_provider = 'studio' + + remote_site = sync_server.get_remote_site(project_name) + remote_provider = sync_server.get_provider_for_site( + project_name, remote_site) + if remote_site == 'studio': # for studio use explicit icon + remote_provider = 'studio' + + repre_icons = lib.get_repre_icons() + + self.repre_icons = repre_icons + self.sync_server = sync_server + self.active_site = active_site + self.active_provider = active_provider + self.remote_site = remote_site + self.remote_provider = remote_provider + + +class SubsetsModel(TreeModel, BaseRepresentationModel): + + doc_fetched = QtCore.Signal() + refreshed = QtCore.Signal(bool) + + Columns = [ + "subset", + "asset", + "family", + "version", + "time", + "author", + "frames", + "duration", + "handles", + "step", + "repre_info" + ] + + column_labels_mapping = { + "subset": "Subset", + "asset": "Asset", + "family": "Family", + "version": "Version", + "time": "Time", + "author": "Author", + "frames": "Frames", + "duration": "Duration", + "handles": "Handles", + "step": "Step", + "repre_info": "Availability" + } + + SortAscendingRole = QtCore.Qt.UserRole + 2 + SortDescendingRole = QtCore.Qt.UserRole + 3 + merged_subset_colors = [ + (55, 161, 222), # Light Blue + (231, 176, 0), # Yellow + (154, 13, 255), # Purple + (130, 184, 30), # Light Green + (211, 79, 63), # Light Red + (179, 181, 182), # Grey + (194, 57, 179), # Pink + (0, 120, 215), # Dark Blue + (0, 204, 106), # Dark Green + (247, 99, 12), # Orange + ] + not_last_hero_brush = QtGui.QBrush(QtGui.QColor(254, 121, 121)) + + # Should be minimum of required asset document keys + asset_doc_projection = { + "name": 1, + "label": 1 + } + # Should be minimum of required subset document keys + subset_doc_projection = { + "name": 1, + "parent": 1, + "schema": 1, + "families": 1, + "data.subsetGroup": 1 + } + + def __init__( + self, + dbcon, + groups_config, + family_config_cache, + grouping=True, + parent=None, + asset_doc_projection=None, + subset_doc_projection=None + ): + super(SubsetsModel, self).__init__(parent=parent) + + self.dbcon = dbcon + + # Projections for Mongo queries + # - let ability to modify them if used in tools that require more than + # defaults + if asset_doc_projection: + self.asset_doc_projection = asset_doc_projection + + if subset_doc_projection: + self.subset_doc_projection = subset_doc_projection + + self.asset_doc_projection = asset_doc_projection + self.subset_doc_projection = subset_doc_projection + + self.repre_icons = {} + self.sync_server = None + self.active_site = self.active_provider = None + + self.columns_index = dict( + (key, idx) for idx, key in enumerate(self.Columns) + ) + self._asset_ids = None + + self.groups_config = groups_config + self.family_config_cache = family_config_cache + self._sorter = None + self._grouping = grouping + self._icons = { + "subset": qtawesome.icon("fa.file-o", color=style.colors.default) + } + + self._doc_fetching_thread = None + self._doc_fetching_stop = False + self._doc_payload = {} + + self.doc_fetched.connect(self.on_doc_fetched) + + self.refresh() + + def set_assets(self, asset_ids): + self._asset_ids = asset_ids + self.refresh() + + def set_grouping(self, state): + self._grouping = state + self.on_doc_fetched() + + def setData(self, index, value, role=QtCore.Qt.EditRole): + # Trigger additional edit when `version` column changed + # because it also updates the information in other columns + if index.column() == self.columns_index["version"]: + item = index.internalPointer() + parent = item["_id"] + if isinstance(value, HeroVersionType): + versions = list(self.dbcon.find({ + "type": {"$in": ["version", "hero_version"]}, + "parent": parent + }, sort=[("name", -1)])) + + version = None + last_version = None + for __version in versions: + if __version["type"] == "hero_version": + version = __version + elif last_version is None: + last_version = __version + + if version is not None and last_version is not None: + break + + _version = None + for __version in versions: + if __version["_id"] == version["version_id"]: + _version = __version + break + + version["data"] = _version["data"] + version["name"] = _version["name"] + version["is_from_latest"] = ( + last_version["_id"] == _version["_id"] + ) + + else: + version = self.dbcon.find_one({ + "name": value, + "type": "version", + "parent": parent + }) + + # update availability on active site when version changes + if self.sync_server.enabled and version: + site = self.active_site + query = self._repre_per_version_pipeline([version["_id"]], + site) + docs = list(self.dbcon.aggregate(query)) + if docs: + repre = docs.pop() + version["data"].update(self._get_repre_dict(repre)) + + self.set_version(index, version) + + return super(SubsetsModel, self).setData(index, value, role) + + def set_version(self, index, version): + """Update the version data of the given index. + + Arguments: + index (QtCore.QModelIndex): The model index. + version (dict) Version document in the database. + + """ + + assert isinstance(index, QtCore.QModelIndex) + if not index.isValid(): + return + + item = index.internalPointer() + + assert version["parent"] == item["_id"], ( + "Version does not belong to subset" + ) + + # Get the data from the version + version_data = version.get("data", dict()) + + # Compute frame ranges (if data is present) + frame_start = version_data.get( + "frameStart", + # backwards compatibility + version_data.get("startFrame", None) + ) + frame_end = version_data.get( + "frameEnd", + # backwards compatibility + version_data.get("endFrame", None) + ) + + handle_start = version_data.get("handleStart", None) + handle_end = version_data.get("handleEnd", None) + if handle_start is not None and handle_end is not None: + handles = "{}-{}".format(str(handle_start), str(handle_end)) + else: + handles = version_data.get("handles", None) + + if frame_start is not None and frame_end is not None: + # Remove superfluous zeros from numbers (3.0 -> 3) to improve + # readability for most frame ranges + start_clean = ("%f" % frame_start).rstrip("0").rstrip(".") + end_clean = ("%f" % frame_end).rstrip("0").rstrip(".") + frames = "{0}-{1}".format(start_clean, end_clean) + duration = frame_end - frame_start + 1 + else: + frames = None + duration = None + + schema_maj_version, _ = schema.get_schema_version(item["schema"]) + if schema_maj_version < 3: + families = version_data.get("families", [None]) + else: + families = item["data"]["families"] + + family = None + if families: + family = families[0] + + family_config = self.family_config_cache.family_config(family) + + item.update({ + "version": version["name"], + "version_document": version, + "author": version_data.get("author", None), + "time": version_data.get("time", None), + "family": family, + "familyLabel": family_config.get("label", family), + "familyIcon": family_config.get("icon", None), + "families": set(families), + "frameStart": frame_start, + "frameEnd": frame_end, + "duration": duration, + "handles": handles, + "frames": frames, + "step": version_data.get("step", None), + }) + + repre_info = version_data.get("repre_info") + if repre_info: + item["repre_info"] = repre_info + item["repre_icon"] = version_data.get("repre_icon") + + def _fetch(self): + asset_docs = self.dbcon.find( + { + "type": "asset", + "_id": {"$in": self._asset_ids} + }, + self.asset_doc_projection + ) + asset_docs_by_id = { + asset_doc["_id"]: asset_doc + for asset_doc in asset_docs + } + + subset_docs_by_id = {} + subset_docs = self.dbcon.find( + { + "type": "subset", + "parent": {"$in": self._asset_ids} + }, + self.subset_doc_projection + ) + for subset in subset_docs: + if self._doc_fetching_stop: + return + subset_docs_by_id[subset["_id"]] = subset + + subset_ids = list(subset_docs_by_id.keys()) + _pipeline = [ + # Find all versions of those subsets + {"$match": { + "type": "version", + "parent": {"$in": subset_ids} + }}, + # Sorting versions all together + {"$sort": {"name": 1}}, + # Group them by "parent", but only take the last + {"$group": { + "_id": "$parent", + "_version_id": {"$last": "$_id"}, + "name": {"$last": "$name"}, + "type": {"$last": "$type"}, + "data": {"$last": "$data"}, + "locations": {"$last": "$locations"}, + "schema": {"$last": "$schema"} + }} + ] + last_versions_by_subset_id = dict() + for doc in self.dbcon.aggregate(_pipeline): + if self._doc_fetching_stop: + return + doc["parent"] = doc["_id"] + doc["_id"] = doc.pop("_version_id") + last_versions_by_subset_id[doc["parent"]] = doc + + hero_versions = self.dbcon.find({ + "type": "hero_version", + "parent": {"$in": subset_ids} + }) + missing_versions = [] + for hero_version in hero_versions: + version_id = hero_version["version_id"] + if version_id not in last_versions_by_subset_id: + missing_versions.append(version_id) + + missing_versions_by_id = {} + if missing_versions: + missing_version_docs = self.dbcon.find({ + "type": "version", + "_id": {"$in": missing_versions} + }) + missing_versions_by_id = { + missing_version_doc["_id"]: missing_version_doc + for missing_version_doc in missing_version_docs + } + + for hero_version in hero_versions: + version_id = hero_version["version_id"] + subset_id = hero_version["parent"] + + version_doc = last_versions_by_subset_id.get(subset_id) + if version_doc is None: + version_doc = missing_versions_by_id.get(version_id) + if version_doc is None: + continue + + hero_version["data"] = version_doc["data"] + hero_version["name"] = HeroVersionType(version_doc["name"]) + # Add information if hero version is from latest version + hero_version["is_from_latest"] = version_id == version_doc["_id"] + + last_versions_by_subset_id[subset_id] = hero_version + + self._doc_payload = { + "asset_docs_by_id": asset_docs_by_id, + "subset_docs_by_id": subset_docs_by_id, + "last_versions_by_subset_id": last_versions_by_subset_id + } + + if self.sync_server.enabled: + version_ids = set() + for _subset_id, doc in last_versions_by_subset_id.items(): + version_ids.add(doc["_id"]) + + site = self.active_site + query = self._repre_per_version_pipeline(list(version_ids), site) + + repre_info = {} + for doc in self.dbcon.aggregate(query): + if self._doc_fetching_stop: + return + doc["provider"] = self.active_provider + repre_info[doc["_id"]] = doc + + self._doc_payload["repre_info_by_version_id"] = repre_info + + self.doc_fetched.emit() + + def fetch_subset_and_version(self): + """Query all subsets and latest versions from aggregation + (NOTE) The returned version documents are NOT the real version + document, it's generated from the MongoDB's aggregation so + some of the first level field may not be presented. + """ + self._doc_payload = {} + self._doc_fetching_stop = False + self._doc_fetching_thread = lib.create_qthread(self._fetch) + self._doc_fetching_thread.start() + + def stop_fetch_thread(self): + if self._doc_fetching_thread is not None: + self._doc_fetching_stop = True + while self._doc_fetching_thread.isRunning(): + pass + + def refresh(self): + self.stop_fetch_thread() + self.clear() + + self.reset_sync_server() + + if not self._asset_ids: + self.doc_fetched.emit() + return + + self.fetch_subset_and_version() + + def on_doc_fetched(self): + self.clear() + self.beginResetModel() + + asset_docs_by_id = self._doc_payload.get( + "asset_docs_by_id" + ) + subset_docs_by_id = self._doc_payload.get( + "subset_docs_by_id" + ) + last_versions_by_subset_id = self._doc_payload.get( + "last_versions_by_subset_id" + ) + + repre_info_by_version_id = self._doc_payload.get( + "repre_info_by_version_id" + ) + + if ( + asset_docs_by_id is None + or subset_docs_by_id is None + or last_versions_by_subset_id is None + or len(self._asset_ids) == 0 + ): + self.endResetModel() + self.refreshed.emit(False) + return + + self._fill_subset_items( + asset_docs_by_id, subset_docs_by_id, last_versions_by_subset_id, + repre_info_by_version_id + ) + + def create_multiasset_group( + self, subset_name, asset_ids, subset_counter, parent_item=None + ): + subset_color = self.merged_subset_colors[ + subset_counter % len(self.merged_subset_colors) + ] + merge_group = Item() + merge_group.update({ + "subset": "{} ({})".format(subset_name, len(asset_ids)), + "isMerged": True, + "childRow": 0, + "subsetColor": subset_color, + "assetIds": list(asset_ids), + "icon": qtawesome.icon( + "fa.circle", + color="#{0:02x}{1:02x}{2:02x}".format(*subset_color) + ) + }) + + subset_counter += 1 + self.add_child(merge_group, parent_item) + + return merge_group + + def _fill_subset_items( + self, asset_docs_by_id, subset_docs_by_id, last_versions_by_subset_id, + repre_info_by_version_id + ): + _groups_tuple = self.groups_config.split_subsets_for_groups( + subset_docs_by_id.values(), self._grouping + ) + groups, subset_docs_without_group, subset_docs_by_group = _groups_tuple + + group_item_by_name = {} + for group_data in groups: + group_name = group_data["name"] + group_item = Item() + group_item.update({ + "subset": group_name, + "isGroup": True, + "childRow": 0 + }) + group_item.update(group_data) + + self.add_child(group_item) + + group_item_by_name[group_name] = { + "item": group_item, + "index": self.index(group_item.row(), 0) + } + + subset_counter = 0 + for group_name, subset_docs_by_name in subset_docs_by_group.items(): + parent_item = group_item_by_name[group_name]["item"] + parent_index = group_item_by_name[group_name]["index"] + for subset_name in sorted(subset_docs_by_name.keys()): + subset_docs = subset_docs_by_name[subset_name] + asset_ids = [ + subset_doc["parent"] for subset_doc in subset_docs + ] + if len(subset_docs) > 1: + _parent_item = self.create_multiasset_group( + subset_name, asset_ids, subset_counter, parent_item + ) + _parent_index = self.index( + _parent_item.row(), 0, parent_index + ) + subset_counter += 1 + else: + _parent_item = parent_item + _parent_index = parent_index + + for subset_doc in subset_docs: + asset_id = subset_doc["parent"] + + data = copy.deepcopy(subset_doc) + data["subset"] = subset_name + data["asset"] = asset_docs_by_id[asset_id]["name"] + + last_version = last_versions_by_subset_id.get( + subset_doc["_id"] + ) + data["last_version"] = last_version + + # do not show subset without version + if not last_version: + continue + + data.update( + self._get_last_repre_info(repre_info_by_version_id, + last_version["_id"])) + + item = Item() + item.update(data) + self.add_child(item, _parent_item) + + index = self.index(item.row(), 0, _parent_index) + self.set_version(index, last_version) + + for subset_name in sorted(subset_docs_without_group.keys()): + subset_docs = subset_docs_without_group[subset_name] + asset_ids = [subset_doc["parent"] for subset_doc in subset_docs] + parent_item = None + parent_index = None + if len(subset_docs) > 1: + parent_item = self.create_multiasset_group( + subset_name, asset_ids, subset_counter + ) + parent_index = self.index(parent_item.row(), 0) + subset_counter += 1 + + for subset_doc in subset_docs: + asset_id = subset_doc["parent"] + + data = copy.deepcopy(subset_doc) + data["subset"] = subset_name + data["asset"] = asset_docs_by_id[asset_id]["name"] + + last_version = last_versions_by_subset_id.get( + subset_doc["_id"] + ) + data["last_version"] = last_version + + # do not show subset without version + if not last_version: + continue + + data.update( + self._get_last_repre_info(repre_info_by_version_id, + last_version["_id"])) + + item = Item() + item.update(data) + self.add_child(item, parent_item) + + index = self.index(item.row(), 0, parent_index) + self.set_version(index, last_version) + + self.endResetModel() + self.refreshed.emit(True) + + def data(self, index, role): + if not index.isValid(): + return + + if role == self.SortDescendingRole: + item = index.internalPointer() + if item.get("isGroup"): + # Ensure groups be on top when sorting by descending order + prefix = "2" + order = item["order"] + else: + if item.get("isMerged"): + prefix = "1" + else: + prefix = "0" + order = str(super(SubsetsModel, self).data( + index, QtCore.Qt.DisplayRole + )) + return prefix + order + + if role == self.SortAscendingRole: + item = index.internalPointer() + if item.get("isGroup"): + # Ensure groups be on top when sorting by ascending order + prefix = "0" + order = item["order"] + else: + if item.get("isMerged"): + prefix = "1" + else: + prefix = "2" + order = str(super(SubsetsModel, self).data( + index, QtCore.Qt.DisplayRole + )) + return prefix + order + + if role == QtCore.Qt.DisplayRole: + if index.column() == self.columns_index["family"]: + # Show familyLabel instead of family + item = index.internalPointer() + return item.get("familyLabel", None) + + elif role == QtCore.Qt.DecorationRole: + + # Add icon to subset column + if index.column() == self.columns_index["subset"]: + item = index.internalPointer() + if item.get("isGroup") or item.get("isMerged"): + return item["icon"] + else: + return self._icons["subset"] + + # Add icon to family column + if index.column() == self.columns_index["family"]: + item = index.internalPointer() + return item.get("familyIcon", None) + + if index.column() == self.columns_index.get("repre_info"): + item = index.internalPointer() + return item.get("repre_icon", None) + + elif role == QtCore.Qt.ForegroundRole: + item = index.internalPointer() + version_doc = item.get("version_document") + if version_doc and version_doc.get("type") == "hero_version": + if not version_doc["is_from_latest"]: + return self.not_last_hero_brush + + return super(SubsetsModel, self).data(index, role) + + def flags(self, index): + flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + # Make the version column editable + if index.column() == self.columns_index["version"]: + flags |= QtCore.Qt.ItemIsEditable + + return flags + + def headerData(self, section, orientation, role): + """Remap column names to labels""" + if role == QtCore.Qt.DisplayRole: + if section < len(self.Columns): + key = self.Columns[section] + return self.column_labels_mapping.get(key) or key + + super(TreeModel, self).headerData(section, orientation, role) + + def _get_last_repre_info(self, repre_info_by_version_id, last_version_id): + data = {} + if repre_info_by_version_id: + repre_info = repre_info_by_version_id.get(last_version_id) + return self._get_repre_dict(repre_info) + + return data + + def _get_repre_dict(self, repre_info): + """Returns icon and str representation of availability""" + data = {} + if repre_info: + repres_str = "{}/{}".format( + int(math.floor(float(repre_info['avail_repre']))), + int(math.floor(float(repre_info['repre_count'])))) + + data["repre_info"] = repres_str + data["repre_icon"] = self.repre_icons.get(self.active_provider) + + return data + + def _repre_per_version_pipeline(self, version_ids, site): + query = [ + {"$match": {"parent": {"$in": version_ids}, + "type": "representation", + "files.sites.name": {"$exists": 1}}}, + {"$unwind": "$files"}, + {'$addFields': { + 'order_local': { + '$filter': {'input': '$files.sites', 'as': 'p', + 'cond': {'$eq': ['$$p.name', site]} + }} + }}, + {'$addFields': { + 'progress_local': {"$arrayElemAt": [{ + '$cond': [{'$size': "$order_local.progress"}, + "$order_local.progress", + # if exists created_dt count is as available + {'$cond': [ + {'$size': "$order_local.created_dt"}, + [1], + [0] + ]} + ]}, 0]} + }}, + {'$group': { # first group by repre + '_id': '$_id', + 'parent': {'$first': '$parent'}, + 'files_count': {'$sum': 1}, + 'files_avail': {'$sum': "$progress_local"}, + 'avail_ratio': {'$first': { + '$divide': [{'$sum': "$progress_local"}, {'$sum': 1}]}} + }}, + {'$group': { # second group by parent, eg version_id + '_id': '$parent', + 'repre_count': {'$sum': 1}, # total representations + # fully available representation for site + 'avail_repre': {'$sum': "$avail_ratio"} + }}, + ] + return query + + +class GroupMemberFilterProxyModel(QtCore.QSortFilterProxyModel): + """Provide the feature of filtering group by the acceptance of members + + The subset group nodes will not be filtered directly, the group node's + acceptance depends on it's child subsets' acceptance. + + """ + + if is_filtering_recursible(): + def _is_group_acceptable(self, index, node): + # (NOTE) With the help of `RecursiveFiltering` feature from + # Qt 5.10, group always not be accepted by default. + return False + filter_accepts_group = _is_group_acceptable + + else: + # Patch future function + setRecursiveFilteringEnabled = (lambda *args: None) + + def _is_group_acceptable(self, index, model): + # (NOTE) This is not recursive. + for child_row in range(model.rowCount(index)): + if self.filterAcceptsRow(child_row, index): + return True + return False + filter_accepts_group = _is_group_acceptable + + def __init__(self, *args, **kwargs): + super(GroupMemberFilterProxyModel, self).__init__(*args, **kwargs) + self.setRecursiveFilteringEnabled(True) + + +class SubsetFilterProxyModel(GroupMemberFilterProxyModel): + def filterAcceptsRow(self, row, parent): + model = self.sourceModel() + index = model.index(row, self.filterKeyColumn(), parent) + item = index.internalPointer() + if item.get("isGroup"): + return self.filter_accepts_group(index, model) + return super( + SubsetFilterProxyModel, self + ).filterAcceptsRow(row, parent) + + +class FamiliesFilterProxyModel(GroupMemberFilterProxyModel): + """Filters to specified families""" + + def __init__(self, family_config_cache, *args, **kwargs): + super(FamiliesFilterProxyModel, self).__init__(*args, **kwargs) + self._families = set() + self.family_config_cache = family_config_cache + + def familyFilter(self): + return self._families + + def setFamiliesFilter(self, values): + """Set the families to include""" + assert isinstance(values, (tuple, list, set)) + self._families = set(values) + self.invalidateFilter() + + def filterAcceptsRow(self, row=0, parent=None): + if not self._families: + return False + + model = self.sourceModel() + index = model.index(row, 0, parent=parent or QtCore.QModelIndex()) + + # Ensure index is valid + if not index.isValid() or index is None: + return True + + # Get the item data and validate + item = model.data(index, TreeModel.ItemRole) + + if item.get("isGroup"): + return self.filter_accepts_group(index, model) + + family = item.get("family") + if not family: + return True + + family_config = self.family_config_cache.family_config(family) + if family_config.get("hideFilter"): + return False + + # We want to keep the families which are not in the list + return family in self._families + + def sort(self, column, order): + proxy = self.sourceModel() + model = proxy.sourceModel() + # We need to know the sorting direction for pinning groups on top + if order == QtCore.Qt.AscendingOrder: + self.setSortRole(model.SortAscendingRole) + else: + self.setSortRole(model.SortDescendingRole) + + super(FamiliesFilterProxyModel, self).sort(column, order) + + +class RepresentationSortProxyModel(GroupMemberFilterProxyModel): + """To properly sort progress string""" + def lessThan(self, left, right): + source_model = self.sourceModel() + progress_indexes = [source_model.Columns.index("active_site"), + source_model.Columns.index("remote_site")] + if left.column() in progress_indexes: + left_data = self.sourceModel().data(left, QtCore.Qt.DisplayRole) + right_data = self.sourceModel().data(right, QtCore.Qt.DisplayRole) + left_val = re.sub("[^0-9]", '', left_data) + right_val = re.sub("[^0-9]", '', right_data) + + return int(left_val) < int(right_val) + + return super(RepresentationSortProxyModel, self).lessThan(left, right) + + +class RepresentationModel(TreeModel, BaseRepresentationModel): + + doc_fetched = QtCore.Signal() + refreshed = QtCore.Signal(bool) + + SiteNameRole = QtCore.Qt.UserRole + 2 + ProgressRole = QtCore.Qt.UserRole + 3 + SiteSideRole = QtCore.Qt.UserRole + 4 + IdRole = QtCore.Qt.UserRole + 5 + ContextRole = QtCore.Qt.UserRole + 6 + + Columns = [ + "name", + "subset", + "asset", + "active_site", + "remote_site" + ] + + column_labels_mapping = { + "name": "Name", + "subset": "Subset", + "asset": "Asset", + "active_site": "Active", + "remote_site": "Remote" + } + + def __init__(self, dbcon, header, version_ids): + super(RepresentationModel, self).__init__() + self.dbcon = dbcon + self._data = [] + self._header = header + self.version_ids = version_ids + + manager = ModulesManager() + sync_server = active_site = remote_site = None + active_provider = remote_provider = None + + project = dbcon.Session["AVALON_PROJECT"] + if project: + sync_server = manager.modules_by_name["sync_server"] + active_site = sync_server.get_active_site(project) + remote_site = sync_server.get_remote_site(project) + + # TODO refactor + active_provider = \ + sync_server.get_provider_for_site(project, + active_site) + if active_site == 'studio': + active_provider = 'studio' + + remote_provider = \ + sync_server.get_provider_for_site(project, + remote_site) + + if remote_site == 'studio': + remote_provider = 'studio' + + self.sync_server = sync_server + self.active_site = active_site + self.active_provider = active_provider + self.remote_site = remote_site + self.remote_provider = remote_provider + + self.doc_fetched.connect(self.on_doc_fetched) + + self._docs = {} + self._icons = lib.get_repre_icons() + self._icons["repre"] = qtawesome.icon("fa.file-o", + color=style.colors.default) + + def set_version_ids(self, version_ids): + self.version_ids = version_ids + self.refresh() + + def data(self, index, role): + item = index.internalPointer() + + if role == self.IdRole: + return item.get("_id") + + if role == QtCore.Qt.DecorationRole: + # Add icon to subset column + if index.column() == self.Columns.index("name"): + if item.get("isMerged"): + return item["icon"] + else: + return self._icons["repre"] + + active_index = self.Columns.index("active_site") + remote_index = self.Columns.index("remote_site") + if role == QtCore.Qt.DisplayRole: + progress = None + label = '' + if index.column() == active_index: + progress = item.get("active_site_progress", 0) + elif index.column() == remote_index: + progress = item.get("remote_site_progress", 0) + + if progress is not None: + # site added, sync in progress + progress_str = "not avail." + if progress >= 0: + # progress == 0 for isMerged is unavailable + if progress == 0 and item.get("isMerged"): + progress_str = "not avail." + else: + progress_str = "{}% {}".format(int(progress * 100), + label) + + return progress_str + + if role == QtCore.Qt.DecorationRole: + if index.column() == active_index: + return item.get("active_site_icon", None) + if index.column() == remote_index: + return item.get("remote_site_icon", None) + + if role == self.SiteNameRole: + if index.column() == active_index: + return item.get("active_site_name", None) + if index.column() == remote_index: + return item.get("remote_site_name", None) + + if role == self.SiteSideRole: + if index.column() == active_index: + return "active" + if index.column() == remote_index: + return "remote" + + if role == self.ProgressRole: + if index.column() == active_index: + return item.get("active_site_progress", 0) + if index.column() == remote_index: + return item.get("remote_site_progress", 0) + + return super(RepresentationModel, self).data(index, role) + + def on_doc_fetched(self): + self.clear() + self.beginResetModel() + subsets = set() + assets = set() + repre_groups = {} + repre_groups_items = {} + group = None + self._items_by_id = {} + for doc in self._docs: + if len(self.version_ids) > 1: + group = repre_groups.get(doc["name"]) + if not group: + group_item = Item() + group_item.update({ + "_id": doc["_id"], + "name": doc["name"], + "isMerged": True, + "childRow": 0, + "active_site_name": self.active_site, + "remote_site_name": self.remote_site, + "icon": qtawesome.icon( + "fa.folder", + color=style.colors.default + ) + }) + self.add_child(group_item, None) + repre_groups[doc["name"]] = group_item + repre_groups_items[doc["name"]] = 0 + group = group_item + + progress = lib.get_progress_for_repre(doc, + self.active_site, + self.remote_site) + + active_site_icon = self._icons.get(self.active_provider) + remote_site_icon = self._icons.get(self.remote_provider) + + data = { + "_id": doc["_id"], + "name": doc["name"], + "subset": doc["context"]["subset"], + "asset": doc["context"]["asset"], + "isMerged": False, + + "active_site_icon": active_site_icon, + "remote_site_icon": remote_site_icon, + "active_site_name": self.active_site, + "remote_site_name": self.remote_site, + "active_site_progress": progress[self.active_site], + "remote_site_progress": progress[self.remote_site] + } + subsets.add(doc["context"]["subset"]) + assets.add(doc["context"]["subset"]) + + item = Item() + item.update(data) + + current_progress = { + 'active_site_progress': progress[self.active_site], + 'remote_site_progress': progress[self.remote_site] + } + if group: + group = self._sum_group_progress(doc["name"], group, + current_progress, + repre_groups_items) + + self.add_child(item, group) + + # finalize group average progress + for group_name, group in repre_groups.items(): + items_cnt = repre_groups_items[group_name] + active_progress = group.get("active_site_progress", 0) + group["active_site_progress"] = active_progress / items_cnt + remote_progress = group.get("remote_site_progress", 0) + group["remote_site_progress"] = remote_progress / items_cnt + + self.endResetModel() + self.refreshed.emit(False) + + def refresh(self): + docs = [] + session_project = self.dbcon.Session['AVALON_PROJECT'] + if not session_project: + return + + if self.version_ids: + # Simple find here for now, expected to receive lower number of + # representations and logic could be in Python + docs = list(self.dbcon.find( + {"type": "representation", "parent": {"$in": self.version_ids}, + "files.sites.name": {"$exists": 1}}, self.projection())) + self._docs = docs + + self.doc_fetched.emit() + + @classmethod + def projection(cls): + return { + "_id": 1, + "name": 1, + "context.subset": 1, + "context.asset": 1, + "context.version": 1, + "context.representation": 1, + 'files.sites': 1 + } + + def _sum_group_progress(self, repre_name, group, current_item_progress, + repre_groups_items): + """ + Update final group progress + Called after every item in group is added + + Args: + repre_name(string) + group(dict): info about group of selected items + current_item_progress(dict): {'active_site_progress': XX, + 'remote_site_progress': YY} + repre_groups_items(dict) + Returns: + (dict): updated group info + """ + repre_groups_items[repre_name] += 1 + + for key, progress in current_item_progress.items(): + group[key] = (group.get(key, 0) + max(progress, 0)) + + return group diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py new file mode 100644 index 0000000000..9479558026 --- /dev/null +++ b/openpype/tools/loader/widgets.py @@ -0,0 +1,1457 @@ +import os +import sys +import inspect +import datetime +import pprint +import traceback +import collections + +from Qt import QtWidgets, QtCore, QtGui + +from avalon import api, io, pipeline +from avalon.lib import HeroVersionType + +from openpype.tools.utils import lib as tools_lib +from openpype.tools.utils.delegates import ( + VersionDelegate, + PrettyTimeDelegate +) +from openpype.tools.utils.widgets import OptionalMenu +from openpype.tools.utils.views import ( + TreeViewSpinner, + DeselectableTreeView +) + +from .model import ( + SubsetsModel, + SubsetFilterProxyModel, + FamiliesFilterProxyModel, + RepresentationModel, + RepresentationSortProxyModel +) +from . import lib + + +class OverlayFrame(QtWidgets.QFrame): + def __init__(self, label, parent): + super(OverlayFrame, self).__init__(parent) + + label_widget = QtWidgets.QLabel(label, self) + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter) + + self.label_widget = label_widget + + label_widget.setStyleSheet("background: transparent;") + self.setStyleSheet(( + "background: rgba(0, 0, 0, 127);" + "font-size: 60pt;" + )) + + def set_label(self, label): + self.label_widget.setText(label) + + +class LoadErrorMessageBox(QtWidgets.QDialog): + def __init__(self, messages, parent=None): + super(LoadErrorMessageBox, self).__init__(parent) + self.setWindowTitle("Loading failed") + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + body_layout = QtWidgets.QVBoxLayout(self) + + main_label = ( + "Failed to load items" + ) + main_label_widget = QtWidgets.QLabel(main_label, self) + body_layout.addWidget(main_label_widget) + + item_name_template = ( + "Subset: {}
    " + "Version: {}
    " + "Representation: {}
    " + ) + exc_msg_template = "{}" + + for exc_msg, tb, repre, subset, version in messages: + line = self._create_line() + body_layout.addWidget(line) + + item_name = item_name_template.format(subset, version, repre) + item_name_widget = QtWidgets.QLabel( + item_name.replace("\n", "
    "), self + ) + body_layout.addWidget(item_name_widget) + + exc_msg = exc_msg_template.format(exc_msg.replace("\n", "
    ")) + message_label_widget = QtWidgets.QLabel(exc_msg, self) + body_layout.addWidget(message_label_widget) + + if tb: + tb_widget = QtWidgets.QLabel(tb.replace("\n", "
    "), self) + tb_widget.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + body_layout.addWidget(tb_widget) + + footer_widget = QtWidgets.QWidget(self) + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + buttonBox = QtWidgets.QDialogButtonBox(QtCore.Qt.Vertical) + buttonBox.setStandardButtons( + QtWidgets.QDialogButtonBox.StandardButton.Ok + ) + buttonBox.accepted.connect(self._on_accept) + footer_layout.addWidget(buttonBox, alignment=QtCore.Qt.AlignRight) + body_layout.addWidget(footer_widget) + + def _on_accept(self): + self.close() + + def _create_line(self): + line = QtWidgets.QFrame(self) + line.setFixedHeight(2) + line.setFrameShape(QtWidgets.QFrame.HLine) + line.setFrameShadow(QtWidgets.QFrame.Sunken) + return line + + +class SubsetWidget(QtWidgets.QWidget): + """A widget that lists the published subsets for an asset""" + + active_changed = QtCore.Signal() # active index changed + version_changed = QtCore.Signal() # version state changed for a subset + load_started = QtCore.Signal() + load_ended = QtCore.Signal() + + default_widths = ( + ("subset", 200), + ("asset", 130), + ("family", 90), + ("version", 60), + ("time", 125), + ("author", 75), + ("frames", 75), + ("duration", 60), + ("handles", 55), + ("step", 10), + ("repre_info", 65) + ) + + def __init__( + self, + dbcon, + groups_config, + family_config_cache, + enable_grouping=True, + tool_name=None, + parent=None + ): + super(SubsetWidget, self).__init__(parent=parent) + + self.dbcon = dbcon + self.tool_name = tool_name + + model = SubsetsModel( + dbcon, + groups_config, + family_config_cache, + grouping=enable_grouping + ) + proxy = SubsetFilterProxyModel() + family_proxy = FamiliesFilterProxyModel(family_config_cache) + family_proxy.setSourceModel(proxy) + + subset_filter = QtWidgets.QLineEdit() + subset_filter.setPlaceholderText("Filter subsets..") + + groupable = QtWidgets.QCheckBox("Enable Grouping") + groupable.setChecked(enable_grouping) + + top_bar_layout = QtWidgets.QHBoxLayout() + top_bar_layout.addWidget(subset_filter) + top_bar_layout.addWidget(groupable) + + view = TreeViewSpinner() + view.setObjectName("SubsetView") + view.setIndentation(20) + view.setStyleSheet(""" + QTreeView::item{ + padding: 5px 1px; + border: 0px; + } + """) + view.setAllColumnsShowFocus(True) + + # Set view delegates + version_delegate = VersionDelegate(self.dbcon) + column = model.Columns.index("version") + view.setItemDelegateForColumn(column, version_delegate) + + time_delegate = PrettyTimeDelegate() + column = model.Columns.index("time") + view.setItemDelegateForColumn(column, time_delegate) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(top_bar_layout) + layout.addWidget(view) + + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + view.setSortingEnabled(True) + view.sortByColumn(1, QtCore.Qt.AscendingOrder) + view.setAlternatingRowColors(True) + + self.data = { + "delegates": { + "version": version_delegate, + "time": time_delegate + }, + "state": { + "groupable": groupable + } + } + + self.proxy = proxy + self.model = model + self.view = view + self.filter = subset_filter + self.family_proxy = family_proxy + + # settings and connections + self.proxy.setSourceModel(self.model) + self.proxy.setDynamicSortFilter(True) + self.proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + + self.view.setModel(self.family_proxy) + self.view.customContextMenuRequested.connect(self.on_context_menu) + + for column_name, width in self.default_widths: + idx = model.Columns.index(column_name) + view.setColumnWidth(idx, width) + + actual_project = dbcon.Session["AVALON_PROJECT"] + self.on_project_change(actual_project) + + selection = view.selectionModel() + selection.selectionChanged.connect(self.active_changed) + + version_delegate.version_changed.connect(self.version_changed) + + groupable.stateChanged.connect(self.set_grouping) + + self.filter.textChanged.connect(self.proxy.setFilterRegExp) + self.filter.textChanged.connect(self.view.expandAll) + + self.model.refresh() + + def set_family_filters(self, families): + self.family_proxy.setFamiliesFilter(families) + + def is_groupable(self): + return self.data["state"]["groupable"].checkState() + + def set_grouping(self, state): + with tools_lib.preserve_selection(tree_view=self.view, + current_index=False): + self.model.set_grouping(state) + + def set_loading_state(self, loading, empty): + view = self.view + + if view.is_loading != loading: + if loading: + view.spinner.repaintNeeded.connect(view.viewport().update) + else: + view.spinner.repaintNeeded.disconnect() + + view.is_loading = loading + view.is_empty = empty + + def _repre_contexts_for_loaders_filter(self, items): + version_docs_by_id = { + item["version_document"]["_id"]: item["version_document"] + for item in items + } + version_docs_by_subset_id = collections.defaultdict(list) + for item in items: + subset_id = item["version_document"]["parent"] + version_docs_by_subset_id[subset_id].append( + item["version_document"] + ) + + subset_docs = list(self.dbcon.find( + { + "_id": {"$in": list(version_docs_by_subset_id.keys())}, + "type": "subset" + }, + { + "schema": 1, + "data.families": 1 + } + )) + subset_docs_by_id = { + subset_doc["_id"]: subset_doc + for subset_doc in subset_docs + } + version_ids = list(version_docs_by_id.keys()) + repre_docs = self.dbcon.find( + # Query all representations for selected versions at once + { + "type": "representation", + "parent": {"$in": version_ids} + }, + # Query only name and parent from representation + { + "name": 1, + "parent": 1 + } + ) + repre_docs_by_version_id = { + version_id: [] + for version_id in version_ids + } + repre_context_by_id = {} + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + repre_docs_by_version_id[version_id].append(repre_doc) + + version_doc = version_docs_by_id[version_id] + repre_context_by_id[repre_doc["_id"]] = { + "representation": repre_doc, + "version": version_doc, + "subset": subset_docs_by_id[version_doc["parent"]] + } + return repre_context_by_id, repre_docs_by_version_id + + def on_project_change(self, project_name): + """ + Called on each project change in parent widget. + + Checks if Sync Server is enabled for a project, pushes changes to + model. + """ + enabled = False + if project_name: + self.model.reset_sync_server(project_name) + if self.model.sync_server: + enabled_proj = self.model.sync_server.get_enabled_projects() + enabled = project_name in enabled_proj + + lib.change_visibility(self.model, self.view, "repre_info", enabled) + + def on_context_menu(self, point): + """Shows menu with loader actions on Right-click. + + Registered actions are filtered by selection and help of + `loaders_from_representation` from avalon api. Intersection of actions + is shown when more subset is selected. When there are not available + actions for selected subsets then special action is shown (works as + info message to user): "*No compatible loaders for your selection" + + """ + + point_index = self.view.indexAt(point) + if not point_index.isValid(): + return + + # Get selected subsets without groups + selection = self.view.selectionModel() + rows = selection.selectedRows(column=0) + + items = lib.get_selected_items(rows, self.model.ItemRole) + + # Get all representation->loader combinations available for the + # index under the cursor, so we can list the user the options. + available_loaders = api.discover(api.Loader) + if self.tool_name: + available_loaders = lib.remove_tool_name_from_loaders( + available_loaders, self.tool_name + ) + + repre_loaders = [] + subset_loaders = [] + for loader in available_loaders: + # Skip if its a SubsetLoader. + if api.SubsetLoader in inspect.getmro(loader): + subset_loaders.append(loader) + else: + repre_loaders.append(loader) + + loaders = list() + + # Bool if is selected only one subset + one_item_selected = (len(items) == 1) + + # Prepare variables for multiple selected subsets + first_loaders = [] + found_combinations = None + + is_first = True + repre_context_by_id, repre_docs_by_version_id = ( + self._repre_contexts_for_loaders_filter(items) + ) + for item in items: + _found_combinations = [] + version_id = item["version_document"]["_id"] + repre_docs = repre_docs_by_version_id[version_id] + for repre_doc in repre_docs: + repre_context = repre_context_by_id[repre_doc["_id"]] + for loader in pipeline.loaders_from_repre_context( + repre_loaders, + repre_context + ): + # do not allow download whole repre, select specific repre + if tools_lib.is_sync_loader(loader): + continue + + # skip multiple select variant if one is selected + if one_item_selected: + loaders.append((repre_doc, loader)) + continue + + # store loaders of first subset + if is_first: + first_loaders.append((repre_doc, loader)) + + # store combinations to compare with other subsets + _found_combinations.append( + (repre_doc["name"].lower(), loader) + ) + + # skip multiple select variant if one is selected + if one_item_selected: + continue + + is_first = False + # Store first combinations to compare + if found_combinations is None: + found_combinations = _found_combinations + # Intersect found combinations with all previous subsets + else: + found_combinations = list( + set(found_combinations) & set(_found_combinations) + ) + + if not one_item_selected: + # Filter loaders from first subset by intersected combinations + for repre, loader in first_loaders: + if (repre["name"], loader) not in found_combinations: + continue + + loaders.append((repre, loader)) + + # Subset Loaders. + for loader in subset_loaders: + loaders.append((None, loader)) + + loaders = lib.sort_loaders(loaders) + + # Prepare menu content based on selected items + menu = OptionalMenu(self) + if not loaders: + action = lib.get_no_loader_action(menu, one_item_selected) + menu.addAction(action) + else: + repre_contexts = pipeline.get_repres_contexts( + repre_context_by_id.keys(), self.dbcon) + + menu = lib.add_representation_loaders_to_menu( + loaders, menu, repre_contexts) + + # Show the context action menu + global_point = self.view.mapToGlobal(point) + action = menu.exec_(global_point) + if not action or not action.data(): + return + + # Find the representation name and loader to trigger + action_representation, loader = action.data() + + self.load_started.emit() + + if api.SubsetLoader in inspect.getmro(loader): + subset_ids = [] + subset_version_docs = {} + for item in items: + subset_id = item["version_document"]["parent"] + subset_ids.append(subset_id) + subset_version_docs[subset_id] = item["version_document"] + + # get contexts only for selected menu option + subset_contexts_by_id = pipeline.get_subset_contexts(subset_ids, + self.dbcon) + subset_contexts = list(subset_contexts_by_id.values()) + options = lib.get_options(action, loader, self, subset_contexts) + + error_info = _load_subsets_by_loader( + loader, subset_contexts, options, subset_version_docs + ) + + else: + representation_name = action_representation["name"] + + # Run the loader for all selected indices, for those that have the + # same representation available + + # Trigger + repre_ids = [] + for item in items: + representation = self.dbcon.find_one( + { + "type": "representation", + "name": representation_name, + "parent": item["version_document"]["_id"] + }, + {"_id": 1} + ) + if not representation: + self.echo("Subset '{}' has no representation '{}'".format( + item["subset"], representation_name + )) + continue + repre_ids.append(representation["_id"]) + + # get contexts only for selected menu option + repre_contexts = pipeline.get_repres_contexts(repre_ids, + self.dbcon) + options = lib.get_options(action, loader, self, + list(repre_contexts.values())) + + error_info = _load_representations_by_loader( + loader, repre_contexts, options=options + ) + + self.load_ended.emit() + + if error_info: + box = LoadErrorMessageBox(error_info) + box.show() + + def selected_subsets(self, _groups=False, _merged=False, _other=True): + selection = self.view.selectionModel() + rows = selection.selectedRows(column=0) + + subsets = list() + if not any([_groups, _merged, _other]): + self.echo(( + "This is a BUG: Selected_subsets args must contain" + " at least one value set to True" + )) + return subsets + + for row in rows: + item = row.data(self.model.ItemRole) + if item.get("isGroup"): + if not _groups: + continue + + elif item.get("isMerged"): + if not _merged: + continue + else: + if not _other: + continue + + subsets.append(item) + + return subsets + + def group_subsets(self, name, asset_ids, items): + field = "data.subsetGroup" + + if name: + update = {"$set": {field: name}} + self.echo("Group subsets to '%s'.." % name) + else: + update = {"$unset": {field: ""}} + self.echo("Ungroup subsets..") + + subsets = list() + for item in items: + subsets.append(item["subset"]) + + for asset_id in asset_ids: + filtr = { + "type": "subset", + "parent": asset_id, + "name": {"$in": subsets}, + } + self.dbcon.update_many(filtr, update) + + def echo(self, message): + print(message) + + +class VersionTextEdit(QtWidgets.QTextEdit): + """QTextEdit that displays version specific information. + + This also overrides the context menu to add actions like copying + source path to clipboard or copying the raw data of the version + to clipboard. + + """ + def __init__(self, dbcon, parent=None): + super(VersionTextEdit, self).__init__(parent=parent) + self.dbcon = dbcon + + self.data = { + "source": None, + "raw": None + } + + # Reset + self.set_version(None) + + def set_version(self, version_doc=None, version_id=None): + # TODO expect only filling data (do not query them here!) + if not version_doc and not version_id: + # Reset state to empty + self.data = { + "source": None, + "raw": None, + } + self.setText("") + self.setEnabled(True) + return + + self.setEnabled(True) + + print("Querying..") + + if not version_doc: + version_doc = self.dbcon.find_one({ + "_id": version_id, + "type": {"$in": ["version", "hero_version"]} + }) + assert version_doc, "Not a valid version id" + + if version_doc["type"] == "hero_version": + _version_doc = self.dbcon.find_one({ + "_id": version_doc["version_id"], + "type": "version" + }) + version_doc["data"] = _version_doc["data"] + version_doc["name"] = HeroVersionType( + _version_doc["name"] + ) + + subset = self.dbcon.find_one({ + "_id": version_doc["parent"], + "type": "subset" + }) + assert subset, "No valid subset parent for version" + + # Define readable creation timestamp + created = version_doc["data"]["time"] + created = datetime.datetime.strptime(created, "%Y%m%dT%H%M%SZ") + created = datetime.datetime.strftime(created, "%b %d %Y %H:%M") + + comment = version_doc["data"].get("comment", None) or "No comment" + + source = version_doc["data"].get("source", None) + source_label = source if source else "No source" + + # Store source and raw data + self.data["source"] = source + self.data["raw"] = version_doc + + if version_doc["type"] == "hero_version": + version_name = "hero" + else: + version_name = tools_lib.format_version(version_doc["name"]) + + data = { + "subset": subset["name"], + "version": version_name, + "comment": comment, + "created": created, + "source": source_label + } + + self.setHtml(( + "

    {subset}

    " + "

    {version}

    " + "Comment
    " + "{comment}

    " + + "Created
    " + "{created}

    " + + "Source
    " + "{source}" + ).format(**data)) + + def contextMenuEvent(self, event): + """Context menu with additional actions""" + menu = self.createStandardContextMenu() + + # Add additional actions when any text so we can assume + # the version is set. + if self.toPlainText().strip(): + + menu.addSeparator() + action = QtWidgets.QAction("Copy source path to clipboard", + menu) + action.triggered.connect(self.on_copy_source) + menu.addAction(action) + + action = QtWidgets.QAction("Copy raw data to clipboard", + menu) + action.triggered.connect(self.on_copy_raw) + menu.addAction(action) + + menu.exec_(event.globalPos()) + del menu + + def on_copy_source(self): + """Copy formatted source path to clipboard""" + source = self.data.get("source", None) + if not source: + return + + path = source.format(root=api.registered_root()) + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(path) + + def on_copy_raw(self): + """Copy raw version data to clipboard + + The data is string formatted with `pprint.pformat`. + + """ + raw = self.data.get("raw", None) + if not raw: + return + + raw_text = pprint.pformat(raw) + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(raw_text) + + +class ThumbnailWidget(QtWidgets.QLabel): + + aspect_ratio = (16, 9) + max_width = 300 + + def __init__(self, dbcon, parent=None): + super(ThumbnailWidget, self).__init__(parent) + self.dbcon = dbcon + + self.current_thumb_id = None + self.current_thumbnail = None + + self.setAlignment(QtCore.Qt.AlignCenter) + + # TODO get res path much better way + loader_path = os.path.dirname(os.path.abspath(__file__)) + avalon_path = os.path.dirname(os.path.dirname(loader_path)) + default_pix_path = os.path.join( + os.path.dirname(avalon_path), + "res", "tools", "images", "default_thumbnail.png" + ) + self.default_pix = QtGui.QPixmap(default_pix_path) + + def height(self): + width = self.width() + asp_w, asp_h = self.aspect_ratio + + return (width / asp_w) * asp_h + + def width(self): + width = super(ThumbnailWidget, self).width() + if width > self.max_width: + width = self.max_width + return width + + def set_pixmap(self, pixmap=None): + if not pixmap: + pixmap = self.default_pix + self.current_thumb_id = None + + self.current_thumbnail = pixmap + + pixmap = self.scale_pixmap(pixmap) + self.setPixmap(pixmap) + + def resizeEvent(self, _event): + if not self.current_thumbnail: + return + cur_pix = self.scale_pixmap(self.current_thumbnail) + self.setPixmap(cur_pix) + + def scale_pixmap(self, pixmap): + return pixmap.scaled( + self.width(), self.height(), QtCore.Qt.KeepAspectRatio + ) + + def set_thumbnail(self, entity=None): + if not entity: + self.set_pixmap() + return + + if isinstance(entity, (list, tuple)): + if len(entity) == 1: + entity = entity[0] + else: + self.set_pixmap() + return + + thumbnail_id = entity.get("data", {}).get("thumbnail_id") + if thumbnail_id == self.current_thumb_id: + if self.current_thumbnail is None: + self.set_pixmap() + return + + self.current_thumb_id = thumbnail_id + if not thumbnail_id: + self.set_pixmap() + return + + thumbnail_ent = self.dbcon.find_one( + {"type": "thumbnail", "_id": thumbnail_id} + ) + if not thumbnail_ent: + return + + thumbnail_bin = pipeline.get_thumbnail_binary( + thumbnail_ent, "thumbnail", self.dbcon + ) + if not thumbnail_bin: + self.set_pixmap() + return + + thumbnail = QtGui.QPixmap() + thumbnail.loadFromData(thumbnail_bin) + + self.set_pixmap(thumbnail) + + +class VersionWidget(QtWidgets.QWidget): + """A Widget that display information about a specific version""" + def __init__(self, dbcon, parent=None): + super(VersionWidget, self).__init__(parent=parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + label = QtWidgets.QLabel("Version", self) + data = VersionTextEdit(dbcon, self) + data.setReadOnly(True) + + layout.addWidget(label) + layout.addWidget(data) + + self.data = data + + def set_version(self, version_doc): + self.data.set_version(version_doc) + + +class FamilyListWidget(QtWidgets.QListWidget): + """A Widget that lists all available families""" + + NameRole = QtCore.Qt.UserRole + 1 + active_changed = QtCore.Signal(list) + + def __init__(self, dbcon, family_config_cache, parent=None): + super(FamilyListWidget, self).__init__(parent=parent) + + self.family_config_cache = family_config_cache + self.dbcon = dbcon + + multi_select = QtWidgets.QAbstractItemView.ExtendedSelection + self.setSelectionMode(multi_select) + self.setAlternatingRowColors(True) + # Enable RMB menu + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_right_mouse_menu) + + self.itemChanged.connect(self._on_item_changed) + + def refresh(self): + """Refresh the listed families. + + This gets all unique families and adds them as checkable items to + the list. + + """ + + families = [] + if self.dbcon.Session.get("AVALON_PROJECT"): + result = list(self.dbcon.aggregate([ + {"$match": { + "type": "subset" + }}, + {"$project": { + "family": {"$arrayElemAt": ["$data.families", 0]} + }}, + {"$group": { + "_id": "family_group", + "families": {"$addToSet": "$family"} + }} + ])) + if result: + families = result[0]["families"] + + # Rebuild list + self.blockSignals(True) + self.clear() + for name in sorted(families): + family = self.family_config_cache.family_config(name) + if family.get("hideFilter"): + continue + + label = family.get("label", name) + icon = family.get("icon", None) + + # TODO: This should be more managable by the artist + # Temporarily implement support for a default state in the project + # configuration + state = family.get("state", True) + state = QtCore.Qt.Checked if state else QtCore.Qt.Unchecked + + item = QtWidgets.QListWidgetItem(parent=self) + item.setText(label) + item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) + item.setData(self.NameRole, name) + item.setCheckState(state) + + if icon: + item.setIcon(icon) + + self.addItem(item) + self.blockSignals(False) + + self.active_changed.emit(self.get_filters()) + + def get_filters(self): + """Return the checked family items""" + + items = [self.item(i) for i in + range(self.count())] + + return [item.data(self.NameRole) for item in items if + item.checkState() == QtCore.Qt.Checked] + + def _on_item_changed(self): + self.active_changed.emit(self.get_filters()) + + def _set_checkstate_all(self, state): + _state = QtCore.Qt.Checked if state is True else QtCore.Qt.Unchecked + self.blockSignals(True) + for i in range(self.count()): + item = self.item(i) + item.setCheckState(_state) + self.blockSignals(False) + self.active_changed.emit(self.get_filters()) + + def show_right_mouse_menu(self, pos): + """Build RMB menu under mouse at current position (within widget)""" + + # Get mouse position + globalpos = self.viewport().mapToGlobal(pos) + + menu = QtWidgets.QMenu(self) + + # Add enable all action + state_checked = QtWidgets.QAction(menu, text="Enable All") + state_checked.triggered.connect( + lambda: self._set_checkstate_all(True)) + # Add disable all action + state_unchecked = QtWidgets.QAction(menu, text="Disable All") + state_unchecked.triggered.connect( + lambda: self._set_checkstate_all(False)) + + menu.addAction(state_checked) + menu.addAction(state_unchecked) + + menu.exec_(globalpos) + + +class RepresentationWidget(QtWidgets.QWidget): + load_started = QtCore.Signal() + load_ended = QtCore.Signal() + + default_widths = ( + ("name", 120), + ("subset", 125), + ("asset", 125), + ("active_site", 85), + ("remote_site", 85) + ) + + commands = {'active': 'Download', 'remote': 'Upload'} + + def __init__(self, dbcon, tool_name=None, parent=None): + super(RepresentationWidget, self).__init__(parent=parent) + self.dbcon = dbcon + self.tool_name = tool_name + + headers = [item[0] for item in self.default_widths] + + model = RepresentationModel(self.dbcon, headers, []) + + proxy_model = RepresentationSortProxyModel(self) + proxy_model.setSourceModel(model) + + label = QtWidgets.QLabel("Representations", self) + + tree_view = DeselectableTreeView() + tree_view.setModel(proxy_model) + tree_view.setAllColumnsShowFocus(True) + tree_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + tree_view.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection) + tree_view.setSortingEnabled(True) + tree_view.sortByColumn(1, QtCore.Qt.AscendingOrder) + tree_view.setAlternatingRowColors(True) + tree_view.setIndentation(20) + tree_view.setStyleSheet(""" + QTreeView::item{ + padding: 5px 1px; + border: 0px; + } + """) + tree_view.collapseAll() + + for column_name, width in self.default_widths: + idx = model.Columns.index(column_name) + tree_view.setColumnWidth(idx, width) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(label) + layout.addWidget(tree_view) + + # self.itemChanged.connect(self._on_item_changed) + tree_view.customContextMenuRequested.connect(self.on_context_menu) + + self.tree_view = tree_view + self.model = model + self.proxy_model = proxy_model + + self.sync_server_enabled = False + actual_project = dbcon.Session["AVALON_PROJECT"] + self.on_project_change(actual_project) + + self.model.refresh() + + def on_project_change(self, project_name): + """ + Called on each project change in parent widget. + + Checks if Sync Server is enabled for a project, pushes changes to + model. + """ + enabled = False + if project_name: + self.model.reset_sync_server(project_name) + if self.model.sync_server: + enabled_proj = self.model.sync_server.get_enabled_projects() + enabled = project_name in enabled_proj + + self.sync_server_enabled = enabled + lib.change_visibility(self.model, self.tree_view, + "active_site", enabled) + lib.change_visibility(self.model, self.tree_view, + "remote_site", enabled) + + def _repre_contexts_for_loaders_filter(self, items): + repre_ids = [] + for item in items: + repre_ids.append(item["_id"]) + + repre_docs = list(self.dbcon.find( + { + "type": "representation", + "_id": {"$in": repre_ids} + }, + { + "name": 1, + "parent": 1 + } + )) + version_ids = [ + repre_doc["parent"] + for repre_doc in repre_docs + ] + version_docs = self.dbcon.find({ + "_id": {"$in": version_ids} + }) + + version_docs_by_id = {} + version_docs_by_subset_id = collections.defaultdict(list) + for version_doc in version_docs: + version_id = version_doc["_id"] + subset_id = version_doc["parent"] + version_docs_by_id[version_id] = version_doc + version_docs_by_subset_id[subset_id].append(version_doc) + + subset_docs = list(self.dbcon.find( + { + "_id": {"$in": list(version_docs_by_subset_id.keys())}, + "type": "subset" + }, + { + "schema": 1, + "data.families": 1 + } + )) + subset_docs_by_id = { + subset_doc["_id"]: subset_doc + for subset_doc in subset_docs + } + repre_context_by_id = {} + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + + version_doc = version_docs_by_id[version_id] + repre_context_by_id[repre_doc["_id"]] = { + "representation": repre_doc, + "version": version_doc, + "subset": subset_docs_by_id[version_doc["parent"]] + } + return repre_context_by_id + + def on_context_menu(self, point): + """Shows menu with loader actions on Right-click. + + Registered actions are filtered by selection and help of + `loaders_from_representation` from avalon api. Intersection of actions + is shown when more subset is selected. When there are not available + actions for selected subsets then special action is shown (works as + info message to user): "*No compatible loaders for your selection" + + """ + point_index = self.tree_view.indexAt(point) + if not point_index.isValid(): + return + + # Get selected subsets without groups + selection = self.tree_view.selectionModel() + rows = selection.selectedRows(column=0) + + items = lib.get_selected_items(rows, self.model.ItemRole) + + selected_side = self._get_selected_side(point_index, rows) + + # Get all representation->loader combinations available for the + # index under the cursor, so we can list the user the options. + available_loaders = api.discover(api.Loader) + + filtered_loaders = [] + for loader in available_loaders: + # Skip subset loaders + if api.SubsetLoader in inspect.getmro(loader): + continue + + if ( + tools_lib.is_sync_loader(loader) + and not self.sync_server_enabled + ): + continue + + filtered_loaders.append(loader) + + if self.tool_name: + filtered_loaders = lib.remove_tool_name_from_loaders( + filtered_loaders, self.tool_name + ) + + loaders = list() + already_added_loaders = set() + label_already_in_menu = set() + + repre_context_by_id = ( + self._repre_contexts_for_loaders_filter(items) + ) + + for item in items: + repre_context = repre_context_by_id[item["_id"]] + for loader in pipeline.loaders_from_repre_context( + filtered_loaders, + repre_context + ): + if tools_lib.is_sync_loader(loader): + both_unavailable = item["active_site_progress"] <= 0 and \ + item["remote_site_progress"] <= 0 + if both_unavailable: + continue + + for selected_side in self.commands.keys(): + item = item.copy() + item["custom_label"] = None + label = None + selected_site_progress = item.get( + "{}_site_progress".format(selected_side), -1) + + # only remove if actually present + if tools_lib.is_remove_site_loader(loader): + label = "Remove {}".format(selected_side) + if selected_site_progress < 1: + continue + + if tools_lib.is_add_site_loader(loader): + label = self.commands[selected_side] + if selected_site_progress >= 0: + label = 'Re-{} {}'.format(label, selected_side) + + if not label: + continue + + item["selected_side"] = selected_side + item["custom_label"] = label + + if label not in label_already_in_menu: + loaders.append((item, loader)) + already_added_loaders.add(loader) + label_already_in_menu.add(label) + + else: + item = item.copy() + item["custom_label"] = None + + if loader not in already_added_loaders: + loaders.append((item, loader)) + already_added_loaders.add(loader) + + loaders = lib.sort_loaders(loaders) + + menu = OptionalMenu(self) + if not loaders: + action = lib.get_no_loader_action(menu) + menu.addAction(action) + else: + repre_contexts = pipeline.get_repres_contexts( + repre_context_by_id.keys(), self.dbcon) + menu = lib.add_representation_loaders_to_menu(loaders, menu, + repre_contexts) + + self._process_action(items, menu, point) + + def _process_action(self, items, menu, point): + """ + Show the context action menu and process selected + + Args: + items(dict): menu items + menu(OptionalMenu) + point(PointIndex) + """ + global_point = self.tree_view.mapToGlobal(point) + action = menu.exec_(global_point) + + if not action or not action.data(): + return + + self.load_started.emit() + + # Find the representation name and loader to trigger + action_representation, loader = action.data() + repre_ids = [] + data_by_repre_id = {} + selected_side = action_representation.get("selected_side") + + for item in items: + if tools_lib.is_sync_loader(loader): + site_name = "{}_site_name".format(selected_side) + data = { + "_id": item.get("_id"), + "site_name": item.get(site_name), + "project_name": self.dbcon.Session["AVALON_PROJECT"] + } + + if not data["site_name"]: + continue + + data_by_repre_id[data["_id"]] = data + + repre_ids.append(item.get("_id")) + + repre_contexts = pipeline.get_repres_contexts(repre_ids, + self.dbcon) + options = lib.get_options(action, loader, self, + list(repre_contexts.values())) + + errors = _load_representations_by_loader( + loader, repre_contexts, + options=options, data_by_repre_id=data_by_repre_id) + + self.model.refresh() + + self.load_ended.emit() + + if errors: + box = LoadErrorMessageBox(errors) + box.show() + + def _get_optional_labels(self, loaders, selected_side): + """Each loader could have specific label + + Args: + loaders (tuple of dict, dict): (item, loader) + selected_side(string): active or remote + + Returns: + (dict) {loader: string} + """ + optional_labels = {} + if selected_side: + if selected_side == 'active': + txt = "Localize" + else: + txt = "Sync to Remote" + optional_labels = {loader: txt for _, loader in loaders + if tools_lib.is_sync_loader(loader)} + return optional_labels + + def _get_selected_side(self, point_index, rows): + """Returns active/remote label according to column in 'point_index'""" + selected_side = None + if self.sync_server_enabled: + if rows: + source_index = self.proxy_model.mapToSource(point_index) + selected_side = self.model.data(source_index, + self.model.SiteSideRole) + return selected_side + + def set_version_ids(self, version_ids): + self.model.set_version_ids(version_ids) + + def _set_download(self): + pass + + def change_visibility(self, column_name, visible): + """ + Hides or shows particular 'column_name'. + + "asset" and "subset" columns should be visible only in multiselect + """ + lib.change_visibility(self.model, self.tree_view, column_name, visible) + + +def _load_representations_by_loader(loader, repre_contexts, + options, + data_by_repre_id=None): + """Loops through list of repre_contexts and loads them with one loader + + Args: + loader (cls of api.Loader) - not initialized yet + repre_contexts (dicts) - full info about selected representations + (containing repre_doc, version_doc, subset_doc, project info) + options (dict) - qargparse arguments to fill OptionDialog + data_by_repre_id (dict) - additional data applicable on top of + options to provide dynamic values + """ + error_info = [] + + if options is None: # not load when cancelled + return + + for repre_context in repre_contexts.values(): + try: + if data_by_repre_id: + _id = repre_context["representation"]["_id"] + data = data_by_repre_id.get(_id) + options.update(data) + pipeline.load_with_repre_context( + loader, + repre_context, + options=options + ) + except pipeline.IncompatibleLoaderError as exc: + print(exc) + error_info.append(( + "Incompatible Loader", + None, + repre_context["representation"]["name"], + repre_context["subset"]["name"], + repre_context["version"]["name"] + )) + + except Exception as exc: + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + error_info.append(( + str(exc), + formatted_traceback, + repre_context["representation"]["name"], + repre_context["subset"]["name"], + repre_context["version"]["name"] + )) + return error_info + + +def _load_subsets_by_loader(loader, subset_contexts, options, + subset_version_docs=None): + """ + Triggers load with SubsetLoader type of loaders + + Args: + loader (SubsetLoder): + subset_contexts (list): + options (dict): + subset_version_docs (dict): {subset_id: version_doc} + """ + error_info = [] + + if options is None: # not load when cancelled + return + + if loader.is_multiple_contexts_compatible: + subset_names = [] + for context in subset_contexts: + subset_name = context.get("subset", {}).get("name") or "N/A" + subset_names.append(subset_name) + + context["version"] = subset_version_docs[context["subset"]["_id"]] + try: + pipeline.load_with_subset_contexts( + loader, + subset_contexts, + options=options + ) + except Exception as exc: + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join( + traceback.format_exception( + exc_type, exc_value, exc_traceback + ) + ) + error_info.append(( + str(exc), + formatted_traceback, + None, + ", ".join(subset_names), + None + )) + else: + for subset_context in subset_contexts: + subset_name = subset_context.get("subset", {}).get("name") or "N/A" + + version_doc = subset_version_docs[subset_context["subset"]["_id"]] + subset_context["version"] = version_doc + try: + pipeline.load_with_subset_context( + loader, + subset_context, + options=options + ) + except Exception as exc: + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "\n".join( + traceback.format_exception( + exc_type, exc_value, exc_traceback + ) + ) + error_info.append(( + str(exc), + formatted_traceback, + None, + subset_name, + None + )) + + return error_info diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/tools/utils/delegates.py b/openpype/tools/utils/delegates.py new file mode 100644 index 0000000000..608762b538 --- /dev/null +++ b/openpype/tools/utils/delegates.py @@ -0,0 +1,448 @@ +import time +from datetime import datetime +import logging +import numbers + +import Qt +from Qt import QtWidgets, QtGui, QtCore + +from avalon.lib import HeroVersionType +from .models import ( + AssetModel, + TreeModel +) +from . import lib + +if Qt.__binding__ == "PySide": + from PySide.QtGui import QStyleOptionViewItemV4 +elif Qt.__binding__ == "PyQt4": + from PyQt4.QtGui import QStyleOptionViewItemV4 + +log = logging.getLogger(__name__) + + +class AssetDelegate(QtWidgets.QItemDelegate): + bar_height = 3 + + def sizeHint(self, option, index): + result = super(AssetDelegate, self).sizeHint(option, index) + height = result.height() + result.setHeight(height + self.bar_height) + + return result + + def paint(self, painter, option, index): + # Qt4 compat + if Qt.__binding__ in ("PySide", "PyQt4"): + option = QStyleOptionViewItemV4(option) + + painter.save() + + item_rect = QtCore.QRect(option.rect) + item_rect.setHeight(option.rect.height() - self.bar_height) + + subset_colors = index.data(AssetModel.subsetColorsRole) + subset_colors_width = 0 + if subset_colors: + subset_colors_width = option.rect.width() / len(subset_colors) + + subset_rects = [] + counter = 0 + for subset_c in subset_colors: + new_color = None + new_rect = None + if subset_c: + new_color = QtGui.QColor(*subset_c) + + new_rect = QtCore.QRect( + option.rect.left() + (counter * subset_colors_width), + option.rect.top() + ( + option.rect.height() - self.bar_height + ), + subset_colors_width, + self.bar_height + ) + subset_rects.append((new_color, new_rect)) + counter += 1 + + # Background + bg_color = QtGui.QColor(60, 60, 60) + if option.state & QtWidgets.QStyle.State_Selected: + if len(subset_colors) == 0: + item_rect.setTop(item_rect.top() + (self.bar_height / 2)) + if option.state & QtWidgets.QStyle.State_MouseOver: + bg_color.setRgb(70, 70, 70) + else: + item_rect.setTop(item_rect.top() + (self.bar_height / 2)) + if option.state & QtWidgets.QStyle.State_MouseOver: + bg_color.setAlpha(100) + else: + bg_color.setAlpha(0) + + # # -- When not needed to do a rounded corners (easier and without painter restore): + # painter.fillRect( + # item_rect, + # QtGui.QBrush(bg_color) + # ) + pen = painter.pen() + pen.setStyle(QtCore.Qt.NoPen) + pen.setWidth(0) + painter.setPen(pen) + painter.setBrush(QtGui.QBrush(bg_color)) + painter.drawRoundedRect(option.rect, 3, 3) + + if option.state & QtWidgets.QStyle.State_Selected: + for color, subset_rect in subset_rects: + if not color or not subset_rect: + continue + painter.fillRect(subset_rect, QtGui.QBrush(color)) + + painter.restore() + painter.save() + + # Icon + icon_index = index.model().index( + index.row(), index.column(), index.parent() + ) + # - Default icon_rect if not icon + icon_rect = QtCore.QRect( + item_rect.left(), + item_rect.top(), + # To make sure it's same size all the time + option.rect.height() - self.bar_height, + option.rect.height() - self.bar_height + ) + icon = index.model().data(icon_index, QtCore.Qt.DecorationRole) + + if icon: + mode = QtGui.QIcon.Normal + if not (option.state & QtWidgets.QStyle.State_Enabled): + mode = QtGui.QIcon.Disabled + elif option.state & QtWidgets.QStyle.State_Selected: + mode = QtGui.QIcon.Selected + + if isinstance(icon, QtGui.QPixmap): + icon = QtGui.QIcon(icon) + option.decorationSize = icon.size() / icon.devicePixelRatio() + + elif isinstance(icon, QtGui.QColor): + pixmap = QtGui.QPixmap(option.decorationSize) + pixmap.fill(icon) + icon = QtGui.QIcon(pixmap) + + elif isinstance(icon, QtGui.QImage): + icon = QtGui.QIcon(QtGui.QPixmap.fromImage(icon)) + option.decorationSize = icon.size() / icon.devicePixelRatio() + + elif isinstance(icon, QtGui.QIcon): + state = QtGui.QIcon.Off + if option.state & QtWidgets.QStyle.State_Open: + state = QtGui.QIcon.On + actualSize = option.icon.actualSize( + option.decorationSize, mode, state + ) + option.decorationSize = QtCore.QSize( + min(option.decorationSize.width(), actualSize.width()), + min(option.decorationSize.height(), actualSize.height()) + ) + + state = QtGui.QIcon.Off + if option.state & QtWidgets.QStyle.State_Open: + state = QtGui.QIcon.On + + icon.paint( + painter, icon_rect, + QtCore.Qt.AlignLeft, mode, state + ) + + # Text + text_rect = QtCore.QRect( + icon_rect.left() + icon_rect.width() + 2, + item_rect.top(), + item_rect.width(), + item_rect.height() + ) + + painter.drawText( + text_rect, QtCore.Qt.AlignVCenter, + index.data(QtCore.Qt.DisplayRole) + ) + + painter.restore() + + +class VersionDelegate(QtWidgets.QStyledItemDelegate): + """A delegate that display version integer formatted as version string.""" + + version_changed = QtCore.Signal() + first_run = False + lock = False + + def __init__(self, dbcon, *args, **kwargs): + self.dbcon = dbcon + super(VersionDelegate, self).__init__(*args, **kwargs) + + def displayText(self, value, locale): + if isinstance(value, HeroVersionType): + return lib.format_version(value, True) + assert isinstance(value, numbers.Integral), ( + "Version is not integer. \"{}\" {}".format(value, str(type(value))) + ) + return lib.format_version(value) + + def paint(self, painter, option, index): + fg_color = index.data(QtCore.Qt.ForegroundRole) + if fg_color: + if isinstance(fg_color, QtGui.QBrush): + fg_color = fg_color.color() + elif isinstance(fg_color, QtGui.QColor): + pass + else: + fg_color = None + + if not fg_color: + return super(VersionDelegate, self).paint(painter, option, index) + + if option.widget: + style = option.widget.style() + else: + style = QtWidgets.QApplication.style() + + style.drawControl( + style.CE_ItemViewItem, option, painter, option.widget + ) + + painter.save() + + text = self.displayText( + index.data(QtCore.Qt.DisplayRole), option.locale + ) + pen = painter.pen() + pen.setColor(fg_color) + painter.setPen(pen) + + text_rect = style.subElementRect(style.SE_ItemViewItemText, option) + text_margin = style.proxy().pixelMetric( + style.PM_FocusFrameHMargin, option, option.widget + ) + 1 + + painter.drawText( + text_rect.adjusted(text_margin, 0, - text_margin, 0), + option.displayAlignment, + text + ) + + painter.restore() + + def createEditor(self, parent, option, index): + item = index.data(TreeModel.ItemRole) + if item.get("isGroup") or item.get("isMerged"): + return + + editor = QtWidgets.QComboBox(parent) + + def commit_data(): + if not self.first_run: + self.commitData.emit(editor) # Update model data + self.version_changed.emit() # Display model data + editor.currentIndexChanged.connect(commit_data) + + self.first_run = True + self.lock = False + + return editor + + def setEditorData(self, editor, index): + if self.lock: + # Only set editor data once per delegation + return + + editor.clear() + + # Current value of the index + item = index.data(TreeModel.ItemRole) + value = index.data(QtCore.Qt.DisplayRole) + if item["version_document"]["type"] != "hero_version": + assert isinstance(value, numbers.Integral), ( + "Version is not integer" + ) + + # Add all available versions to the editor + parent_id = item["version_document"]["parent"] + version_docs = list(self.dbcon.find( + { + "type": "version", + "parent": parent_id + }, + sort=[("name", 1)] + )) + + hero_version_doc = self.dbcon.find_one( + { + "type": "hero_version", + "parent": parent_id + }, { + "name": 1, + "data.tags": 1, + "version_id": 1 + } + ) + + doc_for_hero_version = None + + selected = None + items = [] + for version_doc in version_docs: + version_tags = version_doc["data"].get("tags") or [] + if "deleted" in version_tags: + continue + + if ( + hero_version_doc + and doc_for_hero_version is None + and hero_version_doc["version_id"] == version_doc["_id"] + ): + doc_for_hero_version = version_doc + + label = lib.format_version(version_doc["name"]) + item = QtGui.QStandardItem(label) + item.setData(version_doc, QtCore.Qt.UserRole) + items.append(item) + + if version_doc["name"] == value: + selected = item + + if hero_version_doc and doc_for_hero_version: + version_name = doc_for_hero_version["name"] + label = lib.format_version(version_name, True) + if isinstance(value, HeroVersionType): + index = len(version_docs) + hero_version_doc["name"] = HeroVersionType(version_name) + + item = QtGui.QStandardItem(label) + item.setData(hero_version_doc, QtCore.Qt.UserRole) + items.append(item) + + # Reverse items so latest versions be upper + items = list(reversed(items)) + for item in items: + editor.model().appendRow(item) + + index = 0 + if selected: + index = selected.row() + + # Will trigger index-change signal + editor.setCurrentIndex(index) + self.first_run = False + self.lock = True + + def setModelData(self, editor, model, index): + """Apply the integer version back in the model""" + version = editor.itemData(editor.currentIndex()) + model.setData(index, version["name"]) + + +def pretty_date(t, now=None, strftime="%b %d %Y %H:%M"): + """Parse datetime to readable timestamp + + Within first ten seconds: + - "just now", + Within first minute ago: + - "%S seconds ago" + Within one hour ago: + - "%M minutes ago". + Within one day ago: + - "%H:%M hours ago" + Else: + "%Y-%m-%d %H:%M:%S" + + """ + + assert isinstance(t, datetime) + if now is None: + now = datetime.now() + assert isinstance(now, datetime) + diff = now - t + + second_diff = diff.seconds + day_diff = diff.days + + # future (consider as just now) + if day_diff < 0: + return "just now" + + # history + if day_diff == 0: + if second_diff < 10: + return "just now" + if second_diff < 60: + return str(second_diff) + " seconds ago" + if second_diff < 120: + return "a minute ago" + if second_diff < 3600: + return str(second_diff // 60) + " minutes ago" + if second_diff < 86400: + minutes = (second_diff % 3600) // 60 + hours = second_diff // 3600 + return "{0}:{1:02d} hours ago".format(hours, minutes) + + return t.strftime(strftime) + + +def pretty_timestamp(t, now=None): + """Parse timestamp to user readable format + + >>> pretty_timestamp("20170614T151122Z", now="20170614T151123Z") + 'just now' + + >>> pretty_timestamp("20170614T151122Z", now="20170614T171222Z") + '2:01 hours ago' + + Args: + t (str): The time string to parse. + now (str, optional) + + Returns: + str: human readable "recent" date. + + """ + + if now is not None: + try: + now = time.strptime(now, "%Y%m%dT%H%M%SZ") + now = datetime.fromtimestamp(time.mktime(now)) + except ValueError as e: + log.warning("Can't parse 'now' time format: {0} {1}".format(t, e)) + return None + + if isinstance(t, float): + dt = datetime.fromtimestamp(t) + else: + # Parse the time format as if it is `str` result from + # `pyblish.lib.time()` which usually is stored in Avalon database. + try: + t = time.strptime(t, "%Y%m%dT%H%M%SZ") + except ValueError as e: + log.warning("Can't parse time format: {0} {1}".format(t, e)) + return None + dt = datetime.fromtimestamp(time.mktime(t)) + + # prettify + return pretty_date(dt, now=now) + + +class PrettyTimeDelegate(QtWidgets.QStyledItemDelegate): + """A delegate that displays a timestamp as a pretty date. + + This displays dates like `pretty_date`. + + """ + + def displayText(self, value, locale): + + if value is None: + # Ignore None value + return + + return pretty_timestamp(value) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py new file mode 100644 index 0000000000..e83f663b2e --- /dev/null +++ b/openpype/tools/utils/lib.py @@ -0,0 +1,622 @@ +import os +import sys +import contextlib +import collections + +from Qt import QtWidgets, QtCore, QtGui + +from avalon import io, api, style +from avalon.vendor import qtawesome + +self = sys.modules[__name__] +self._jobs = dict() + + +class SharedObjects: + # Variable for family cache in global context + # QUESTION is this safe? More than one tool can refresh at the same time. + family_cache = None + + +def global_family_cache(): + if SharedObjects.family_cache is None: + SharedObjects.family_cache = FamilyConfigCache(io) + return SharedObjects.family_cache + + +def format_version(value, hero_version=False): + """Formats integer to displayable version name""" + label = "v{0:03d}".format(value) + if not hero_version: + return label + return "[{}]".format(label) + + +@contextlib.contextmanager +def application(): + app = QtWidgets.QApplication.instance() + + if not app: + print("Starting new QApplication..") + app = QtWidgets.QApplication(sys.argv) + yield app + app.exec_() + else: + print("Using existing QApplication..") + yield app + + +def defer(delay, func): + """Append artificial delay to `func` + + This aids in keeping the GUI responsive, but complicates logic + when producing tests. To combat this, the environment variable ensures + that every operation is synchonous. + + Arguments: + delay (float): Delay multiplier; default 1, 0 means no delay + func (callable): Any callable + + """ + + delay *= float(os.getenv("PYBLISH_DELAY", 1)) + if delay > 0: + return QtCore.QTimer.singleShot(delay, func) + else: + return func() + + +def schedule(func, time, channel="default"): + """Run `func` at a later `time` in a dedicated `channel` + + Given an arbitrary function, call this function after a given + timeout. It will ensure that only one "job" is running within + the given channel at any one time and cancel any currently + running job if a new job is submitted before the timeout. + + """ + + try: + self._jobs[channel].stop() + except (AttributeError, KeyError, RuntimeError): + pass + + timer = QtCore.QTimer() + timer.setSingleShot(True) + timer.timeout.connect(func) + timer.start(time) + + self._jobs[channel] = timer + + +@contextlib.contextmanager +def dummy(): + """Dummy context manager + + Usage: + >> with some_context() if False else dummy(): + .. pass + + """ + + yield + + +def iter_model_rows(model, column, include_root=False): + """Iterate over all row indices in a model""" + indices = [QtCore.QModelIndex()] # start iteration at root + + for index in indices: + # Add children to the iterations + child_rows = model.rowCount(index) + for child_row in range(child_rows): + child_index = model.index(child_row, column, index) + indices.append(child_index) + + if not include_root and not index.isValid(): + continue + + yield index + + +@contextlib.contextmanager +def preserve_states(tree_view, + column=0, + role=None, + preserve_expanded=True, + preserve_selection=True, + expanded_role=QtCore.Qt.DisplayRole, + selection_role=QtCore.Qt.DisplayRole): + """Preserves row selection in QTreeView by column's data role. + This function is created to maintain the selection status of + the model items. When refresh is triggered the items which are expanded + will stay expanded and vise versa. + tree_view (QWidgets.QTreeView): the tree view nested in the application + column (int): the column to retrieve the data from + role (int): the role which dictates what will be returned + Returns: + None + """ + # When `role` is set then override both expanded and selection roles + if role: + expanded_role = role + selection_role = role + + model = tree_view.model() + selection_model = tree_view.selectionModel() + flags = selection_model.Select | selection_model.Rows + + expanded = set() + + if preserve_expanded: + for index in iter_model_rows( + model, column=column, include_root=False + ): + if tree_view.isExpanded(index): + value = index.data(expanded_role) + expanded.add(value) + + selected = None + + if preserve_selection: + selected_rows = selection_model.selectedRows() + if selected_rows: + selected = set(row.data(selection_role) for row in selected_rows) + + try: + yield + finally: + if expanded: + for index in iter_model_rows( + model, column=0, include_root=False + ): + value = index.data(expanded_role) + is_expanded = value in expanded + # skip if new index was created meanwhile + if is_expanded is None: + continue + tree_view.setExpanded(index, is_expanded) + + if selected: + # Go through all indices, select the ones with similar data + for index in iter_model_rows( + model, column=column, include_root=False + ): + value = index.data(selection_role) + state = value in selected + if state: + tree_view.scrollTo(index) # Ensure item is visible + selection_model.select(index, flags) + + +@contextlib.contextmanager +def preserve_expanded_rows(tree_view, column=0, role=None): + """Preserves expanded row in QTreeView by column's data role. + + This function is created to maintain the expand vs collapse status of + the model items. When refresh is triggered the items which are expanded + will stay expanded and vise versa. + + Arguments: + tree_view (QWidgets.QTreeView): the tree view which is + nested in the application + column (int): the column to retrieve the data from + role (int): the role which dictates what will be returned + + Returns: + None + + """ + if role is None: + role = QtCore.Qt.DisplayRole + model = tree_view.model() + + expanded = set() + + for index in iter_model_rows(model, column=column, include_root=False): + if tree_view.isExpanded(index): + value = index.data(role) + expanded.add(value) + + try: + yield + finally: + if not expanded: + return + + for index in iter_model_rows(model, column=column, include_root=False): + value = index.data(role) + state = value in expanded + if state: + tree_view.expand(index) + else: + tree_view.collapse(index) + + +@contextlib.contextmanager +def preserve_selection(tree_view, column=0, role=None, current_index=True): + """Preserves row selection in QTreeView by column's data role. + + This function is created to maintain the selection status of + the model items. When refresh is triggered the items which are expanded + will stay expanded and vise versa. + + tree_view (QWidgets.QTreeView): the tree view nested in the application + column (int): the column to retrieve the data from + role (int): the role which dictates what will be returned + + Returns: + None + + """ + if role is None: + role = QtCore.Qt.DisplayRole + model = tree_view.model() + selection_model = tree_view.selectionModel() + flags = selection_model.Select | selection_model.Rows + + if current_index: + current_index_value = tree_view.currentIndex().data(role) + else: + current_index_value = None + + selected_rows = selection_model.selectedRows() + if not selected_rows: + yield + return + + selected = set(row.data(role) for row in selected_rows) + try: + yield + finally: + if not selected: + return + + # Go through all indices, select the ones with similar data + for index in iter_model_rows(model, column=column, include_root=False): + value = index.data(role) + state = value in selected + if state: + tree_view.scrollTo(index) # Ensure item is visible + selection_model.select(index, flags) + + if current_index_value and value == current_index_value: + selection_model.setCurrentIndex( + index, selection_model.NoUpdate + ) + + +class FamilyConfigCache: + default_color = "#0091B2" + _default_icon = None + _default_item = None + + def __init__(self, dbcon): + self.dbcon = dbcon + self.family_configs = {} + + @classmethod + def default_icon(cls): + if cls._default_icon is None: + cls._default_icon = qtawesome.icon( + "fa.folder", color=cls.default_color + ) + return cls._default_icon + + @classmethod + def default_item(cls): + if cls._default_item is None: + cls._default_item = {"icon": cls.default_icon()} + return cls._default_item + + def family_config(self, family_name): + """Get value from config with fallback to default""" + return self.family_configs.get(family_name, self.default_item()) + + def refresh(self): + """Get the family configurations from the database + + The configuration must be stored on the project under `config`. + For example: + + {"config": { + "families": [ + {"name": "avalon.camera", label: "Camera", "icon": "photo"}, + {"name": "avalon.anim", label: "Animation", "icon": "male"}, + ] + }} + + It is possible to override the default behavior and set specific + families checked. For example we only want the families imagesequence + and camera to be visible in the Loader. + + # This will turn every item off + api.data["familyStateDefault"] = False + + # Only allow the imagesequence and camera + api.data["familyStateToggled"] = ["imagesequence", "camera"] + + """ + + self.family_configs.clear() + + families = [] + + # Update the icons from the project configuration + project_name = self.dbcon.Session.get("AVALON_PROJECT") + if project_name: + project_doc = self.dbcon.find_one( + {"type": "project"}, + projection={"config.families": True} + ) + + if not project_doc: + print(( + "Project \"{}\" not found!" + " Can't refresh family icons cache." + ).format(project_name)) + else: + families = project_doc["config"].get("families") or [] + + # Check if any family state are being overwritten by the configuration + default_state = api.data.get("familiesStateDefault", True) + toggled = set(api.data.get("familiesStateToggled") or []) + + # Replace icons with a Qt icon we can use in the user interfaces + for family in families: + name = family["name"] + # Set family icon + icon = family.get("icon", None) + if icon: + family["icon"] = qtawesome.icon( + "fa.{}".format(icon), + color=self.default_color + ) + else: + family["icon"] = self.default_icon() + + # Update state + if name in toggled: + state = True + else: + state = default_state + family["state"] = state + + self.family_configs[name] = family + + return self.family_configs + + +class GroupsConfig: + # Subset group item's default icon and order + _default_group_config = None + + def __init__(self, dbcon): + self.dbcon = dbcon + self.groups = {} + + @classmethod + def default_group_config(cls): + if cls._default_group_config is None: + cls._default_group_config = { + "icon": qtawesome.icon( + "fa.object-group", + color=style.colors.default + ), + "order": 0 + } + return cls._default_group_config + + def refresh(self): + """Get subset group configurations from the database + + The 'group' configuration must be stored in the project `config` field. + See schema `config-1.0.json` + + """ + # Clear cached groups + self.groups.clear() + + group_configs = [] + project_name = self.dbcon.Session.get("AVALON_PROJECT") + if project_name: + # Get pre-defined group name and apperance from project config + project_doc = self.dbcon.find_one( + {"type": "project"}, + projection={"config.groups": True} + ) + + if project_doc: + group_configs = project_doc["config"].get("groups") or [] + else: + print("Project not found! \"{}\"".format(project_name)) + + # Build pre-defined group configs + for config in group_configs: + name = config["name"] + icon = "fa." + config.get("icon", "object-group") + color = config.get("color", style.colors.default) + order = float(config.get("order", 0)) + + self.groups[name] = { + "icon": qtawesome.icon(icon, color=color), + "order": order + } + + return self.groups + + def ordered_groups(self, group_names): + # default order zero included + _orders = set([0]) + for config in self.groups.values(): + _orders.add(config["order"]) + + # Remap order to list index + orders = sorted(_orders) + + _groups = list() + for name in group_names: + # Get group config + config = self.groups.get(name) or self.default_group_config() + # Base order + remapped_order = orders.index(config["order"]) + + data = { + "name": name, + "icon": config["icon"], + "_order": remapped_order, + } + + _groups.append(data) + + # Sort by tuple (base_order, name) + # If there are multiple groups in same order, will sorted by name. + ordered_groups = sorted( + _groups, key=lambda _group: (_group.pop("_order"), _group["name"]) + ) + + total = len(ordered_groups) + order_temp = "%0{}d".format(len(str(total))) + + # Update sorted order to config + for index, group_data in enumerate(ordered_groups): + order = index + inverse_order = total - index + + # Format orders into fixed length string for groups sorting + group_data["order"] = order_temp % order + group_data["inverseOrder"] = order_temp % inverse_order + + return ordered_groups + + def active_groups(self, asset_ids, include_predefined=True): + """Collect all active groups from each subset""" + # Collect groups from subsets + group_names = set( + self.dbcon.distinct( + "data.subsetGroup", + {"type": "subset", "parent": {"$in": asset_ids}} + ) + ) + if include_predefined: + # Ensure all predefined group configs will be included + group_names.update(self.groups.keys()) + + return self.ordered_groups(group_names) + + def split_subsets_for_groups(self, subset_docs, grouping): + """Collect all active groups from each subset""" + subset_docs_without_group = collections.defaultdict(list) + subset_docs_by_group = collections.defaultdict(dict) + for subset_doc in subset_docs: + subset_name = subset_doc["name"] + if grouping: + group_name = subset_doc["data"].get("subsetGroup") + if group_name: + if subset_name not in subset_docs_by_group[group_name]: + subset_docs_by_group[group_name][subset_name] = [] + + subset_docs_by_group[group_name][subset_name].append( + subset_doc + ) + continue + + subset_docs_without_group[subset_name].append(subset_doc) + + ordered_groups = self.ordered_groups(subset_docs_by_group.keys()) + + return ordered_groups, subset_docs_without_group, subset_docs_by_group + + +def create_qthread(func, *args, **kwargs): + class Thread(QtCore.QThread): + def run(self): + func(*args, **kwargs) + return Thread() + + +def get_repre_icons(): + try: + from openpype_modules import sync_server + except Exception: + # Backwards compatibility + from openpype.modules import sync_server + + resource_path = os.path.join( + os.path.dirname(sync_server.sync_server_module.__file__), + "providers", "resources" + ) + icons = {} + # TODO get from sync module + for provider in ['studio', 'local_drive', 'gdrive']: + pix_url = "{}/{}.png".format(resource_path, provider) + icons[provider] = QtGui.QIcon(pix_url) + + return icons + + +def get_progress_for_repre(doc, active_site, remote_site): + """ + Calculates average progress for representation. + + If site has created_dt >> fully available >> progress == 1 + + Could be calculated in aggregate if it would be too slow + Args: + doc(dict): representation dict + Returns: + (dict) with active and remote sites progress + {'studio': 1.0, 'gdrive': -1} - gdrive site is not present + -1 is used to highlight the site should be added + {'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not + uploaded yet + """ + progress = {active_site: -1, + remote_site: -1} + if not doc: + return progress + + files = {active_site: 0, remote_site: 0} + doc_files = doc.get("files") or [] + for doc_file in doc_files: + if not isinstance(doc_file, dict): + continue + + sites = doc_file.get("sites") or [] + for site in sites: + if ( + # Pype 2 compatibility + not isinstance(site, dict) + # Check if site name is one of progress sites + or site["name"] not in progress + ): + continue + + files[site["name"]] += 1 + norm_progress = max(progress[site["name"]], 0) + if site.get("created_dt"): + progress[site["name"]] = norm_progress + 1 + elif site.get("progress"): + progress[site["name"]] = norm_progress + site["progress"] + else: # site exists, might be failed, do not add again + progress[site["name"]] = 0 + + # for example 13 fully avail. files out of 26 >> 13/26 = 0.5 + avg_progress = {} + avg_progress[active_site] = \ + progress[active_site] / max(files[active_site], 1) + avg_progress[remote_site] = \ + progress[remote_site] / max(files[remote_site], 1) + return avg_progress + + +def is_sync_loader(loader): + return is_remove_site_loader(loader) or is_add_site_loader(loader) + + +def is_remove_site_loader(loader): + return hasattr(loader, "remove_site_on_representation") + + +def is_add_site_loader(loader): + return hasattr(loader, "add_site_to_representation") diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py new file mode 100644 index 0000000000..d09d16d898 --- /dev/null +++ b/openpype/tools/utils/models.py @@ -0,0 +1,626 @@ +import re +import time +import logging +import collections + +import Qt +from Qt import QtCore, QtGui +from avalon.vendor import qtawesome +from avalon import style, io +from . import lib + +log = logging.getLogger(__name__) + + +class TreeModel(QtCore.QAbstractItemModel): + + Columns = list() + ItemRole = QtCore.Qt.UserRole + 1 + item_class = None + + def __init__(self, parent=None): + super(TreeModel, self).__init__(parent) + self._root_item = self.ItemClass() + + @property + def ItemClass(self): + if self.item_class is not None: + return self.item_class + return Item + + def rowCount(self, parent=None): + if parent is None or not parent.isValid(): + parent_item = self._root_item + else: + parent_item = parent.internalPointer() + return parent_item.childCount() + + def columnCount(self, parent): + return len(self.Columns) + + def data(self, index, role): + if not index.isValid(): + return None + + if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: + item = index.internalPointer() + column = index.column() + + key = self.Columns[column] + return item.get(key, None) + + if role == self.ItemRole: + return index.internalPointer() + + def setData(self, index, value, role=QtCore.Qt.EditRole): + """Change the data on the items. + + Returns: + bool: Whether the edit was successful + """ + + if index.isValid(): + if role == QtCore.Qt.EditRole: + + item = index.internalPointer() + column = index.column() + key = self.Columns[column] + item[key] = value + + # passing `list()` for PyQt5 (see PYSIDE-462) + if Qt.__binding__ in ("PyQt4", "PySide"): + self.dataChanged.emit(index, index) + else: + self.dataChanged.emit(index, index, [role]) + + # must return true if successful + return True + + return False + + def setColumns(self, keys): + assert isinstance(keys, (list, tuple)) + self.Columns = keys + + def headerData(self, section, orientation, role): + + if role == QtCore.Qt.DisplayRole: + if section < len(self.Columns): + return self.Columns[section] + + super(TreeModel, self).headerData(section, orientation, role) + + def flags(self, index): + flags = QtCore.Qt.ItemIsEnabled + + item = index.internalPointer() + if item.get("enabled", True): + flags |= QtCore.Qt.ItemIsSelectable + + return flags + + def parent(self, index): + + item = index.internalPointer() + parent_item = item.parent() + + # If it has no parents we return invalid + if parent_item == self._root_item or not parent_item: + return QtCore.QModelIndex() + + return self.createIndex(parent_item.row(), 0, parent_item) + + def index(self, row, column, parent=None): + """Return index for row/column under parent""" + + if parent is None or not parent.isValid(): + parent_item = self._root_item + else: + parent_item = parent.internalPointer() + + child_item = parent_item.child(row) + if child_item: + return self.createIndex(row, column, child_item) + else: + return QtCore.QModelIndex() + + def add_child(self, item, parent=None): + if parent is None: + parent = self._root_item + + parent.add_child(item) + + def column_name(self, column): + """Return column key by index""" + + if column < len(self.Columns): + return self.Columns[column] + + def clear(self): + self.beginResetModel() + self._root_item = self.ItemClass() + self.endResetModel() + + +class Item(dict): + """An item that can be represented in a tree view using `TreeModel`. + + The item can store data just like a regular dictionary. + + >>> data = {"name": "John", "score": 10} + >>> item = Item(data) + >>> assert item["name"] == "John" + + """ + + def __init__(self, data=None): + super(Item, self).__init__() + + self._children = list() + self._parent = None + + if data is not None: + assert isinstance(data, dict) + self.update(data) + + def childCount(self): + return len(self._children) + + def child(self, row): + + if row >= len(self._children): + log.warning("Invalid row as child: {0}".format(row)) + return + + return self._children[row] + + def children(self): + return self._children + + def parent(self): + return self._parent + + def row(self): + """ + Returns: + int: Index of this item under parent""" + if self._parent is not None: + siblings = self.parent().children() + return siblings.index(self) + return -1 + + def add_child(self, child): + """Add a child to this item""" + child._parent = self + self._children.append(child) + + +class TasksModel(TreeModel): + """A model listing the tasks combined for a list of assets""" + + Columns = ["name", "count"] + + def __init__(self, dbcon, parent=None): + super(TasksModel, self).__init__(parent=parent) + self.dbcon = dbcon + self._num_assets = 0 + self._icons = { + "__default__": qtawesome.icon( + "fa.male", + color=style.colors.default + ), + "__no_task__": qtawesome.icon( + "fa.exclamation-circle", + color=style.colors.mid + ) + } + + self._refresh_task_icons() + + def _refresh_task_icons(self): + # Get the project configured icons from database + project = self.dbcon.find_one({"type": "project"}, {"config.tasks"}) + tasks = project["config"].get("tasks", {}) + for task_name, task in tasks.items(): + icon_name = task.get("icon", None) + if icon_name: + icon = qtawesome.icon( + "fa.{}".format(icon_name), + color=style.colors.default + ) + self._icons[task_name] = icon + + def set_assets(self, asset_ids=None, asset_docs=None): + """Set assets to track by their database id + + Arguments: + asset_ids (list): List of asset ids. + asset_docs (list): List of asset entities from MongoDB. + + """ + + if asset_docs is None and asset_ids is not None: + # prepare filter query + _filter = {"type": "asset", "_id": {"$in": asset_ids}} + _projection = {"data.tasks"} + + # find assets in db by query + asset_docs = list(self.dbcon.find(_filter, _projection)) + db_assets_ids = [asset["_id"] for asset in asset_docs] + + # check if all assets were found + not_found = [ + str(a_id) for a_id in asset_ids if a_id not in db_assets_ids + ] + + assert not not_found, "Assets not found by id: {0}".format( + ", ".join(not_found) + ) + + if asset_docs is None: + asset_docs = list() + + self._num_assets = len(asset_docs) + + tasks = collections.Counter() + for asset_doc in asset_docs: + asset_tasks = asset_doc.get("data", {}).get("tasks", {}) + tasks.update(asset_tasks.keys()) + + self.clear() + self.beginResetModel() + + default_icon = self._icons["__default__"] + + if not tasks: + no_task_icon = self._icons["__no_task__"] + item = Item({ + "name": "No task", + "count": 0, + "icon": no_task_icon, + "enabled": False, + }) + + self.add_child(item) + + else: + for task, count in sorted(tasks.items()): + icon = self._icons.get(task, default_icon) + + item = Item({ + "name": task, + "count": count, + "icon": icon + }) + + self.add_child(item) + + self.endResetModel() + + def headerData(self, section, orientation, role): + # Override header for count column to show amount of assets + # it is listing the tasks for + if role == QtCore.Qt.DisplayRole: + if orientation == QtCore.Qt.Horizontal: + if section == 0: + return "Tasks" + elif section == 1: # count column + return "count ({0})".format(self._num_assets) + + return super(TasksModel, self).headerData(section, orientation, role) + + def data(self, index, role): + if not index.isValid(): + return + + # Add icon to the first column + if role == QtCore.Qt.DecorationRole: + if index.column() == 0: + return index.internalPointer()["icon"] + + return super(TasksModel, self).data(index, role) + + +class AssetModel(TreeModel): + """A model listing assets in the silo in the active project. + + The assets are displayed in a treeview, they are visually parented by + a `visualParent` field in the database containing an `_id` to a parent + asset. + + """ + + Columns = ["label"] + Name = 0 + Deprecated = 2 + ObjectId = 3 + + DocumentRole = QtCore.Qt.UserRole + 2 + ObjectIdRole = QtCore.Qt.UserRole + 3 + subsetColorsRole = QtCore.Qt.UserRole + 4 + + doc_fetched = QtCore.Signal(bool) + refreshed = QtCore.Signal(bool) + + # Asset document projection + asset_projection = { + "type": 1, + "schema": 1, + "name": 1, + "silo": 1, + "data.visualParent": 1, + "data.label": 1, + "data.tags": 1, + "data.icon": 1, + "data.color": 1, + "data.deprecated": 1 + } + + def __init__(self, dbcon=None, parent=None, asset_projection=None): + super(AssetModel, self).__init__(parent=parent) + if dbcon is None: + dbcon = io + self.dbcon = dbcon + self.asset_colors = {} + + # Projections for Mongo queries + # - let ability to modify them if used in tools that require more than + # defaults + if asset_projection: + self.asset_projection = asset_projection + + self.asset_projection = asset_projection + + self._doc_fetching_thread = None + self._doc_fetching_stop = False + self._doc_payload = {} + + self.doc_fetched.connect(self.on_doc_fetched) + + self.refresh() + + def _add_hierarchy(self, assets, parent=None, silos=None): + """Add the assets that are related to the parent as children items. + + This method does *not* query the database. These instead are queried + in a single batch upfront as an optimization to reduce database + queries. Resulting in up to 10x speed increase. + + Args: + assets (dict): All assets in the currently active silo stored + by key/value + + Returns: + None + + """ + # Reset colors + self.asset_colors = {} + + if silos: + # WARNING: Silo item "_id" is set to silo value + # mainly because GUI issue with perserve selection and expanded row + # and because of easier hierarchy parenting (in "assets") + for silo in silos: + item = Item({ + "_id": silo, + "name": silo, + "label": silo, + "type": "silo" + }) + self.add_child(item, parent=parent) + self._add_hierarchy(assets, parent=item) + + parent_id = parent["_id"] if parent else None + current_assets = assets.get(parent_id, list()) + + for asset in current_assets: + # get label from data, otherwise use name + data = asset.get("data", {}) + label = data.get("label", asset["name"]) + tags = data.get("tags", []) + + # store for the asset for optimization + deprecated = "deprecated" in tags + + item = Item({ + "_id": asset["_id"], + "name": asset["name"], + "label": label, + "type": asset["type"], + "tags": ", ".join(tags), + "deprecated": deprecated, + "_document": asset + }) + self.add_child(item, parent=parent) + + # Add asset's children recursively if it has children + if asset["_id"] in assets: + self._add_hierarchy(assets, parent=item) + + self.asset_colors[asset["_id"]] = [] + + def on_doc_fetched(self, was_stopped): + if was_stopped: + self.stop_fetch_thread() + return + + self.beginResetModel() + + assets_by_parent = self._doc_payload.get("assets_by_parent") + silos = self._doc_payload.get("silos") + if assets_by_parent is not None: + # Build the hierarchical tree items recursively + self._add_hierarchy( + assets_by_parent, + parent=None, + silos=silos + ) + + self.endResetModel() + + has_content = bool(assets_by_parent) or bool(silos) + self.refreshed.emit(has_content) + + self.stop_fetch_thread() + + def fetch(self): + self._doc_payload = self._fetch() or {} + # Emit doc fetched only if was not stopped + self.doc_fetched.emit(self._doc_fetching_stop) + + def _fetch(self): + if not self.dbcon.Session.get("AVALON_PROJECT"): + return + + project_doc = self.dbcon.find_one( + {"type": "project"}, + {"_id": True} + ) + if not project_doc: + return + + # Get all assets sorted by name + db_assets = self.dbcon.find( + {"type": "asset"}, + self.asset_projection + ).sort("name", 1) + + # Group the assets by their visual parent's id + assets_by_parent = collections.defaultdict(list) + for asset in db_assets: + if self._doc_fetching_stop: + return + parent_id = asset.get("data", {}).get("visualParent") + assets_by_parent[parent_id].append(asset) + + return { + "assets_by_parent": assets_by_parent, + "silos": None + } + + def stop_fetch_thread(self): + if self._doc_fetching_thread is not None: + self._doc_fetching_stop = True + while self._doc_fetching_thread.isRunning(): + time.sleep(0.001) + self._doc_fetching_thread = None + + def refresh(self, force=False): + """Refresh the data for the model.""" + # Skip fetch if there is already other thread fetching documents + if self._doc_fetching_thread is not None: + if not force: + return + self.stop_fetch_thread() + + # Clear model items + self.clear() + + # Fetch documents from mongo + # Restart payload + self._doc_payload = {} + self._doc_fetching_stop = False + self._doc_fetching_thread = lib.create_qthread(self.fetch) + self._doc_fetching_thread.start() + + def flags(self, index): + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + def setData(self, index, value, role=QtCore.Qt.EditRole): + if not index.isValid(): + return False + + if role == self.subsetColorsRole: + asset_id = index.data(self.ObjectIdRole) + self.asset_colors[asset_id] = value + + if Qt.__binding__ in ("PyQt4", "PySide"): + self.dataChanged.emit(index, index) + else: + self.dataChanged.emit(index, index, [role]) + + return True + + return super(AssetModel, self).setData(index, value, role) + + def data(self, index, role): + if not index.isValid(): + return + + item = index.internalPointer() + if role == QtCore.Qt.DecorationRole: + column = index.column() + if column == self.Name: + # Allow a custom icon and custom icon color to be defined + data = item.get("_document", {}).get("data", {}) + icon = data.get("icon", None) + if icon is None and item.get("type") == "silo": + icon = "database" + color = data.get("color", style.colors.default) + + if icon is None: + # Use default icons if no custom one is specified. + # If it has children show a full folder, otherwise + # show an open folder + has_children = self.rowCount(index) > 0 + icon = "folder" if has_children else "folder-o" + + # Make the color darker when the asset is deprecated + if item.get("deprecated", False): + color = QtGui.QColor(color).darker(250) + + try: + key = "fa.{0}".format(icon) # font-awesome key + icon = qtawesome.icon(key, color=color) + return icon + except Exception as exception: + # Log an error message instead of erroring out completely + # when the icon couldn't be created (e.g. invalid name) + log.error(exception) + + return + + if role == QtCore.Qt.ForegroundRole: # font color + if "deprecated" in item.get("tags", []): + return QtGui.QColor(style.colors.light).darker(250) + + if role == self.ObjectIdRole: + return item.get("_id", None) + + if role == self.DocumentRole: + return item.get("_document", None) + + if role == self.subsetColorsRole: + asset_id = item.get("_id", None) + return self.asset_colors.get(asset_id) or [] + + return super(AssetModel, self).data(index, role) + + +class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): + """Filters to the regex if any of the children matches allow parent""" + def filterAcceptsRow(self, row, parent): + regex = self.filterRegExp() + if not regex.isEmpty(): + pattern = regex.pattern() + model = self.sourceModel() + source_index = model.index(row, self.filterKeyColumn(), parent) + if source_index.isValid(): + # Check current index itself + key = model.data(source_index, self.filterRole()) + if re.search(pattern, key, re.IGNORECASE): + return True + + # Check children + rows = model.rowCount(source_index) + for i in range(rows): + if self.filterAcceptsRow(i, source_index): + return True + + # Otherwise filter it + return False + + return super( + RecursiveSortFilterProxyModel, self + ).filterAcceptsRow(row, parent) diff --git a/openpype/tools/utils/views.py b/openpype/tools/utils/views.py new file mode 100644 index 0000000000..bed5655647 --- /dev/null +++ b/openpype/tools/utils/views.py @@ -0,0 +1,86 @@ +import os +from avalon import style +from Qt import QtWidgets, QtCore, QtGui, QtSvg + + +class DeselectableTreeView(QtWidgets.QTreeView): + """A tree view that deselects on clicking on an empty area in the view""" + + def mousePressEvent(self, event): + + index = self.indexAt(event.pos()) + if not index.isValid(): + # clear the selection + self.clearSelection() + # clear the current index + self.setCurrentIndex(QtCore.QModelIndex()) + + QtWidgets.QTreeView.mousePressEvent(self, event) + + +class TreeViewSpinner(QtWidgets.QTreeView): + size = 160 + + def __init__(self, parent=None): + super(TreeViewSpinner, self).__init__(parent=parent) + + loading_image_path = os.path.join( + os.path.dirname(os.path.abspath(style.__file__)), + "svg", + "spinner-200.svg" + ) + self.spinner = QtSvg.QSvgRenderer(loading_image_path) + + self.is_loading = False + self.is_empty = True + + def paint_loading(self, event): + rect = event.rect() + rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight()) + rect.moveTo( + rect.x() + rect.width() / 2 - self.size / 2, + rect.y() + rect.height() / 2 - self.size / 2 + ) + rect.setSize(QtCore.QSizeF(self.size, self.size)) + painter = QtGui.QPainter(self.viewport()) + self.spinner.render(painter, rect) + + def paint_empty(self, event): + painter = QtGui.QPainter(self.viewport()) + rect = event.rect() + rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight()) + qtext_opt = QtGui.QTextOption( + QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter + ) + painter.drawText(rect, "No Data", qtext_opt) + + def paintEvent(self, event): + if self.is_loading: + self.paint_loading(event) + elif self.is_empty: + self.paint_empty(event) + else: + super(TreeViewSpinner, self).paintEvent(event) + + +class AssetsView(TreeViewSpinner, DeselectableTreeView): + """Item view. + This implements a context menu. + """ + + def __init__(self): + super(AssetsView, self).__init__() + self.setIndentation(15) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setHeaderHidden(True) + + def mousePressEvent(self, event): + index = self.indexAt(event.pos()) + if not index.isValid(): + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers == QtCore.Qt.ShiftModifier: + return + elif modifiers == QtCore.Qt.ControlModifier: + return + + super(AssetsView, self).mousePressEvent(event) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py new file mode 100644 index 0000000000..153f012ff6 --- /dev/null +++ b/openpype/tools/utils/widgets.py @@ -0,0 +1,499 @@ +import logging +import time + +from . import lib + +from Qt import QtWidgets, QtCore, QtGui +from avalon.vendor import qtawesome, qargparse + +from avalon import style, io + +from .models import AssetModel, RecursiveSortFilterProxyModel +from .views import AssetsView +from .delegates import AssetDelegate + +log = logging.getLogger(__name__) + + +class AssetWidget(QtWidgets.QWidget): + """A Widget to display a tree of assets with filter + + To list the assets of the active project: + >>> # widget = AssetWidget() + >>> # widget.refresh() + >>> # widget.show() + + """ + + refresh_triggered = QtCore.Signal() # on model refresh + refreshed = QtCore.Signal() + selection_changed = QtCore.Signal() # on view selection change + current_changed = QtCore.Signal() # on view current index change + + def __init__(self, dbcon, multiselection=False, parent=None): + super(AssetWidget, self).__init__(parent=parent) + + self.dbcon = dbcon + + self.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + + # Tree View + model = AssetModel(dbcon=self.dbcon, parent=self) + proxy = RecursiveSortFilterProxyModel() + proxy.setSourceModel(model) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + + view = AssetsView() + view.setModel(proxy) + if multiselection: + asset_delegate = AssetDelegate() + view.setSelectionMode(view.ExtendedSelection) + view.setItemDelegate(asset_delegate) + + # Header + header = QtWidgets.QHBoxLayout() + + icon = qtawesome.icon("fa.arrow-down", color=style.colors.light) + set_current_asset_btn = QtWidgets.QPushButton(icon, "") + set_current_asset_btn.setToolTip("Go to Asset from current Session") + # Hide by default + set_current_asset_btn.setVisible(False) + + icon = qtawesome.icon("fa.refresh", color=style.colors.light) + refresh = QtWidgets.QPushButton(icon, "") + refresh.setToolTip("Refresh items") + + filter = QtWidgets.QLineEdit() + filter.textChanged.connect(proxy.setFilterFixedString) + filter.setPlaceholderText("Filter assets..") + + header.addWidget(filter) + header.addWidget(set_current_asset_btn) + header.addWidget(refresh) + + # Layout + layout.addLayout(header) + layout.addWidget(view) + + # Signals/Slots + selection = view.selectionModel() + selection.selectionChanged.connect(self.selection_changed) + selection.currentChanged.connect(self.current_changed) + refresh.clicked.connect(self.refresh) + set_current_asset_btn.clicked.connect(self.set_current_session_asset) + + self.set_current_asset_btn = set_current_asset_btn + self.model = model + self.proxy = proxy + self.view = view + + self.model_selection = {} + + def set_current_asset_btn_visibility(self, visible=None): + """Hide set current asset button. + + Not all tools support using of current context asset. + """ + if visible is None: + visible = not self.set_current_asset_btn.isVisible() + self.set_current_asset_btn.setVisible(visible) + + def _refresh_model(self): + # Store selection + self._store_model_selection() + time_start = time.time() + + self.set_loading_state( + loading=True, + empty=True + ) + + def on_refreshed(has_item): + self.set_loading_state(loading=False, empty=not has_item) + self._restore_model_selection() + self.model.refreshed.disconnect() + self.refreshed.emit() + print("Duration: %.3fs" % (time.time() - time_start)) + + # Connect to signal + self.model.refreshed.connect(on_refreshed) + # Trigger signal before refresh is called + self.refresh_triggered.emit() + # Refresh model + self.model.refresh() + + def refresh(self): + self._refresh_model() + + def get_active_asset(self): + """Return the asset item of the current selection.""" + current = self.view.currentIndex() + return current.data(self.model.ItemRole) + + def get_active_asset_document(self): + """Return the asset document of the current selection.""" + current = self.view.currentIndex() + return current.data(self.model.DocumentRole) + + def get_active_index(self): + return self.view.currentIndex() + + def get_selected_assets(self): + """Return the documents of selected assets.""" + selection = self.view.selectionModel() + rows = selection.selectedRows() + assets = [row.data(self.model.DocumentRole) for row in rows] + + # NOTE: skip None object assumed they are silo (backwards comp.) + return [asset for asset in assets if asset] + + def select_assets(self, assets, expand=True, key="name"): + """Select assets by item key. + + Args: + assets (list): List of asset values that can be found under + specified `key` + expand (bool): Whether to also expand to the asset in the view + key (string): Key that specifies where to look for `assets` values + + Returns: + None + + Default `key` is "name" in that case `assets` should contain single + asset name or list of asset names. (It is good idea to use "_id" key + instead of name in that case `assets` must contain `ObjectId` object/s) + It is expected that each value in `assets` will be found only once. + If the filters according to the `key` and `assets` correspond to + the more asset, only the first found will be selected. + + """ + + if not isinstance(assets, (tuple, list)): + assets = [assets] + + # convert to list - tuple cant be modified + assets = set(assets) + + # Clear selection + selection_model = self.view.selectionModel() + selection_model.clearSelection() + + # Select + mode = selection_model.Select | selection_model.Rows + for index in lib.iter_model_rows( + self.proxy, column=0, include_root=False + ): + # stop iteration if there are no assets to process + if not assets: + break + + value = index.data(self.model.ItemRole).get(key) + if value not in assets: + continue + + # Remove processed asset + assets.discard(value) + + selection_model.select(index, mode) + if expand: + # Expand parent index + self.view.expand(self.proxy.parent(index)) + + # Set the currently active index + self.view.setCurrentIndex(index) + + def set_loading_state(self, loading, empty): + if self.view.is_loading != loading: + if loading: + self.view.spinner.repaintNeeded.connect( + self.view.viewport().update + ) + else: + self.view.spinner.repaintNeeded.disconnect() + + self.view.is_loading = loading + self.view.is_empty = empty + + def _store_model_selection(self): + index = self.view.currentIndex() + current = None + if index and index.isValid(): + current = index.data(self.model.ObjectIdRole) + + expanded = set() + model = self.view.model() + for index in lib.iter_model_rows( + model, column=0, include_root=False + ): + if self.view.isExpanded(index): + value = index.data(self.model.ObjectIdRole) + expanded.add(value) + + selection_model = self.view.selectionModel() + + selected = None + selected_rows = selection_model.selectedRows() + if selected_rows: + selected = set( + row.data(self.model.ObjectIdRole) + for row in selected_rows + ) + + self.model_selection = { + "expanded": expanded, + "selected": selected, + "current": current + } + + def _restore_model_selection(self): + model = self.view.model() + not_set = object() + expanded = self.model_selection.pop("expanded", not_set) + selected = self.model_selection.pop("selected", not_set) + current = self.model_selection.pop("current", not_set) + + if ( + expanded is not_set + or selected is not_set + or current is not_set + ): + return + + if expanded: + for index in lib.iter_model_rows( + model, column=0, include_root=False + ): + is_expanded = index.data(self.model.ObjectIdRole) in expanded + self.view.setExpanded(index, is_expanded) + + if not selected and not current: + self.set_current_session_asset() + return + + current_index = None + selected_indexes = [] + # Go through all indices, select the ones with similar data + for index in lib.iter_model_rows( + model, column=0, include_root=False + ): + object_id = index.data(self.model.ObjectIdRole) + if object_id in selected: + selected_indexes.append(index) + + if not current_index and object_id == current: + current_index = index + + if current_index: + self.view.setCurrentIndex(current_index) + + if not selected_indexes: + return + selection_model = self.view.selectionModel() + flags = selection_model.Select | selection_model.Rows + for index in selected_indexes: + # Ensure item is visible + self.view.scrollTo(index) + selection_model.select(index, flags) + + def set_current_session_asset(self): + asset_name = self.dbcon.Session.get("AVALON_ASSET") + if asset_name: + self.select_assets([asset_name]) + + +class OptionalMenu(QtWidgets.QMenu): + """A subclass of `QtWidgets.QMenu` to work with `OptionalAction` + + This menu has reimplemented `mouseReleaseEvent`, `mouseMoveEvent` and + `leaveEvent` to provide better action hightlighting and triggering for + actions that were instances of `QtWidgets.QWidgetAction`. + + """ + + def mouseReleaseEvent(self, event): + """Emit option clicked signal if mouse released on it""" + active = self.actionAt(event.pos()) + if active and active.use_option: + option = active.widget.option + if option.is_hovered(event.globalPos()): + option.clicked.emit() + super(OptionalMenu, self).mouseReleaseEvent(event) + + def mouseMoveEvent(self, event): + """Add highlight to active action""" + active = self.actionAt(event.pos()) + for action in self.actions(): + action.set_highlight(action is active, event.globalPos()) + super(OptionalMenu, self).mouseMoveEvent(event) + + def leaveEvent(self, event): + """Remove highlight from all actions""" + for action in self.actions(): + action.set_highlight(False) + super(OptionalMenu, self).leaveEvent(event) + + +class OptionalAction(QtWidgets.QWidgetAction): + """Menu action with option box + + A menu action like Maya's menu item with option box, implemented by + subclassing `QtWidgets.QWidgetAction`. + + """ + + def __init__(self, label, icon, use_option, parent): + super(OptionalAction, self).__init__(parent) + self.label = label + self.icon = icon + self.use_option = use_option + self.option_tip = "" + self.optioned = False + + def createWidget(self, parent): + widget = OptionalActionWidget(self.label, parent) + self.widget = widget + + if self.icon: + widget.setIcon(self.icon) + + if self.use_option: + widget.option.clicked.connect(self.on_option) + widget.option.setToolTip(self.option_tip) + else: + widget.option.setVisible(False) + + return widget + + def set_option_tip(self, options): + sep = "\n\n" + mak = (lambda opt: opt["name"] + " :\n " + opt["help"]) + self.option_tip = sep.join(mak(opt) for opt in options) + + def on_option(self): + self.optioned = True + + def set_highlight(self, state, global_pos=None): + body = self.widget.body + option = self.widget.option + + role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window + body.setBackgroundRole(role) + body.setAutoFillBackground(state) + + if not self.use_option: + return + + state = option.is_hovered(global_pos) + role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window + option.setBackgroundRole(role) + option.setAutoFillBackground(state) + + +class OptionalActionWidget(QtWidgets.QWidget): + """Main widget class for `OptionalAction`""" + + def __init__(self, label, parent=None): + super(OptionalActionWidget, self).__init__(parent) + + body = QtWidgets.QWidget() + body.setStyleSheet("background: transparent;") + + icon = QtWidgets.QLabel() + label = QtWidgets.QLabel(label) + option = OptionBox(body) + + icon.setFixedSize(24, 16) + option.setFixedSize(30, 30) + + layout = QtWidgets.QHBoxLayout(body) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(2) + layout.addWidget(icon) + layout.addWidget(label) + layout.addSpacing(6) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(6, 1, 2, 1) + layout.setSpacing(0) + layout.addWidget(body) + layout.addWidget(option) + + body.setMouseTracking(True) + label.setMouseTracking(True) + option.setMouseTracking(True) + self.setMouseTracking(True) + self.setFixedHeight(32) + + self.icon = icon + self.label = label + self.option = option + self.body = body + + # (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke. + # See https://stackoverflow.com/q/52838690/4145300 + label.setStyle(QtWidgets.QStyleFactory.create("Plastique")) + + def setIcon(self, icon): + pixmap = icon.pixmap(16, 16) + self.icon.setPixmap(pixmap) + + +class OptionBox(QtWidgets.QLabel): + """Option box widget class for `OptionalActionWidget`""" + + clicked = QtCore.Signal() + + def __init__(self, parent): + super(OptionBox, self).__init__(parent) + + self.setAlignment(QtCore.Qt.AlignCenter) + + icon = qtawesome.icon("fa.sticky-note-o", color="#c6c6c6") + pixmap = icon.pixmap(18, 18) + self.setPixmap(pixmap) + + self.setStyleSheet("background: transparent;") + + def is_hovered(self, global_pos): + if global_pos is None: + return False + pos = self.mapFromGlobal(global_pos) + return self.rect().contains(pos) + + +class OptionDialog(QtWidgets.QDialog): + """Option dialog shown by option box""" + + def __init__(self, parent=None): + super(OptionDialog, self).__init__(parent) + self.setModal(True) + self._options = dict() + + def create(self, options): + parser = qargparse.QArgumentParser(arguments=options) + + decision = QtWidgets.QWidget() + accept = QtWidgets.QPushButton("Accept") + cancel = QtWidgets.QPushButton("Cancel") + + layout = QtWidgets.QHBoxLayout(decision) + layout.addWidget(accept) + layout.addWidget(cancel) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(parser) + layout.addWidget(decision) + + accept.clicked.connect(self.accept) + cancel.clicked.connect(self.reject) + parser.changed.connect(self.on_changed) + + def on_changed(self, argument): + self._options[argument["name"]] = argument.read() + + def parse(self): + return self._options.copy() From 658583fb19568bd683cd54f51329f395c4124d90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:12:16 +0200 Subject: [PATCH 207/450] added missing settings key for delete old versions action --- .../ftrack/event_handlers_user/action_delete_old_versions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_old_versions.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_old_versions.py index 063f086e9c..c66d1819ac 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_old_versions.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_old_versions.py @@ -23,6 +23,8 @@ class DeleteOldVersions(BaseAction): ) icon = statics_icon("ftrack", "action_icons", "OpenPypeAdmin.svg") + settings_key = "delete_old_versions" + dbcon = AvalonMongoDB() inteface_title = "Choose your preferences" From 07bc5c5c32eed74f1f605838d5bacc114f1db766 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:18:53 +0200 Subject: [PATCH 208/450] use loader from openpype in houdini --- openpype/hosts/houdini/startup/MainMenuCommon.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index 77ee182e7c..76585085e2 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -15,8 +15,8 @@ creator.show() From b66eadef3b9424b528a9b1d43c93fd522d1a8258 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:28:51 +0200 Subject: [PATCH 209/450] use openpype loader and library loader in fusion --- openpype/hosts/fusion/api/menu.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 3f04bf839b..9093aa9e5e 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -10,8 +10,10 @@ from .pipeline import ( from avalon.tools import ( creator, - loader, sceneinventory, +) +from openpype.tools import ( + loader, libraryloader ) From 2e33f024071c18aa0ee0eecc72d3eaa359346802 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:57:33 +0200 Subject: [PATCH 210/450] use openpype workfiles tools in hosts --- openpype/hosts/fusion/api/pipeline.py | 2 +- openpype/hosts/hiero/api/pipeline.py | 6 ++---- openpype/hosts/maya/api/__init__.py | 2 +- openpype/hosts/nuke/api/lib.py | 2 +- openpype/hosts/resolve/api/pipeline.py | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 0095530087..688e75f6fe 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -3,7 +3,7 @@ Basic avalon integration """ import os -from avalon.tools import workfiles +from openpype.tools import workfiles from avalon import api as avalon from pyblish import api as pyblish from openpype.api import Logger diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index ab7e2bdabf..12f6923de7 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -4,10 +4,8 @@ Basic avalon integration import os import contextlib from collections import OrderedDict -from avalon.tools import ( - workfiles, - publish as _publish -) +from avalon.tools import publish as _publish +from openpype.tools import workfiles from avalon.pipeline import AVALON_CONTAINER_ID from avalon import api as avalon from avalon import schema diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 9219da407f..db4dbf29c5 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -8,7 +8,7 @@ from avalon import api as avalon from avalon import pipeline from avalon.maya import suspended_refresh from avalon.maya.pipeline import IS_HEADLESS -from avalon.tools import workfiles +from openpype.tools import workfiles from pyblish import api as pyblish from openpype.lib import any_outdated import openpype.hosts.maya diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 257bf8d64e..34cf34392e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -7,7 +7,7 @@ from collections import OrderedDict from avalon import api, io, lib -from avalon.tools import workfiles +from openpype.tools import workfiles import avalon.nuke from avalon.nuke import lib as anlib from avalon.nuke import ( diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index a659ac7e51..80249310e8 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -4,7 +4,7 @@ Basic avalon integration import os import contextlib from collections import OrderedDict -from avalon.tools import workfiles +from openpype.tools import workfiles from avalon import api as avalon from avalon import schema from avalon.pipeline import AVALON_CONTAINER_ID From 9aae8f9f489540aa74d4bbc1a9f195ad47139081 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:57:46 +0200 Subject: [PATCH 211/450] use openpype loader --- openpype/hosts/hiero/api/menu.py | 5 +++-- openpype/hosts/maya/api/customize.py | 2 +- openpype/hosts/resolve/api/menu.py | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/hiero/api/menu.py b/openpype/hosts/hiero/api/menu.py index ab49251093..bcd78aa5bb 100644 --- a/openpype/hosts/hiero/api/menu.py +++ b/openpype/hosts/hiero/api/menu.py @@ -41,7 +41,8 @@ def menu_install(): apply_colorspace_project, apply_colorspace_clips ) # here is the best place to add menu - from avalon.tools import cbloader, creator, sceneinventory + from avalon.tools import creator, sceneinventory + from openpype.tools import loader from avalon.vendor.Qt import QtGui menu_name = os.environ['AVALON_LABEL'] @@ -90,7 +91,7 @@ def menu_install(): loader_action = menu.addAction("Load ...") loader_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png")) - loader_action.triggered.connect(cbloader.show) + loader_action.triggered.connect(loader.show) sceneinventory_action = menu.addAction("Manage ...") sceneinventory_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png")) diff --git a/openpype/hosts/maya/api/customize.py b/openpype/hosts/maya/api/customize.py index 22945471b7..a84412963b 100644 --- a/openpype/hosts/maya/api/customize.py +++ b/openpype/hosts/maya/api/customize.py @@ -79,7 +79,7 @@ def override_toolbox_ui(): log.warning("Could not import SceneInventory tool") try: - import avalon.tools.loader as loader + import openpype.tools.loader as loader except Exception: log.warning("Could not import Loader tool") diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index e7be3fc963..c639fd2db8 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -10,11 +10,13 @@ from .pipeline import ( from avalon.tools import ( creator, - loader, sceneinventory, - libraryloader, subsetmanager ) +from openpype.tools import ( + loader, + libraryloader, +) def load_stylesheet(): From 54e11a52ea76f28558190ab18e84e68a60de094a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 12:01:38 +0200 Subject: [PATCH 212/450] use pretty_timestamp from openpype tools utils --- openpype/modules/default_modules/sync_server/tray/models.py | 2 +- openpype/modules/default_modules/sync_server/tray/widgets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/models.py b/openpype/modules/default_modules/sync_server/tray/models.py index 8c86d3b98f..c2c63c68ea 100644 --- a/openpype/modules/default_modules/sync_server/tray/models.py +++ b/openpype/modules/default_modules/sync_server/tray/models.py @@ -5,7 +5,7 @@ from bson.objectid import ObjectId from Qt import QtCore from Qt.QtCore import Qt -from avalon.tools.delegates import pretty_timestamp +from openpype.tools.utils.delegates import pretty_timestamp from avalon.vendor import qtawesome from openpype.lib import PypeLogger diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index c9160733a0..c9b58ebe7c 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -14,7 +14,7 @@ from openpype.tools.settings import ( from openpype.api import get_local_site_id from openpype.lib import PypeLogger -from avalon.tools.delegates import pretty_timestamp +from openpype.tools.utils.delegates import pretty_timestamp from avalon.vendor import qtawesome from .models import ( From 548100e04fa901bd8b7f2f58e9ecbaced2a7c6f0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 12:01:51 +0200 Subject: [PATCH 213/450] library loader in tray is using openpype library loader --- openpype/modules/default_modules/avalon_apps/avalon_app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index b3b7dd8484..4459fa2cac 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -55,11 +55,11 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes): def tray_init(self): # Add library tool try: - from avalon.tools.libraryloader import app - from avalon import style from Qt import QtGui + from avalon import style + from openpype.tools.libraryloader import LibraryLoaderWindow - self.libraryloader = app.Window( + self.libraryloader = LibraryLoaderWindow( icon=QtGui.QIcon(resources.get_openpype_icon_filepath()), show_projects=True, show_libraries=True From cf5bf6f49c262452b4bd86b719eb884f740c246f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 12:14:49 +0200 Subject: [PATCH 214/450] fixed few hounds --- openpype/tools/loader/__main__.py | 10 ++++------ openpype/tools/loader/widgets.py | 8 +++++--- openpype/tools/utils/delegates.py | 3 ++- openpype/tools/utils/widgets.py | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/openpype/tools/loader/__main__.py b/openpype/tools/loader/__main__.py index 27794b9bc5..146ba7fd10 100644 --- a/openpype/tools/loader/__main__.py +++ b/openpype/tools/loader/__main__.py @@ -1,10 +1,12 @@ -"""Main entrypoint for standalone debugging""" -""" +"""Main entrypoint for standalone debugging + Used for running 'avalon.tool.loader.__main__' as a module (-m), useful for debugging without need to start host. Modify AVALON_MONGO accordingly """ +import os +import sys from . import cli @@ -17,7 +19,6 @@ def my_exception_hook(exctype, value, traceback): if __name__ == '__main__': - import os os.environ["AVALON_MONGO"] = "mongodb://localhost:27017" os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017" os.environ["AVALON_DB"] = "avalon" @@ -26,9 +27,6 @@ if __name__ == '__main__': os.environ["AVALON_CONFIG"] = "pype" os.environ["AVALON_ASSET"] = "Jungle" - - import sys - # Set the exception hook to our wrapping function sys.excepthook = my_exception_hook diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 9479558026..91e6f20518 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -8,7 +8,7 @@ import collections from Qt import QtWidgets, QtCore, QtGui -from avalon import api, io, pipeline +from avalon import api, pipeline from avalon.lib import HeroVersionType from openpype.tools.utils import lib as tools_lib @@ -1173,8 +1173,10 @@ class RepresentationWidget(QtWidgets.QWidget): repre_context ): if tools_lib.is_sync_loader(loader): - both_unavailable = item["active_site_progress"] <= 0 and \ - item["remote_site_progress"] <= 0 + both_unavailable = ( + item["active_site_progress"] <= 0 + and item["remote_site_progress"] <= 0 + ) if both_unavailable: continue diff --git a/openpype/tools/utils/delegates.py b/openpype/tools/utils/delegates.py index 608762b538..1827bc7e9b 100644 --- a/openpype/tools/utils/delegates.py +++ b/openpype/tools/utils/delegates.py @@ -79,7 +79,8 @@ class AssetDelegate(QtWidgets.QItemDelegate): else: bg_color.setAlpha(0) - # # -- When not needed to do a rounded corners (easier and without painter restore): + # When not needed to do a rounded corners (easier and without + # painter restore): # painter.fillRect( # item_rect, # QtGui.QBrush(bg_color) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index 153f012ff6..b9b542c123 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -6,7 +6,7 @@ from . import lib from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome, qargparse -from avalon import style, io +from avalon import style from .models import AssetModel, RecursiveSortFilterProxyModel from .views import AssetsView From 354fdd1cdc36ee6236527596f2c12b422bc4f39e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 12:18:54 +0200 Subject: [PATCH 215/450] removed unused TaskModel --- openpype/tools/utils/models.py | 126 --------------------------------- 1 file changed, 126 deletions(-) diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index d09d16d898..c5e1ce1b12 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -195,132 +195,6 @@ class Item(dict): self._children.append(child) -class TasksModel(TreeModel): - """A model listing the tasks combined for a list of assets""" - - Columns = ["name", "count"] - - def __init__(self, dbcon, parent=None): - super(TasksModel, self).__init__(parent=parent) - self.dbcon = dbcon - self._num_assets = 0 - self._icons = { - "__default__": qtawesome.icon( - "fa.male", - color=style.colors.default - ), - "__no_task__": qtawesome.icon( - "fa.exclamation-circle", - color=style.colors.mid - ) - } - - self._refresh_task_icons() - - def _refresh_task_icons(self): - # Get the project configured icons from database - project = self.dbcon.find_one({"type": "project"}, {"config.tasks"}) - tasks = project["config"].get("tasks", {}) - for task_name, task in tasks.items(): - icon_name = task.get("icon", None) - if icon_name: - icon = qtawesome.icon( - "fa.{}".format(icon_name), - color=style.colors.default - ) - self._icons[task_name] = icon - - def set_assets(self, asset_ids=None, asset_docs=None): - """Set assets to track by their database id - - Arguments: - asset_ids (list): List of asset ids. - asset_docs (list): List of asset entities from MongoDB. - - """ - - if asset_docs is None and asset_ids is not None: - # prepare filter query - _filter = {"type": "asset", "_id": {"$in": asset_ids}} - _projection = {"data.tasks"} - - # find assets in db by query - asset_docs = list(self.dbcon.find(_filter, _projection)) - db_assets_ids = [asset["_id"] for asset in asset_docs] - - # check if all assets were found - not_found = [ - str(a_id) for a_id in asset_ids if a_id not in db_assets_ids - ] - - assert not not_found, "Assets not found by id: {0}".format( - ", ".join(not_found) - ) - - if asset_docs is None: - asset_docs = list() - - self._num_assets = len(asset_docs) - - tasks = collections.Counter() - for asset_doc in asset_docs: - asset_tasks = asset_doc.get("data", {}).get("tasks", {}) - tasks.update(asset_tasks.keys()) - - self.clear() - self.beginResetModel() - - default_icon = self._icons["__default__"] - - if not tasks: - no_task_icon = self._icons["__no_task__"] - item = Item({ - "name": "No task", - "count": 0, - "icon": no_task_icon, - "enabled": False, - }) - - self.add_child(item) - - else: - for task, count in sorted(tasks.items()): - icon = self._icons.get(task, default_icon) - - item = Item({ - "name": task, - "count": count, - "icon": icon - }) - - self.add_child(item) - - self.endResetModel() - - def headerData(self, section, orientation, role): - # Override header for count column to show amount of assets - # it is listing the tasks for - if role == QtCore.Qt.DisplayRole: - if orientation == QtCore.Qt.Horizontal: - if section == 0: - return "Tasks" - elif section == 1: # count column - return "count ({0})".format(self._num_assets) - - return super(TasksModel, self).headerData(section, orientation, role) - - def data(self, index, role): - if not index.isValid(): - return - - # Add icon to the first column - if role == QtCore.Qt.DecorationRole: - if index.column() == 0: - return index.internalPointer()["icon"] - - return super(TasksModel, self).data(index, role) - - class AssetModel(TreeModel): """A model listing assets in the silo in the active project. From e3ba7e9c15ee9f1e361390683db2bc877178b7c4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 17 Sep 2021 12:53:08 +0200 Subject: [PATCH 216/450] Removal of unwanted change --- .../defaults/project_settings/nuke.json | 10 ++++++ .../schemas/schema_nuke_publish.json | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 136f1d6b42..c1c3e77684 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -96,6 +96,16 @@ }, "ExtractSlateFrame": { "viewer_lut_raw": false + }, + "IncrementScriptVersion": { + "optional": true, + "active": true, + "families": [ + "workfile", + "render", + "render.local", + "render.farm" + ] } }, "load": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 782179cfd1..d354ff15f8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -152,6 +152,38 @@ "label": "Viewer LUT raw" } ] + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Integrators" + }, + { + "type": "dict", + "collapsible": false, + "key": "IncrementScriptVersion", + "label": "IncrementScriptVersion", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "list", + "key": "families", + "object_type": "text", + "label": "Trigger on families" + } + ] } ] } From 0a033f4dbe2f3981097d3ca7623e655d939ff245 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 17 Sep 2021 13:28:49 +0200 Subject: [PATCH 217/450] Lowercased task type --- .../plugins/publish/collect_published_files.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 434f82d3ea..7e9b98956a 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -177,9 +177,11 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): (family, [families], subset_template_name, tags) tuple AssertionError if not matching family found """ - if task_type: - task_type = task_type.capitalize() - task_obj = settings.get(task_type) + task_type = task_type.lower() + lower_cased_task_types = {} + for t_type, task in settings.items(): + lower_cased_task_types[t_type.lower()] = task + task_obj = lower_cased_task_types.get(task_type) assert task_obj, "No family configuration for '{}'".format(task_type) found_family = None From 61cf8c8ecb3fcf01e32f06d14c32e770b36ab904 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 13:59:44 +0200 Subject: [PATCH 218/450] use right model for getting selection index --- openpype/tools/workfiles/app.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 3d2633f8dc..6fff0d0278 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -376,6 +376,9 @@ class TasksWidget(QtWidgets.QWidget): task (str): Name of the task to select. """ + task_view_model = self._tasks_view.model() + if not task_view_model: + return # Clear selection selection_model = self._tasks_view.selectionModel() @@ -383,8 +386,8 @@ class TasksWidget(QtWidgets.QWidget): # Select the task mode = selection_model.Select | selection_model.Rows - for row in range(self._tasks_model.rowCount()): - index = self._tasks_model.index(row, 0) + for row in range(task_view_model.rowCount()): + index = task_view_model.index(row, 0) name = index.data(TASK_NAME_ROLE) if name == task_name: selection_model.select(index, mode) From 9e72c62b201f2b95ec32b57ad0a366789bc2f9c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 13:59:56 +0200 Subject: [PATCH 219/450] fix usage of get_openpype_version function --- openpype/lib/pype_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index 8fe8701908..33715e369d 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -64,7 +64,7 @@ def is_running_staging(): Returns: bool: True if openpype version containt 'staging'. """ - if "staging" in get_pype_version(): + if "staging" in get_openpype_version(): return True return False From 83b6f112c6f4c8d2cf68fae854dc658513256f79 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 14:33:28 +0200 Subject: [PATCH 220/450] added default thumbnail image to loader --- .../tools/loader/images/default_thumbnail.png | Bin 0 -> 4018 bytes openpype/tools/loader/widgets.py | 7 +++---- 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 openpype/tools/loader/images/default_thumbnail.png diff --git a/openpype/tools/loader/images/default_thumbnail.png b/openpype/tools/loader/images/default_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..97bd958e0daf01e740db9f426e1f9b3e22484c87 GIT binary patch literal 4018 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 aQ8TBK`_#VgnlokXr%hW!U;VAB9rQ1Cc_|D4 literal 0 HcmV?d00001 diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 91e6f20518..39d162613a 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -744,11 +744,10 @@ class ThumbnailWidget(QtWidgets.QLabel): self.setAlignment(QtCore.Qt.AlignCenter) # TODO get res path much better way - loader_path = os.path.dirname(os.path.abspath(__file__)) - avalon_path = os.path.dirname(os.path.dirname(loader_path)) default_pix_path = os.path.join( - os.path.dirname(avalon_path), - "res", "tools", "images", "default_thumbnail.png" + os.path.dirname(os.path.abspath(__file__)), + "images", + "default_thumbnail.png" ) self.default_pix = QtGui.QPixmap(default_pix_path) From d960ec7e2344ec401af2a17b451f01ede624625b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 14:52:30 +0200 Subject: [PATCH 221/450] defined family view, model and proxy model --- openpype/tools/libraryloader/app.py | 4 +- openpype/tools/loader/app.py | 4 +- openpype/tools/loader/widgets.py | 180 +++++++++++++++++----------- 3 files changed, 112 insertions(+), 76 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 362d05cce6..3f7979ff1c 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -9,7 +9,7 @@ from openpype.tools.utils import lib as tools_lib from openpype.tools.loader.widgets import ( ThumbnailWidget, VersionWidget, - FamilyListWidget, + FamilyListView, RepresentationWidget ) from openpype.tools.utils.widgets import AssetWidget @@ -65,7 +65,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): assets = AssetWidget( self.dbcon, multiselection=True, parent=self ) - families = FamilyListWidget( + families = FamilyListView( self.dbcon, self.family_config_cache, parent=self ) subsets = LibrarySubsetWidget( diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 381d6b25d8..4beebe43b8 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -11,7 +11,7 @@ from openpype.tools.utils import lib from .widgets import ( SubsetWidget, VersionWidget, - FamilyListWidget, + FamilyListView, ThumbnailWidget, RepresentationWidget, OverlayFrame @@ -64,7 +64,7 @@ class LoaderWidow(QtWidgets.QDialog): assets = AssetWidget(io, multiselection=True, parent=self) assets.set_current_asset_btn_visibility(True) - families = FamilyListWidget(io, self.family_config_cache, self) + families = FamilyListView(io, self.family_config_cache, self) subsets = SubsetWidget( io, self.groups_config, diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 39d162613a..2953179509 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -846,36 +846,17 @@ class VersionWidget(QtWidgets.QWidget): self.data.set_version(version_doc) -class FamilyListWidget(QtWidgets.QListWidget): - """A Widget that lists all available families""" +class FamilyModel(QtGui.QStandardItemModel): + def __init__(self, dbcon, family_config_cache): + super(FamilyModel, self).__init__() - NameRole = QtCore.Qt.UserRole + 1 - active_changed = QtCore.Signal(list) - - def __init__(self, dbcon, family_config_cache, parent=None): - super(FamilyListWidget, self).__init__(parent=parent) - - self.family_config_cache = family_config_cache self.dbcon = dbcon + self.family_config_cache = family_config_cache - multi_select = QtWidgets.QAbstractItemView.ExtendedSelection - self.setSelectionMode(multi_select) - self.setAlternatingRowColors(True) - # Enable RMB menu - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.show_right_mouse_menu) - - self.itemChanged.connect(self._on_item_changed) + self._items_by_family = {} def refresh(self): - """Refresh the listed families. - - This gets all unique families and adds them as checkable items to - the list. - - """ - - families = [] + families = set() if self.dbcon.Session.get("AVALON_PROJECT"): result = list(self.dbcon.aggregate([ {"$match": { @@ -890,81 +871,136 @@ class FamilyListWidget(QtWidgets.QListWidget): }} ])) if result: - families = result[0]["families"] + families = set(result[0]["families"]) - # Rebuild list + root_item = self.invisibleRootItem() self.blockSignals(True) - self.clear() - for name in sorted(families): - family = self.family_config_cache.family_config(name) - if family.get("hideFilter"): + for family in tuple(self._items_by_family.keys()): + if family not in families: + item = self._items_by_family.pop(family) + root_item.removeRow(item.row()) + self.blockSignals(False) + + new_items = [] + for family in families: + if family in self._items_by_family: continue - label = family.get("label", name) - icon = family.get("icon", None) + family_config = self.family_config_cache.family_config(family) + if family_config.get("hideFilter"): + continue - # TODO: This should be more managable by the artist - # Temporarily implement support for a default state in the project - # configuration - state = family.get("state", True) - state = QtCore.Qt.Checked if state else QtCore.Qt.Unchecked + label = family_config.get("label", family) + icon = family_config.get("icon", None) - item = QtWidgets.QListWidgetItem(parent=self) - item.setText(label) - item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) - item.setData(self.NameRole, name) + if family_config.get("state", True): + state = QtCore.Qt.Checked + else: + state = QtCore.Qt.Unchecked + + item = QtGui.QStandardItem(label) + item.setFlags( + QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsUserCheckable + ) item.setCheckState(state) if icon: item.setIcon(icon) - self.addItem(item) - self.blockSignals(False) + new_items.append(item) - self.active_changed.emit(self.get_filters()) + if new_items: + root_item.appendRows(new_items) - def get_filters(self): + +class FamilyListView(QtWidgets.QListView): + active_changed = QtCore.Signal(list) + + def __init__(self, dbcon, family_config_cache, parent=None): + super(FamilyListView, self).__init__(parent=parent) + + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setAlternatingRowColors(True) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + family_model = FamilyModel(dbcon, family_config_cache) + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setDynamicSortFilter(True) + proxy_model.setSourceModel(family_model) + + self.setModel(proxy_model) + + family_model.dataChanged.connect(self._on_data_change) + self.customContextMenuRequested.connect(self._on_context_menu) + + self._family_model = family_model + self._proxy_model = proxy_model + + def refresh(self): + self._family_model.refresh() + + self.active_changed.emit(self.get_enabled_families()) + + def get_enabled_families(self): """Return the checked family items""" + model = self.model() + checked_families = [] + for row in range(model.rowCount()): + index = model.index(row, 0) + if index.data(QtCore.Qt.CheckStateRole) == QtCore.Qt.Checked: + family = index.data(QtCore.Qt.DisplayRole) + checked_families.append(family) - items = [self.item(i) for i in - range(self.count())] + return checked_families - return [item.data(self.NameRole) for item in items if - item.checkState() == QtCore.Qt.Checked] + def set_all_unchecked(self): + self._set_all_checkstate(False) - def _on_item_changed(self): - self.active_changed.emit(self.get_filters()) + def set_all_checked(self): + self._set_all_checkstate(True) + + def _set_all_checkstate(self, checked): + if checked: + state = QtCore.Qt.Checked + else: + state = QtCore.Qt.Unchecked - def _set_checkstate_all(self, state): - _state = QtCore.Qt.Checked if state is True else QtCore.Qt.Unchecked self.blockSignals(True) - for i in range(self.count()): - item = self.item(i) - item.setCheckState(_state) + + model = self._family_model + for row in range(model.rowCount()): + index = model.index(row, 0) + if index.data(QtCore.Qt.CheckStateRole) != state: + model.setData(index, state, QtCore.Qt.CheckStateRole) + self.blockSignals(False) - self.active_changed.emit(self.get_filters()) - def show_right_mouse_menu(self, pos): + self.active_changed.emit(self.get_enabled_families()) + + def _on_data_change(self, *_args): + self.active_changed.emit(self.get_enabled_families()) + + def _on_context_menu(self, pos): """Build RMB menu under mouse at current position (within widget)""" - - # Get mouse position - globalpos = self.viewport().mapToGlobal(pos) - menu = QtWidgets.QMenu(self) # Add enable all action - state_checked = QtWidgets.QAction(menu, text="Enable All") - state_checked.triggered.connect( - lambda: self._set_checkstate_all(True)) + action_check_all = QtWidgets.QAction(menu) + action_check_all.setText("Enable All") + action_check_all.triggered.connect(self.set_all_checked) # Add disable all action - state_unchecked = QtWidgets.QAction(menu, text="Disable All") - state_unchecked.triggered.connect( - lambda: self._set_checkstate_all(False)) + action_uncheck_all = QtWidgets.QAction(menu) + action_uncheck_all.setText("Disable All") + action_uncheck_all.triggered.connect(self.set_all_unchecked) - menu.addAction(state_checked) - menu.addAction(state_unchecked) + menu.addAction(action_check_all) + menu.addAction(action_uncheck_all) - menu.exec_(globalpos) + # Get mouse position + global_pos = self.viewport().mapToGlobal(pos) + menu.exec_(global_pos) class RepresentationWidget(QtWidgets.QWidget): From 767142b764194aa232a60310c969c7d42a9e1ef4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 15:04:50 +0200 Subject: [PATCH 222/450] added simpler checkstate changes with space, enter and backspace --- openpype/tools/loader/widgets.py | 63 +++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 2953179509..21018671be 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -945,7 +945,7 @@ class FamilyListView(QtWidgets.QListView): def get_enabled_families(self): """Return the checked family items""" - model = self.model() + model = self._family_model checked_families = [] for row in range(model.rowCount()): index = model.index(row, 0) @@ -956,29 +956,54 @@ class FamilyListView(QtWidgets.QListView): return checked_families def set_all_unchecked(self): - self._set_all_checkstate(False) + self._set_checkstates(False, self._get_all_indexes()) def set_all_checked(self): - self._set_all_checkstate(True) + self._set_checkstates(True, self._get_all_indexes()) - def _set_all_checkstate(self, checked): - if checked: + def _get_all_indexes(self): + indexes = [] + model = self._family_model + for row in range(model.rowCount()): + index = model.index(row, 0) + indexes.append(index) + return indexes + + def _set_checkstates(self, checked, indexes): + if not indexes: + return + + if checked is None: + state = None + elif checked: state = QtCore.Qt.Checked else: state = QtCore.Qt.Unchecked self.blockSignals(True) - model = self._family_model - for row in range(model.rowCount()): - index = model.index(row, 0) - if index.data(QtCore.Qt.CheckStateRole) != state: - model.setData(index, state, QtCore.Qt.CheckStateRole) + for index in indexes: + index_state = index.data(QtCore.Qt.CheckStateRole) + if index_state == state: + continue + + new_state = state + if new_state is None: + if index_state == QtCore.Qt.Checked: + new_state = QtCore.Qt.Unchecked + else: + new_state = QtCore.Qt.Checked + + index.model().setData(index, new_state, QtCore.Qt.CheckStateRole) self.blockSignals(False) self.active_changed.emit(self.get_enabled_families()) + def _change_selection_state(self, checked): + indexes = self.selectionModel().selectedIndexes() + self._set_checkstates(checked, indexes) + def _on_data_change(self, *_args): self.active_changed.emit(self.get_enabled_families()) @@ -1002,6 +1027,24 @@ class FamilyListView(QtWidgets.QListView): global_pos = self.viewport().mapToGlobal(pos) menu.exec_(global_pos) + def event(self, event): + if not event.type() == QtCore.QEvent.KeyPress: + pass + + elif event.key() == QtCore.Qt.Key_Space: + self._change_selection_state(None) + return True + + elif event.key() == QtCore.Qt.Key_Backspace: + self._change_selection_state(False) + return True + + elif event.key() == QtCore.Qt.Key_Return: + self._change_selection_state(True) + return True + + return super(FamilyListView, self).event(event) + class RepresentationWidget(QtWidgets.QWidget): load_started = QtCore.Signal() From ab98bf5358fc8529535481f3acf5e41a7616cdb4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 15:23:16 +0200 Subject: [PATCH 223/450] added filtering model --- openpype/tools/loader/widgets.py | 50 +++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 21018671be..0c61db2623 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -915,6 +915,46 @@ class FamilyModel(QtGui.QStandardItemModel): root_item.appendRows(new_items) +class FamilyProxyFiler(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(FamilyProxyFiler, self).__init__(*args, **kwargs) + + self._filtering_enabled = False + self._enabled_families = set() + + def set_enabled_families(self, families): + if self._enabled_families == families: + return + + self._enabled_families = families + if self._filtering_enabled: + self.invalidateFilter() + + def is_filter_enabled(self): + return self._filtering_enabled + + def set_filter_enabled(self, enabled=None): + if enabled is None: + enabled = not self._filtering_enabled + if self._filtering_enabled == enabled: + return + + self._filtering_enabled = enabled + self.invalidateFilter() + + def filterAcceptsRow(self, row, parent): + if not self._filtering_enabled: + return True + + if not self._enabled_families: + return False + + index = self.sourceModel().index(row, self.filterKeyColumn(), parent) + if index.data(QtCore.Qt.DisplayRole) in self._enabled_families: + return True + return False + + class FamilyListView(QtWidgets.QListView): active_changed = QtCore.Signal(list) @@ -926,7 +966,7 @@ class FamilyListView(QtWidgets.QListView): self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) family_model = FamilyModel(dbcon, family_config_cache) - proxy_model = QtCore.QSortFilterProxyModel() + proxy_model = FamilyProxyFiler() proxy_model.setDynamicSortFilter(True) proxy_model.setSourceModel(family_model) @@ -938,6 +978,14 @@ class FamilyListView(QtWidgets.QListView): self._family_model = family_model self._proxy_model = proxy_model + def set_enabled_families(self, families): + self._proxy_model.set_enabled_families(families) + + self.set_enabled_family_filtering(True) + + def set_enabled_family_filtering(self, enabled=None): + self._proxy_model.set_filter_enabled(enabled) + def refresh(self): self._family_model.refresh() From e402c9c51f83ac4abcda56c5b006e086e50acd4b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 17 Sep 2021 16:18:36 +0200 Subject: [PATCH 224/450] Added possibility to configure of synchronization of workfile version with selected families --- .../plugins/publish/precollect_instances.py | 13 ++++----- .../defaults/project_settings/nuke.json | 8 +++++- .../schemas/schema_nuke_publish.json | 27 ++++++++++++++++--- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index c2c25d0627..d9aec14dc2 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -13,7 +13,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): hosts = ["nuke", "nukeassist"] # presets - sync_workfile_version = False + sync_workfile_version_on_families = [] def process(self, context): asset_data = io.find_one({ @@ -120,11 +120,12 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): # sync workfile version _families_test = [family] + families self.log.debug("__ _families_test: `{}`".format(_families_test)) - if not next((f for f in _families_test - if "prerender" in f), - None) and self.sync_workfile_version: - # get version to instance for integration - instance.data['version'] = instance.context.data['version'] + for family_test in _families_test: + if family_test in self.sync_workfile_version_on_families: + self.log.debug("Syncing version with workfile for '{}'" + .format(family_test)) + # get version to instance for integration + instance.data['version'] = instance.context.data['version'] instance.data.update({ "subset": subset, diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 136f1d6b42..6ee7c2cd39 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -30,7 +30,13 @@ }, "publish": { "PreCollectNukeInstances": { - "sync_workfile_version": true + "sync_workfile_version_on_families": [ + "nukenodes", + "camera", + "gizmo", + "source", + "render" + ] }, "ValidateContainers": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 782179cfd1..2772c5f3a6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -16,9 +16,30 @@ "is_group": true, "children": [ { - "type": "boolean", - "key": "sync_workfile_version", - "label": "Sync Version from workfile" + "type": "enum", + "key": "sync_workfile_version_on_families", + "label": "Sync workfile version for families", + "multiselection": true, + "enum_items": [ + { + "nukenodes": "nukenodes" + }, + { + "camera": "camera" + }, + { + "gizmo": "gizmo" + }, + { + "source": "source" + }, + { + "prerender": "prerender" + }, + { + "render": "render" + } + ] } ] }, From 7696fbd2de0ddb239f9d257da4def93478580fb2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:21:43 +0200 Subject: [PATCH 225/450] pass refreshed to subset widget --- openpype/tools/loader/app.py | 17 +++++++---------- openpype/tools/loader/widgets.py | 2 ++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 4beebe43b8..5cb0bf41a9 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -146,6 +146,7 @@ class LoaderWidow(QtWidgets.QDialog): assets.view.clicked.connect(self.on_assetview_click) subsets.active_changed.connect(self.on_subsetschanged) subsets.version_changed.connect(self.on_versionschanged) + subsets.refreshed.connect(self._on_subset_refresh) subsets.load_started.connect(self._on_load_start) subsets.load_ended.connect(self._on_load_end) @@ -215,6 +216,12 @@ class LoaderWidow(QtWidgets.QDialog): def _hide_overlay(self): self._overlay_frame.setVisible(False) + def _on_subset_refresh(self, has_item): + subsets_widget = self.data["widgets"]["subsets"] + familis_widget = self.data["widgets"]["families"] + + subsets_widget.set_loading_state(loading=False, empty=not has_item) + def _on_load_end(self): # Delay hiding as click events happened during loading should be # blocked @@ -264,8 +271,6 @@ class LoaderWidow(QtWidgets.QDialog): def _assetschanged(self): """Selected assets have changed""" - t1 = time.time() - assets_widget = self.data["widgets"]["assets"] subsets_widget = self.data["widgets"]["subsets"] subsets_model = subsets_widget.model @@ -283,14 +288,6 @@ class LoaderWidow(QtWidgets.QDialog): empty=True ) - def on_refreshed(has_item): - empty = not has_item - subsets_widget.set_loading_state(loading=False, empty=empty) - subsets_model.refreshed.disconnect() - self.echo("Duration: %.3fs" % (time.time() - t1)) - - subsets_model.refreshed.connect(on_refreshed) - subsets_model.set_assets(asset_ids) subsets_widget.view.setColumnHidden( subsets_model.Columns.index("asset"), diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 0c61db2623..5a04cbac8f 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -122,6 +122,7 @@ class SubsetWidget(QtWidgets.QWidget): version_changed = QtCore.Signal() # version state changed for a subset load_started = QtCore.Signal() load_ended = QtCore.Signal() + refreshed = QtCore.Signal(bool) default_widths = ( ("subset", 200), @@ -242,6 +243,7 @@ class SubsetWidget(QtWidgets.QWidget): self.filter.textChanged.connect(self.proxy.setFilterRegExp) self.filter.textChanged.connect(self.view.expandAll) + model.refreshed.connect(self.refreshed) self.model.refresh() From aad79964a621ac95ed9ae1f79118cbe814c9035c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:21:53 +0200 Subject: [PATCH 226/450] fixed subset projection --- openpype/tools/loader/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 253341f70d..184d488efc 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -128,7 +128,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): "name": 1, "parent": 1, "schema": 1, - "families": 1, + "data.families": 1, "data.subsetGroup": 1 } From 2e871a7f136055be1abadcc343d4280feed0d508 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:22:10 +0200 Subject: [PATCH 227/450] store subset families --- openpype/tools/loader/model.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 184d488efc..1668fc4a27 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -70,7 +70,6 @@ class BaseRepresentationModel(object): class SubsetsModel(TreeModel, BaseRepresentationModel): - doc_fetched = QtCore.Signal() refreshed = QtCore.Signal(bool) @@ -354,10 +353,16 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): }, self.subset_doc_projection ) - for subset in subset_docs: + subset_families = set() + for subset_doc in subset_docs: if self._doc_fetching_stop: return - subset_docs_by_id[subset["_id"]] = subset + + families = subset_doc.get("data", {}).get("families") + if families: + subset_families.add(families[0]) + + subset_docs_by_id[subset_doc["_id"]] = subset_doc subset_ids = list(subset_docs_by_id.keys()) _pipeline = [ @@ -428,6 +433,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): self._doc_payload = { "asset_docs_by_id": asset_docs_by_id, "subset_docs_by_id": subset_docs_by_id, + "subset_families": subset_families, "last_versions_by_subset_id": last_versions_by_subset_id } From e6abb640d732f6e4e267c172a4ebc1f14aa464bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:22:25 +0200 Subject: [PATCH 228/450] added ability to return families of current subsets --- openpype/tools/loader/app.py | 2 ++ openpype/tools/loader/model.py | 3 +++ openpype/tools/loader/widgets.py | 3 +++ 3 files changed, 8 insertions(+) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 5cb0bf41a9..f36248b0c0 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -221,6 +221,8 @@ class LoaderWidow(QtWidgets.QDialog): familis_widget = self.data["widgets"]["families"] subsets_widget.set_loading_state(loading=False, empty=not has_item) + families = subsets_widget.get_subsets_families() + familis_widget.set_enabled_families(families) def _on_load_end(self): # Delay hiding as click events happened during loading should be diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 1668fc4a27..0ad8e88593 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -190,6 +190,9 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): self._grouping = state self.on_doc_fetched() + def get_subsets_families(self): + return self._doc_payload.get("subset_families") or set() + def setData(self, index, value, role=QtCore.Qt.EditRole): # Trigger additional edit when `version` column changed # because it also updates the information in other columns diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 5a04cbac8f..22d5f8ec3a 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -247,6 +247,9 @@ class SubsetWidget(QtWidgets.QWidget): self.model.refresh() + def get_subsets_families(self): + return self.model.get_subsets_families() + def set_family_filters(self, families): self.family_proxy.setFamiliesFilter(families) From 0a2ff39c4a1cdc478198e7a6c6441c11510a383d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 17 Sep 2021 16:18:36 +0200 Subject: [PATCH 229/450] Added possibility to configure of synchronization of workfile version with selected families --- .../plugins/publish/precollect_instances.py | 13 ++++----- .../defaults/project_settings/nuke.json | 8 +++++- .../schemas/schema_nuke_publish.json | 27 ++++++++++++++++--- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index 75d0b4f9a9..5c30df9a62 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -13,7 +13,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): hosts = ["nuke", "nukeassist"] # presets - sync_workfile_version = False + sync_workfile_version_on_families = [] def process(self, context): asset_data = io.find_one({ @@ -120,11 +120,12 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): # sync workfile version _families_test = [family] + families self.log.debug("__ _families_test: `{}`".format(_families_test)) - if not next((f for f in _families_test - if "prerender" in f), - None) and self.sync_workfile_version: - # get version to instance for integration - instance.data['version'] = instance.context.data['version'] + for family_test in _families_test: + if family_test in self.sync_workfile_version_on_families: + self.log.debug("Syncing version with workfile for '{}'" + .format(family_test)) + # get version to instance for integration + instance.data['version'] = instance.context.data['version'] instance.data.update({ "subset": subset, diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 136f1d6b42..6ee7c2cd39 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -30,7 +30,13 @@ }, "publish": { "PreCollectNukeInstances": { - "sync_workfile_version": true + "sync_workfile_version_on_families": [ + "nukenodes", + "camera", + "gizmo", + "source", + "render" + ] }, "ValidateContainers": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 782179cfd1..2772c5f3a6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -16,9 +16,30 @@ "is_group": true, "children": [ { - "type": "boolean", - "key": "sync_workfile_version", - "label": "Sync Version from workfile" + "type": "enum", + "key": "sync_workfile_version_on_families", + "label": "Sync workfile version for families", + "multiselection": true, + "enum_items": [ + { + "nukenodes": "nukenodes" + }, + { + "camera": "camera" + }, + { + "gizmo": "gizmo" + }, + { + "source": "source" + }, + { + "prerender": "prerender" + }, + { + "render": "render" + } + ] } ] }, From 5c1f34a7eb4d2dd29230b407e52aeeb452b8d475 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:37:26 +0200 Subject: [PATCH 230/450] families filtering by asset added to library loader --- openpype/tools/libraryloader/app.py | 43 ++++++++++++++--------------- openpype/tools/loader/app.py | 11 ++++---- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 3f7979ff1c..6dbe47301c 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -151,6 +151,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): assets.view.clicked.connect(self.on_assetview_click) subsets.active_changed.connect(self.on_subsetschanged) subsets.version_changed.connect(self.on_versionschanged) + subsets.refreshed.connect(self._on_subset_refresh) self.combo_projects.currentTextChanged.connect(self.on_project_change) self.sync_server = sync_server @@ -242,6 +243,12 @@ class LibraryLoaderWindow(QtWidgets.QDialog): "Config `%s` has no function `install`" % _config.__name__ ) + subsets = self.data["widgets"]["subsets"] + representations = self.data["widgets"]["representations"] + + subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) + representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) + self.family_config_cache.refresh() self.groups_config.refresh() @@ -252,12 +259,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): title = "{} - {}".format(self.tool_title, project_name) self.setWindowTitle(title) - subsets = self.data["widgets"]["subsets"] - subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) - - representations = self.data["widgets"]["representations"] - representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) - @property def current_project(self): if ( @@ -288,6 +289,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self.echo("Fetching version..") tools_lib.schedule(self._versionschanged, 150, channel="mongo") + def _on_subset_refresh(self, has_item): + subsets_widget = self.data["widgets"]["subsets"] + families_view = self.data["widgets"]["families"] + + subsets_widget.set_loading_state(loading=False, empty=not has_item) + families = subsets_widget.get_subsets_families() + families_view.set_enabled_families(families) + def set_context(self, context, refresh=True): self.echo("Setting context: {}".format(context)) lib.schedule( @@ -312,13 +321,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog): assert project_doc, "This is a bug" assets_widget = self.data["widgets"]["assets"] + families_view = self.data["widgets"]["families"] + families_view.set_enabled_families(set()) + families_view.refresh() + assets_widget.model.stop_fetch_thread() assets_widget.refresh() assets_widget.setFocus() - families = self.data["widgets"]["families"] - families.refresh() - def clear_assets_underlines(self): last_asset_ids = self.data["state"]["assetIds"] if not last_asset_ids: @@ -337,8 +347,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): def _assetschanged(self): """Selected assets have changed""" - t1 = time.time() - assets_widget = self.data["widgets"]["assets"] subsets_widget = self.data["widgets"]["subsets"] subsets_model = subsets_widget.model @@ -365,14 +373,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): empty=True ) - def on_refreshed(has_item): - empty = not has_item - subsets_widget.set_loading_state(loading=False, empty=empty) - subsets_model.refreshed.disconnect() - self.echo("Duration: %.3fs" % (time.time() - t1)) - - subsets_model.refreshed.connect(on_refreshed) - subsets_model.set_assets(asset_ids) subsets_widget.view.setColumnHidden( subsets_model.Columns.index("asset"), @@ -386,9 +386,8 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self.data["state"]["assetIds"] = asset_ids representations = self.data["widgets"]["representations"] - representations.set_version_ids([]) # reset repre list - - self.echo("Duration: %.3fs" % (time.time() - t1)) + # reset repre list + representations.set_version_ids([]) def _subsetschanged(self): asset_ids = self.data["state"]["assetIds"] diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index f36248b0c0..cce05c1d3e 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -218,11 +218,11 @@ class LoaderWidow(QtWidgets.QDialog): def _on_subset_refresh(self, has_item): subsets_widget = self.data["widgets"]["subsets"] - familis_widget = self.data["widgets"]["families"] + families_view = self.data["widgets"]["families"] subsets_widget.set_loading_state(loading=False, empty=not has_item) families = subsets_widget.get_subsets_families() - familis_widget.set_enabled_families(families) + families_view.set_enabled_families(families) def _on_load_end(self): # Delay hiding as click events happened during loading should be @@ -247,8 +247,8 @@ class LoaderWidow(QtWidgets.QDialog): assets_widget.refresh() assets_widget.setFocus() - families = self.data["widgets"]["families"] - families.refresh() + families_view = self.data["widgets"]["families"] + families_view.refresh() def clear_assets_underlines(self): """Clear colors from asset data to remove colored underlines @@ -303,7 +303,8 @@ class LoaderWidow(QtWidgets.QDialog): self.data["state"]["assetIds"] = asset_ids representations = self.data["widgets"]["representations"] - representations.set_version_ids([]) # reset repre list + # reset repre list + representations.set_version_ids([]) def _subsetschanged(self): asset_ids = self.data["state"]["assetIds"] From 0639cfa35f9a330b52aa7b5c282b38d6db2a6a0f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:42:52 +0200 Subject: [PATCH 231/450] fixed duplication of families --- openpype/tools/loader/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 22d5f8ec3a..79a31a787f 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -915,6 +915,7 @@ class FamilyModel(QtGui.QStandardItemModel): item.setIcon(icon) new_items.append(item) + self._items_by_family[family] = item if new_items: root_item.appendRows(new_items) From 152549bd4f34b25e1b67d7cac92c571a188818bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 17:24:00 +0200 Subject: [PATCH 232/450] disable projects view if defaults are modified --- .../tools/settings/settings/categories.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index c420a8cdc5..be2264340b 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -609,14 +609,23 @@ class ProjectWidget(SettingsCategoryWidget): self.project_list_widget.refresh() def _on_reset_crash(self): - self.project_list_widget.setEnabled(False) + self._set_enabled_project_list(False) super(ProjectWidget, self)._on_reset_crash() def _on_reset_success(self): - if not self.project_list_widget.isEnabled(): - self.project_list_widget.setEnabled(True) + self._set_enabled_project_list(True) super(ProjectWidget, self)._on_reset_success() + def _set_enabled_project_list(self, enabled): + if ( + enabled + and self.modify_defaults_checkbox + and self.modify_defaults_checkbox.isChecked() + ): + enabled = False + if self.project_list_widget.isEnabled() != enabled: + self.project_list_widget.setEnabled(enabled) + def _create_root_entity(self): self.entity = ProjectSettings(change_state=False) self.entity.on_change_callbacks.append(self._on_entity_change) @@ -637,7 +646,8 @@ class ProjectWidget(SettingsCategoryWidget): if self.modify_defaults_checkbox: self.modify_defaults_checkbox.setEnabled(True) - self.project_list_widget.setEnabled(True) + + self._set_enabled_project_list(True) except DefaultsNotDefined: if not self.modify_defaults_checkbox: @@ -646,7 +656,7 @@ class ProjectWidget(SettingsCategoryWidget): self.entity.set_defaults_state() self.modify_defaults_checkbox.setChecked(True) self.modify_defaults_checkbox.setEnabled(False) - self.project_list_widget.setEnabled(False) + self._set_enabled_project_list(False) except StudioDefaultsNotDefined: self.select_default_project() @@ -666,8 +676,10 @@ class ProjectWidget(SettingsCategoryWidget): def _on_modify_defaults(self): if self.modify_defaults_checkbox.isChecked(): + self._set_enabled_project_list(False) if not self.entity.is_in_defaults_state(): self.reset() else: + self._set_enabled_project_list(True) if not self.entity.is_in_studio_state(): self.reset() From 8c0a9add2de64db00f8590a187b5db5a04b8d080 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 17:24:14 +0200 Subject: [PATCH 233/450] added better colors for disabled view --- openpype/tools/settings/settings/style/style.css | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css index d9d85a481e..32259af30c 100644 --- a/openpype/tools/settings/settings/style/style.css +++ b/openpype/tools/settings/settings/style/style.css @@ -146,6 +146,15 @@ QSlider::handle:vertical { border: 1px solid #464b54; background: #21252B; } + +#ProjectListWidget QListView:disabled { + background: #282C34; +} + +#ProjectListWidget QListView::item:disabled { + color: #4e5254; +} + #ProjectListWidget QLabel { background: transparent; font-weight: bold; @@ -249,8 +258,6 @@ QTabBar::tab:!selected:hover { background: #333840; } - - QTabBar::tab:first:selected { margin-left: 0; } From aadd769ef44485d23f6b1729009da07969e1284f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 17:27:22 +0200 Subject: [PATCH 234/450] keep selected color unchanged even if view loose focus --- openpype/tools/settings/settings/style/style.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css index 32259af30c..b77b575204 100644 --- a/openpype/tools/settings/settings/style/style.css +++ b/openpype/tools/settings/settings/style/style.css @@ -412,12 +412,15 @@ QHeaderView::section { font-weight: bold; } -QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { +QAbstractItemView::item:pressed { background: #78879b; color: #FFFFFF; } -QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { +QAbstractItemView::item:selected:active { + background: #3d8ec9; +} +QAbstractItemView::item:selected:!active { background: #3d8ec9; } From 11ad2a87b7fb87e8ff4ff0d35e1675b62ca0b497 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:36:51 +0200 Subject: [PATCH 235/450] FamiliesFilterProxyModel does not need family config --- openpype/tools/loader/model.py | 7 +------ openpype/tools/loader/widgets.py | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 0ad8e88593..6e9c7bf220 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -860,10 +860,9 @@ class SubsetFilterProxyModel(GroupMemberFilterProxyModel): class FamiliesFilterProxyModel(GroupMemberFilterProxyModel): """Filters to specified families""" - def __init__(self, family_config_cache, *args, **kwargs): + def __init__(self, *args, **kwargs): super(FamiliesFilterProxyModel, self).__init__(*args, **kwargs) self._families = set() - self.family_config_cache = family_config_cache def familyFilter(self): return self._families @@ -895,10 +894,6 @@ class FamiliesFilterProxyModel(GroupMemberFilterProxyModel): if not family: return True - family_config = self.family_config_cache.family_config(family) - if family_config.get("hideFilter"): - return False - # We want to keep the families which are not in the list return family in self._families diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 79a31a787f..6d29dee6ec 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -159,7 +159,7 @@ class SubsetWidget(QtWidgets.QWidget): grouping=enable_grouping ) proxy = SubsetFilterProxyModel() - family_proxy = FamiliesFilterProxyModel(family_config_cache) + family_proxy = FamiliesFilterProxyModel() family_proxy.setSourceModel(proxy) subset_filter = QtWidgets.QLineEdit() From eb9b88068c656cf7ec2d73032b4b174950e33388 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:37:11 +0200 Subject: [PATCH 236/450] refresh family configu during refresh of family model --- openpype/tools/loader/widgets.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 6d29dee6ec..650879ac86 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -879,12 +879,13 @@ class FamilyModel(QtGui.QStandardItemModel): families = set(result[0]["families"]) root_item = self.invisibleRootItem() - self.blockSignals(True) + for family in tuple(self._items_by_family.keys()): if family not in families: item = self._items_by_family.pop(family) root_item.removeRow(item.row()) - self.blockSignals(False) + + self.family_config_cache.refresh() new_items = [] for family in families: @@ -892,8 +893,6 @@ class FamilyModel(QtGui.QStandardItemModel): continue family_config = self.family_config_cache.family_config(family) - if family_config.get("hideFilter"): - continue label = family_config.get("label", family) icon = family_config.get("icon", None) From 84cab2bcdd24f04522b1552ad429613c8c05a6d9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:37:24 +0200 Subject: [PATCH 237/450] removed unused global_family_cache --- openpype/tools/utils/lib.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index e83f663b2e..c402b1f169 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -12,18 +12,6 @@ self = sys.modules[__name__] self._jobs = dict() -class SharedObjects: - # Variable for family cache in global context - # QUESTION is this safe? More than one tool can refresh at the same time. - family_cache = None - - -def global_family_cache(): - if SharedObjects.family_cache is None: - SharedObjects.family_cache = FamilyConfigCache(io) - return SharedObjects.family_cache - - def format_version(value, hero_version=False): """Formats integer to displayable version name""" label = "v{0:03d}".format(value) From 9433ffe049216f6f549d98ef3dcfb896831253ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:37:56 +0200 Subject: [PATCH 238/450] modified family config to suit more for settings --- openpype/tools/utils/lib.py | 119 +++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 50 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index c402b1f169..db34389434 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -5,9 +5,13 @@ import collections from Qt import QtWidgets, QtCore, QtGui -from avalon import io, api, style +import avalon.api +from avalon import style from avalon.vendor import qtawesome +from openpype.api import get_project_settings +from openpype.lib import filter_profiles + self = sys.modules[__name__] self._jobs = dict() @@ -277,11 +281,12 @@ def preserve_selection(tree_view, column=0, role=None, current_index=True): class FamilyConfigCache: default_color = "#0091B2" _default_icon = None - _default_item = None def __init__(self, dbcon): self.dbcon = dbcon self.family_configs = {} + self._family_filters_set = False + self._require_refresh = True @classmethod def default_icon(cls): @@ -293,15 +298,29 @@ class FamilyConfigCache: @classmethod def default_item(cls): - if cls._default_item is None: - cls._default_item = {"icon": cls.default_icon()} - return cls._default_item + return { + "icon": cls.default_icon() + } def family_config(self, family_name): """Get value from config with fallback to default""" - return self.family_configs.get(family_name, self.default_item()) + if self._require_refresh: + self._refresh() - def refresh(self): + item = self.family_configs.get(family_name) + if not item: + item = self.default_item() + if self._family_filters_set: + item["state"] = False + return item + + def refresh(self, force=False): + self._require_refresh = True + + if force: + self._refresh() + + def _refresh(self): """Get the family configurations from the database The configuration must be stored on the project under `config`. @@ -317,62 +336,62 @@ class FamilyConfigCache: It is possible to override the default behavior and set specific families checked. For example we only want the families imagesequence and camera to be visible in the Loader. - - # This will turn every item off - api.data["familyStateDefault"] = False - - # Only allow the imagesequence and camera - api.data["familyStateToggled"] = ["imagesequence", "camera"] - """ + self._require_refresh = False + self._family_filters_set = False self.family_configs.clear() - - families = [] + # Skip if we're not in host context + if not avalon.api.registered_host(): + return # Update the icons from the project configuration project_name = self.dbcon.Session.get("AVALON_PROJECT") - if project_name: - project_doc = self.dbcon.find_one( - {"type": "project"}, - projection={"config.families": True} + asset_name = self.dbcon.Session.get("AVALON_ASSET") + task_name = self.dbcon.Session.get("AVALON_TASK") + if not all((project_name, asset_name, task_name)): + return + + matching_item = None + project_settings = get_project_settings(project_name) + profiles = ( + project_settings + ["global"] + ["tools"] + ["loader"] + ["family_filter_profiles"] + ) + if profiles: + asset_doc = self.dbcon.find_one( + {"type": "asset", "name": asset_name}, + {"data.tasks": True} ) + tasks_info = asset_doc.get("data", {}).get("tasks") or {} + task_type = tasks_info.get(task_name, {}).get("type") + profiles_filter = { + "task_types": task_type, + "hosts": os.environ["AVALON_APP"] + } + matching_item = filter_profiles(profiles, profiles_filter) - if not project_doc: - print(( - "Project \"{}\" not found!" - " Can't refresh family icons cache." - ).format(project_name)) - else: - families = project_doc["config"].get("families") or [] + families = [] + if matching_item: + families = matching_item["filter_families"] - # Check if any family state are being overwritten by the configuration - default_state = api.data.get("familiesStateDefault", True) - toggled = set(api.data.get("familiesStateToggled") or []) + if not families: + return + + self._family_filters_set = True # Replace icons with a Qt icon we can use in the user interfaces for family in families: - name = family["name"] - # Set family icon - icon = family.get("icon", None) - if icon: - family["icon"] = qtawesome.icon( - "fa.{}".format(icon), - color=self.default_color - ) - else: - family["icon"] = self.default_icon() + family_info = { + "name": family, + "icon": self.default_icon(), + "state": True + } - # Update state - if name in toggled: - state = True - else: - state = default_state - family["state"] = state - - self.family_configs[name] = family - - return self.family_configs + self.family_configs[family] = family_info class GroupsConfig: From 7ccc1bc01077bab2f1afc004cdfcf86568c0ebbd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:43:07 +0200 Subject: [PATCH 239/450] added initial settings for families --- .../defaults/project_settings/global.json | 9 ++++ .../schemas/schema_global_tools.json | 42 +++++++++++++++++++ .../schemas/template_publish_families.json | 20 +++++++++ 3 files changed, 71 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index a53ae14914..6a61f2f5c3 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -287,6 +287,15 @@ "textures" ] } + }, + "loader": { + "family_filter_profiles": [ + { + "hosts": [], + "task_types": [], + "filter_families": [] + } + ] } }, "project_folder_structure": "{\"__project_root__\": {\"prod\": {}, \"resources\": {\"footage\": {\"plates\": {}, \"offline\": {}}, \"audio\": {}, \"art_dept\": {}}, \"editorial\": {}, \"assets[ftrack.Library]\": {\"characters[ftrack]\": {}, \"locations[ftrack]\": {}}, \"shots[ftrack.Sequence]\": {\"scripts\": {}, \"editorial[ftrack.Folder]\": {}}}}", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 245560f115..8382bfe3f6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -190,6 +190,48 @@ } } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "loader", + "label": "Loader", + "children": [ + { + "type": "list", + "key": "family_filter_profiles", + "label": "Family filtering", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "type": "task-types-enum", + "key": "task_types", + "label": "Task types" + }, + { + "type": "splitter" + }, + { + "type": "template", + "name": "template_publish_families", + "template_data": { + "key": "filter_families", + "label": "Filter families", + "multiselection": true + } + } + ] + } + } + ] } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json new file mode 100644 index 0000000000..edec3bad3d --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json @@ -0,0 +1,20 @@ +[ + { + "__default_values__": { + "multiselection": true + } + }, + { + "key": "{key}", + "label": "{label}", + "multiselection": "{multiselection}", + "type": "enum", + "enum_items": [ + {"family1": "family1"}, + {"family2": "family2"}, + {"family3": "family3"}, + {"family4": "family4"}, + {"family5": "family5"} + ] + } +] From 1bbfa72b18440b52735d7c782fa79d00948fb41e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:57:05 +0200 Subject: [PATCH 240/450] added more suitable families --- .../schemas/template_publish_families.json | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json index edec3bad3d..9db1427562 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json @@ -10,11 +10,23 @@ "multiselection": "{multiselection}", "type": "enum", "enum_items": [ - {"family1": "family1"}, - {"family2": "family2"}, - {"family3": "family3"}, - {"family4": "family4"}, - {"family5": "family5"} + {"action": "action"}, + {"animation": "animation"}, + {"audio": "audio"}, + {"camera": "camera"}, + {"editorial": "editorial"}, + {"layout": "layout"}, + {"look": "look"}, + {"mayaAscii": "mayaAscii"}, + {"model": "model"}, + {"pointcache": "pointcache"}, + {"reference": "reference"}, + {"render": "render"}, + {"review": "review"}, + {"rig": "rig"}, + {"setdress": "setdress"}, + {"workfile": "workfile"}, + {"xgen": "xgen"} ] } ] From 622dfa0d4fbbabec98f6fbb242f06629ad361c38 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:19:03 +0200 Subject: [PATCH 241/450] use SharedObject class for jobs --- openpype/tools/utils/lib.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index db34389434..00f64211b8 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -12,9 +12,6 @@ from avalon.vendor import qtawesome from openpype.api import get_project_settings from openpype.lib import filter_profiles -self = sys.modules[__name__] -self._jobs = dict() - def format_version(value, hero_version=False): """Formats integer to displayable version name""" @@ -58,6 +55,10 @@ def defer(delay, func): return func() +class SharedObjects: + jobs = {} + + def schedule(func, time, channel="default"): """Run `func` at a later `time` in a dedicated `channel` @@ -69,7 +70,7 @@ def schedule(func, time, channel="default"): """ try: - self._jobs[channel].stop() + SharedObjects.jobs[channel].stop() except (AttributeError, KeyError, RuntimeError): pass @@ -78,7 +79,7 @@ def schedule(func, time, channel="default"): timer.timeout.connect(func) timer.start(time) - self._jobs[channel] = timer + SharedObjects.jobs[channel] = timer @contextlib.contextmanager From 424c76e3ea42a84eed6b5772ba2f52988492cefa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:19:22 +0200 Subject: [PATCH 242/450] replaced default item with just using new dictionary --- openpype/tools/utils/lib.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 00f64211b8..8454dad0e5 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -91,7 +91,6 @@ def dummy(): .. pass """ - yield @@ -297,11 +296,6 @@ class FamilyConfigCache: ) return cls._default_icon - @classmethod - def default_item(cls): - return { - "icon": cls.default_icon() - } def family_config(self, family_name): """Get value from config with fallback to default""" @@ -310,7 +304,9 @@ class FamilyConfigCache: item = self.family_configs.get(family_name) if not item: - item = self.default_item() + item = { + "icon": self.default_icon() + } if self._family_filters_set: item["state"] = False return item From 62b975dde29e5cbce88336b7d6be0b8705934666 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:26:52 +0200 Subject: [PATCH 243/450] update family filters on context change --- openpype/tools/loader/app.py | 5 ++++- openpype/tools/loader/widgets.py | 27 ++++++++++++++------------- openpype/tools/utils/lib.py | 7 +++---- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index cce05c1d3e..18e94b7474 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -232,8 +232,11 @@ class LoaderWidow(QtWidgets.QDialog): # ------------------------------ def on_context_task_change(self, *args, **kwargs): - # Change to context asset on context change assets_widget = self.data["widgets"]["assets"] + families_view = self.data["widgets"]["families"] + # Refresh families config + families_view.refresh() + # Change to context asset on context change assets_widget.select_assets(io.Session["AVALON_ASSET"]) def _refresh(self): diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 650879ac86..e94942e7b7 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -889,11 +889,7 @@ class FamilyModel(QtGui.QStandardItemModel): new_items = [] for family in families: - if family in self._items_by_family: - continue - family_config = self.family_config_cache.family_config(family) - label = family_config.get("label", family) icon = family_config.get("icon", None) @@ -902,20 +898,25 @@ class FamilyModel(QtGui.QStandardItemModel): else: state = QtCore.Qt.Unchecked - item = QtGui.QStandardItem(label) - item.setFlags( - QtCore.Qt.ItemIsEnabled - | QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsUserCheckable - ) + if family not in self._items_by_family: + item = QtGui.QStandardItem(label) + item.setFlags( + QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsUserCheckable + ) + + else: + item = self._items_by_family[label] + item.setData(QtCore.Qt.DisplayRole, label) + new_items.append(item) + self._items_by_family[family] = item + item.setCheckState(state) if icon: item.setIcon(icon) - new_items.append(item) - self._items_by_family[family] = item - if new_items: root_item.appendRows(new_items) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 8454dad0e5..d01dbbd169 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -296,7 +296,6 @@ class FamilyConfigCache: ) return cls._default_icon - def family_config(self, family_name): """Get value from config with fallback to default""" if self._require_refresh: @@ -343,9 +342,9 @@ class FamilyConfigCache: return # Update the icons from the project configuration - project_name = self.dbcon.Session.get("AVALON_PROJECT") - asset_name = self.dbcon.Session.get("AVALON_ASSET") - task_name = self.dbcon.Session.get("AVALON_TASK") + project_name = os.environ.get("AVALON_PROJECT") + asset_name = os.environ.get("AVALON_ASSET") + task_name = os.environ.get("AVALON_TASK") if not all((project_name, asset_name, task_name)): return From 7ab717b03f6bf42fd1888d143f3609a612d917e7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:30:00 +0200 Subject: [PATCH 244/450] fix class name --- openpype/tools/loader/__init__.py | 4 ++-- openpype/tools/loader/app.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/tools/loader/__init__.py b/openpype/tools/loader/__init__.py index c7bd6148a7..a5fda8f018 100644 --- a/openpype/tools/loader/__init__.py +++ b/openpype/tools/loader/__init__.py @@ -1,11 +1,11 @@ from .app import ( - LoaderWidow, + LoaderWindow, show, cli, ) __all__ = ( - "LoaderWidow", + "LoaderWindow", "show", "cli", ) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 381d6b25d8..5db7a3bcb1 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -34,13 +34,13 @@ def on_context_task_change(*args, **kwargs): pipeline.on("taskChanged", on_context_task_change) -class LoaderWidow(QtWidgets.QDialog): +class LoaderWindow(QtWidgets.QDialog): """Asset loader interface""" tool_name = "loader" def __init__(self, parent=None): - super(LoaderWidow, self).__init__(parent) + super(LoaderWindow, self).__init__(parent) title = "Asset Loader 2.1" project_name = api.Session.get("AVALON_PROJECT") if project_name: @@ -169,11 +169,11 @@ class LoaderWidow(QtWidgets.QDialog): self.resize(1300, 700) def resizeEvent(self, event): - super(LoaderWidow, self).resizeEvent(event) + super(LoaderWindow, self).resizeEvent(event) self._overlay_frame.resize(self.size()) def moveEvent(self, event): - super(LoaderWidow, self).moveEvent(event) + super(LoaderWindow, self).moveEvent(event) self._overlay_frame.move(0, 0) # ------------------------------- @@ -454,7 +454,7 @@ class LoaderWidow(QtWidgets.QDialog): self.setAttribute(QtCore.Qt.WA_DeleteOnClose) print("Good bye") - return super(LoaderWidow, self).closeEvent(event) + return super(LoaderWindow, self).closeEvent(event) def keyPressEvent(self, event): modifiers = event.modifiers() @@ -466,7 +466,7 @@ class LoaderWidow(QtWidgets.QDialog): self.show_grouping_dialog() return - super(LoaderWidow, self).keyPressEvent(event) + super(LoaderWindow, self).keyPressEvent(event) event.setAccepted(True) # Avoid interfering other widgets def show_grouping_dialog(self): @@ -627,7 +627,7 @@ def show(debug=False, parent=None, use_context=False): module.project = any_project["name"] with lib.application(): - window = LoaderWidow(parent) + window = LoaderWindow(parent) window.setStyleSheet(style.load_stylesheet()) window.show() From e7310036645ae00db4a556ed7ab3de6dbdcb2a8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:30:00 +0200 Subject: [PATCH 245/450] fix class name --- openpype/tools/loader/__init__.py | 4 ++-- openpype/tools/loader/app.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/tools/loader/__init__.py b/openpype/tools/loader/__init__.py index c7bd6148a7..a5fda8f018 100644 --- a/openpype/tools/loader/__init__.py +++ b/openpype/tools/loader/__init__.py @@ -1,11 +1,11 @@ from .app import ( - LoaderWidow, + LoaderWindow, show, cli, ) __all__ = ( - "LoaderWidow", + "LoaderWindow", "show", "cli", ) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 18e94b7474..342a00eded 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -34,13 +34,13 @@ def on_context_task_change(*args, **kwargs): pipeline.on("taskChanged", on_context_task_change) -class LoaderWidow(QtWidgets.QDialog): +class LoaderWindow(QtWidgets.QDialog): """Asset loader interface""" tool_name = "loader" def __init__(self, parent=None): - super(LoaderWidow, self).__init__(parent) + super(LoaderWindow, self).__init__(parent) title = "Asset Loader 2.1" project_name = api.Session.get("AVALON_PROJECT") if project_name: @@ -170,11 +170,11 @@ class LoaderWidow(QtWidgets.QDialog): self.resize(1300, 700) def resizeEvent(self, event): - super(LoaderWidow, self).resizeEvent(event) + super(LoaderWindow, self).resizeEvent(event) self._overlay_frame.resize(self.size()) def moveEvent(self, event): - super(LoaderWidow, self).moveEvent(event) + super(LoaderWindow, self).moveEvent(event) self._overlay_frame.move(0, 0) # ------------------------------- @@ -457,7 +457,7 @@ class LoaderWidow(QtWidgets.QDialog): self.setAttribute(QtCore.Qt.WA_DeleteOnClose) print("Good bye") - return super(LoaderWidow, self).closeEvent(event) + return super(LoaderWindow, self).closeEvent(event) def keyPressEvent(self, event): modifiers = event.modifiers() @@ -469,7 +469,7 @@ class LoaderWidow(QtWidgets.QDialog): self.show_grouping_dialog() return - super(LoaderWidow, self).keyPressEvent(event) + super(LoaderWindow, self).keyPressEvent(event) event.setAccepted(True) # Avoid interfering other widgets def show_grouping_dialog(self): @@ -630,7 +630,7 @@ def show(debug=False, parent=None, use_context=False): module.project = any_project["name"] with lib.application(): - window = LoaderWidow(parent) + window = LoaderWindow(parent) window.setStyleSheet(style.load_stylesheet()) window.show() From 1ca05efbaa134999e0dc3f3ed2b35e3e2feb4a8d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:34:55 +0200 Subject: [PATCH 246/450] hound fixes --- openpype/tools/libraryloader/app.py | 1 - openpype/tools/loader/app.py | 1 - openpype/tools/loader/widgets.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 6dbe47301c..8080c547c9 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -1,5 +1,4 @@ import sys -import time from Qt import QtWidgets, QtCore, QtGui diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 342a00eded..c18b6e798a 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -1,5 +1,4 @@ import sys -import time from Qt import QtWidgets, QtCore from avalon import api, io, style, pipeline diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index e94942e7b7..881e9c206b 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -911,7 +911,7 @@ class FamilyModel(QtGui.QStandardItemModel): item.setData(QtCore.Qt.DisplayRole, label) new_items.append(item) self._items_by_family[family] = item - + item.setCheckState(state) if icon: From 5e40602881c51eb52d162de6e1f9b1c64c57f4f1 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Sep 2021 19:28:14 +0100 Subject: [PATCH 247/450] add new labels to changelog categories --- .github/workflows/prerelease.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 82f9a6ae9d..6c2fd07f78 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -43,11 +43,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - breakingLabel: '**💥 Breaking**' - enhancementLabel: '**🚀 Enhancements**' - bugsLabel: '**🐛 Bug fixes**' - deprecatedLabel: '**⚠️ Deprecations**' - addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"### 🆕 New features","labels":["feature"]},}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"feature":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"feature":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"feature":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"feature":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, }' issues: false issuesWoLabels: false sinceTag: "3.0.0" From 1737a8cb6818aab92f76c13b35bdc464afc69574 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Sep 2021 19:40:24 +0100 Subject: [PATCH 248/450] fix wrong keys in changelog categories --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 6c2fd07f78..ddab0e59a8 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -43,7 +43,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"feature":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"feature":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"feature":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"feature":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, }' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}' issues: false issuesWoLabels: false sinceTag: "3.0.0" From 4eb3f5fb66080d2df42201bf96c9ce11e2487c53 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Fri, 17 Sep 2021 18:46:00 +0000 Subject: [PATCH 249/450] [Automated] Bump version --- CHANGELOG.md | 137 +++++++++++++++++++++++++++----------------- openpype/version.py | 2 +- 2 files changed, 85 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1737458b2..3106a878b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,35 +1,72 @@ # Changelog -## [3.4.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.4.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...HEAD) -**Merged pull requests:** +### 📖 Documentation -- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972) -- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) +- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) +- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) +- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) + +**🚀 Enhancements** + +- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) +- General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) +- Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) +- Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) +- General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) +- Ftrack: Sync to avalon actions have jobs [\#2015](https://github.com/pypeclub/OpenPype/pull/2015) +- Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009) +- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) +- Settings UI: Number with configurable steps [\#2001](https://github.com/pypeclub/OpenPype/pull/2001) +- Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996) +- Configurable items for providers without Settings [\#1987](https://github.com/pypeclub/OpenPype/pull/1987) +- Global: Example addons [\#1986](https://github.com/pypeclub/OpenPype/pull/1986) +- Settings UI: Number sliders [\#1978](https://github.com/pypeclub/OpenPype/pull/1978) +- Workfiles: Support more workfile templates [\#1966](https://github.com/pypeclub/OpenPype/pull/1966) - Launcher: Fix crashes on action click [\#1964](https://github.com/pypeclub/OpenPype/pull/1964) - Settings: Minor fixes in UI and missing default values [\#1963](https://github.com/pypeclub/OpenPype/pull/1963) - Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962) -- Resolve path when adding to zip [\#1960](https://github.com/pypeclub/OpenPype/pull/1960) -- Bump url-parse from 1.5.1 to 1.5.3 in /website [\#1958](https://github.com/pypeclub/OpenPype/pull/1958) +- Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959) - Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949) -- Global: Define hosts in CollectSceneVersion [\#1948](https://github.com/pypeclub/OpenPype/pull/1948) -- Maya: Add Xgen family support [\#1947](https://github.com/pypeclub/OpenPype/pull/1947) -- Add face sets to exported alembics [\#1942](https://github.com/pypeclub/OpenPype/pull/1942) -- Bump path-parse from 1.0.6 to 1.0.7 in /website [\#1933](https://github.com/pypeclub/OpenPype/pull/1933) -- \#1894 - adds host to template\_name\_profiles for filtering [\#1915](https://github.com/pypeclub/OpenPype/pull/1915) -- Environments: Tool environments in alphabetical order [\#1910](https://github.com/pypeclub/OpenPype/pull/1910) -- Disregard publishing time. [\#1888](https://github.com/pypeclub/OpenPype/pull/1888) -- Feature/webpublisher backend [\#1876](https://github.com/pypeclub/OpenPype/pull/1876) -- Dynamic modules [\#1872](https://github.com/pypeclub/OpenPype/pull/1872) -- Houdini: add Camera, Point Cache, Composite, Redshift ROP and VDB Cache support [\#1821](https://github.com/pypeclub/OpenPype/pull/1821) + +**🐛 Bug fixes** + +- Workfiles tool: Task selection [\#2040](https://github.com/pypeclub/OpenPype/pull/2040) +- Ftrack: Delete old versions missing settings key [\#2037](https://github.com/pypeclub/OpenPype/pull/2037) +- Nuke: typo on a button [\#2034](https://github.com/pypeclub/OpenPype/pull/2034) +- Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033) +- General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016) +- Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006) +- Nuke thumbnails generated from middle of the sequence [\#1992](https://github.com/pypeclub/OpenPype/pull/1992) +- Nuke: last version from path gets correct version [\#1990](https://github.com/pypeclub/OpenPype/pull/1990) +- nuke, resolve, hiero: precollector order lest then 0.5 [\#1984](https://github.com/pypeclub/OpenPype/pull/1984) +- Last workfile with multiple work templates [\#1981](https://github.com/pypeclub/OpenPype/pull/1981) +- Collectors order [\#1977](https://github.com/pypeclub/OpenPype/pull/1977) +- Stop timer was within validator order range. [\#1975](https://github.com/pypeclub/OpenPype/pull/1975) +- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972) +- Deadline: Houdini plugins in different hierarchy [\#1970](https://github.com/pypeclub/OpenPype/pull/1970) +- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) +- Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961) + +**Merged pull requests:** + +- Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982) +- Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974) +- Bump url-parse from 1.5.1 to 1.5.3 in /website [\#1958](https://github.com/pypeclub/OpenPype/pull/1958) +- CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) -**Merged pull requests:** +**🚀 Enhancements** + +- Global: Define hosts in CollectSceneVersion [\#1948](https://github.com/pypeclub/OpenPype/pull/1948) + +**🐛 Bug fixes** - TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) - Maya: Menu actions fix [\#1945](https://github.com/pypeclub/OpenPype/pull/1945) @@ -40,52 +77,46 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.0-nightly.11...3.3.0) -**Merged pull requests:** +### 📖 Documentation + +- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) +- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) + +**🆕 New features** + +- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) + +**🚀 Enhancements** - Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940) -- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937) -- Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) -- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) -- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) -- Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929) - Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) -- Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926) - Check for missing ✨ Python when using `pyenv` [\#1925](https://github.com/pypeclub/OpenPype/pull/1925) -- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) -- Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922) - Settings: Default values for enum [\#1920](https://github.com/pypeclub/OpenPype/pull/1920) - Settings UI: Modifiable dict view enhance [\#1919](https://github.com/pypeclub/OpenPype/pull/1919) -- standalone: validator asset parents [\#1917](https://github.com/pypeclub/OpenPype/pull/1917) -- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916) -- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914) - submodules: avalon-core update [\#1911](https://github.com/pypeclub/OpenPype/pull/1911) -- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906) -- Add support for multiple Deadline ☠️➖ servers [\#1905](https://github.com/pypeclub/OpenPype/pull/1905) -- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904) -- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903) -- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902) -- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) - Ftrack: Where I run action enhancement [\#1900](https://github.com/pypeclub/OpenPype/pull/1900) - Ftrack: Private project server actions [\#1899](https://github.com/pypeclub/OpenPype/pull/1899) - Support nested studio plugins paths. [\#1898](https://github.com/pypeclub/OpenPype/pull/1898) -- Bug: fixed python detection [\#1893](https://github.com/pypeclub/OpenPype/pull/1893) -- Settings: global validators with options [\#1892](https://github.com/pypeclub/OpenPype/pull/1892) -- Settings: Conditional dict enum positioning [\#1891](https://github.com/pypeclub/OpenPype/pull/1891) -- global: integrate name missing default template [\#1890](https://github.com/pypeclub/OpenPype/pull/1890) -- publisher: editorial plugins fixes [\#1889](https://github.com/pypeclub/OpenPype/pull/1889) -- Expose stop timer through rest api. [\#1886](https://github.com/pypeclub/OpenPype/pull/1886) -- TVPaint: Increment workfile [\#1885](https://github.com/pypeclub/OpenPype/pull/1885) -- Allow Multiple Notes to run on tasks. [\#1882](https://github.com/pypeclub/OpenPype/pull/1882) -- Normalize path returned from Workfiles. [\#1880](https://github.com/pypeclub/OpenPype/pull/1880) -- Prepare for pyside2 [\#1869](https://github.com/pypeclub/OpenPype/pull/1869) -- Filter hosts in settings host-enum [\#1868](https://github.com/pypeclub/OpenPype/pull/1868) -- Local actions with process identifier [\#1867](https://github.com/pypeclub/OpenPype/pull/1867) -- Workfile tool start at host launch support [\#1865](https://github.com/pypeclub/OpenPype/pull/1865) -- Maya: add support for `RedshiftNormalMap` node, fix `tx` linear space 🚀 [\#1863](https://github.com/pypeclub/OpenPype/pull/1863) -- Workfiles tool event arguments fix [\#1862](https://github.com/pypeclub/OpenPype/pull/1862) -- Maya: support for configurable `dirmap` 🗺️ [\#1859](https://github.com/pypeclub/OpenPype/pull/1859) -- Maya: don't add reference members as connections to the container set 📦 [\#1855](https://github.com/pypeclub/OpenPype/pull/1855) -- Settings list can use template or schema as object type [\#1815](https://github.com/pypeclub/OpenPype/pull/1815) + +**🐛 Bug fixes** + +- Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) +- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) +- Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929) +- Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926) +- Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922) +- standalone: validator asset parents [\#1917](https://github.com/pypeclub/OpenPype/pull/1917) +- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916) +- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914) +- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906) +- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904) +- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903) +- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902) + +**Merged pull requests:** + +- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937) +- Add support for multiple Deadline ☠️➖ servers [\#1905](https://github.com/pypeclub/OpenPype/pull/1905) ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) diff --git a/openpype/version.py b/openpype/version.py index 17bd0ff892..3f166f0735 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.0-nightly.4" +__version__ = "3.4.0-nightly.5" From 90cea71a024532d46fbe8b9eca865b19cb07cb37 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Sep 2021 20:06:19 +0100 Subject: [PATCH 250/450] Fix changelog generation for release --- .github/workflows/release.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37e1cb4b15..5d3f301b99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,11 +39,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - breakingLabel: '**💥 Breaking**' - enhancementLabel: '**🚀 Enhancements**' - bugsLabel: '**🐛 Bug fixes**' - deprecatedLabel: '**⚠️ Deprecations**' - addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' + addSections: '{"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]},"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]}}' issues: false issuesWoLabels: false sinceTag: "3.0.0" @@ -85,11 +81,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - breakingLabel: '**💥 Breaking**' - enhancementLabel: '**🚀 Enhancements**' - bugsLabel: '**🐛 Bug fixes**' - deprecatedLabel: '**⚠️ Deprecations**' - addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}' issues: false issuesWoLabels: false sinceTag: ${{ steps.version.outputs.last_release }} From 912b92d00bb6baf39ae9ecc86f27ce86573630af Mon Sep 17 00:00:00 2001 From: OpenPype Date: Fri, 17 Sep 2021 19:17:01 +0000 Subject: [PATCH 251/450] [Automated] Bump version --- CHANGELOG.md | 18 +++++++++--------- openpype/version.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3106a878b0..c5bd5d5554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.4.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.4.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...HEAD) @@ -8,7 +8,10 @@ - Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) - Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) -- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) + +**🆕 New features** + +- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) **🚀 Enhancements** @@ -19,7 +22,6 @@ - General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) - Ftrack: Sync to avalon actions have jobs [\#2015](https://github.com/pypeclub/OpenPype/pull/2015) - Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009) -- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) - Settings UI: Number with configurable steps [\#2001](https://github.com/pypeclub/OpenPype/pull/2001) - Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996) - Configurable items for providers without Settings [\#1987](https://github.com/pypeclub/OpenPype/pull/1987) @@ -31,6 +33,7 @@ - Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962) - Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959) - Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949) +- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) **🐛 Bug fixes** @@ -38,6 +41,7 @@ - Ftrack: Delete old versions missing settings key [\#2037](https://github.com/pypeclub/OpenPype/pull/2037) - Nuke: typo on a button [\#2034](https://github.com/pypeclub/OpenPype/pull/2034) - Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033) +- FFmpeg: Subprocess arguments as list [\#2032](https://github.com/pypeclub/OpenPype/pull/2032) - General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016) - Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006) - Nuke thumbnails generated from middle of the sequence [\#1992](https://github.com/pypeclub/OpenPype/pull/1992) @@ -55,7 +59,6 @@ - Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982) - Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974) -- Bump url-parse from 1.5.1 to 1.5.3 in /website [\#1958](https://github.com/pypeclub/OpenPype/pull/1958) - CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) @@ -77,14 +80,11 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.0-nightly.11...3.3.0) -### 📖 Documentation - -- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) -- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) - **🆕 New features** - Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) +- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) +- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) **🚀 Enhancements** diff --git a/openpype/version.py b/openpype/version.py index 3f166f0735..faf171c92b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.0-nightly.5" +__version__ = "3.4.0-nightly.6" From b201acdda477d82a461641667f237616a1ae2644 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Fri, 17 Sep 2021 19:32:05 +0000 Subject: [PATCH 252/450] [Automated] Release --- CHANGELOG.md | 23 ++++++++--------------- openpype/version.py | 2 +- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5bd5d5554..ffeb09a531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,8 @@ # Changelog -## [3.4.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.4.0](https://github.com/pypeclub/OpenPype/tree/3.4.0) (2021-09-17) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...HEAD) - -### 📖 Documentation - -- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) -- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...3.4.0) **🆕 New features** @@ -26,12 +21,14 @@ - Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996) - Configurable items for providers without Settings [\#1987](https://github.com/pypeclub/OpenPype/pull/1987) - Global: Example addons [\#1986](https://github.com/pypeclub/OpenPype/pull/1986) +- Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982) - Settings UI: Number sliders [\#1978](https://github.com/pypeclub/OpenPype/pull/1978) - Workfiles: Support more workfile templates [\#1966](https://github.com/pypeclub/OpenPype/pull/1966) - Launcher: Fix crashes on action click [\#1964](https://github.com/pypeclub/OpenPype/pull/1964) - Settings: Minor fixes in UI and missing default values [\#1963](https://github.com/pypeclub/OpenPype/pull/1963) - Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962) - Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959) +- CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) - Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949) - OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) @@ -50,25 +47,21 @@ - Last workfile with multiple work templates [\#1981](https://github.com/pypeclub/OpenPype/pull/1981) - Collectors order [\#1977](https://github.com/pypeclub/OpenPype/pull/1977) - Stop timer was within validator order range. [\#1975](https://github.com/pypeclub/OpenPype/pull/1975) +- Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974) - Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972) - Deadline: Houdini plugins in different hierarchy [\#1970](https://github.com/pypeclub/OpenPype/pull/1970) - Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) - Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961) -**Merged pull requests:** +### 📖 Documentation -- Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982) -- Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974) -- CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) +- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) +- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) -**🚀 Enhancements** - -- Global: Define hosts in CollectSceneVersion [\#1948](https://github.com/pypeclub/OpenPype/pull/1948) - **🐛 Bug fixes** - TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) diff --git a/openpype/version.py b/openpype/version.py index faf171c92b..2d9c68a032 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.0-nightly.6" +__version__ = "3.4.0" From 4faf6d11b71255191a7a98ad5c71a2f5afd68a80 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Sep 2021 23:09:20 +0100 Subject: [PATCH 253/450] get release type from labels on github PR --- .github/workflows/prerelease.yml | 4 ++-- tools/ci_tools.py | 40 ++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index ddab0e59a8..0fb07be79d 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -20,12 +20,12 @@ jobs: python-version: 3.7 - name: Install Python requirements - run: pip install gitpython semver + run: pip install gitpython semver PyGithub - name: 🔎 Determine next version type id: version_type run: | - TYPE=$(python ./tools/ci_tools.py --bump) + TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.GITHUB_TOKEN }}) echo ::set-output name=type::$TYPE diff --git a/tools/ci_tools.py b/tools/ci_tools.py index 3c1aaae991..d087ee08e5 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -3,7 +3,31 @@ import sys from semver import VersionInfo from git import Repo from optparse import OptionParser +from github import Github +import os +def get_release_type_github(Log, github_token): + # print(Log) + minor_labels = ["type: feature", "type: deprecated"] + patch_labels = ["type: enhancement", "type: bug"] + + g = Github(github_token) + repo = g.get_repo("pypeclub/ci-testing") + + for line in Log.splitlines(): + print(line) + match = re.search("pull request #(\d+)", line) + if match: + pr_number = match.group(1) + pr = repo.get_pull(int(pr_number)) + for label in pr.labels: + print(label.name) + if label.name in minor_labels: + return ("minor") + elif label.name in patch_labels: + return("patch") + return None + def remove_prefix(text, prefix): return text[text.startswith(prefix) and len(prefix):] @@ -36,7 +60,7 @@ def get_log_since_tag(version): def release_type(log): regex_minor = ["feature/", "(feat)"] - regex_patch = ["bugfix/", "fix/", "(fix)", "enhancement/"] + regex_patch = ["bugfix/", "fix/", "(fix)", "enhancement/", "update"] for reg in regex_minor: if re.search(reg, log): return "minor" @@ -135,17 +159,23 @@ def main(): parser.add_option("-l", "--lastversion", dest="lastversion", action="store", help="work with explicit version") + parser.add_option("-g", "--github_token", + dest="github_token", action="store", + help="github token") (options, args) = parser.parse_args() if options.bump: - last_CI, last_CI_tag = get_last_version("CI") last_release, last_release_tag = get_last_version("release") - bump_type_CI = release_type(get_log_since_tag(last_CI_tag)) - bump_type_release = release_type(get_log_since_tag(last_release_tag)) - if bump_type_CI is None or bump_type_release is None: + bump_type_release = get_release_type_github( + get_log_since_tag(last_release_tag), + options.github_token + ) + if bump_type_release is None: print("skip") + else: + print(bump_type_release) if options.nightly: next_tag_v = calculate_next_nightly() From da5fca2aac36da5c40827bd377bf1a4081603765 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Sep 2021 23:10:33 +0100 Subject: [PATCH 254/450] get correct repo --- tools/ci_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci_tools.py b/tools/ci_tools.py index d087ee08e5..3e20c1b21c 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -12,7 +12,7 @@ def get_release_type_github(Log, github_token): patch_labels = ["type: enhancement", "type: bug"] g = Github(github_token) - repo = g.get_repo("pypeclub/ci-testing") + repo = g.get_repo("pypeclub/OpenPype") for line in Log.splitlines(): print(line) From 463fbe93bc7bcaa3d139808f4f3f16e9db91c8e7 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 18 Sep 2021 03:38:17 +0000 Subject: [PATCH 255/450] [Automated] Bump version --- CHANGELOG.md | 26 ++++++++++++++++++-------- openpype/version.py | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffeb09a531..2ca8de17ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ # Changelog +## [3.5.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...HEAD) + +**🚀 Enhancements** + +- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) + ## [3.4.0](https://github.com/pypeclub/OpenPype/tree/3.4.0) (2021-09-17) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...3.4.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0) + +### 📖 Documentation + +- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) +- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) **🆕 New features** @@ -53,15 +66,14 @@ - Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) - Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961) -### 📖 Documentation - -- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) -- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) - ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) +**🚀 Enhancements** + +- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) + **🐛 Bug fixes** - TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) @@ -82,7 +94,6 @@ **🚀 Enhancements** - Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940) -- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) - Check for missing ✨ Python when using `pyenv` [\#1925](https://github.com/pypeclub/OpenPype/pull/1925) - Settings: Default values for enum [\#1920](https://github.com/pypeclub/OpenPype/pull/1920) - Settings UI: Modifiable dict view enhance [\#1919](https://github.com/pypeclub/OpenPype/pull/1919) @@ -94,7 +105,6 @@ **🐛 Bug fixes** - Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) -- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) - Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929) - Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926) - Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922) diff --git a/openpype/version.py b/openpype/version.py index 2d9c68a032..f8ed9c7c2f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.0" +__version__ = "3.5.0-nightly.1" From f1960cd240491b4b2dfed1e80a18cee8774424e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:17:12 +0200 Subject: [PATCH 256/450] function to create deffered value change timer --- openpype/tools/settings/settings/lib.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 openpype/tools/settings/settings/lib.py diff --git a/openpype/tools/settings/settings/lib.py b/openpype/tools/settings/settings/lib.py new file mode 100644 index 0000000000..577aaa5671 --- /dev/null +++ b/openpype/tools/settings/settings/lib.py @@ -0,0 +1,18 @@ +from Qt import QtCore + +# Offset of value change trigger in ms +VALUE_CHANGE_OFFSET_MS = 300 + + +def create_deffered_value_change_timer(callback): + """Deffer value change callback. + + UI won't trigger all callbacks on each value change but after predefined + time. Timer is reset on each start so callback is triggered after user + finish editing. + """ + timer = QtCore.QTimer() + timer.setSingleShot(True) + timer.setInterval(VALUE_CHANGE_OFFSET_MS) + timer.timeout.connect(callback) + return timer From ab30681017cd973c177e7fb03f9deb4893866e55 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:18:10 +0200 Subject: [PATCH 257/450] InputWidget has value change timer all the time --- openpype/tools/settings/settings/base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 8235cf8642..ab6b27bdaf 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -3,6 +3,7 @@ import json from Qt import QtWidgets, QtGui, QtCore from openpype.tools.settings import CHILD_OFFSET from .widgets import ExpandingWidget +from .lib import create_deffered_value_change_timer class BaseWidget(QtWidgets.QWidget): @@ -329,6 +330,20 @@ class BaseWidget(QtWidgets.QWidget): class InputWidget(BaseWidget): + def __init__(self, *args, **kwargs): + super(InputWidget, self).__init__(*args, **kwargs) + + # Input widgets have always timer available (but may not be used). + self._value_change_timer = create_deffered_value_change_timer( + self._on_value_change_timer + ) + + def start_value_timer(self): + self._value_change_timer.start() + + def _on_value_change_timer(self): + pass + def create_ui(self): if self.entity.use_label_wrap: label = None From b4669e9ca6f7af1285faf4bd90515a0cc9db60f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:18:41 +0200 Subject: [PATCH 258/450] use value change deffer in basic input widgets --- openpype/tools/settings/settings/item_widgets.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index da74c2adc5..a28bee8d36 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -400,7 +400,9 @@ class TextWidget(InputWidget): def _on_value_change(self): if self.ignore_input_changes: return + self.start_value_timer() + def _on_value_change_timer(self): self.entity.set(self.input_value()) @@ -474,6 +476,9 @@ class NumberWidget(InputWidget): if self.ignore_input_changes: return + self.start_value_timer() + + def _on_value_change_timer(self): value = self.input_field.value() if self._slider_widget is not None and not self._ignore_input_change: self._ignore_slider_change = True @@ -571,7 +576,9 @@ class RawJsonWidget(InputWidget): def _on_value_change(self): if self.ignore_input_changes: return + self.start_value_timer() + def _on_value_change_timer(self): self._is_invalid = self.input_field.has_invalid_value() if not self.is_invalid: self.entity.set(self.input_field.json_value()) @@ -786,4 +793,7 @@ class PathInputWidget(InputWidget): def _on_value_change(self): if self.ignore_input_changes: return + self.start_value_timer() + + def _on_value_change_timer(self): self.entity.set(self.input_value()) From a1709c9f6cd4b39945d27db06b90b3662011a44b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:19:03 +0200 Subject: [PATCH 259/450] modifiable dictionary has offset key change --- openpype/tools/settings/settings/dict_mutable_widget.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index ba86fe82dd..21cd5c8962 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -3,6 +3,7 @@ from uuid import uuid4 from Qt import QtWidgets, QtCore, QtGui from .base import BaseWidget +from .lib import create_deffered_value_change_timer from .widgets import ( ExpandingWidget, IconButton @@ -284,6 +285,8 @@ class ModifiableDictItem(QtWidgets.QWidget): self.confirm_btn = None + self._key_change_timer = create_deffered_value_change_timer(self._on_timeout) + if collapsible_key: self.create_collapsible_ui() else: @@ -516,6 +519,10 @@ class ModifiableDictItem(QtWidgets.QWidget): if self.ignore_input_changes: return + self._key_change_timer.start() + + def _on_timeout(self): + key = self.key_value() is_key_duplicated = self.entity_widget.validate_key_duplication( self.temp_key, key, self ) From fd17935a6935e986f690aa55a75b2ddc56e126f9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:25:24 +0200 Subject: [PATCH 260/450] fix hound --- openpype/tools/settings/settings/dict_mutable_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 21cd5c8962..cfb9d4a4b1 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -285,7 +285,9 @@ class ModifiableDictItem(QtWidgets.QWidget): self.confirm_btn = None - self._key_change_timer = create_deffered_value_change_timer(self._on_timeout) + self._key_change_timer = create_deffered_value_change_timer( + self._on_timeout + ) if collapsible_key: self.create_collapsible_ui() From e086e2127678d42c466eca1e79e7cf4a5b90b2cb Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 20 Sep 2021 09:08:06 +0100 Subject: [PATCH 261/450] use github labels for nightly releases too --- tools/ci_tools.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tools/ci_tools.py b/tools/ci_tools.py index 3e20c1b21c..69f5158bb3 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -93,7 +93,7 @@ def bump_file_versions(version): file_regex_replace(filename, regex, pyproject_version) -def calculate_next_nightly(token="nightly"): +def calculate_next_nightly(type="nightly", github_token=None): last_prerelease, last_pre_tag = get_last_version("CI") last_pre_v = VersionInfo.parse(last_prerelease) last_pre_v_finalized = last_pre_v.finalize_version() @@ -102,7 +102,10 @@ def calculate_next_nightly(token="nightly"): last_release, last_release_tag = get_last_version("release") last_release_v = VersionInfo.parse(last_release) - bump_type = release_type(get_log_since_tag(last_release)) + bump_type = get_release_type_github( + get_log_since_tag(last_release_tag), + github_token + ) if not bump_type: return None @@ -110,10 +113,10 @@ def calculate_next_nightly(token="nightly"): # print(next_release_v) if next_release_v > last_pre_v_finalized: - next_tag = next_release_v.bump_prerelease(token=token).__str__() + next_tag = next_release_v.bump_prerelease(token=type).__str__() return next_tag elif next_release_v == last_pre_v_finalized: - next_tag = last_pre_v.bump_prerelease(token=token).__str__() + next_tag = last_pre_v.bump_prerelease(token=type).__str__() return next_tag def finalize_latest_nightly(): @@ -149,10 +152,10 @@ def main(): help="finalize latest prerelease to a release") parser.add_option("-p", "--prerelease", dest="prerelease", action="store", - help="define prerelease token") + help="define prerelease type") parser.add_option("-f", "--finalize", dest="finalize", action="store", - help="define prerelease token") + help="define prerelease type") parser.add_option("-v", "--version", dest="version", action="store", help="work with explicit version") @@ -178,7 +181,7 @@ def main(): print(bump_type_release) if options.nightly: - next_tag_v = calculate_next_nightly() + next_tag_v = calculate_next_nightly(github_token=options.github_token) print(next_tag_v) bump_file_versions(next_tag_v) From e86675a5afa388b5f156019ff783e7ff6bb68180 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 20 Sep 2021 10:32:36 +0200 Subject: [PATCH 262/450] Removed unwanted configuration of families Plugin should run only when publishing workfile, configuration of families doesn't make sense. --- .../nuke/plugins/publish/increment_script_version.py | 2 +- openpype/settings/defaults/project_settings/nuke.json | 8 +------- .../projects_schema/schemas/schema_nuke_publish.json | 6 ------ 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/increment_script_version.py b/openpype/hosts/nuke/plugins/publish/increment_script_version.py index 47fccb9125..f55ed21ee2 100644 --- a/openpype/hosts/nuke/plugins/publish/increment_script_version.py +++ b/openpype/hosts/nuke/plugins/publish/increment_script_version.py @@ -9,7 +9,7 @@ class IncrementScriptVersion(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder + 0.9 label = "Increment Script Version" optional = True - families = ["workfile", "render", "render.local", "render.farm"] + families = ["workfile"] hosts = ['nuke'] def process(self, context): diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index c1c3e77684..467849cc36 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -99,13 +99,7 @@ }, "IncrementScriptVersion": { "optional": true, - "active": true, - "families": [ - "workfile", - "render", - "render.local", - "render.farm" - ] + "active": true } }, "load": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index d354ff15f8..df5015b551 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -176,12 +176,6 @@ "type": "boolean", "key": "active", "label": "Active" - }, - { - "type": "list", - "key": "families", - "object_type": "text", - "label": "Trigger on families" } ] } From f31ec7bbf3e35365ec6a265237e77becce0f9412 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 20 Sep 2021 11:32:40 +0200 Subject: [PATCH 263/450] Removed shell flag in subprocess call Shell flag causes issue when ffmpeg is called with a list of arguments --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 2 +- .../plugins/publish/extract_trim_video_audio.py | 2 +- openpype/plugins/publish/extract_jpeg_exr.py | 3 ++- openpype/plugins/publish/extract_review.py | 2 +- openpype/plugins/publish/extract_review_slate.py | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index cdbfe942f0..62e2cf7328 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -108,7 +108,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # run subprocess self.log.debug("Executing: {}".format(" ".join(subprocess_args))) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_args, logger=self.log ) # remove thumbnail key from origin repre diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py index 1cbf186a6c..c18de5bc1c 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py @@ -84,7 +84,7 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): joined_args = " ".join(ffmpeg_args) self.log.info(f"Processing: {joined_args}") openpype.api.run_subprocess( - ffmpeg_args, shell=True, logger=self.log + ffmpeg_args, logger=self.log ) repre = { diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 31e58025d5..25fb3b7b41 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -122,13 +122,14 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.debug("{}".format(subprocess_command)) try: # temporary until oiiotool is supported cross platform run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_args, logger=self.log ) except RuntimeError as exp: if "Compression" in str(exp): self.log.debug("Unsupported compression on input files. " + "Skipping!!!") return + self.log.warning("Conversion crashed", exc_info=True) raise if "representations" not in instance.data: diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index ecc49a8da6..bdcb595197 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -228,7 +228,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_args, logger=self.log ) # delete files added to fill gaps diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 4d26fd1ebc..fbd57bdf36 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -209,7 +209,7 @@ class ExtractReviewSlate(openpype.api.Extractor): "Slate Executing: {}".format(" ".join(slate_subprocess_args)) ) openpype.api.run_subprocess( - slate_subprocess_args, shell=True, logger=self.log + slate_subprocess_args, logger=self.log ) # create ffmpeg concat text file path @@ -244,7 +244,7 @@ class ExtractReviewSlate(openpype.api.Extractor): "Executing concat: {}".format(" ".join(concat_args)) ) openpype.api.run_subprocess( - concat_args, shell=True, logger=self.log + concat_args, logger=self.log ) self.log.debug("__ repre[tags]: {}".format(repre["tags"])) From 7bb87a14a2221ad0c85ad89e263cc23bb5de90ea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 11:45:30 +0200 Subject: [PATCH 264/450] fixed filling of families --- openpype/tools/loader/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 881e9c206b..d8c42250c7 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -905,12 +905,12 @@ class FamilyModel(QtGui.QStandardItemModel): | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable ) + new_items.append(item) + self._items_by_family[family] = item else: item = self._items_by_family[label] item.setData(QtCore.Qt.DisplayRole, label) - new_items.append(item) - self._items_by_family[family] = item item.setCheckState(state) @@ -942,7 +942,7 @@ class FamilyProxyFiler(QtCore.QSortFilterProxyModel): def set_filter_enabled(self, enabled=None): if enabled is None: enabled = not self._filtering_enabled - if self._filtering_enabled == enabled: + elif self._filtering_enabled == enabled: return self._filtering_enabled = enabled From 30cadfd6ad989317710f2668643e6df4e9e902bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 13:09:01 +0200 Subject: [PATCH 265/450] don't use 'split_command_to_list' which may break paths if they are incorrectly used --- .../plugins/publish/extract_thumbnail.py | 5 +--- openpype/lib/__init__.py | 2 -- openpype/lib/execute.py | 30 ------------------- openpype/plugins/publish/extract_jpeg_exr.py | 4 +-- .../publish/extract_otio_audio_tracks.py | 6 +--- openpype/plugins/publish/extract_review.py | 8 ++--- .../plugins/publish/extract_review_slate.py | 8 ++--- 7 files changed, 8 insertions(+), 55 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index cdbfe942f0..d5eb0a8a45 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -101,14 +101,11 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): jpeg_items.append("\"{}\"".format(full_thumbnail_path)) subprocess_jpeg = " ".join(jpeg_items) - subprocess_args = openpype.lib.split_command_to_list( - subprocess_jpeg - ) # run subprocess self.log.debug("Executing: {}".format(" ".join(subprocess_args))) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_jpeg, shell=True, logger=self.log ) # remove thumbnail key from origin repre diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 4cf4a2f8ef..74004a1239 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -27,7 +27,6 @@ from .execute import ( get_pype_execute_args, execute, run_subprocess, - split_command_to_list, path_to_subprocess_arg, CREATE_NO_WINDOW ) @@ -174,7 +173,6 @@ __all__ = [ "get_pype_execute_args", "execute", "run_subprocess", - "split_command_to_list", "path_to_subprocess_arg", "CREATE_NO_WINDOW", diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 3e5b6d3853..a1111fba29 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -147,36 +147,6 @@ def path_to_subprocess_arg(path): return subprocess.list2cmdline([path]) -def split_command_to_list(string_command): - """Split string subprocess command to list. - - Should be able to split complex subprocess command to separated arguments: - `"C:\\ffmpeg folder\\ffmpeg.exe" -i \"D:\\input.mp4\\" \"D:\\output.mp4\"` - - Should result into list: - `["C:\ffmpeg folder\ffmpeg.exe", "-i", "D:\input.mp4", "D:\output.mp4"]` - - This may be required on few versions of python where subprocess can handle - only list of arguments. - - To be able do that is using `shlex` python module. - - Args: - string_command(str): Full subprocess command. - - Returns: - list: Command separated into individual arguments. - """ - if not string_command: - return [] - - kwargs = {} - # Use 'posix' argument only on windows - if platform.system().lower() == "windows": - kwargs["posix"] = False - return shlex.split(string_command, **kwargs) - - def get_pype_execute_args(*args): """Arguments to run pype command. diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 31e58025d5..725afb57e7 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -5,7 +5,6 @@ from openpype.lib import ( get_ffmpeg_tool_path, run_subprocess, - split_command_to_list, path_to_subprocess_arg, should_decompress, @@ -116,13 +115,12 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): jpeg_items.append(path_to_subprocess_arg(full_output_path)) subprocess_command = " ".join(jpeg_items) - subprocess_args = split_command_to_list(subprocess_command) # run subprocess self.log.debug("{}".format(subprocess_command)) try: # temporary until oiiotool is supported cross platform run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_command, shell=True, logger=self.log ) except RuntimeError as exp: if "Compression" in str(exp): diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 2cdc072ffd..9750a6df22 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -3,7 +3,6 @@ import pyblish import openpype.api from openpype.lib import ( get_ffmpeg_tool_path, - split_command_to_list, path_to_subprocess_arg ) import tempfile @@ -62,13 +61,10 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): cmd += self.create_cmd(audio_inputs) cmd += path_to_subprocess_arg(audio_temp_fpath) - # Split command to list for subprocess - cmd_list = split_command_to_list(cmd) - # run subprocess self.log.debug("Executing: {}".format(cmd)) openpype.api.run_subprocess( - cmd_list, logger=self.log + cmd, logger=self.log ) # remove empty diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index ecc49a8da6..f5d6789dd4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -14,7 +14,6 @@ from openpype.lib import ( get_ffmpeg_tool_path, ffprobe_streams, - split_command_to_list, path_to_subprocess_arg, should_decompress, @@ -220,15 +219,12 @@ class ExtractReview(pyblish.api.InstancePlugin): raise NotImplementedError subprcs_cmd = " ".join(ffmpeg_args) - subprocess_args = split_command_to_list(subprcs_cmd) # run subprocess - self.log.debug( - "Executing: {}".format(" ".join(subprocess_args)) - ) + self.log.debug("Executing: {}".format(subprcs_cmd)) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprcs_cmd, shell=True, logger=self.log ) # delete files added to fill gaps diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 4d26fd1ebc..aed146bb69 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -200,16 +200,14 @@ class ExtractReviewSlate(openpype.api.Extractor): " ".join(input_args), " ".join(output_args) ] - slate_subprocess_args = openpype.lib.split_command_to_list( - " ".join(slate_args) - ) + slate_subprocess_cmd = " ".join(slate_args) # run slate generation subprocess self.log.debug( - "Slate Executing: {}".format(" ".join(slate_subprocess_args)) + "Slate Executing: {}".format(slate_subprocess_cmd) ) openpype.api.run_subprocess( - slate_subprocess_args, shell=True, logger=self.log + slate_subprocess_cmd, shell=True, logger=self.log ) # create ffmpeg concat text file path From 2506d0f0d6e4e7a419f078f2b2b644a8f1a31627 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 20 Sep 2021 17:44:02 +0200 Subject: [PATCH 266/450] Changed setting schema to include enabled pill --- openpype/settings/defaults/project_settings/nuke.json | 1 + .../projects_schema/schemas/schema_nuke_publish.json | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 467849cc36..fb10d30f67 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -98,6 +98,7 @@ "viewer_lut_raw": false }, "IncrementScriptVersion": { + "enabled": true, "optional": true, "active": true } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index df5015b551..f385f6149f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -162,11 +162,17 @@ }, { "type": "dict", - "collapsible": false, + "collapsible": true, + "checkbox_key": "enabled", "key": "IncrementScriptVersion", "label": "IncrementScriptVersion", "is_group": true, "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "boolean", "key": "optional", From d0958304fab082b270b82bbbf1bcd5c3c866c613 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 17:48:53 +0200 Subject: [PATCH 267/450] fixed method arguments order --- openpype/tools/loader/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index d8c42250c7..6b94fc6e44 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -910,7 +910,7 @@ class FamilyModel(QtGui.QStandardItemModel): else: item = self._items_by_family[label] - item.setData(QtCore.Qt.DisplayRole, label) + item.setData(label, QtCore.Qt.DisplayRole) item.setCheckState(state) From 885b84fc1584d3648eefeedd08f2aa4dff35d904 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 19:19:15 +0200 Subject: [PATCH 268/450] ftrack module does not check IFtrackEventHandlerPaths --- .../default_modules/ftrack/ftrack_module.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 3732e762b4..cfce38d125 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -8,8 +8,7 @@ from openpype_interfaces import ( ITrayModule, IPluginPaths, ILaunchHookPaths, - ISettingsChangeListener, - IFtrackEventHandlerPaths + ISettingsChangeListener ) from openpype.settings import SaveWarningExc @@ -81,9 +80,17 @@ class FtrackModule( def connect_with_modules(self, enabled_modules): for module in enabled_modules: - if not isinstance(module, IFtrackEventHandlerPaths): + if not hasattr(module, "get_event_handler_paths"): continue - paths_by_type = module.get_event_handler_paths() or {} + + try: + paths_by_type = module.get_event_handler_paths() + except Exception: + continue + + if not isinstance(paths_by_type, dict): + continue + for key, value in paths_by_type.items(): if not value: continue From 9bf53533cd4e1f4afe6b127241bb15d75e301800 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 19:19:20 +0200 Subject: [PATCH 269/450] removed IFtrackEventHandlerPaths --- .../modules/default_modules/ftrack/interfaces.py | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 openpype/modules/default_modules/ftrack/interfaces.py diff --git a/openpype/modules/default_modules/ftrack/interfaces.py b/openpype/modules/default_modules/ftrack/interfaces.py deleted file mode 100644 index 16ce0d2e62..0000000000 --- a/openpype/modules/default_modules/ftrack/interfaces.py +++ /dev/null @@ -1,12 +0,0 @@ -from abc import abstractmethod -from openpype.modules import OpenPypeInterface - - -class IFtrackEventHandlerPaths(OpenPypeInterface): - """Other modules interface to return paths to ftrack event handlers. - - Expected output is dictionary with "server" and "user" keys. - """ - @abstractmethod - def get_event_handler_paths(self): - pass From db095da94a4dc1b22a893286476e633fdf3f1b00 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 19:19:31 +0200 Subject: [PATCH 270/450] clockify is not using IFtrackEventHandlerPaths --- .../modules/default_modules/clockify/clockify_module.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/clockify/clockify_module.py b/openpype/modules/default_modules/clockify/clockify_module.py index 0de62d8ba4..5f3c247413 100644 --- a/openpype/modules/default_modules/clockify/clockify_module.py +++ b/openpype/modules/default_modules/clockify/clockify_module.py @@ -10,16 +10,14 @@ from .constants import ( from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITrayModule, - IPluginPaths, - IFtrackEventHandlerPaths + IPluginPaths ) class ClockifyModule( OpenPypeModule, ITrayModule, - IPluginPaths, - IFtrackEventHandlerPaths + IPluginPaths ): name = "clockify" @@ -94,7 +92,7 @@ class ClockifyModule( } def get_event_handler_paths(self): - """Implementaton of IFtrackEventHandlerPaths to get plugin paths.""" + """Function for Ftrack module to add ftrack event handler paths.""" return { "user": [CLOCKIFY_FTRACK_USER_PATH], "server": [CLOCKIFY_FTRACK_SERVER_PATH] From ba6934946f585899da148c924359d30e262312a6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 19:20:21 +0200 Subject: [PATCH 271/450] removed unused functions --- openpype/modules/default_modules/ftrack/lib/__init__.py | 4 +--- openpype/modules/default_modules/ftrack/lib/settings.py | 9 --------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/lib/__init__.py b/openpype/modules/default_modules/ftrack/lib/__init__.py index 9dc2d67279..433a1f7881 100644 --- a/openpype/modules/default_modules/ftrack/lib/__init__.py +++ b/openpype/modules/default_modules/ftrack/lib/__init__.py @@ -5,8 +5,7 @@ from .constants import ( CUST_ATTR_TOOLS, CUST_ATTR_APPLICATIONS ) -from . settings import ( - get_ftrack_url_from_settings, +from .settings import ( get_ftrack_event_mongo_info ) from .custom_attributes import ( @@ -31,7 +30,6 @@ __all__ = ( "CUST_ATTR_TOOLS", "CUST_ATTR_APPLICATIONS", - "get_ftrack_url_from_settings", "get_ftrack_event_mongo_info", "default_custom_attributes_definition", diff --git a/openpype/modules/default_modules/ftrack/lib/settings.py b/openpype/modules/default_modules/ftrack/lib/settings.py index 027356edc6..bf44981de0 100644 --- a/openpype/modules/default_modules/ftrack/lib/settings.py +++ b/openpype/modules/default_modules/ftrack/lib/settings.py @@ -1,13 +1,4 @@ import os -from openpype.api import get_system_settings - - -def get_ftrack_settings(): - return get_system_settings()["modules"]["ftrack"] - - -def get_ftrack_url_from_settings(): - return get_ftrack_settings()["ftrack_server"] def get_ftrack_event_mongo_info(): From 886188a489f739c6609c9c7df35d86c8c5a0eff5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 19:23:55 +0200 Subject: [PATCH 272/450] change expected method name to 'get_ftrack_event_handler_paths' --- openpype/modules/default_modules/clockify/clockify_module.py | 2 +- openpype/modules/default_modules/ftrack/ftrack_module.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/clockify/clockify_module.py b/openpype/modules/default_modules/clockify/clockify_module.py index 5f3c247413..932ce87c36 100644 --- a/openpype/modules/default_modules/clockify/clockify_module.py +++ b/openpype/modules/default_modules/clockify/clockify_module.py @@ -91,7 +91,7 @@ class ClockifyModule( "actions": [actions_path] } - def get_event_handler_paths(self): + def get_ftrack_event_handler_paths(self): """Function for Ftrack module to add ftrack event handler paths.""" return { "user": [CLOCKIFY_FTRACK_USER_PATH], diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index cfce38d125..c73f9b100d 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -80,11 +80,11 @@ class FtrackModule( def connect_with_modules(self, enabled_modules): for module in enabled_modules: - if not hasattr(module, "get_event_handler_paths"): + if not hasattr(module, "get_ftrack_event_handler_paths"): continue try: - paths_by_type = module.get_event_handler_paths() + paths_by_type = module.get_ftrack_event_handler_paths() except Exception: continue From 5b8832f4e3f1faa0d44a655fa4261f4ea2c5b94a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Sep 2021 22:23:38 +0000 Subject: [PATCH 273/450] Bump prismjs from 1.24.0 to 1.25.0 in /website Bumps [prismjs](https://github.com/PrismJS/prism) from 1.24.0 to 1.25.0. - [Release notes](https://github.com/PrismJS/prism/releases) - [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md) - [Commits](https://github.com/PrismJS/prism/compare/v1.24.0...v1.25.0) --- updated-dependencies: - dependency-name: prismjs dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index b4c12edeb6..066d156d97 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -6594,9 +6594,9 @@ prism-react-renderer@^1.1.1: integrity sha512-GHqzxLYImx1iKN1jJURcuRoA/0ygCcNhfGw1IT8nPIMzarmKQ3Nc+JcG0gi8JXQzuh0C5ShE4npMIoqNin40hg== prismjs@^1.23.0: - version "1.24.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.0.tgz#0409c30068a6c52c89ef7f1089b3ca4de56be2ac" - integrity sha512-SqV5GRsNqnzCL8k5dfAjCNhUrF3pR0A9lTDSCUZeh/LIshheXJEaP0hwLz2t4XHivd2J/v2HR+gRnigzeKe3cQ== + version "1.25.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756" + integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg== process-nextick-args@~2.0.0: version "2.0.1" From a19ffc1e56e17f765c416ffa3a8ee821ae229d14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 10:39:38 +0200 Subject: [PATCH 274/450] added shell=True back where command is executed as string --- openpype/plugins/publish/extract_jpeg_exr.py | 2 +- openpype/plugins/publish/extract_otio_audio_tracks.py | 2 +- openpype/plugins/publish/extract_review.py | 2 +- openpype/plugins/publish/extract_review_slate.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 48db35801e..3c08c1862d 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -120,7 +120,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.debug("{}".format(subprocess_command)) try: # temporary until oiiotool is supported cross platform run_subprocess( - subprocess_command, logger=self.log + subprocess_command, shell=True, logger=self.log ) except RuntimeError as exp: if "Compression" in str(exp): diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 9750a6df22..be0bae5cdc 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -64,7 +64,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # run subprocess self.log.debug("Executing: {}".format(cmd)) openpype.api.run_subprocess( - cmd, logger=self.log + cmd, shell=True, logger=self.log ) # remove empty diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index a6d652f00b..f5d6789dd4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -224,7 +224,7 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("Executing: {}".format(subprcs_cmd)) openpype.api.run_subprocess( - subprcs_cmd, logger=self.log + subprcs_cmd, shell=True, logger=self.log ) # delete files added to fill gaps diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index c3f8c78c61..7002168cdb 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -207,7 +207,7 @@ class ExtractReviewSlate(openpype.api.Extractor): "Slate Executing: {}".format(slate_subprocess_cmd) ) openpype.api.run_subprocess( - slate_subprocess_cmd, logger=self.log + slate_subprocess_cmd, shell=True, logger=self.log ) # create ffmpeg concat text file path From 4343cf7e27dae1566522e956327220374090f00e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 10:41:00 +0200 Subject: [PATCH 275/450] one more plugin where ffmpeg command is used as string --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index ab079f6c9c..24690cb840 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -105,7 +105,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # run subprocess self.log.debug("Executing: {}".format(subprocess_jpeg)) openpype.api.run_subprocess( - subprocess_jpeg, logger=self.log + subprocess_jpeg, shell=True, logger=self.log ) # remove thumbnail key from origin repre From 1bea470dd0e4c50f78a8dd971bd32ddcb4890376 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 21 Sep 2021 11:14:35 +0200 Subject: [PATCH 276/450] add support for pyenv on windows --- tools/build_win_installer.ps1 | 42 ++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/tools/build_win_installer.ps1 b/tools/build_win_installer.ps1 index a0832e0135..49fa803742 100644 --- a/tools/build_win_installer.ps1 +++ b/tools/build_win_installer.ps1 @@ -105,6 +105,46 @@ $env:BUILD_VERSION = $openpype_version iscc +Write-Host ">>> " -NoNewline -ForegroundColor green +Write-Host "Detecting host Python ... " -NoNewline +$python = "python" +if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { + $pyenv_python = & pyenv which python + if (Test-Path -PathType Leaf -Path "$($pyenv_python)") { + $python = $pyenv_python + } +} +if (-not (Get-Command $python -ErrorAction SilentlyContinue)) { + Write-Host "!!! Python not detected" -ForegroundColor red + Set-Location -Path $current_dir + Exit-WithCode 1 +} +$version_command = @' +import sys +print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1])) +'@ + +$p = & $python -c $version_command +$env:PYTHON_VERSION = $p +$m = $p -match '(\d+)\.(\d+)' +if(-not $m) { + Write-Host "!!! Cannot determine version" -ForegroundColor red + Set-Location -Path $current_dir + Exit-WithCode 1 +} +# We are supporting python 3.7 only +if (($matches[1] -lt 3) -or ($matches[2] -lt 7)) { + Write-Host "FAILED Version [ $p ] is old and unsupported" -ForegroundColor red + Set-Location -Path $current_dir + Exit-WithCode 1 +} elseif (($matches[1] -eq 3) -and ($matches[2] -gt 7)) { + Write-Host "WARNING Version [ $p ] is unsupported, use at your own risk." -ForegroundColor yellow + Write-Host "*** " -NoNewline -ForegroundColor yellow + Write-Host "OpenPype supports only Python 3.7" -ForegroundColor white +} else { + Write-Host "OK [ $p ]" -ForegroundColor green +} + Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Creating OpenPype installer ... " -ForegroundColor white @@ -114,7 +154,7 @@ from distutils.util import get_platform print('exe.{}-{}'.format(get_platform(), sys.version[0:3])) "@ -$build_dir = & python -c $build_dir_command +$build_dir = & $python -c $build_dir_command Write-Host "Build directory ... ${build_dir}" -ForegroundColor white $env:BUILD_DIR = $build_dir From ce98319ef264612f7b9eb19f58d634deafb8544c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 11:45:20 +0200 Subject: [PATCH 277/450] Nuke adding proxy mode validator --- .../plugins/publish/validate_proxy_mode.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py diff --git a/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py b/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py new file mode 100644 index 0000000000..9c6ca03ffd --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py @@ -0,0 +1,33 @@ +import pyblish +import nuke + + +class FixProxyMode(pyblish.api.Action): + """ + Togger off proxy switch OFF + """ + + label = "Proxy toggle to OFF" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + rootNode = nuke.root() + rootNode["proxy"].setValue(False) + + +@pyblish.api.log +class ValidateProxyMode(pyblish.api.ContextPlugin): + """Validate active proxy mode""" + + order = pyblish.api.ValidatorOrder + label = "Validate Proxy Mode" + hosts = ["nuke"] + actions = [FixProxyMode] + + def process(self, context): + + rootNode = nuke.root() + isProxy = rootNode["proxy"].value() + + assert not isProxy, "Proxy mode should be toggled OFF" From e24b142962b90d25f1bf371292c2966cdf953f36 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 12:53:23 +0200 Subject: [PATCH 278/450] added startup validations of ffmpeg and oiio tool --- start.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/start.py b/start.py index 00f9a50cbb..a02c06661f 100644 --- a/start.py +++ b/start.py @@ -96,6 +96,7 @@ Attributes: import os import re import sys +import platform import traceback import subprocess import site @@ -339,6 +340,60 @@ def set_modules_environments(): os.environ.update(env) +def is_tool(name): + try: + import os.errno as errno + except ImportError: + import errno + + try: + devnull = open(os.devnull, "w") + subprocess.Popen( + [name], stdout=devnull, stderr=devnull + ).communicate() + except OSError as exc: + if exc.errno == errno.ENOENT: + return False + return True + + +def _startup_validations(): + """Validations before OpenPype starts.""" + _validate_thirdparty_binaries() + + +def _validate_thirdparty_binaries(): + """Check existence of thirdpart executables.""" + low_platform = platform.system().lower() + binary_vendors_dir = os.path.join( + os.environ["OPENPYPE_ROOT"], + "vendor", + "bin" + ) + + error_msg = ( + "Missing binary dependency {}. Please fetch thirdparty dependencies." + ) + # Validate existence of FFmpeg + ffmpeg_dir = os.path.join(binary_vendors_dir, "ffmpeg", low_platform) + if low_platform == "windows": + ffmpeg_dir = os.path.join(ffmpeg_dir, "bin") + ffmpeg_executable = os.path.join(ffmpeg_dir, "ffmpeg") + if not is_tool(ffmpeg_executable): + raise RuntimeError(error_msg.format("FFmpeg")) + + # Validate existence of OpenImageIO (not on MacOs) + if low_platform != "darwin": + oiio_tool_path = os.path.join( + binary_vendors_dir, + "oiio", + low_platform, + "oiio_tool" + ) + if not is_tool(oiio_tool_path): + raise RuntimeError(error_msg.format("OpenImageIO")) + + def _process_arguments() -> tuple: """Process command line arguments. @@ -767,6 +822,11 @@ def boot(): # ------------------------------------------------------------------------ os.environ["OPENPYPE_ROOT"] = OPENPYPE_ROOT + # ------------------------------------------------------------------------ + # Do necessary startup validations + # ------------------------------------------------------------------------ + _startup_validations() + # ------------------------------------------------------------------------ # Play animation # ------------------------------------------------------------------------ From cda626d76fce511be8524b5358db6b37a977684f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 12:55:01 +0200 Subject: [PATCH 279/450] removed validator of ffmpeg --- .../publish/validate_ffmpeg_installed.py | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 openpype/plugins/publish/validate_ffmpeg_installed.py diff --git a/openpype/plugins/publish/validate_ffmpeg_installed.py b/openpype/plugins/publish/validate_ffmpeg_installed.py deleted file mode 100644 index a5390a07b2..0000000000 --- a/openpype/plugins/publish/validate_ffmpeg_installed.py +++ /dev/null @@ -1,34 +0,0 @@ -import pyblish.api -import os -import subprocess -import openpype.lib -try: - import os.errno as errno -except ImportError: - import errno - - -class ValidateFFmpegInstalled(pyblish.api.ContextPlugin): - """Validate availability of ffmpeg tool in PATH""" - - order = pyblish.api.ValidatorOrder - label = 'Validate ffmpeg installation' - optional = True - - def is_tool(self, name): - try: - devnull = open(os.devnull, "w") - subprocess.Popen( - [name], stdout=devnull, stderr=devnull - ).communicate() - except OSError as e: - if e.errno == errno.ENOENT: - return False - return True - - def process(self, context): - ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") - self.log.info("ffmpeg path: `{}`".format(ffmpeg_path)) - if self.is_tool("{}".format(ffmpeg_path)) is False: - self.log.error("ffmpeg not found in PATH") - raise RuntimeError('ffmpeg not installed.') From 7fe3fd1d11a66de9dbfbc479bd91cd0cb7d35013 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 14:06:25 +0200 Subject: [PATCH 280/450] show tkinter message box if validation crashes --- start.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/start.py b/start.py index a02c06661f..8b4ee97b09 100644 --- a/start.py +++ b/start.py @@ -359,7 +359,23 @@ def is_tool(name): def _startup_validations(): """Validations before OpenPype starts.""" - _validate_thirdparty_binaries() + try: + _validate_thirdparty_binaries() + except Exception as exc: + if os.environ.get("OPENPYPE_HEADLESS_MODE"): + raise + + from tkinter import Tk + from tkinter.messagebox import showerror + + root = Tk() + root.withdraw() + showerror( + "Startup validations didn't pass", + str(exc) + ) + root.destroy() + sys.exit(1) def _validate_thirdparty_binaries(): From 38bc581b574d9740e0b2f2865b582638f04ce73f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 14:41:43 +0200 Subject: [PATCH 281/450] nuke: securing plugin is loading only sequence representation --- openpype/hosts/nuke/plugins/load/load_sequence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/nuke/plugins/load/load_sequence.py b/openpype/hosts/nuke/plugins/load/load_sequence.py index 5f2128b10f..003b406ee7 100644 --- a/openpype/hosts/nuke/plugins/load/load_sequence.py +++ b/openpype/hosts/nuke/plugins/load/load_sequence.py @@ -76,6 +76,8 @@ class LoadSequence(api.Loader): file = file.replace("\\", "/") repr_cont = context["representation"]["context"] + assert repr_cont.get("frame"), "Representation is not sequence" + if "#" not in file: frame = repr_cont.get("frame") if frame: @@ -170,6 +172,7 @@ class LoadSequence(api.Loader): assert read_node.Class() == "Read", "Must be Read" repr_cont = representation["context"] + assert repr_cont.get("frame"), "Representation is not sequence" file = api.get_representation_path(representation) From 9df99db56ee338caa1b0af39c2b4434247917a24 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 14:42:12 +0200 Subject: [PATCH 282/450] standalone: jpg renamed to thumbnail --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 24690cb840..23f0b104c8 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -116,7 +116,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # create new thumbnail representation representation = { - 'name': 'jpg', + 'name': 'thumbnail', 'ext': 'jpg', 'files': filename, "stagingDir": staging_dir, From ce07679ead6414639f81872eee75f4f7b9c6e6a5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 18:02:38 +0200 Subject: [PATCH 283/450] fixing global plugin orders --- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_workfile.py | 2 ++ openpype/plugins/publish/collect_hierarchy.py | 2 +- openpype/plugins/publish/collect_otio_frame_ranges.py | 2 +- openpype/plugins/publish/collect_otio_review.py | 2 +- openpype/plugins/publish/collect_otio_subset_resources.py | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 936ea2be58..85b4e273d5 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -131,7 +131,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): self.create_shot_instance(context, **data) self.log.info("Creating instance: {}".format(instance)) - self.log.debug( + self.log.info( "_ instance.data: {}".format(pformat(instance.data))) if not with_audio: diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index ff5d516065..4f164acc91 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -7,6 +7,8 @@ from pprint import pformat from openpype.hosts.hiero.otio import hiero_export from Qt.QtGui import QPixmap import tempfile +reload(hiero_export) + class PrecollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" diff --git a/openpype/plugins/publish/collect_hierarchy.py b/openpype/plugins/publish/collect_hierarchy.py index 1aa10fcb9b..f7d1c6b4be 100644 --- a/openpype/plugins/publish/collect_hierarchy.py +++ b/openpype/plugins/publish/collect_hierarchy.py @@ -13,7 +13,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): """ label = "Collect Hierarchy" - order = pyblish.api.CollectorOrder - 0.57 + order = pyblish.api.CollectorOrder - 0.47 families = ["shot"] hosts = ["resolve", "hiero"] diff --git a/openpype/plugins/publish/collect_otio_frame_ranges.py b/openpype/plugins/publish/collect_otio_frame_ranges.py index e1b8b95a46..a35ef47e79 100644 --- a/openpype/plugins/publish/collect_otio_frame_ranges.py +++ b/openpype/plugins/publish/collect_otio_frame_ranges.py @@ -18,7 +18,7 @@ class CollectOcioFrameRanges(pyblish.api.InstancePlugin): Adding timeline and source ranges to instance data""" label = "Collect OTIO Frame Ranges" - order = pyblish.api.CollectorOrder - 0.58 + order = pyblish.api.CollectorOrder - 0.48 families = ["shot", "clip"] hosts = ["resolve", "hiero"] diff --git a/openpype/plugins/publish/collect_otio_review.py b/openpype/plugins/publish/collect_otio_review.py index e78ccc032c..10ceafdcca 100644 --- a/openpype/plugins/publish/collect_otio_review.py +++ b/openpype/plugins/publish/collect_otio_review.py @@ -20,7 +20,7 @@ class CollectOcioReview(pyblish.api.InstancePlugin): """Get matching otio track from defined review layer""" label = "Collect OTIO Review" - order = pyblish.api.CollectorOrder - 0.57 + order = pyblish.api.CollectorOrder - 0.47 families = ["clip"] hosts = ["resolve", "hiero"] diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 010430a303..dd670ff850 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -18,7 +18,7 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): """Get Resources for a subset version""" label = "Collect OTIO Subset Resources" - order = pyblish.api.CollectorOrder - 0.57 + order = pyblish.api.CollectorOrder - 0.47 families = ["clip"] hosts = ["resolve", "hiero"] From 0abede9e5d06bbd5431574159dcfab7fd2a89b24 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 18:24:32 +0200 Subject: [PATCH 284/450] hiero: otio is not ignoring disabled and offline clips --- openpype/hosts/hiero/otio/hiero_export.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/hiero/otio/hiero_export.py b/openpype/hosts/hiero/otio/hiero_export.py index ccc05d5fd7..af4322e3d9 100644 --- a/openpype/hosts/hiero/otio/hiero_export.py +++ b/openpype/hosts/hiero/otio/hiero_export.py @@ -378,6 +378,17 @@ def add_otio_metadata(otio_item, media_source, **kwargs): def create_otio_timeline(): + def set_prev_item(itemindex, track_item): + # Add Gap if needed + if itemindex == 0: + # if it is first track item at track then add + # it to previouse item + return track_item + + else: + # get previouse item + return track_item.parent().items()[itemindex - 1] + # get current timeline self.timeline = hiero.ui.activeSequence() self.project_fps = self.timeline.framerate().toFloat() @@ -396,14 +407,6 @@ def create_otio_timeline(): type(track), track.name()) for itemindex, track_item in enumerate(track): - # skip offline track items - if not track_item.isMediaPresent(): - continue - - # skip if track item is disabled - if not track_item.isEnabled(): - continue - # Add Gap if needed if itemindex == 0: # if it is first track item at track then add From 8cd3821ee4e000863bc7337dda5e486bea6103cd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 18:26:45 +0200 Subject: [PATCH 285/450] hound: suggestion --- openpype/hosts/hiero/plugins/publish/precollect_workfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 4f164acc91..7db155048f 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -7,7 +7,6 @@ from pprint import pformat from openpype.hosts.hiero.otio import hiero_export from Qt.QtGui import QPixmap import tempfile -reload(hiero_export) class PrecollectWorkfile(pyblish.api.ContextPlugin): From 1cd8aec44c07c75c264110b0a9bfecb035e093d2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 22 Sep 2021 03:07:20 +0200 Subject: [PATCH 286/450] wip on fixing camera tokens --- openpype/hosts/maya/api/lib_renderproducts.py | 106 ++++++++++++------ .../plugins/publish/submit_publish_job.py | 7 +- 2 files changed, 77 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index fb99584c5d..29f216be8c 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -114,6 +114,8 @@ class RenderProduct(object): aov = attr.ib(default=None) # source aov driver = attr.ib(default=None) # source driver multipart = attr.ib(default=False) # multichannel file + camera = attr.ib(default=None) # used only when rendering + # from multiple cameras def get(layer, render_instance=None): @@ -307,7 +309,7 @@ class ARenderProducts: # Deadline allows submitting renders with a custom frame list # to support those cases we might want to allow 'custom frames' # to be overridden to `ExpectFiles` class? - layer_data = LayerMetadata( + return LayerMetadata( frameStart=int(self.get_render_attribute("startFrame")), frameEnd=int(self.get_render_attribute("endFrame")), frameStep=int(self.get_render_attribute("byFrameStep")), @@ -321,7 +323,6 @@ class ARenderProducts: defaultExt=self._get_attr("defaultRenderGlobals.imfPluginKey"), filePrefix=file_prefix ) - return layer_data def _generate_file_sequence( self, layer_data, @@ -330,7 +331,7 @@ class ARenderProducts: force_cameras=None): # type: (LayerMetadata, str, str, list) -> list expected_files = [] - cameras = force_cameras if force_cameras else layer_data.cameras + cameras = force_cameras or layer_data.cameras ext = force_ext or layer_data.defaultExt for cam in cameras: file_prefix = layer_data.filePrefix @@ -460,15 +461,19 @@ class RenderProductsArnold(ARenderProducts): return prefix - def _get_aov_render_products(self, aov): + def _get_aov_render_products(self, aov, cameras=None): """Return all render products for the AOV""" - products = list() + products = [] aov_name = self._get_attr(aov, "name") ai_drivers = cmds.listConnections("{}.outputs".format(aov), source=True, destination=False, type="aiAOVDriver") or [] + use_single_camera = False + if not cameras: + cameras = ["__default__"] + use_single_camera = True for ai_driver in ai_drivers: # todo: check aiAOVDriver.prefix as it could have @@ -497,30 +502,43 @@ class RenderProductsArnold(ARenderProducts): name = "beauty" # Support Arnold light groups for AOVs - # Global AOV: When disabled the main layer is not written: `{pass}` + # Global AOV: When disabled the main layer is + # not written: `{pass}` # All Light Groups: When enabled, a `{pass}_lgroups` file is - # written and is always merged into a single file - # Light Groups List: When set, a product per light group is written + # written and is always merged into a + # single file + # Light Groups List: When set, a product per light + # group is written # e.g. {pass}_front, {pass}_rim global_aov = self._get_attr(aov, "globalAov") if global_aov: - product = RenderProduct(productName=name, - ext=ext, - aov=aov_name, - driver=ai_driver) - products.append(product) + for camera in cameras: + c = camera + if use_single_camera: + c = None + product = RenderProduct(productName=name, + ext=ext, + aov=aov_name, + driver=ai_driver, + camera=c) + products.append(product) all_light_groups = self._get_attr(aov, "lightGroups") if all_light_groups: # All light groups is enabled. A single multipart # Render Product - product = RenderProduct(productName=name + "_lgroups", - ext=ext, - aov=aov_name, - driver=ai_driver, - # Always multichannel output - multipart=True) - products.append(product) + for camera in cameras: + c = camera + if use_single_camera: + c = None + product = RenderProduct(productName=name + "_lgroups", + ext=ext, + aov=aov_name, + driver=ai_driver, + # Always multichannel output + multipart=True, + camera=c) + products.append(product) else: value = self._get_attr(aov, "lightGroupsList") if not value: @@ -529,11 +547,16 @@ class RenderProductsArnold(ARenderProducts): for light_group in selected_light_groups: # Render Product per selected light group aov_light_group_name = "{}_{}".format(name, light_group) - product = RenderProduct(productName=aov_light_group_name, - aov=aov_name, - driver=ai_driver, - ext=ext) - products.append(product) + for camera in cameras: + c = camera + if use_single_camera: + c = None + product = RenderProduct(productName=aov_light_group_name, + aov=aov_name, + driver=ai_driver, + ext=ext, + camera=c) + products.append(product) return products @@ -556,17 +579,31 @@ class RenderProductsArnold(ARenderProducts): # anyway. return [] - default_ext = self._get_attr("defaultRenderGlobals.imfPluginKey") - beauty_product = RenderProduct(productName="beauty", - ext=default_ext, - driver="defaultArnoldDriver") + # check if camera token is in prefix. If so, and we have list of + # renderable cameras, generate render product for each and every + # of them. + has_camera_token = ( + "" in self.layer_data.filePrefix.lower() + ) + cameras = [] + if has_camera_token: + cameras = [ + self.sanitize_camera_name(c) + for c in self.get_renderable_cameras() + ] + default_ext = self._get_attr("defaultRenderGlobals.imfPluginKey") + beauty_products = [RenderProduct( + productName="beauty", + ext=default_ext, + driver="defaultArnoldDriver", + camera=camera) for camera in cameras] # AOVs > Legacy > Maya Render View > Mode aovs_enabled = bool( self._get_attr("defaultArnoldRenderOptions.aovMode") ) if not aovs_enabled: - return [beauty_product] + return beauty_products # Common > File Output > Merge AOVs or # We don't need to check for Merge AOVs due to overridden @@ -575,8 +612,7 @@ class RenderProductsArnold(ARenderProducts): "" in self.layer_data.filePrefix.lower() ) if not has_renderpass_token: - beauty_product.multipart = True - return [beauty_product] + return [setattr(bp, "multipart", True) for bp in beauty_products] # AOVs are set to be rendered separately. We should expect # token in path. @@ -598,14 +634,14 @@ class RenderProductsArnold(ARenderProducts): continue # For now stick to the legacy output format. - aov_products = self._get_aov_render_products(aov) + aov_products = self._get_aov_render_products(aov, cameras) products.extend(aov_products) - if not any(product.aov == "RGBA" for product in products): + if all(product.aov != "RGBA" for product in products): # Append default 'beauty' as this is arnolds default. # However, it is excluded whenever a RGBA pass is enabled. # For legibility add the beauty layer as first entry - products.insert(0, beauty_product) + products += beauty_products # TODO: Output Denoising AOVs? diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py index 19e3174384..6b07749819 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py @@ -385,6 +385,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): """ task = os.environ["AVALON_TASK"] subset = instance_data["subset"] + cameras = instance_data.get("cameras", []) instances = [] # go through aovs in expected files for aov, files in exp_files[0].items(): @@ -410,7 +411,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): task[0].upper(), task[1:], subset[0].upper(), subset[1:]) - subset_name = '{}_{}'.format(group_name, aov) + cam = [c for c in cameras if c in col.head] + if cam: + subset_name = '{}_{}_{}'.format(group_name, cam, aov) + else: + subset_name = '{}_{}'.format(group_name, aov) if isinstance(col, (list, tuple)): staging = os.path.dirname(col[0]) From 0b4cd5885801487f92f622335f4c774738bcc8f7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 10:48:48 +0200 Subject: [PATCH 287/450] fix oiio executable name --- start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.py b/start.py index 8b4ee97b09..451db03a54 100644 --- a/start.py +++ b/start.py @@ -404,7 +404,7 @@ def _validate_thirdparty_binaries(): binary_vendors_dir, "oiio", low_platform, - "oiio_tool" + "oiiotool" ) if not is_tool(oiio_tool_path): raise RuntimeError(error_msg.format("OpenImageIO")) From 81f743bd21fcb16254c08f759fac209b5a3b8931 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 11:28:32 +0200 Subject: [PATCH 288/450] tkinter message is visible in taskbar --- start.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/start.py b/start.py index 451db03a54..f3adabd942 100644 --- a/start.py +++ b/start.py @@ -365,16 +365,20 @@ def _startup_validations(): if os.environ.get("OPENPYPE_HEADLESS_MODE"): raise - from tkinter import Tk + import tkinter from tkinter.messagebox import showerror - root = Tk() - root.withdraw() + root = tkinter.Tk() + root.attributes("-alpha", 0.0) + root.wm_state("iconic") + if platform.system().lower() != "windows": + root.withdraw() + showerror( "Startup validations didn't pass", str(exc) ) - root.destroy() + root.withdraw() sys.exit(1) From 48e120b25c687886d6ad45f52d69906f4fda63ba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 13:32:37 +0200 Subject: [PATCH 289/450] fix typo --- .../modules/default_modules/timers_manager/timers_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index e2c421bcfe..47ba0b4059 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -209,7 +209,7 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): self.widget_user_idle.refresh_context() self.is_running = False - self.timer_stopper(None) + self.timer_stopped(None) def connect_with_modules(self, enabled_modules): for module in enabled_modules: From 950da8749efaed7eeb17ab9511de1c7ddd1075ed Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Sep 2021 16:11:43 +0200 Subject: [PATCH 290/450] Fix - project lists refresh each show up event Fix can_edit method --- .../modules/default_modules/sync_server/tray/app.py | 1 + .../default_modules/sync_server/tray/widgets.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/app.py b/openpype/modules/default_modules/sync_server/tray/app.py index 0299edb2eb..5298c7be1d 100644 --- a/openpype/modules/default_modules/sync_server/tray/app.py +++ b/openpype/modules/default_modules/sync_server/tray/app.py @@ -97,6 +97,7 @@ class SyncServerWindow(QtWidgets.QDialog): def showEvent(self, event): self.representationWidget.model.set_project( self.projects.current_project) + self.projects.refresh() self._set_running(True) super().showEvent(event) diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index e2009bd219..4fc5723f42 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -65,6 +65,7 @@ class SyncProjectListWidget(QtWidgets.QWidget): self.current_project = None self.project_name = None self.local_site = None + self.remote_site = None self.icons = {} def _on_index_change(self, new_idx, _old_idx): @@ -99,6 +100,11 @@ class SyncProjectListWidget(QtWidgets.QWidget): if project_name: self.local_site = self.sync_server.get_active_site(project_name) + self.remote_site = self.sync_server.get_remote_site(project_name) + + def _can_edit(self): + """Returns true if some site is user local site, eg. could edit""" + return get_local_site_id() in (self.local_site, self.remote_site) def _get_icon(self, status): if not self.icons.get(status): @@ -122,9 +128,7 @@ class SyncProjectListWidget(QtWidgets.QWidget): menu = QtWidgets.QMenu(self) actions_mapping = {} - can_edit = self.model.can_edit - - if can_edit: + if self._can_edit(): if self.sync_server.is_project_paused(self.project_name): action = QtWidgets.QAction("Unpause") actions_mapping[action] = self._unpause From 7d2d621c9ab589d1b34638fec182e2939290f77c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 22 Sep 2021 18:07:48 +0200 Subject: [PATCH 291/450] camera token for all renderers --- openpype/hosts/maya/api/lib_renderproducts.py | 150 +++++++++++------- .../maya/plugins/publish/collect_render.py | 14 +- .../publish/validate_rendersettings.py | 6 +- 3 files changed, 107 insertions(+), 63 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 29f216be8c..39d894a204 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -185,6 +185,16 @@ class ARenderProducts: self.layer_data = self._get_layer_data() self.layer_data.products = self.get_render_products() + def has_camera_token(self): + # type: () -> bool + """Check if camera token is in image prefix. + + Returns: + bool: True/False if camera token is present. + + """ + return "" in self.layer_data.filePrefix.lower() + @abstractmethod def get_render_products(self): """To be implemented by renderer class. @@ -362,8 +372,8 @@ class ARenderProducts: ) return expected_files - def get_files(self, product, camera): - # type: (RenderProduct, str) -> list + def get_files(self, product): + # type: (RenderProduct) -> list """Return list of expected files. It will translate render token strings ('', etc.) to @@ -374,7 +384,6 @@ class ARenderProducts: Args: product (RenderProduct): Render product to be used for file generation. - camera (str): Camera name. Returns: List of files @@ -384,7 +393,7 @@ class ARenderProducts: self.layer_data, force_aov_name=product.productName, force_ext=product.ext, - force_cameras=[camera] + force_cameras=[product.camera] or None ) def get_renderable_cameras(self): @@ -470,10 +479,11 @@ class RenderProductsArnold(ARenderProducts): source=True, destination=False, type="aiAOVDriver") or [] - use_single_camera = False if not cameras: - cameras = ["__default__"] - use_single_camera = True + cameras = [ + self.sanitize_camera_name( + self.get_renderable_cameras()[0]) + ] for ai_driver in ai_drivers: # todo: check aiAOVDriver.prefix as it could have @@ -513,14 +523,11 @@ class RenderProductsArnold(ARenderProducts): global_aov = self._get_attr(aov, "globalAov") if global_aov: for camera in cameras: - c = camera - if use_single_camera: - c = None product = RenderProduct(productName=name, ext=ext, aov=aov_name, driver=ai_driver, - camera=c) + camera=camera) products.append(product) all_light_groups = self._get_attr(aov, "lightGroups") @@ -528,16 +535,13 @@ class RenderProductsArnold(ARenderProducts): # All light groups is enabled. A single multipart # Render Product for camera in cameras: - c = camera - if use_single_camera: - c = None product = RenderProduct(productName=name + "_lgroups", ext=ext, aov=aov_name, driver=ai_driver, # Always multichannel output multipart=True, - camera=c) + camera=camera) products.append(product) else: value = self._get_attr(aov, "lightGroupsList") @@ -548,14 +552,11 @@ class RenderProductsArnold(ARenderProducts): # Render Product per selected light group aov_light_group_name = "{}_{}".format(name, light_group) for camera in cameras: - c = camera - if use_single_camera: - c = None product = RenderProduct(productName=aov_light_group_name, aov=aov_name, driver=ai_driver, ext=ext, - camera=c) + camera=camera) products.append(product) return products @@ -582,15 +583,10 @@ class RenderProductsArnold(ARenderProducts): # check if camera token is in prefix. If so, and we have list of # renderable cameras, generate render product for each and every # of them. - has_camera_token = ( - "" in self.layer_data.filePrefix.lower() - ) - cameras = [] - if has_camera_token: - cameras = [ - self.sanitize_camera_name(c) - for c in self.get_renderable_cameras() - ] + cameras = [ + self.sanitize_camera_name(c) + for c in self.get_renderable_cameras() + ] default_ext = self._get_attr("defaultRenderGlobals.imfPluginKey") beauty_products = [RenderProduct( @@ -706,6 +702,11 @@ class RenderProductsVray(ARenderProducts): # anyway. return [] + cameras = [ + self.sanitize_camera_name(c) + for c in self.get_renderable_cameras() + ] + image_format_str = self._get_attr("vraySettings.imageFormatStr") default_ext = image_format_str if default_ext in {"exr (multichannel)", "exr (deep)"}: @@ -716,13 +717,21 @@ class RenderProductsVray(ARenderProducts): # add beauty as default when not disabled dont_save_rgb = self._get_attr("vraySettings.dontSaveRgbChannel") if not dont_save_rgb: - products.append(RenderProduct(productName="", ext=default_ext)) + for camera in cameras: + products.append( + RenderProduct(productName="", + ext=default_ext, + camera=camera)) # separate alpha file separate_alpha = self._get_attr("vraySettings.separateAlpha") if separate_alpha: - products.append(RenderProduct(productName="Alpha", - ext=default_ext)) + for camera in cameras: + products.append( + RenderProduct(productName="Alpha", + ext=default_ext, + camera=camera) + ) if image_format_str == "exr (multichannel)": # AOVs are merged in m-channel file, only main layer is rendered @@ -752,19 +761,23 @@ class RenderProductsVray(ARenderProducts): # instead seems to output multiple Render Products, # specifically "Self_Illumination" and "Environment" product_names = ["Self_Illumination", "Environment"] - for name in product_names: - product = RenderProduct(productName=name, - ext=default_ext, - aov=aov) - products.append(product) + for camera in cameras: + for name in product_names: + product = RenderProduct(productName=name, + ext=default_ext, + aov=aov, + camera=camera) + products.append(product) # Continue as we've processed this special case AOV continue aov_name = self._get_vray_aov_name(aov) - product = RenderProduct(productName=aov_name, - ext=default_ext, - aov=aov) - products.append(product) + for camera in cameras: + product = RenderProduct(productName=aov_name, + ext=default_ext, + aov=aov, + camera=camera) + products.append(product) return products @@ -911,6 +924,11 @@ class RenderProductsRedshift(ARenderProducts): # anyway. return [] + cameras = [ + self.sanitize_camera_name(c) + for c in self.get_renderable_cameras() + ] + # For Redshift we don't directly return upon forcing multilayer # due to some AOVs still being written into separate files, # like Cryptomatte. @@ -969,11 +987,13 @@ class RenderProductsRedshift(ARenderProducts): for light_group in light_groups: aov_light_group_name = "{}_{}".format(aov_name, light_group) - product = RenderProduct(productName=aov_light_group_name, - aov=aov_name, - ext=ext, - multipart=aov_multipart) - products.append(product) + for camera in cameras: + product = RenderProduct(productName=aov_light_group_name, + aov=aov_name, + ext=ext, + multipart=aov_multipart, + camera=camera) + products.append(product) if light_groups: light_groups_enabled = True @@ -981,11 +1001,13 @@ class RenderProductsRedshift(ARenderProducts): # Redshift AOV Light Select always renders the global AOV # even when light groups are present so we don't need to # exclude it when light groups are active - product = RenderProduct(productName=aov_name, - aov=aov_name, - ext=ext, - multipart=aov_multipart) - products.append(product) + for camera in cameras: + product = RenderProduct(productName=aov_name, + aov=aov_name, + ext=ext, + multipart=aov_multipart, + camera=camera) + products.append(product) # When a Beauty AOV is added manually, it will be rendered as # 'Beauty_other' in file name and "standard" beauty will have @@ -995,10 +1017,12 @@ class RenderProductsRedshift(ARenderProducts): return products beauty_name = "Beauty_other" if has_beauty_aov else "" - products.insert(0, - RenderProduct(productName=beauty_name, - ext=ext, - multipart=multipart)) + for camera in cameras: + products.insert(0, + RenderProduct(productName=beauty_name, + ext=ext, + multipart=multipart, + camera=camera)) return products @@ -1023,6 +1047,16 @@ class RenderProductsRenderman(ARenderProducts): :func:`ARenderProducts.get_render_products()` """ + cameras = [ + self.sanitize_camera_name(c) + for c in self.get_renderable_cameras() + ] + + if not cameras: + cameras = [ + self.sanitize_camera_name( + self.get_renderable_cameras()[0]) + ] products = [] default_ext = "exr" @@ -1036,9 +1070,11 @@ class RenderProductsRenderman(ARenderProducts): if aov_name == "rmanDefaultDisplay": aov_name = "beauty" - product = RenderProduct(productName=aov_name, - ext=default_ext) - products.append(product) + for camera in cameras: + product = RenderProduct(productName=aov_name, + ext=default_ext, + camera=camera) + products.append(product) return products diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 5049647ff9..46d1c9350d 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -174,10 +174,16 @@ class CollectMayaRender(pyblish.api.ContextPlugin): assert render_products, "no render products generated" exp_files = [] for product in render_products: - for camera in layer_render_products.layer_data.cameras: - exp_files.append( - {product.productName: layer_render_products.get_files( - product, camera)}) + product_name = product.productName + if product.camera and layer_render_products.has_camera_token(): + product_name = "{}{}".format( + product.camera, + "_" + product_name if product_name else "") + exp_files.append( + { + product_name: layer_render_products.get_files( + product) + }) self.log.info("multipart: {}".format( layer_render_products.multipart)) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 7c795db43d..65ddacfc57 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -76,7 +76,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): r'%a||', re.IGNORECASE) R_LAYER_TOKEN = re.compile( r'%l||', re.IGNORECASE) - R_CAMERA_TOKEN = re.compile(r'%c|', re.IGNORECASE) + R_CAMERA_TOKEN = re.compile(r'%c|Camera>') R_SCENE_TOKEN = re.compile(r'%s|', re.IGNORECASE) DEFAULT_PADDING = 4 @@ -126,7 +126,9 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): if len(cameras) > 1 and not re.search(cls.R_CAMERA_TOKEN, prefix): invalid = True cls.log.error("Wrong image prefix [ {} ] - " - "doesn't have: '' token".format(prefix)) + "doesn't have: '' token".format(prefix)) + cls.log.error( + "Note that to needs to have capital 'C' at the beginning") # renderer specific checks if renderer == "vray": From 97ed419fc93fe134d950591891c1c1b325923c5d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 23 Sep 2021 09:11:29 +0100 Subject: [PATCH 292/450] echo tag name in ci --- .github/workflows/prerelease.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 0fb07be79d..60ce608b21 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -76,6 +76,7 @@ jobs: git add . git commit -m "[Automated] Bump version" tag_name="CI/${{ steps.version.outputs.next_tag }}" + echo $tag_name git tag -a $tag_name -m "nightly build" - name: Push to protected main branch From bbf4964235fba34f63513a4af0c7b0b6535a3c97 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 23 Sep 2021 09:23:32 +0100 Subject: [PATCH 293/450] update to latest avalon-core --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 1e94241ffe..8aee68fa10 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 1e94241ffe2dd7ce65ca66b08e452ffc03180235 +Subproject commit 8aee68fa10ab4d79be1a91e7728a609748e7c3c6 From e6093a7835146c28ff5166ffc8b399ae977540cf Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 23 Sep 2021 09:24:31 +0100 Subject: [PATCH 294/450] (fix) double print in ci tag --- tools/ci_tools.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/ci_tools.py b/tools/ci_tools.py index 69f5158bb3..bbe0c699d1 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -15,13 +15,11 @@ def get_release_type_github(Log, github_token): repo = g.get_repo("pypeclub/OpenPype") for line in Log.splitlines(): - print(line) match = re.search("pull request #(\d+)", line) if match: pr_number = match.group(1) pr = repo.get_pull(int(pr_number)) for label in pr.labels: - print(label.name) if label.name in minor_labels: return ("minor") elif label.name in patch_labels: From cee7541fd0fe251ba6ed3409030cd19d459ff30e Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 23 Sep 2021 08:29:15 +0000 Subject: [PATCH 295/450] [Automated] Bump version --- CHANGELOG.md | 51 ++++++++++++++++++++++++++------------------- openpype/version.py | 2 +- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca8de17ae..78d6ca2dc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,38 @@ # Changelog -## [3.5.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.4.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...HEAD) +**🆕 New features** + +- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008) + **🚀 Enhancements** +- General: Startup validations [\#2054](https://github.com/pypeclub/OpenPype/pull/2054) +- Nuke: proxy mode validator [\#2052](https://github.com/pypeclub/OpenPype/pull/2052) +- Ftrack: Removed ftrack interface [\#2049](https://github.com/pypeclub/OpenPype/pull/2049) +- Settings UI: Deffered set value on entity [\#2044](https://github.com/pypeclub/OpenPype/pull/2044) +- Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043) +- Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042) +- Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) - Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) +- Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) +- TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024) +- Feature Maya import asset from scene inventory [\#2018](https://github.com/pypeclub/OpenPype/pull/2018) + +**🐛 Bug fixes** + +- Timers manger: Typo fix [\#2058](https://github.com/pypeclub/OpenPype/pull/2058) +- Hiero: Editorial fixes [\#2057](https://github.com/pypeclub/OpenPype/pull/2057) +- Differentiate jpg sequences from thumbnail [\#2056](https://github.com/pypeclub/OpenPype/pull/2056) +- FFmpeg: Split command to list does not work [\#2046](https://github.com/pypeclub/OpenPype/pull/2046) +- Removed shell flag in subprocess call [\#2045](https://github.com/pypeclub/OpenPype/pull/2045) + +**Merged pull requests:** + +- Bump prismjs from 1.24.0 to 1.25.0 in /website [\#2050](https://github.com/pypeclub/OpenPype/pull/2050) ## [3.4.0](https://github.com/pypeclub/OpenPype/tree/3.4.0) (2021-09-17) @@ -25,6 +51,7 @@ - Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) +- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) - Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) - General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) @@ -70,10 +97,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) -**🚀 Enhancements** - -- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) - **🐛 Bug fixes** - TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) @@ -89,37 +112,23 @@ - Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) - Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) -- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) **🚀 Enhancements** - Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940) +- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) - Check for missing ✨ Python when using `pyenv` [\#1925](https://github.com/pypeclub/OpenPype/pull/1925) -- Settings: Default values for enum [\#1920](https://github.com/pypeclub/OpenPype/pull/1920) -- Settings UI: Modifiable dict view enhance [\#1919](https://github.com/pypeclub/OpenPype/pull/1919) -- submodules: avalon-core update [\#1911](https://github.com/pypeclub/OpenPype/pull/1911) -- Ftrack: Where I run action enhancement [\#1900](https://github.com/pypeclub/OpenPype/pull/1900) -- Ftrack: Private project server actions [\#1899](https://github.com/pypeclub/OpenPype/pull/1899) -- Support nested studio plugins paths. [\#1898](https://github.com/pypeclub/OpenPype/pull/1898) **🐛 Bug fixes** - Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) +- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) - Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929) - Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926) -- Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922) -- standalone: validator asset parents [\#1917](https://github.com/pypeclub/OpenPype/pull/1917) -- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916) -- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914) -- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906) -- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904) -- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903) -- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902) **Merged pull requests:** - Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937) -- Add support for multiple Deadline ☠️➖ servers [\#1905](https://github.com/pypeclub/OpenPype/pull/1905) ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) diff --git a/openpype/version.py b/openpype/version.py index f8ed9c7c2f..3582fb27e2 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.5.0-nightly.1" +__version__ = "3.4.1-nightly.1" From 0c8af1f7b6e7b3e27e0b9230abb4ecddc92d4bf2 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 23 Sep 2021 11:10:36 +0200 Subject: [PATCH 296/450] add collector --- .../maya/plugins/publish/collect_loaded_plugin.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py diff --git a/openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py new file mode 100644 index 0000000000..2624bcfd6b --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py @@ -0,0 +1,15 @@ +import pyblish.api +import avalon.api +from maya import cmds + + +class CollectLoadedPlugin(pyblish.api.ContextPlugin): + """Collect loaded plugins""" + + order = pyblish.api.CollectorOrder + label = "Loaded Plugins" + hosts = ["maya"] + + def process(self, context): + + context.data["loadedPlugins"] = cmds.pluginInfo(query=True, listPlugins=True) From d4827bcc7aecadbce4c8430cf379456e28f01afc Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 23 Sep 2021 11:10:47 +0200 Subject: [PATCH 297/450] add validator --- .../plugins/publish/validate_loaded_plugin.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py diff --git a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py new file mode 100644 index 0000000000..81f40abcc3 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py @@ -0,0 +1,39 @@ +import pyblish.api +import maya.cmds as cmds +import openpype.api +from openpype import lib + + +class ValidateLoadedPlugin(pyblish.api.ContextPlugin): + """Ensure there are no unauthorized loaded plugins""" + + label = "Loaded Plugin" + order = pyblish.api.ValidatorOrder + host = ["maya"] + actions = [openpype.api.RepairContextAction] + + @classmethod + def get_invalid(cls, context): + + invalid = [] + + for plugin in context.data.get("loadedPlugins"): + if plugin not in cls.authorized_plugins: + invalid.append(plugin) + + return invalid + + def process(self, context): + + invalid = self.get_invalid(context) + if invalid: + raise RuntimeError( + "Found forbidden plugin name: {}".format(", ".join(invalid)) + ) + + @classmethod + def repair(cls, context): + """Unload forbidden plugins""" + + for plugin in cls.get_invalid(context): + cmds.unloadPlugin(plugin, force=True) From 9b712d5a62e4c526daf9c002acbfc81ed12b70fc Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 23 Sep 2021 11:11:02 +0200 Subject: [PATCH 298/450] add openpype settings --- .../defaults/project_settings/maya.json | 49 +++++++++++++++++++ .../schemas/schema_maya_publish.json | 21 ++++++++ 2 files changed, 70 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 3540c3eb29..b19d544fed 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -169,6 +169,55 @@ "enabled": false, "attributes": {} }, + "ValidateLoadedPlugin": { + "enabled": false, + "authorized_plugins": [ + "stereoCamera", + "svgFileTranslator", + "invertShape", + "mayaHIK", + "GamePipeline", + "curveWarp", + "tiffFloatReader", + "MASH", + "poseInterpolator", + "ATFPlugin", + "hairPhysicalShader", + "cacheEvaluator", + "ikSpringSolver", + "ik2Bsolver", + "xgenToolkit", + "AbcExport", + "retargeterNodes", + "gameFbxExporter", + "VectorRender", + "OpenEXRLoader", + "lookdevKit", + "Unfold3D", + "Type", + "mayaCharacterization", + "meshReorder", + "modelingToolkit", + "MayaMuscle", + "rotateHelper", + "dx11Shader", + "matrixNodes", + "AbcImport", + "autoLoader", + "deformerEvaluator", + "sceneAssembly", + "gpuCache", + "OneClick", + "shaderFXPlugin", + "objExport", + "renderSetup", + "GPUBuiltInDeformer", + "ArubaTessellator", + "quatNodes", + "fbxmaya", + "Turtle" + ] + }, "ValidateRenderSettings": { "arnold_render_attributes": [], "vray_render_attributes": [], diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 89cd30aed0..e2df6654f2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -82,6 +82,27 @@ ] }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateLoadedPlugin", + "label": "Validate Loaded Plugin", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "authorized_plugins", + "label": "Authorized plugins", + "object_type": "text" + } + ] + }, + { "type": "dict", "collapsible": true, From 13ae6dac3197cc5d91d66c05063a244a91b11c05 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 23 Sep 2021 11:23:25 +0200 Subject: [PATCH 299/450] Fix unused import and line too long --- .../hosts/maya/plugins/publish/collect_loaded_plugin.py | 6 ++++-- .../hosts/maya/plugins/publish/validate_loaded_plugin.py | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py index 2624bcfd6b..7ee7021962 100644 --- a/openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py +++ b/openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py @@ -1,5 +1,4 @@ import pyblish.api -import avalon.api from maya import cmds @@ -12,4 +11,7 @@ class CollectLoadedPlugin(pyblish.api.ContextPlugin): def process(self, context): - context.data["loadedPlugins"] = cmds.pluginInfo(query=True, listPlugins=True) + context.data["loadedPlugins"] = cmds.pluginInfo( + query=True, + listPlugins=True, + ) diff --git a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py index 81f40abcc3..7798cbab4e 100644 --- a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py @@ -1,8 +1,6 @@ import pyblish.api import maya.cmds as cmds import openpype.api -from openpype import lib - class ValidateLoadedPlugin(pyblish.api.ContextPlugin): """Ensure there are no unauthorized loaded plugins""" From e729204cb81063283c1a296b11c5878272ee4d55 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 23 Sep 2021 11:24:50 +0200 Subject: [PATCH 300/450] fix syntax --- openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py index 7798cbab4e..01705e8b13 100644 --- a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py @@ -2,6 +2,7 @@ import pyblish.api import maya.cmds as cmds import openpype.api + class ValidateLoadedPlugin(pyblish.api.ContextPlugin): """Ensure there are no unauthorized loaded plugins""" From 53888bb5105fb507b5a8d6b9b5374c10eb454e73 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 23 Sep 2021 11:34:23 +0100 Subject: [PATCH 301/450] fix missing import --- .github/workflows/release.yml | 2 +- tools/ci_tools.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d3f301b99..3f85525c26 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: with: python-version: 3.7 - name: Install Python requirements - run: pip install gitpython semver + run: pip install gitpython semver PyGithub - name: 💉 Inject new version into files id: version diff --git a/tools/ci_tools.py b/tools/ci_tools.py index bbe0c699d1..337b19a346 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -14,16 +14,21 @@ def get_release_type_github(Log, github_token): g = Github(github_token) repo = g.get_repo("pypeclub/OpenPype") + labels = set() for line in Log.splitlines(): match = re.search("pull request #(\d+)", line) if match: pr_number = match.group(1) pr = repo.get_pull(int(pr_number)) for label in pr.labels: - if label.name in minor_labels: - return ("minor") - elif label.name in patch_labels: - return("patch") + labels.add(label.name) + + if any(label in labels for label in minor_labels): + return "minor" + + if any(label in labels for label in patch_labels): + return "path" + return None From 14d4d5e6f2b6ac11cb0f23cacfa2a266780ac77e Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 23 Sep 2021 10:40:44 +0000 Subject: [PATCH 302/450] [Automated] Release --- CHANGELOG.md | 24 ++++++++++-------------- openpype/version.py | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78d6ca2dc3..d2ae3c9eae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,8 @@ # Changelog -## [3.4.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...HEAD) - -**🆕 New features** - -- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...3.4.1) **🚀 Enhancements** @@ -17,10 +13,11 @@ - Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043) - Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042) - Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) -- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) +- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024) - Feature Maya import asset from scene inventory [\#2018](https://github.com/pypeclub/OpenPype/pull/2018) +- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008) **🐛 Bug fixes** @@ -38,11 +35,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0) -### 📖 Documentation - -- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) -- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) - **🆕 New features** - Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) @@ -50,8 +42,8 @@ **🚀 Enhancements** - Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) +- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) -- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) - Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) - General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) @@ -93,6 +85,11 @@ - Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) - Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961) +### 📖 Documentation + +- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) +- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) + ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) @@ -111,7 +108,6 @@ **🆕 New features** - Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) -- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) **🚀 Enhancements** diff --git a/openpype/version.py b/openpype/version.py index 3582fb27e2..0e52014a56 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.1-nightly.1" +__version__ = "3.4.1" From 3661d11976a4839a97c7d6db4e303e3d8bd418b3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Sep 2021 12:58:44 +0200 Subject: [PATCH 303/450] Fix - concurrent change in Settings wont trigger exception --- openpype/modules/default_modules/sync_server/tray/app.py | 1 + .../modules/default_modules/sync_server/tray/widgets.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/openpype/modules/default_modules/sync_server/tray/app.py b/openpype/modules/default_modules/sync_server/tray/app.py index 5298c7be1d..0996cbc468 100644 --- a/openpype/modules/default_modules/sync_server/tray/app.py +++ b/openpype/modules/default_modules/sync_server/tray/app.py @@ -84,6 +84,7 @@ class SyncServerWindow(QtWidgets.QDialog): self.pause_btn.setAutoDefault(False) self.pause_btn.setDefault(False) repres.message_generated.connect(self._update_message) + self.projects.message_generated.connect(self._update_message) self.representationWidget = repres diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index 4fc5723f42..b0730c9c8d 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -30,6 +30,7 @@ class SyncProjectListWidget(QtWidgets.QWidget): Lists all projects that are synchronized to choose from """ project_changed = QtCore.Signal() + message_generated = QtCore.Signal(str) def __init__(self, sync_server, parent): super(SyncProjectListWidget, self).__init__(parent) @@ -71,6 +72,12 @@ class SyncProjectListWidget(QtWidgets.QWidget): def _on_index_change(self, new_idx, _old_idx): project_name = new_idx.data(QtCore.Qt.DisplayRole) + if not self.sync_server.get_sync_project_setting(project_name): + self.message_generated.emit( + "Project {} not active anymore".format(project_name)) + self.refresh() + return + self.current_project = project_name self.project_changed.emit() From fd7438c2aca6e8effcbf5b46e54c2d8e1c95eeb4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 14:02:30 +0200 Subject: [PATCH 304/450] defined PROJECT_NAME_ROLE --- openpype/tools/project_manager/project_manager/__init__.py | 4 +++- openpype/tools/project_manager/project_manager/constants.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/__init__.py b/openpype/tools/project_manager/project_manager/__init__.py index 49ade4a989..3001f2d160 100644 --- a/openpype/tools/project_manager/project_manager/__init__.py +++ b/openpype/tools/project_manager/project_manager/__init__.py @@ -1,5 +1,6 @@ __all__ = ( "IDENTIFIER_ROLE", + "PROJECT_NAME_ROLE", "HierarchyView", @@ -20,7 +21,8 @@ __all__ = ( from .constants import ( - IDENTIFIER_ROLE + IDENTIFIER_ROLE, + PROJECT_NAME_ROLE ) from .widgets import CreateProjectDialog from .view import HierarchyView diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py index 67dea79e59..7ca4aa9492 100644 --- a/openpype/tools/project_manager/project_manager/constants.py +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -17,6 +17,9 @@ ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5 # Item has opened editor (per column) EDITOR_OPENED_ROLE = QtCore.Qt.UserRole + 6 +# Role for project model +PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 7 + # Allowed symbols for any name NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" NAME_REGEX = re.compile("^[" + NAME_ALLOWED_SYMBOLS + "]*$") From 29490f1efb4320c6a0bc2e006bc5738f34a9eba5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 14:04:03 +0200 Subject: [PATCH 305/450] model is not cleared on refresh --- .../project_manager/project_manager/model.py | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 7036b65f87..af0dab453c 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -9,7 +9,8 @@ from .constants import ( DUPLICATED_ROLE, HIERARCHY_CHANGE_ABLE_ROLE, REMOVED_ROLE, - EDITOR_OPENED_ROLE + EDITOR_OPENED_ROLE, + PROJECT_NAME_ROLE ) from .style import ResourceCache @@ -29,7 +30,7 @@ class ProjectModel(QtGui.QStandardItemModel): def __init__(self, dbcon, *args, **kwargs): self.dbcon = dbcon - self._project_names = set() + self._items_by_name = {} super(ProjectModel, self).__init__(*args, **kwargs) @@ -37,29 +38,41 @@ class ProjectModel(QtGui.QStandardItemModel): """Reload projects.""" self.dbcon.Session["AVALON_PROJECT"] = None - project_items = [] + new_project_items = [] - none_project = QtGui.QStandardItem("< Select Project >") - none_project.setData(None) - project_items.append(none_project) + if None not in self._items_by_name: + none_project = QtGui.QStandardItem("< Select Project >") + self._items_by_name[None] = none_project + new_project_items.append(none_project) + project_docs = self.dbcon.projects( + projection={"name": 1}, + only_active=True + ) project_names = set() + for project_doc in project_docs: + project_name = project_doc.get("name") + if not project_name: + continue - for doc in sorted( - self.dbcon.projects(projection={"name": 1}, only_active=True), - key=lambda x: x["name"] - ): + project_names.add(project_name) + if project_name not in self._items_by_name: + project_item = QtGui.QStandardItem(project_name) - project_name = doc.get("name") - if project_name: - project_names.add(project_name) - project_items.append(QtGui.QStandardItem(project_name)) + self._items_by_name[project_name] = project_item + new_project_items.append(project_item) - self.clear() + root_item = self.invisibleRootItem() + for project_name in tuple(self._items_by_name.keys()): + if project_name is None or project_name in project_names: + continue + project_item = self._items_by_name.pop(project_name) + root_item.removeRow(project_item.row()) + + if new_project_items: + root_item.appendRows(new_project_items) - self._project_names = project_names - self.invisibleRootItem().appendRows(project_items) class HierarchySelectionModel(QtCore.QItemSelectionModel): From 9270d603545c97eadf557d4517715a7b55e76698 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 14:04:21 +0200 Subject: [PATCH 306/450] set PROJECT_NAME_ROLE on project items --- openpype/tools/project_manager/project_manager/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index af0dab453c..d20ec337b0 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -58,6 +58,7 @@ class ProjectModel(QtGui.QStandardItemModel): project_names.add(project_name) if project_name not in self._items_by_name: project_item = QtGui.QStandardItem(project_name) + project_item.setData(project_name, PROJECT_NAME_ROLE) self._items_by_name[project_name] = project_item new_project_items.append(project_item) From 6579fade0092e85701f22ffb54eece1172434f63 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 14:04:54 +0200 Subject: [PATCH 307/450] added project proxy model for filtering first item --- .../project_manager/__init__.py | 2 ++ .../project_manager/project_manager/model.py | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/__init__.py b/openpype/tools/project_manager/project_manager/__init__.py index 3001f2d160..6e44afd841 100644 --- a/openpype/tools/project_manager/project_manager/__init__.py +++ b/openpype/tools/project_manager/project_manager/__init__.py @@ -5,6 +5,7 @@ __all__ = ( "HierarchyView", "ProjectModel", + "ProjectProxyFilter", "CreateProjectDialog", "HierarchyModel", @@ -28,6 +29,7 @@ from .widgets import CreateProjectDialog from .view import HierarchyView from .model import ( ProjectModel, + ProjectProxyFilter, HierarchyModel, HierarchySelectionModel, diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index d20ec337b0..5b6ed78b50 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -74,6 +74,26 @@ class ProjectModel(QtGui.QStandardItemModel): root_item.appendRows(new_project_items) +class ProjectProxyFilter(QtCore.QSortFilterProxyModel): + """Filters default project item.""" + def __init__(self, *args, **kwargs): + super(ProjectProxyFilter, self).__init__(*args, **kwargs) + self._filter_default = False + + def set_filter_default(self, enabled=True): + """Set if filtering of default item is enabled.""" + if enabled == self._filter_default: + return + self._filter_default = enabled + self.invalidateFilter() + + def filterAcceptsRow(self, row, parent): + if not self._filter_default: + return True + + model = self.sourceModel() + source_index = model.index(row, self.filterKeyColumn(), parent) + return source_index.data(PROJECT_NAME_ROLE) is not None class HierarchySelectionModel(QtCore.QItemSelectionModel): From eada5f463dccf30f6ffc05a21d34279ede79adbc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 14:08:32 +0200 Subject: [PATCH 308/450] _current_project is method instead of attribute --- .../project_manager/project_manager/window.py | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 79eb9651e9..41ae59c0f9 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -7,7 +7,8 @@ from . import ( HierarchySelectionModel, HierarchyView, - CreateProjectDialog + CreateProjectDialog, + PROJECT_NAME_ROLE ) from openpype.style import load_stylesheet from .style import ResourceCache @@ -35,9 +36,6 @@ class ProjectManagerWindow(QtWidgets.QWidget): self._password_dialog = None self._user_passed = False - # keep track of the current project PM is viewing - self._current_project = None - self.setWindowTitle("OpenPype Project Manager") self.setWindowIcon(QtGui.QIcon(resources.get_openpype_icon_filepath())) @@ -167,6 +165,13 @@ class ProjectManagerWindow(QtWidgets.QWidget): def _set_project(self, project_name=None): self.hierarchy_view.set_project(project_name) + def _current_project(self): + row = self._project_combobox.currentIndex() + if row < 0: + return None + index = self._project_proxy_model.index(row, 0) + return index.data(PROJECT_NAME_ROLE) + def showEvent(self, event): super(ProjectManagerWindow, self).showEvent(event) @@ -194,12 +199,13 @@ class ProjectManagerWindow(QtWidgets.QWidget): if row >= 0: self._project_combobox.setCurrentIndex(row) - self._set_project(self._project_combobox.currentText()) + selected_project = self._current_project() + + self._set_project(selected_project) def _on_project_change(self): - if self._project_combobox.currentIndex() != 0: - self._current_project = self._project_combobox.currentText() - self._set_project(self._current_project) + selected_project = self._current_project() + self._set_project(selected_project) def _on_project_refresh(self): self.refresh_projects() @@ -214,7 +220,8 @@ class ProjectManagerWindow(QtWidgets.QWidget): self.hierarchy_view.add_task() def _on_create_folders(self): - if not self._current_project: + project_name = self._current_project() + if not project_name: return qm = QtWidgets.QMessageBox @@ -225,11 +232,11 @@ class ProjectManagerWindow(QtWidgets.QWidget): if ans == qm.Yes: try: # Get paths based on presets - basic_paths = get_project_basic_paths(self._current_project) + basic_paths = get_project_basic_paths(project_name) if not basic_paths: pass # Invoking OpenPype API to create the project folders - create_project_folders(basic_paths, self._current_project) + create_project_folders(basic_paths, project_name) except Exception as exc: self.log.warning( "Cannot create starting folders: {}".format(exc), @@ -246,11 +253,9 @@ class ProjectManagerWindow(QtWidgets.QWidget): if dialog.result() != 1: return - self._current_project = dialog.project_name - self.show_message( - "Created project \"{}\"".format(self._current_project) - ) - self.refresh_projects(self._current_project) + project_name = dialog.project_name + self.show_message("Created project \"{}\"".format(project_name)) + self.refresh_projects(project_name) def _show_password_dialog(self): if self._password_dialog: From 6b3a451e4b82b46adcfc1a8f2cc7558f9cf2b149 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 14:08:50 +0200 Subject: [PATCH 309/450] create_folders_btn is disabled on invalid project --- openpype/tools/project_manager/project_manager/window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 41ae59c0f9..18c2d0f0e7 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -70,6 +70,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): "Create Starting Folders", project_widget ) + create_folders_btn.setEnabled(False) project_layout = QtWidgets.QHBoxLayout(project_widget) project_layout.setContentsMargins(0, 0, 0, 0) @@ -163,6 +164,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): self.setStyleSheet(load_stylesheet()) def _set_project(self, project_name=None): + self._create_folders_btn.setEnabled(project_name is not None) self.hierarchy_view.set_project(project_name) def _current_project(self): From 6a64e6f06d6cf6c89045fc2be61030a5d618c4fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 14:10:39 +0200 Subject: [PATCH 310/450] use project proxy for filtering and sorting --- .../tools/project_manager/project_manager/window.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 18c2d0f0e7..a19031ceda 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -2,6 +2,7 @@ from Qt import QtWidgets, QtCore, QtGui from . import ( ProjectModel, + ProjectProxyFilter, HierarchyModel, HierarchySelectionModel, @@ -10,8 +11,8 @@ from . import ( CreateProjectDialog, PROJECT_NAME_ROLE ) -from openpype.style import load_stylesheet from .style import ResourceCache +from openpype.style import load_stylesheet from openpype.lib import is_admin_password_required from openpype.widgets import PasswordDialog @@ -48,11 +49,15 @@ class ProjectManagerWindow(QtWidgets.QWidget): dbcon = AvalonMongoDB() project_model = ProjectModel(dbcon) + project_proxy = ProjectProxyFilter() + project_proxy.setSourceModel(project_model) + project_proxy.setDynamicSortFilter(True) + project_combobox = QtWidgets.QComboBox(project_widget) project_combobox.setSizeAdjustPolicy( QtWidgets.QComboBox.AdjustToContents ) - project_combobox.setModel(project_model) + project_combobox.setModel(project_proxy) project_combobox.setRootModelIndex(QtCore.QModelIndex()) style_delegate = QtWidgets.QStyledItemDelegate() project_combobox.setItemDelegate(style_delegate) @@ -146,6 +151,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): add_task_btn.clicked.connect(self._on_add_task) self._project_model = project_model + self._project_proxy_model = project_proxy self.hierarchy_view = hierarchy_view self.hierarchy_model = hierarchy_model @@ -165,6 +171,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): def _set_project(self, project_name=None): self._create_folders_btn.setEnabled(project_name is not None) + self._project_proxy_model.set_filter_default(project_name is not None) self.hierarchy_view.set_project(project_name) def _current_project(self): @@ -192,6 +199,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): project_name = self._project_combobox.currentText() self._project_model.refresh() + self._project_proxy_model.sort(0, QtCore.Qt.AscendingOrder) if self._project_combobox.count() == 0: return self._set_project() @@ -202,7 +210,6 @@ class ProjectManagerWindow(QtWidgets.QWidget): self._project_combobox.setCurrentIndex(row) selected_project = self._current_project() - self._set_project(selected_project) def _on_project_change(self): From 0b7ae4f7358257d412500aa747659e3a447fb562 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Sep 2021 14:53:38 +0200 Subject: [PATCH 311/450] nuke: adding exception for `farm` rendering --- openpype/hosts/nuke/api/lib.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 8948cb4d78..ab4c992719 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -295,7 +295,7 @@ def add_button_write_to_read(node): def create_write_node(name, data, input=None, prenodes=None, - review=True, linked_knobs=None): + review=True, linked_knobs=None, farm=True): ''' Creating write node which is group node Arguments: @@ -421,7 +421,15 @@ def create_write_node(name, data, input=None, prenodes=None, )) continue - if knob and value: + if not knob and not value: + continue + + log.info((knob, value)) + + if isinstance(value, str): + if "[" in value: + now_node[knob].setExpression(value) + else: now_node[knob].setValue(value) # connect to previous node @@ -466,7 +474,7 @@ def create_write_node(name, data, input=None, prenodes=None, # imprinting group node anlib.set_avalon_knob_data(GN, data["avalon"]) anlib.add_publish_knob(GN) - add_rendering_knobs(GN) + add_rendering_knobs(GN, farm) if review: add_review_knob(GN) @@ -526,7 +534,7 @@ def create_write_node(name, data, input=None, prenodes=None, return GN -def add_rendering_knobs(node): +def add_rendering_knobs(node, farm=True): ''' Adds additional rendering knobs to given node Arguments: @@ -535,9 +543,13 @@ def add_rendering_knobs(node): Return: node (obj): with added knobs ''' + knob_options = [ + "Use existing frames", "Local"] + if farm: + knob_options.append("On farm") + if "render" not in node.knobs(): - knob = nuke.Enumeration_Knob("render", "", [ - "Use existing frames", "Local", "On farm"]) + knob = nuke.Enumeration_Knob("render", "", knob_options) knob.clearFlag(nuke.STARTLINE) node.addKnob(knob) return node From a403e4afdf6abaf27e6f77bca5277d20bd0e986d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Sep 2021 14:54:48 +0200 Subject: [PATCH 312/450] nuke: adding still render family workflow --- .../nuke/plugins/create/create_write_still.py | 142 ++++++++++++++++++ .../hosts/nuke/plugins/load/load_image.py | 4 +- .../plugins/publish/extract_render_local.py | 29 +++- .../nuke/plugins/publish/precollect_writes.py | 7 +- .../publish/validate_rendered_frames.py | 5 +- openpype/plugins/publish/integrate_new.py | 1 + .../defaults/project_anatomy/imageio.json | 40 ++++- .../defaults/project_settings/nuke.json | 6 +- 8 files changed, 218 insertions(+), 16 deletions(-) create mode 100644 openpype/hosts/nuke/plugins/create/create_write_still.py diff --git a/openpype/hosts/nuke/plugins/create/create_write_still.py b/openpype/hosts/nuke/plugins/create/create_write_still.py new file mode 100644 index 0000000000..1178928652 --- /dev/null +++ b/openpype/hosts/nuke/plugins/create/create_write_still.py @@ -0,0 +1,142 @@ +from collections import OrderedDict +from openpype.hosts.nuke.api import ( + plugin, + lib) +import nuke + + +class CreateWriteStill(plugin.PypeCreator): + # change this to template preset + name = "WriteStillFrame" + label = "Create Write Still Image" + hosts = ["nuke"] + n_class = "Write" + family = "still" + icon = "image" + defaults = [ + "ImageFrame{:0>4}".format(nuke.frame()), + "MPFrame{:0>4}".format(nuke.frame()), + "LayoutFrame{:0>4}".format(nuke.frame()) + ] + + def __init__(self, *args, **kwargs): + super(CreateWriteStill, self).__init__(*args, **kwargs) + + data = OrderedDict() + + data["family"] = self.family + data["families"] = self.n_class + + for k, v in self.data.items(): + if k not in data.keys(): + data.update({k: v}) + + self.data = data + self.nodes = nuke.selectedNodes() + self.log.debug("_ self.data: '{}'".format(self.data)) + + def process(self): + + inputs = [] + outputs = [] + instance = nuke.toNode(self.data["subset"]) + selected_node = None + + # use selection + if (self.options or {}).get("useSelection"): + nodes = self.nodes + + if not (len(nodes) < 2): + msg = ("Select only one node. " + "The node you want to connect to, " + "or tick off `Use selection`") + self.log.error(msg) + nuke.message(msg) + return + + if len(nodes) == 0: + msg = ( + "No nodes selected. Please select a single node to connect" + " to or tick off `Use selection`" + ) + self.log.error(msg) + nuke.message(msg) + return + + selected_node = nodes[0] + inputs = [selected_node] + outputs = selected_node.dependent() + + if instance: + if (instance.name() in selected_node.name()): + selected_node = instance.dependencies()[0] + + # if node already exist + if instance: + # collect input / outputs + inputs = instance.dependencies() + outputs = instance.dependent() + selected_node = inputs[0] + # remove old one + nuke.delete(instance) + + # recreate new + write_data = { + "nodeclass": self.n_class, + "families": [self.family], + "avalon": self.data + } + + # add creator data + creator_data = {"creator": self.__class__.__name__} + self.data.update(creator_data) + write_data.update(creator_data) + + + self.log.info("Adding template path from plugin") + write_data.update({ + "fpath_template": ("{work}/renders/nuke/{subset}" + "/{subset}.{ext}")}) + + _prenodes = [ + { + "name": "FrameHold01", + "class": "FrameHold", + "knobs": [ + ("first_frame", nuke.frame()) + ], + "dependent": None + } + ] + + write_node = lib.create_write_node( + self.name, + write_data, + input=selected_node, + review=False, + prenodes=_prenodes, + farm=False, + linked_knobs=["channels", "___", "first", "last", "use_limit"]) + + # relinking to collected connections + for i, input in enumerate(inputs): + write_node.setInput(i, input) + + write_node.autoplace() + + for output in outputs: + output.setInput(0, write_node) + + # link frame hold to group node + write_node.begin() + for n in nuke.allNodes(): + # get write node + if n.Class() in "Write": + w_node = n + write_node.end() + + w_node["use_limit"].setValue(True) + w_node["first"].setValue(nuke.frame()) + w_node["last"].setValue(nuke.frame()) + + return write_node diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 8bc266f01b..afd1a173b6 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -12,8 +12,8 @@ from openpype.hosts.nuke.api.lib import ( class LoadImage(api.Loader): """Load still image into Nuke""" - families = ["render", "source", "plate", "review", "image"] - representations = ["exr", "dpx", "jpg", "jpeg", "png", "psd"] + families = ["render", "source", "plate", "review", "image", "still"] + representations = ["exr", "dpx", "jpg", "jpeg", "png", "psd", "tiff"] label = "Load Image" order = -10 diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index 49609f70e0..253fc5e6a3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -17,7 +17,7 @@ class NukeRenderLocal(openpype.api.Extractor): order = pyblish.api.ExtractorOrder label = "Render Local" hosts = ["nuke"] - families = ["render.local", "prerender.local"] + families = ["render.local", "prerender.local", "still.local"] def process(self, instance): families = instance.data["families"] @@ -66,13 +66,23 @@ class NukeRenderLocal(openpype.api.Extractor): instance.data["representations"] = [] collected_frames = os.listdir(out_dir) - repre = { - 'name': ext, - 'ext': ext, - 'frameStart': "%0{}d".format(len(str(last_frame))) % first_frame, - 'files': collected_frames, - "stagingDir": out_dir - } + + if len(collected_frames) == 1: + repre = { + 'name': ext, + 'ext': ext, + 'files': collected_frames.pop(), + "stagingDir": out_dir + } + else: + repre = { + 'name': ext, + 'ext': ext, + 'frameStart': "%0{}d".format( + len(str(last_frame))) % first_frame, + 'files': collected_frames, + "stagingDir": out_dir + } instance.data["representations"].append(repre) self.log.info("Extracted instance '{0}' to: {1}".format( @@ -89,6 +99,9 @@ class NukeRenderLocal(openpype.api.Extractor): instance.data['family'] = 'prerender' families.remove('prerender.local') families.insert(0, "prerender") + elif "still.local" in families: + instance.data['family'] = 'still' + families.remove('still.local') instance.data["families"] = families collections, remainder = clique.assemble(collected_frames) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index 47189c31fc..4d9bf26457 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -64,7 +64,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): ) if [fm for fm in _families_test - if fm in ["render", "prerender"]]: + if fm in ["render", "prerender", "still"]]: if "representations" not in instance.data: instance.data["representations"] = list() @@ -100,7 +100,10 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): frame_start_str, frame_slate_str) collected_frames.insert(0, slate_frame) - representation['files'] = collected_frames + if collected_frames_len == 1: + representation['files'] = collected_frames.pop() + else: + representation['files'] = collected_frames instance.data["representations"].append(representation) except Exception: instance.data["representations"].append(representation) diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index 0c88014649..29faf867d2 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -55,7 +55,7 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): """ Validates file output. """ order = pyblish.api.ValidatorOrder + 0.1 - families = ["render", "prerender"] + families = ["render", "prerender", "still"] label = "Validate rendered frame" hosts = ["nuke", "nukestudio"] @@ -71,6 +71,9 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): self.log.error(msg) raise ValidationException(msg) + if isinstance(repre["files"], str): + return + collections, remainder = clique.assemble(repre["files"]) self.log.info("collections: {}".format(str(collections))) self.log.info("remainder: {}".format(str(remainder))) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 3bff3ff79c..13815d5dd5 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -86,6 +86,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "source", "matchmove", "image", + "still", "source", "assembly", "fbx", diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index fcebc876f5..38313a3d84 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -124,9 +124,47 @@ "value": "True" } ] + }, + { + "plugins": [ + "CreateWriteStill" + ], + "nukeNodeClass": "Write", + "knobs": [ + { + "name": "file_type", + "value": "tiff" + }, + { + "name": "datatype", + "value": "16 bit" + }, + { + "name": "compression", + "value": "Deflate" + }, + { + "name": "tile_color", + "value": "0x23ff00ff" + }, + { + "name": "channels", + "value": "rgb" + }, + { + "name": "colorspace", + "value": "sRGB" + }, + { + "name": "create_directories", + "value": "True" + } + ] } ], - "customNodes": [] + "customNodes": [ + + ] }, "regexInputs": { "inputs": [ diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index ac35349415..0ea6c47027 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -119,7 +119,8 @@ "render", "prerender", "review", - "image" + "image", + "still" ], "representations": [ "exr", @@ -127,7 +128,8 @@ "jpg", "jpeg", "png", - "psd" + "psd", + "tiff" ], "node_name_template": "{class_name}_{ext}" }, From c643431aa87308c4db740e5df649490f811cfecd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Sep 2021 15:03:38 +0200 Subject: [PATCH 313/450] hound: suggestions --- openpype/hosts/nuke/plugins/create/create_write_still.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/plugins/create/create_write_still.py b/openpype/hosts/nuke/plugins/create/create_write_still.py index 1178928652..eebb5613c3 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_still.py +++ b/openpype/hosts/nuke/plugins/create/create_write_still.py @@ -17,7 +17,7 @@ class CreateWriteStill(plugin.PypeCreator): "ImageFrame{:0>4}".format(nuke.frame()), "MPFrame{:0>4}".format(nuke.frame()), "LayoutFrame{:0>4}".format(nuke.frame()) - ] + ] def __init__(self, *args, **kwargs): super(CreateWriteStill, self).__init__(*args, **kwargs) @@ -92,11 +92,10 @@ class CreateWriteStill(plugin.PypeCreator): self.data.update(creator_data) write_data.update(creator_data) - self.log.info("Adding template path from plugin") write_data.update({ - "fpath_template": ("{work}/renders/nuke/{subset}" - "/{subset}.{ext}")}) + "fpath_template": ( + "{work}/renders/nuke/{subset}/{subset}.{ext}")}) _prenodes = [ { From de69cd69c75596da40755d6cb3bc8f9d19ad1f3a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 23 Sep 2021 16:42:54 +0200 Subject: [PATCH 314/450] support `` token for directories --- .../maya/plugins/publish/collect_render.py | 21 +++++++++++++++++++ .../plugins/publish/submit_maya_deadline.py | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 46d1c9350d..1c9b6c95ef 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -205,12 +205,14 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # replace relative paths with absolute. Render products are # returned as list of dictionaries. + publish_meta_path = None for aov in exp_files: full_paths = [] for file in aov[aov.keys()[0]]: full_path = os.path.join(workspace, "renders", file) full_path = full_path.replace("\\", "/") full_paths.append(full_path) + publish_meta_path = os.path.dirname(full_path) aov_dict[aov.keys()[0]] = full_paths frame_start_render = int(self.get_render_attribute( @@ -236,6 +238,24 @@ class CollectMayaRender(pyblish.api.ContextPlugin): frame_end_handle = frame_end_render full_exp_files.append(aov_dict) + + # find common path to store metadata + # so if image prefix is branching to many directories + # metadata file will be located in top-most common + # directory. + # TODO: use `os.path.commonpath()` after switch to Python 3 + common_publish_meta_path = os.path.splitdrive( + publish_meta_path)[0] + if common_publish_meta_path: + common_publish_meta_path += os.path.sep + for part in publish_meta_path.split("/"): + common_publish_meta_path = os.path.join( + common_publish_meta_path, part) + if part == expected_layer_name: + break + common_publish_meta_path = common_publish_meta_path.replace("\\", "/") + self.log.info("Publish meta path: {}".format(common_publish_meta_path)) + self.log.info(full_exp_files) self.log.info("collecting layer: {}".format(layer_name)) # Get layer specific settings, might be overrides @@ -268,6 +288,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # which was submitted originally "source": filepath, "expectedFiles": full_exp_files, + "publishRenderMetadataFolder": common_publish_meta_path, "resolutionWidth": cmds.getAttr("defaultResolution.width"), "resolutionHeight": cmds.getAttr("defaultResolution.height"), "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py index 1ab3dc2554..5936e600ee 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py @@ -351,6 +351,12 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): f.replace(orig_scene, new_scene) ) instance.data["expectedFiles"] = [new_exp] + + if instance.data.get("publishRenderMetadataFolder"): + instance.data["publishRenderMetadataFolder"] = \ + instance.data["publishRenderMetadataFolder"].replace( + orig_scene, new_scene + ) self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) From ee7ebc11fac6f9e9c1ff9fab4a2d56385ae3e23c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 23 Sep 2021 17:41:45 +0200 Subject: [PATCH 315/450] =?UTF-8?q?fix=20hound=20=F0=9F=90=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/maya/api/lib_renderproducts.py | 32 +++++++++++-------- .../maya/plugins/publish/collect_render.py | 6 ++-- .../plugins/publish/submit_maya_deadline.py | 3 +- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 39d894a204..bdb1c619ff 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -482,8 +482,9 @@ class RenderProductsArnold(ARenderProducts): if not cameras: cameras = [ self.sanitize_camera_name( - self.get_renderable_cameras()[0]) - ] + self.get_renderable_cameras()[0] + ) + ] for ai_driver in ai_drivers: # todo: check aiAOVDriver.prefix as it could have @@ -552,11 +553,13 @@ class RenderProductsArnold(ARenderProducts): # Render Product per selected light group aov_light_group_name = "{}_{}".format(name, light_group) for camera in cameras: - product = RenderProduct(productName=aov_light_group_name, - aov=aov_name, - driver=ai_driver, - ext=ext, - camera=camera) + product = RenderProduct( + productName=aov_light_group_name, + aov=aov_name, + driver=ai_driver, + ext=ext, + camera=camera + ) products.append(product) return products @@ -608,7 +611,9 @@ class RenderProductsArnold(ARenderProducts): "" in self.layer_data.filePrefix.lower() ) if not has_renderpass_token: - return [setattr(bp, "multipart", True) for bp in beauty_products] + for product in beauty_products: + product.multipart = True + return beauty_products # AOVs are set to be rendered separately. We should expect # token in path. @@ -988,11 +993,12 @@ class RenderProductsRedshift(ARenderProducts): aov_light_group_name = "{}_{}".format(aov_name, light_group) for camera in cameras: - product = RenderProduct(productName=aov_light_group_name, - aov=aov_name, - ext=ext, - multipart=aov_multipart, - camera=camera) + product = RenderProduct( + productName=aov_light_group_name, + aov=aov_name, + ext=ext, + multipart=aov_multipart, + camera=camera) products.append(product) if light_groups: diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 1c9b6c95ef..575cc2456b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -253,8 +253,10 @@ class CollectMayaRender(pyblish.api.ContextPlugin): common_publish_meta_path, part) if part == expected_layer_name: break - common_publish_meta_path = common_publish_meta_path.replace("\\", "/") - self.log.info("Publish meta path: {}".format(common_publish_meta_path)) + common_publish_meta_path = common_publish_meta_path.replace( + "\\", "/") + self.log.info( + "Publish meta path: {}".format(common_publish_meta_path)) self.log.info(full_exp_files) self.log.info("collecting layer: {}".format(layer_name)) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py index 5936e600ee..2d43b0d085 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py @@ -355,8 +355,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): if instance.data.get("publishRenderMetadataFolder"): instance.data["publishRenderMetadataFolder"] = \ instance.data["publishRenderMetadataFolder"].replace( - orig_scene, new_scene - ) + orig_scene, new_scene) self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) From f907eb00fbc6d03772a9b7da8cefafb5b6565fab Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 23 Sep 2021 17:43:41 +0200 Subject: [PATCH 316/450] =?UTF-8?q?fix=20hound=20=F0=9F=90=95=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/maya/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index bdb1c619ff..b198052c93 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -482,7 +482,7 @@ class RenderProductsArnold(ARenderProducts): if not cameras: cameras = [ self.sanitize_camera_name( - self.get_renderable_cameras()[0] + self.get_renderable_cameras()[0] ) ] From 85ae0b4b8418137bc25129804e1d8576ea0abf05 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Sep 2021 20:05:26 +0200 Subject: [PATCH 317/450] Fix - better way to handle concurrend deactivate of projects Covers some weird edge cases --- openpype/modules/default_modules/sync_server/tray/app.py | 8 ++++++++ .../modules/default_modules/sync_server/tray/models.py | 4 ++++ .../modules/default_modules/sync_server/tray/widgets.py | 6 ------ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/app.py b/openpype/modules/default_modules/sync_server/tray/app.py index 0996cbc468..fdf8e61965 100644 --- a/openpype/modules/default_modules/sync_server/tray/app.py +++ b/openpype/modules/default_modules/sync_server/tray/app.py @@ -91,10 +91,18 @@ class SyncServerWindow(QtWidgets.QDialog): def _on_project_change(self): if self.projects.current_project is None: return + self.representationWidget.table_view.model().set_project( self.projects.current_project ) + project_name = self.projects.current_project + if not self.sync_server.get_sync_project_setting(project_name): + self.projects.message_generated.emit( + "Project {} not active anymore".format(project_name)) + self.projects.refresh() + return + def showEvent(self, event): self.representationWidget.model.set_project( self.projects.current_project) diff --git a/openpype/modules/default_modules/sync_server/tray/models.py b/openpype/modules/default_modules/sync_server/tray/models.py index 96d09b8786..63c9dfa16e 100644 --- a/openpype/modules/default_modules/sync_server/tray/models.py +++ b/openpype/modules/default_modules/sync_server/tray/models.py @@ -301,6 +301,10 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): """ self._project = project self.sync_server.set_sync_project_settings() + # project might have been deactivated in the meantime + if not self.sync_server.get_sync_project_setting(project): + return + self.active_site = self.sync_server.get_active_site(self.project) self.remote_site = self.sync_server.get_remote_site(self.project) self.refresh() diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index b0730c9c8d..5806179f61 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -72,12 +72,6 @@ class SyncProjectListWidget(QtWidgets.QWidget): def _on_index_change(self, new_idx, _old_idx): project_name = new_idx.data(QtCore.Qt.DisplayRole) - if not self.sync_server.get_sync_project_setting(project_name): - self.message_generated.emit( - "Project {} not active anymore".format(project_name)) - self.refresh() - return - self.current_project = project_name self.project_changed.emit() From da7dda6b6a8b7d8cd240292c73bb7eec641490e5 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 24 Sep 2021 02:35:59 +0800 Subject: [PATCH 318/450] read studio scene type config on extracting look --- .../plugins/inventory/import_modelrender.py | 4 ++-- .../maya/plugins/publish/extract_look.py | 21 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/inventory/import_modelrender.py b/openpype/hosts/maya/plugins/inventory/import_modelrender.py index 3675b757ea..e3cad4cf2e 100644 --- a/openpype/hosts/maya/plugins/inventory/import_modelrender.py +++ b/openpype/hosts/maya/plugins/inventory/import_modelrender.py @@ -7,7 +7,7 @@ class ImportModelRender(api.InventoryAction): icon = "industry" color = "#55DDAA" - scene_type = "meta.render.ma" + scene_type_regex = "meta.render.m[ab]" look_data_type = "meta.render.json" @staticmethod @@ -58,7 +58,7 @@ class ImportModelRender(api.InventoryAction): look_repr = io.find_one({ "type": "representation", "parent": version_id, - "name": self.scene_type, + "name": {"$regex": self.scene_type_regex}, }) if not look_repr: print("No model render sets for this model version..") diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 0a3a8d2e79..bbf25ebdc7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -122,7 +122,7 @@ def no_workspace_dir(): class ExtractLook(openpype.api.Extractor): - """Extract Look (Maya Ascii + JSON) + """Extract Look (Maya Scene + JSON) Only extracts the sets (shadingEngines and alike) alongside a .json file that stores it relationships for the sets and "attribute" data for the @@ -130,7 +130,7 @@ class ExtractLook(openpype.api.Extractor): """ - label = "Extract Look (Maya ASCII + JSON)" + label = "Extract Look (Maya Scene + JSON)" hosts = ["maya"] families = ["look"] order = pyblish.api.ExtractorOrder + 0.2 @@ -177,6 +177,8 @@ class ExtractLook(openpype.api.Extractor): # no preset found pass + return "mayaAscii" if self.scene_type == "ma" else "mayaBinary" + def process(self, instance): """Plugin entry point. @@ -184,6 +186,8 @@ class ExtractLook(openpype.api.Extractor): instance: Instance to process. """ + _scene_type = self.get_maya_scene_type(instance) + # Define extract output file path dir_path = self.staging_dir(instance) maya_fname = "{0}.{1}".format(instance.name, self.scene_type) @@ -197,7 +201,7 @@ class ExtractLook(openpype.api.Extractor): # Remove all members of the sets so they are not included in the # exported file by accident - self.log.info("Extract sets (Maya ASCII) ...") + self.log.info("Extract sets (%s) ..." % _scene_type) lookdata = instance.data["lookData"] relationships = lookdata["relationships"] sets = relationships.keys() @@ -224,7 +228,7 @@ class ExtractLook(openpype.api.Extractor): cmds.file( maya_path, force=True, - typ="mayaAscii", + typ=_scene_type, exportSelected=True, preserveReferences=False, channels=True, @@ -498,5 +502,12 @@ class ExtractModelRenderSets(ExtractLook): label = "Model Render Sets" hosts = ["maya"] families = ["model"] - scene_type = "meta.render.ma" + scene_type_prefix = "meta.render." look_data_type = "meta.render.json" + + def get_maya_scene_type(self, instance): + typ = super(ExtractModelRenderSets, self).get_maya_scene_type(instance) + # add prefix + self.scene_type = self.scene_type_prefix + self.scene_type + + return typ From 5b4991f14bd4f6a47cf207500894174ce60ee2fa Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 24 Sep 2021 02:58:29 +0800 Subject: [PATCH 319/450] add setdress root validator --- .../maya/plugins/create/create_setdress.py | 5 ++++ .../plugins/publish/validate_setdress_root.py | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/validate_setdress_root.py diff --git a/openpype/hosts/maya/plugins/create/create_setdress.py b/openpype/hosts/maya/plugins/create/create_setdress.py index 8274a6dd83..4246183fdb 100644 --- a/openpype/hosts/maya/plugins/create/create_setdress.py +++ b/openpype/hosts/maya/plugins/create/create_setdress.py @@ -9,3 +9,8 @@ class CreateSetDress(plugin.Creator): family = "setdress" icon = "cubes" defaults = ["Main", "Anim"] + + def __init__(self, *args, **kwargs): + super(CreateSetDress, self).__init__(*args, **kwargs) + + self.data["exactSetMembersOnly"] = True diff --git a/openpype/hosts/maya/plugins/publish/validate_setdress_root.py b/openpype/hosts/maya/plugins/publish/validate_setdress_root.py new file mode 100644 index 0000000000..0b4842d208 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_setdress_root.py @@ -0,0 +1,25 @@ + +import pyblish.api +import openpype.api + + +class ValidateSetdressRoot(pyblish.api.InstancePlugin): + """ + """ + + order = openpype.api.ValidateContentsOrder + label = "SetDress Root" + hosts = ["maya"] + families = ["setdress"] + + def process(self, instance): + from maya import cmds + + if instance.data.get("exactSetMembersOnly"): + return + + set_member = instance.data["setMembers"] + root = cmds.ls(set_member, assemblies=True, long=True) + + if not root or root[0] not in set_member: + raise Exception("Setdress top root node is not being published.") From 01e5145134490156698cc31b0294f1194f81fd59 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 24 Sep 2021 11:28:54 +0100 Subject: [PATCH 320/450] Use CRF for burnin when available - passing ffmpeg cmd from review to burnin. --- openpype/plugins/publish/extract_burnin.py | 3 ++- openpype/plugins/publish/extract_review.py | 3 ++- openpype/scripts/otio_burnin.py | 28 +++++++++++++++------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 625125321c..e386f97551 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -226,7 +226,8 @@ class ExtractBurnin(openpype.api.Extractor): "options": copy.deepcopy(burnin_options), "values": burnin_values, "full_input_path": temp_data["full_input_paths"][0], - "first_frame": temp_data["first_frame"] + "first_frame": temp_data["first_frame"], + "ffmpeg_cmd": new_repre.get("ffmpeg_cmd", "") } self.log.debug( diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index f5d6789dd4..a2d1b4dbfc 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -241,7 +241,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "outputName": output_name, "outputDef": output_def, "frameStartFtrack": temp_data["output_frame_start"], - "frameEndFtrack": temp_data["output_frame_end"] + "frameEndFtrack": temp_data["output_frame_end"], + "ffmpeg_cmd": subprcs_cmd }) # Force to pop these key if are in new repre diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index dc8d60cb37..5b48a4f3f4 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -69,7 +69,7 @@ def get_fps(str_value): return str(fps) -def _prores_codec_args(ffprobe_data): +def _prores_codec_args(ffprobe_data, ffmpeg_cmd): output = [] tags = ffprobe_data.get("tags") or {} @@ -108,13 +108,22 @@ def _prores_codec_args(ffprobe_data): return output -def _h264_codec_args(ffprobe_data): +def _h264_codec_args(ffprobe_data, ffmpeg_cmd): output = [] output.extend(["-codec:v", "h264"]) + args = ffmpeg_cmd.split(" ") + crf = "" + for count, arg in enumerate(args): + if arg == "-crf": + crf = args[count + 1] + break + if crf: + output.extend(["-crf", crf]) + bit_rate = ffprobe_data.get("bit_rate") - if bit_rate: + if bit_rate and not crf: output.extend(["-b:v", bit_rate]) pix_fmt = ffprobe_data.get("pix_fmt") @@ -127,15 +136,15 @@ def _h264_codec_args(ffprobe_data): return output -def get_codec_args(ffprobe_data): +def get_codec_args(ffprobe_data, ffmpeg_cmd): codec_name = ffprobe_data.get("codec_name") # Codec "prores" if codec_name == "prores": - return _prores_codec_args(ffprobe_data) + return _prores_codec_args(ffprobe_data, ffmpeg_cmd) # Codec "h264" if codec_name == "h264": - return _h264_codec_args(ffprobe_data) + return _h264_codec_args(ffprobe_data, ffmpeg_cmd) output = [] if codec_name: @@ -469,7 +478,7 @@ def example(input_path, output_path): def burnins_from_data( input_path, output_path, data, codec_data=None, options=None, burnin_values=None, overwrite=True, - full_input_path=None, first_frame=None + full_input_path=None, first_frame=None, ffmpeg_cmd=None ): """This method adds burnins to video/image file based on presets setting. @@ -647,7 +656,7 @@ def burnins_from_data( else: ffprobe_data = burnin._streams[0] - ffmpeg_args.extend(get_codec_args(ffprobe_data)) + ffmpeg_args.extend(get_codec_args(ffprobe_data, ffmpeg_cmd)) # Use group one (same as `-intra` argument, which is deprecated) ffmpeg_args_str = " ".join(ffmpeg_args) @@ -670,6 +679,7 @@ if __name__ == "__main__": options=in_data.get("options"), burnin_values=in_data.get("values"), full_input_path=in_data.get("full_input_path"), - first_frame=in_data.get("first_frame") + first_frame=in_data.get("first_frame"), + ffmpeg_cmd=in_data.get("ffmpeg_cmd") ) print("* Burnin script has finished") From 51b8a2227f3e00b36001ffd34a474cad6c83e542 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Sep 2021 18:22:43 +0200 Subject: [PATCH 321/450] Committed changes from SyncServer: Dropbox Provider #1979 Useful for future, PR wasnt merged yet --- .../providers/abstract_provider.py | 3 +- .../sync_server/providers/gdrive.py | 9 ++- .../schema_project_syncserver.json | 75 +++++++++++++++---- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py index 7fd25b9852..37e0fe9421 100644 --- a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py +++ b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py @@ -80,7 +80,8 @@ class AbstractProvider: representation (dict): complete repre containing 'file' site (str): site name Returns: - (string) file_id of created file, raises exception + (string) file_id of created/modified file , + throws FileExistsError, FileNotFoundError exceptions """ pass diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index f1ec0b6a0d..0aabd9fbcd 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -61,7 +61,6 @@ class GDriveHandler(AbstractProvider): CHUNK_SIZE = 2097152 # must be divisible by 256! used for upload chunks def __init__(self, project_name, site_name, tree=None, presets=None): - self.presets = None self.active = False self.project_name = project_name self.site_name = site_name @@ -74,7 +73,13 @@ class GDriveHandler(AbstractProvider): format(site_name)) return - cred_path = self.presets.get("credentials_url", {}).\ + provider_presets = self.presets.get(self.CODE) + if not provider_presets: + msg = "Sync Server: No provider presets for {}".format(self.CODE) + log.info(msg) + return + + cred_path = self.presets[self.CODE].get("credentials_url", {}).\ get(platform.system().lower()) or '' if not os.path.exists(cred_path): msg = "Sync Server: No credentials for gdrive provider " + \ diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json b/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json index cb2cc9c9d1..75574ed703 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json @@ -47,20 +47,67 @@ { "type": "dict", "children": [ - { - "type": "path", - "key": "credentials_url", - "label": "Credentials url", - "multiplatform": true - }, - { - "type": "dict-modifiable", - "key": "root", - "label": "Roots", - "collapsable": false, - "collapsable_key": false, - "object_type": "text" - } + { + "type": "dict", + "key": "gdrive", + "label": "Google Drive", + "collapsible": true, + "children": [ + { + "type": "path", + "key": "credentials_url", + "label": "Credentials url", + "multiplatform": true + } + ] + }, + { + "type": "dict", + "key": "sftp", + "label": "SFTP", + "collapsible": true, + "children": [ + { + "type": "text", + "key": "sftp_host", + "label": "SFTP host" + }, + { + "type": "number", + "key": "sftp_port", + "label": "SFTP port" + }, + { + "type": "text", + "key": "sftp_user", + "label": "SFTP user" + }, + { + "type": "text", + "key": "sftp_pass", + "label": "SFTP pass" + }, + { + "type": "path", + "key": "sftp_key", + "label": "SFTP user ssh key", + "multiplatform": true + }, + { + "type": "text", + "key": "sftp_key_pass", + "label": "SFTP user ssh key password" + } + ] + }, + { + "type": "dict-modifiable", + "key": "root", + "label": "Roots", + "collapsable": false, + "collapsable_key": false, + "object_type": "text" + } ] } } From 99f6d6617b01d882165bc99e764c4faa9a8cdefe Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Sep 2021 18:45:51 +0200 Subject: [PATCH 322/450] Implemented SFTP provider for Site Sync --- .../sync_server/providers/lib.py | 2 + .../sync_server/providers/resources/sftp.png | Bin 0 -> 2105 bytes .../sync_server/providers/sftp.py | 450 ++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 453 insertions(+) create mode 100644 openpype/modules/default_modules/sync_server/providers/resources/sftp.png create mode 100644 openpype/modules/default_modules/sync_server/providers/sftp.py diff --git a/openpype/modules/default_modules/sync_server/providers/lib.py b/openpype/modules/default_modules/sync_server/providers/lib.py index 463e49dd4d..c0dd7cee94 100644 --- a/openpype/modules/default_modules/sync_server/providers/lib.py +++ b/openpype/modules/default_modules/sync_server/providers/lib.py @@ -1,5 +1,6 @@ from .gdrive import GDriveHandler from .local_drive import LocalDriveHandler +from .sftp import SFTPHandler class ProviderFactory: @@ -112,3 +113,4 @@ factory = ProviderFactory() # trial and error factory.register_provider(GDriveHandler.CODE, GDriveHandler, 7) factory.register_provider(LocalDriveHandler.CODE, LocalDriveHandler, 50) +factory.register_provider(SFTPHandler.CODE, SFTPHandler, 20) diff --git a/openpype/modules/default_modules/sync_server/providers/resources/sftp.png b/openpype/modules/default_modules/sync_server/providers/resources/sftp.png new file mode 100644 index 0000000000000000000000000000000000000000..56c7a5cca3628fac9229cc73a65b65d27878c7c0 GIT binary patch literal 2105 zcmbVN3v3is6rB<+g;@Q5TZgN1jMKTYS1^^?NUrZ;wH26=DqXo zJ@?%E?pxd3R9`x}ax{ivrJ;sk1ij1cr=%GDpUe2Kqt__Cp~b{7N0t2)WqanNF|0Tu zM`KnjJf8=e;t(WFgpRDDBQ%E1nv>N9uoPOj2otjEC$4_6hrngYPb_kU>9Af4lX63c z0T*^OMM1|>;FE|sv+-G39vM)eCE!^lrJ8)!Pvq?KXlzeY1fGLfOZ`N^c8JHq&3LV5 zK-}%1NkG#a&Mkh?m=CF@C8z*~f_?(Y;*e#D7d@hjkvJ#maXD!cdYoR;D~c@1 zaV!vAlF!RQ=ivUJ2HNc8+xrJjf}{at<2F4`^aubTv1l*DkWg|mr08MeB;)bWUch*L z94xR|V94k&3aO!1ZB`Pp=#pG+QQ$lz&Crmfp_3)OZrVvYeJ<9;xq#CJ+;+j|YKU); zO_bgC{1I9RpU;m{GM+0NUI6xS@DsqU11J&sX?Yku?#pw2esrd2`)?*r|#hB7GfO zeYP~>EdMB%TwYch)9}cTs z6U<$K=lY^YFFw7udfbm?t-n3EVQhQb*mdqrEz4UUxIi42D|YX@s!pnW{MpLH$i}_N z%bn#@8?oQZW-V$Jj;<#+|JqkF=C45WisFj$l~-T?vu*6OsuE`-z4M`s6Ty|w`h72d z)WR(OrK-d_(;4aP+H1V~`Q$U?{9CR+icXi`a6Q)i=$5%Hh;!wkgH->`*Fe|C=`9m~ zdgkHks9%}(P6V<&jgmWJK><-NiN7rP;f8J$Z88-$$2p z&e+|rU5l?ieXxB>;t+SauD|pBW$s=%(9znvbME->_I>%rlyl#{R1^+edGYwRQ)>?# mu1Jljk7vt{O}wPud;vQW&wM&zUgmrIau3xt1-I9{}".format(source_path, target_path)) + conn = self._get_conn() + conn.put(source_path, target_path) + + def download_file(self, source_path, target_path, + server, collection, file, representation, site, + overwrite=False): + """ + Downloads single file from 'source_path' (remote) to 'target_path'. + It creates all folders on the local_path if are not existing. + By default existing file on 'target_path' will trigger an exception + + Args: + source_path (string): absolute path on provider + target_path (string): absolute path with or without name of a file + overwrite (boolean): replace existing file + + arguments for saving progress: + server (SyncServer): server instance to call update_db on + collection (str): name of collection + file (dict): info about uploaded file (matches structure from db) + representation (dict): complete repre containing 'file' + site (str): site name + + Returns: + (string) file_id of created/modified file , + throws FileExistsError, FileNotFoundError exceptions + """ + if not self.file_path_exists(source_path): + raise FileNotFoundError("Source file {} doesn't exist." + .format(source_path)) + + if os.path.isfile(target_path): + if not overwrite: + raise ValueError("File {} exists, set overwrite". + format(target_path)) + + thread = threading.Thread(target=self._download, + args=(source_path, target_path)) + thread.start() + self._mark_progress(collection, file, representation, server, + site, source_path, target_path, "download") + + return os.path.basename(target_path) + + def _download(self, source_path, target_path): + print("downloading {}->{}".format(source_path, target_path)) + conn = self._get_conn() + conn.get(source_path, target_path) + + def delete_file(self, path): + """ + Deletes file from 'path'. Expects path to specific file. + + Args: + path: absolute path to particular file + + Returns: + None + """ + if not self.file_path_exists(path): + raise FileNotFoundError("File {} to be deleted doesn't exist." + .format(path)) + self.conn.remove(path) + + def list_folder(self, folder_path): + """ + List all files and subfolders of particular path non-recursively. + + Args: + folder_path (string): absolut path on provider + Returns: + (list) + """ + return list(pysftp.path_advance(folder_path)) + + def folder_path_exists(self, file_path): + """ + Checks if path from 'file_path' exists. If so, return its + folder id. + Args: + file_path (string): path with / as a separator + Returns: + (string) folder id or False + """ + if not file_path: + return False + + return self.conn.isdir(file_path) + + def file_path_exists(self, file_path): + """ + Checks if 'file_path' exists on GDrive + + Args: + file_path (string): separated by '/', from root, with file name + Returns: + (dictionary|boolean) file metadata | False if not found + """ + if not file_path: + return False + + return self.conn.isfile(file_path) + + @classmethod + def get_presets(cls): + """ + Get presets for this provider + Returns: + (dictionary) of configured sites + """ + provider_presets = None + try: + provider_presets = ( + get_system_settings()["modules"] + ["sync_server"] + ["providers"] + ["sftp"] + ) + except KeyError: + log.info(("Sync Server: There are no presets for SFTP " + + "provider."). + format(str(provider_presets))) + return + return provider_presets + + def _get_conn(self): + """ + Returns fresh sftp connection. + + It seems that connection cannot be cached into self.conn, at least + for get and put which run in separate threads. + + Returns: + pysftp.Connection + """ + cnopts = pysftp.CnOpts() + cnopts.hostkeys = None + + conn_params = { + 'host': self.sftp_host, + 'port': self.sftp_port, + 'username': self.sftp_user, + 'cnopts': cnopts + } + if self.sftp_pass and self.sftp_pass.strip(): + conn_params['password'] = self.sftp_pass + if self.sftp_key: + conn_params['private_key'] = self.sftp_key + if self.sftp_key_pass: + conn_params['private_key_pass'] = self.sftp_key_pass + + return pysftp.Connection(**conn_params) + + 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. + """ + pass + if direction == "upload": + source_file_size = os.path.getsize(source_path) + else: + source_file_size = self.conn.stat(source_path).st_size + + 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 + ) + try: + if direction == "upload": + target_file_size = self.conn.stat(target_path).st_size + else: + target_file_size = os.path.getsize(target_path) + except FileNotFoundError: + pass + time.sleep(0.5) diff --git a/pyproject.toml b/pyproject.toml index e376986606..6ad090f30f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ jinxed = [ python3-xlib = { version="*", markers = "sys_platform == 'linux'"} enlighten = "^1.9.0" slack-sdk = "^3.6.0" +pysftp = "^0.2.9" [tool.poetry.dev-dependencies] flake8 = "^3.7" From 5452a0eb60e5b9ed31bb821f391bd14ab3e288bb Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 25 Sep 2021 03:38:56 +0000 Subject: [PATCH 323/450] [Automated] Bump version --- CHANGELOG.md | 48 ++++++++++++++++++++++++++++++++------------- openpype/version.py | 2 +- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2ae3c9eae..99c994f13d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,31 @@ # Changelog +## [3.5.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...HEAD) + +**🆕 New features** + +- Maya: Validate setdress top group [\#2068](https://github.com/pypeclub/OpenPype/pull/2068) +- Maya: Enable publishing render attrib sets \(e.g. V-Ray Displacement\) with model [\#1955](https://github.com/pypeclub/OpenPype/pull/1955) + +**🚀 Enhancements** + +- Project manager: Filter first item after selection of project [\#2069](https://github.com/pypeclub/OpenPype/pull/2069) +- Tools: add support for pyenv on windows [\#2051](https://github.com/pypeclub/OpenPype/pull/2051) + +**🐛 Bug fixes** + +- Fix Sync Queue when project disabled [\#2063](https://github.com/pypeclub/OpenPype/pull/2063) +- TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) + ## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...3.4.1) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.1-nightly.1...3.4.1) + +**🆕 New features** + +- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008) **🚀 Enhancements** @@ -13,11 +36,11 @@ - Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043) - Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042) - Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) +- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) - WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024) - Feature Maya import asset from scene inventory [\#2018](https://github.com/pypeclub/OpenPype/pull/2018) -- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008) **🐛 Bug fixes** @@ -35,6 +58,11 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0) +### 📖 Documentation + +- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) +- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) + **🆕 New features** - Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) @@ -42,7 +70,6 @@ **🚀 Enhancements** - Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) -- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) - Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) - Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) @@ -62,7 +89,6 @@ - Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959) - CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) - Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949) -- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) **🐛 Bug fixes** @@ -85,21 +111,18 @@ - Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) - Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961) -### 📖 Documentation - -- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) -- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) - ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) +**🚀 Enhancements** + +- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) + **🐛 Bug fixes** -- TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) - Maya: Menu actions fix [\#1945](https://github.com/pypeclub/OpenPype/pull/1945) - standalone: editorial shared object problem [\#1941](https://github.com/pypeclub/OpenPype/pull/1941) -- Bugfix nuke deadline app name [\#1928](https://github.com/pypeclub/OpenPype/pull/1928) ## [3.3.0](https://github.com/pypeclub/OpenPype/tree/3.3.0) (2021-08-17) @@ -112,15 +135,12 @@ **🚀 Enhancements** - Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940) -- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) -- Check for missing ✨ Python when using `pyenv` [\#1925](https://github.com/pypeclub/OpenPype/pull/1925) **🐛 Bug fixes** - Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) - Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) - Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929) -- Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 0e52014a56..f8ed9c7c2f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.1" +__version__ = "3.5.0-nightly.1" From 7f22301670c5e6d3070d2caab3ca733a6da8f6d4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 25 Sep 2021 11:43:08 +0100 Subject: [PATCH 324/450] Add required settings methods. --- .../sync_server/providers/dropbox.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/openpype/modules/default_modules/sync_server/providers/dropbox.py b/openpype/modules/default_modules/sync_server/providers/dropbox.py index 31459f1074..0d735a0b59 100644 --- a/openpype/modules/default_modules/sync_server/providers/dropbox.py +++ b/openpype/modules/default_modules/sync_server/providers/dropbox.py @@ -61,6 +61,63 @@ class DropboxHandler(AbstractProvider): super(AbstractProvider, self).__init__() + @classmethod + def get_system_settings_schema(cls): + """ + Returns dict for editable properties on system settings level + + + Returns: + (list) of dict + """ + return [] + + @classmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + # {platform} tells that value is multiplatform and only specific OS + # should be returned + return [ + { + "type": "text", + "key": "token", + "label": "Access Token" + }, + { + "type": "text", + "key": "team_folder_name", + "label": "Team Folder Name" + }, + { + "type": "text", + "key": "acting_as_member", + "label": "Acting As Member" + }, + # roots could be overriden only on Project level, User cannot + { + 'key': "roots", + 'label': "Roots", + 'type': 'dict' + } + ] + + @classmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level + + + Returns: + (dict) + """ + return [] + def _get_service(self, token, acting_as_member, team_folder_name): dbx = dropbox.DropboxTeam(token) From 8b11a574c4b504f269c88a347362e3b1e6afa299 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Sep 2021 09:30:51 +0100 Subject: [PATCH 325/450] Update get_task_time --- .../timers_manager/timers_manager.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 69f7c26fc2..829a6badb4 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -106,13 +106,14 @@ class TimersManager( self.timer_started(None, data) def get_task_time(self, project_name, asset_name, task_name): - time = {} - for module in self.modules: - time[module.name] = module.get_task_time( - project_name, asset_name, task_name - ) - - return time + times = {} + for module_id, connector in self._connectors_by_module_id.items(): + if hasattr(connector, "get_task_time"): + module = self._modules_by_id[module_id] + times[module.name] = connector.get_task_time( + project_name, asset_name, task_name + ) + return times def timer_started(self, source_id, data): for module in self.modules: From 9d6c2583ed7f822b90619a793fdb9c8e009d5c33 Mon Sep 17 00:00:00 2001 From: karimmozilla Date: Mon, 27 Sep 2021 11:36:29 +0200 Subject: [PATCH 326/450] Change mayaAscii family to mayaScene --- openpype/hosts/maya/plugins/create/create_mayaascii.py | 6 +++--- openpype/hosts/maya/plugins/load/load_reference.py | 4 ++-- openpype/hosts/maya/plugins/publish/collect_mayaascii.py | 4 ++-- .../hosts/maya/plugins/publish/extract_maya_scene_raw.py | 2 +- openpype/plugins/publish/collect_resources_path.py | 2 +- openpype/plugins/publish/integrate_new.py | 2 +- openpype/settings/defaults/project_settings/global.json | 2 +- openpype/settings/defaults/project_settings/maya.json | 2 +- .../schemas/projects_schema/schemas/schema_maya_load.json | 2 +- website/docs/pype2/admin_presets_plugins.md | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_mayaascii.py b/openpype/hosts/maya/plugins/create/create_mayaascii.py index f51e126c00..5ce634cec4 100644 --- a/openpype/hosts/maya/plugins/create/create_mayaascii.py +++ b/openpype/hosts/maya/plugins/create/create_mayaascii.py @@ -1,11 +1,11 @@ from openpype.hosts.maya.api import plugin -class CreateMayaAscii(plugin.Creator): +class CreateMayaScene(plugin.Creator): """Raw Maya Ascii file export""" - name = "mayaAscii" + name = "mayaScene" label = "Maya Ascii" - family = "mayaAscii" + family = "mayaScene" icon = "file-archive-o" defaults = ['Main'] diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index d5952ed267..544544a823 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -12,7 +12,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): families = ["model", "pointcache", "animation", - "mayaAscii", + "mayaScene", "setdress", "layout", "camera", @@ -71,7 +71,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): except: # noqa: E722 pass - if family not in ["layout", "setdress", "mayaAscii"]: + if family not in ["layout", "setdress", "mayaScene"]: for root in roots: root.setParent(world=True) diff --git a/openpype/hosts/maya/plugins/publish/collect_mayaascii.py b/openpype/hosts/maya/plugins/publish/collect_mayaascii.py index b02f61b7c6..199fb4197c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_mayaascii.py +++ b/openpype/hosts/maya/plugins/publish/collect_mayaascii.py @@ -3,14 +3,14 @@ from maya import cmds import pyblish.api -class CollectMayaAscii(pyblish.api.InstancePlugin): +class CollectMayaScene(pyblish.api.InstancePlugin): """Collect May Ascii Data """ order = pyblish.api.CollectorOrder + 0.2 label = 'Collect Model Data' - families = ["mayaAscii"] + families = ["mayaScene"] def process(self, instance): # Extract only current frame (override) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index 3c2b70900d..ccae7351dc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -16,7 +16,7 @@ class ExtractMayaSceneRaw(openpype.api.Extractor): label = "Maya Scene (Raw)" hosts = ["maya"] - families = ["mayaAscii", + families = ["mayaScene", "setdress", "layout", "camerarig", diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index 98b59332da..4f15a391c7 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -25,7 +25,7 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "camera", "animation", "model", - "mayaAscii", + "mayaScene", "setdress", "layout", "ass", diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index f9e9b43f08..1c3a8adb78 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -62,7 +62,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "camera", "animation", "model", - "mayaAscii", + "mayaScene", "setdress", "layout", "ass", diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index a53ae14914..1de2b172a2 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -19,7 +19,7 @@ "animation", "setdress", "layout", - "mayaAscii" + "mayaScene" ] }, "ExtractJpegEXR": { diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index f9911897d7..6bda996eef 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -473,7 +473,7 @@ 255, 255 ], - "mayaAscii": [ + "mayaScene": [ 67, 174, 255, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json index 0b09d08700..a21f59c8e5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json @@ -48,7 +48,7 @@ { "type": "color", "label": "Maya Scene:", - "key": "mayaAscii" + "key": "mayaScene" }, { "type": "color", diff --git a/website/docs/pype2/admin_presets_plugins.md b/website/docs/pype2/admin_presets_plugins.md index 797995d2b7..eb97a1262f 100644 --- a/website/docs/pype2/admin_presets_plugins.md +++ b/website/docs/pype2/admin_presets_plugins.md @@ -468,7 +468,7 @@ maya outliner colours for various families "ass": [1.0, 0.332, 0.312], "camera": [0.447, 0.312, 1.0], "fbx": [1.0, 0.931, 0.312], - "mayaAscii": [0.312, 1.0, 0.747], + "mayaScene": [0.312, 1.0, 0.747], "setdress": [0.312, 1.0, 0.747], "layout": [0.312, 1.0, 0.747], "vdbcache": [0.312, 1.0, 0.428], From 13945937d2172de77580f76669a8af3ecd979828 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 27 Sep 2021 13:25:32 +0200 Subject: [PATCH 327/450] Added pysftp library to poetry --- poetry.lock | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index e011b781c9..f3b26c2ba1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -144,6 +144,22 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pytz = ">=2015.7" +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "blessed" version = "1.18.0" @@ -704,6 +720,25 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" +[[package]] +name = "paramiko" +version = "2.7.2" +description = "SSH2 protocol library" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" + +[package.extras] +all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] + [[package]] name = "parso" version = "0.8.2" @@ -888,6 +923,22 @@ srv = ["dnspython (>=1.16.0,<1.17.0)"] tls = ["ipaddress"] zstd = ["zstandard"] +[[package]] +name = "pynacl" +version = "1.4.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +cffi = ">=1.4.1" +six = "*" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] + [[package]] name = "pynput" version = "1.7.3" @@ -977,6 +1028,17 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "pysftp" +version = "0.2.9" +description = "A friendly face on SFTP" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +paramiko = ">=1.17" + [[package]] name = "pytest" version = "6.2.4" @@ -1466,7 +1528,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "8875d530ae66f9763b5b0cb84d9d35edc184ef5c141b63d38bf1ff5a1226e556" +content-hash = "ff2bfa35a7304378917a0c25d7d7af9f81a130288d95789bdf7429f071e80b69" [metadata.files] acre = [] @@ -1553,6 +1615,15 @@ babel = [ {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] blessed = [ {file = "blessed-1.18.0-py2.py3-none-any.whl", hash = "sha256:5b5e2f0563d5a668c282f3f5946f7b1abb70c85829461900e607e74d7725106e"}, {file = "blessed-1.18.0.tar.gz", hash = "sha256:1312879f971330a1b7f2c6341f2ae7e2cbac244bfc9d0ecfbbecd4b0293bc755"}, @@ -1582,24 +1653,36 @@ cffi = [ {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55"}, {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc"}, {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76"}, {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7"}, {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, @@ -1935,6 +2018,10 @@ packaging = [ {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] +paramiko = [ + {file = "paramiko-2.7.2-py2.py3-none-any.whl", hash = "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898"}, + {file = "paramiko-2.7.2.tar.gz", hash = "sha256:7f36f4ba2c0d81d219f4595e35f70d56cc94f9ac40a6acdf51d6ca210ce65035"}, +] parso = [ {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, @@ -2006,9 +2093,13 @@ protobuf = [ {file = "protobuf-3.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87"}, {file = "protobuf-3.17.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1"}, {file = "protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d"}, + {file = "protobuf-3.17.3-cp38-cp38-win32.whl", hash = "sha256:59e5cf6b737c3a376932fbfb869043415f7c16a0cf176ab30a5bbc419cd709c1"}, + {file = "protobuf-3.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ebcb546f10069b56dc2e3da35e003a02076aaa377caf8530fe9789570984a8d2"}, {file = "protobuf-3.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde"}, {file = "protobuf-3.17.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539"}, {file = "protobuf-3.17.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47"}, + {file = "protobuf-3.17.3-cp39-cp39-win32.whl", hash = "sha256:a38bac25f51c93e4be4092c88b2568b9f407c27217d3dd23c7a57fa522a17554"}, + {file = "protobuf-3.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:85d6303e4adade2827e43c2b54114d9a6ea547b671cb63fafd5011dc47d0e13d"}, {file = "protobuf-3.17.3-py2.py3-none-any.whl", hash = "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e"}, {file = "protobuf-3.17.3.tar.gz", hash = "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b"}, ] @@ -2144,6 +2235,26 @@ pymongo = [ {file = "pymongo-3.11.4-py2.7-macosx-10.14-intel.egg", hash = "sha256:506a6dab4c7ffdcacdf0b8e70bd20eb2e77fa994519547c9d88d676400fcad58"}, {file = "pymongo-3.11.4.tar.gz", hash = "sha256:539d4cb1b16b57026999c53e5aab857fe706e70ae5310cc8c232479923f932e6"}, ] +pynacl = [ + {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, + {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, + {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, + {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, + {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, +] pynput = [ {file = "pynput-1.7.3-py2.py3-none-any.whl", hash = "sha256:fea5777454f896bd79d35393088cd29a089f3b2da166f0848a922b1d5a807d4f"}, {file = "pynput-1.7.3-py3.8.egg", hash = "sha256:6626e8ea9ca482bb5628a7169e1193824e382c4ad3053e40f4f24f41ee7b41c9"}, @@ -2212,6 +2323,9 @@ pyqt5-sip = [ pyrsistent = [ {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, ] +pysftp = [ + {file = "pysftp-0.2.9.tar.gz", hash = "sha256:fbf55a802e74d663673400acd92d5373c1c7ee94d765b428d9f977567ac4854a"}, +] pytest = [ {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, From 5d70ab2a0e9d4dd43c50c454fcdda449f3b5e346 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 13:42:52 +0200 Subject: [PATCH 328/450] store context task type to context data --- openpype/plugins/publish/collect_avalon_entities.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/plugins/publish/collect_avalon_entities.py b/openpype/plugins/publish/collect_avalon_entities.py index 0b6423818e..a6120d42fe 100644 --- a/openpype/plugins/publish/collect_avalon_entities.py +++ b/openpype/plugins/publish/collect_avalon_entities.py @@ -22,6 +22,7 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): io.install() project_name = api.Session["AVALON_PROJECT"] asset_name = api.Session["AVALON_ASSET"] + task_name = api.Session["AVALON_TASK"] project_entity = io.find_one({ "type": "project", @@ -48,6 +49,12 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): data = asset_entity['data'] + # Task type + asset_tasks = data.get("tasks") or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + context.data["taskType"] = task_type + frame_start = data.get("frameStart") if frame_start is None: frame_start = 1 From e397f441324083c435e777e72be90ad543d80f39 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 14:46:41 +0200 Subject: [PATCH 329/450] pop keys that were removed during environments merging --- openpype/lib/applications.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 245f2ee9a2..6676f6e79f 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1162,8 +1162,12 @@ def prepare_host_environments(data, implementation_envs=True): if final_env is None: final_env = loaded_env + keys_to_remove = set(data["env"].keys()) - set(final_env.keys()) + # Update env data["env"].update(final_env) + for key in keys_to_remove: + data["env"].pop(key, None) def apply_project_environments_value(project_name, env, project_settings=None): From 4c07c43a08aef252cacbfcdfa6e35b0606b1a921 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 14:46:56 +0200 Subject: [PATCH 330/450] pop QT_AUTO_SCREEN_SCALE_FACTOR environmet variable before nuke launch --- openpype/hosts/nuke/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/__init__.py b/openpype/hosts/nuke/__init__.py index f1e81617e0..366f704dd8 100644 --- a/openpype/hosts/nuke/__init__.py +++ b/openpype/hosts/nuke/__init__.py @@ -21,6 +21,7 @@ def add_implementation_envs(env, _app): new_nuke_paths.append(norm_path) env["NUKE_PATH"] = os.pathsep.join(new_nuke_paths) + env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) # Try to add QuickTime to PATH quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem" From 3f1f3f727df75e839409adecf856ba1f334fbfbb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Sep 2021 14:33:39 +0100 Subject: [PATCH 331/450] Add dropbox python module. --- poetry.lock | 67 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index e011b781c9..bc308d63e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -313,6 +313,19 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "dropbox" +version = "11.20.0" +description = "Official Dropbox API Client" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.16.2" +six = ">=1.12.0" +stone = ">=2" + [[package]] name = "enlighten" version = "1.10.1" @@ -749,6 +762,14 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +[[package]] +name = "ply" +version = "3.11" +description = "Python Lex & Yacc" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "prefixed" version = "0.3.2" @@ -1341,6 +1362,18 @@ sphinxcontrib-serializinghtml = "*" lint = ["flake8"] test = ["pytest", "sqlalchemy", "whoosh", "sphinx"] +[[package]] +name = "stone" +version = "3.2.1" +description = "Stone is an interface description language (IDL) for APIs." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +ply = ">=3.4" +six = ">=1.3.0" + [[package]] name = "termcolor" version = "1.1.0" @@ -1466,7 +1499,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "8875d530ae66f9763b5b0cb84d9d35edc184ef5c141b63d38bf1ff5a1226e556" +content-hash = "63ab0f15fa9d40931622f71ad6e8d5810e1f9b7ef5d27e1d9a8d00caad767c1d" [metadata.files] acre = [] @@ -1582,24 +1615,36 @@ cffi = [ {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55"}, {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc"}, {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76"}, {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7"}, {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, @@ -1692,8 +1737,10 @@ cryptography = [ {file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"}, {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"}, {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"}, + {file = "cryptography-3.4.7-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b01fd6f2737816cb1e08ed4807ae194404790eac7ad030b34f2ce72b332f5586"}, {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"}, {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, + {file = "cryptography-3.4.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:bf40af59ca2465b24e54f671b2de2c59257ddc4f7e5706dbd6930e26823668d3"}, {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, ] cx-freeze = [ @@ -1730,6 +1777,11 @@ docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] +dropbox = [ + {file = "dropbox-11.20.0-py2-none-any.whl", hash = "sha256:0926aab25445fe78b0284e0b86f4126ec4e5e2bf6cd2ac8562002008a21073b8"}, + {file = "dropbox-11.20.0-py3-none-any.whl", hash = "sha256:f2106aa566f9e3c175879c226c60b7089a39099b228061acbb7258670f6b859c"}, + {file = "dropbox-11.20.0.tar.gz", hash = "sha256:1aa351ec8bbb11cf3560e731b81d25f39c7edcb5fa92c06c5d68866cb9f90d54"}, +] enlighten = [ {file = "enlighten-1.10.1-py2.py3-none-any.whl", hash = "sha256:3d6c3eec8cf3eb626ee7b65eddc1b3e904d01f4547a2b9fe7f1da8892a0297e8"}, {file = "enlighten-1.10.1.tar.gz", hash = "sha256:3391916586364aedced5d6926482b48745e4948f822de096d32258ba238ea984"}, @@ -1983,6 +2035,10 @@ pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +ply = [ + {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, + {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, +] prefixed = [ {file = "prefixed-0.3.2-py2.py3-none-any.whl", hash = "sha256:5e107306462d63f2f03c529dbf11b0026fdfec621a9a008ca639d71de22995c3"}, {file = "prefixed-0.3.2.tar.gz", hash = "sha256:ca48277ba5fa8346dd4b760847da930c7b84416387c39e93affef086add2c029"}, @@ -2006,9 +2062,13 @@ protobuf = [ {file = "protobuf-3.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87"}, {file = "protobuf-3.17.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1"}, {file = "protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d"}, + {file = "protobuf-3.17.3-cp38-cp38-win32.whl", hash = "sha256:59e5cf6b737c3a376932fbfb869043415f7c16a0cf176ab30a5bbc419cd709c1"}, + {file = "protobuf-3.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ebcb546f10069b56dc2e3da35e003a02076aaa377caf8530fe9789570984a8d2"}, {file = "protobuf-3.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde"}, {file = "protobuf-3.17.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539"}, {file = "protobuf-3.17.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47"}, + {file = "protobuf-3.17.3-cp39-cp39-win32.whl", hash = "sha256:a38bac25f51c93e4be4092c88b2568b9f407c27217d3dd23c7a57fa522a17554"}, + {file = "protobuf-3.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:85d6303e4adade2827e43c2b54114d9a6ea547b671cb63fafd5011dc47d0e13d"}, {file = "protobuf-3.17.3-py2.py3-none-any.whl", hash = "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e"}, {file = "protobuf-3.17.3.tar.gz", hash = "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b"}, ] @@ -2339,6 +2399,11 @@ sphinxcontrib-websupport = [ {file = "sphinxcontrib-websupport-1.2.4.tar.gz", hash = "sha256:4edf0223a0685a7c485ae5a156b6f529ba1ee481a1417817935b20bde1956232"}, {file = "sphinxcontrib_websupport-1.2.4-py2.py3-none-any.whl", hash = "sha256:6fc9287dfc823fe9aa432463edd6cea47fa9ebbf488d7f289b322ffcfca075c7"}, ] +stone = [ + {file = "stone-3.2.1-py2-none-any.whl", hash = "sha256:2a50866528f60cc7cedd010def733e8ae9d581d17f967278a08059bffaea3c57"}, + {file = "stone-3.2.1-py3-none-any.whl", hash = "sha256:76235137c09ee88aa53e8c1e666819f6c20ac8064c4ac6c4ee4194eac0e3b7af"}, + {file = "stone-3.2.1.tar.gz", hash = "sha256:9bc78b40143b4ef33bf569e515408c2996ffebefbb1a897616ebe8aa6f2d7e75"}, +] termcolor = [ {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, ] diff --git a/pyproject.toml b/pyproject.toml index e376986606..baa897cc81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ jinxed = [ python3-xlib = { version="*", markers = "sys_platform == 'linux'"} enlighten = "^1.9.0" slack-sdk = "^3.6.0" +dropbox = "^11.20.0" [tool.poetry.dev-dependencies] flake8 = "^3.7" From f58cacb716c0968a3eaa2acea9258c81b7368fbf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 15:47:17 +0200 Subject: [PATCH 332/450] validate intent has settings and can work on more hosts --- openpype/plugins/publish/validate_intent.py | 53 +++++++++++++++---- .../defaults/project_settings/global.json | 4 ++ .../schemas/schema_global_publish.json | 53 +++++++++++++++++++ 3 files changed, 99 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/validate_intent.py b/openpype/plugins/publish/validate_intent.py index 80bcb0e164..23d57bb2b7 100644 --- a/openpype/plugins/publish/validate_intent.py +++ b/openpype/plugins/publish/validate_intent.py @@ -1,5 +1,7 @@ -import pyblish.api import os +import pyblish.api + +from openpype.lib import filter_profiles class ValidateIntent(pyblish.api.ContextPlugin): @@ -12,20 +14,49 @@ class ValidateIntent(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder label = "Validate Intent" - # TODO: this should be off by default and only activated viac config - tasks = ["animation"] - hosts = ["harmony"] - if os.environ.get("AVALON_TASK") not in tasks: - active = False + enabled = False + + # Can be modified by settings + profiles = [{ + "hosts": [], + "task_types": [], + "tasks": [], + "validate": False + }] def process(self, context): + # Skip if there are no profiles + validate = True + if self.profiles: + # Collect data from context + task_name = context.data.get("task") + task_type = context.data.get("taskType") + host_name = context.data.get("hostName") + + filter_data = { + "hosts": host_name, + "task_types": task_type, + "tasks": task_name + } + matching_profile = filter_profiles( + self.profiles, filter_data, logger=self.log + ) + if matching_profile: + validate = matching_profile["validate"] + + if not validate: + self.log.debug(( + "Validation of intent was skipped." + " Matching profile for current context disabled validation." + )) + return + msg = ( "Please make sure that you select the intent of this publish." ) - intent = context.data.get("intent") - self.log.debug(intent) - assert intent, msg - + intent = context.data.get("intent") or {} + self.log.debug(str(intent)) intent_value = intent.get("value") - assert intent is not "", msg + if not intent_value: + raise AssertionError(msg) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 8cc8d28e5f..0374ab9066 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -8,6 +8,10 @@ "enabled": true, "optional": false }, + "ValidateIntent": { + "enabled": false, + "profiles": [] + }, "IntegrateHeroVersion": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index e59d22aa89..8613743ef2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -44,6 +44,59 @@ } ] }, + { + "type": "dict", + "label": "Validate Intent", + "key": "ValidateIntent", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "Validate if Publishing intent was selected. It is possible to disable validation for specific publishing context with profiles." + }, + { + "type": "list", + "collapsible": true, + "key": "profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "hosts", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "validate", + "label": "Validate", + "type": "boolean" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From eeda206278a5cc4d0a627513798394ef326907cb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 18:07:26 +0200 Subject: [PATCH 333/450] ValidateInstanceInContext was split into 2 separated plugins with own settings in maya and nuke host --- .../publish/validate_instance_in_context.py | 57 +++------ .../publish/validate_instance_in_context.py | 110 ++++++++++++++++++ .../defaults/project_settings/maya.json | 5 + .../defaults/project_settings/nuke.json | 5 + .../schemas/schema_maya_publish.json | 10 ++ .../schemas/schema_nuke_publish.json | 10 ++ 6 files changed, 157 insertions(+), 40 deletions(-) rename openpype/{ => hosts/maya}/plugins/publish/validate_instance_in_context.py (65%) create mode 100644 openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py diff --git a/openpype/plugins/publish/validate_instance_in_context.py b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py similarity index 65% rename from openpype/plugins/publish/validate_instance_in_context.py rename to openpype/hosts/maya/plugins/publish/validate_instance_in_context.py index 61b4d82027..7b8c335062 100644 --- a/openpype/plugins/publish/validate_instance_in_context.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py @@ -5,6 +5,8 @@ from __future__ import absolute_import import pyblish.api import openpype.api +from maya import cmds + class SelectInvalidInstances(pyblish.api.Action): """Select invalid instances in Outliner.""" @@ -18,13 +20,12 @@ class SelectInvalidInstances(pyblish.api.Action): # Get the errored instances failed = [] for result in context.data["results"]: - if result["error"] is None: - continue - if result["instance"] is None: - continue - if result["instance"] in failed: - continue - if result["plugin"] != plugin: + if ( + result["error"] is None + or result["instance"] is None + or result["instance"] in failed + or result["plugin"] != plugin + ): continue failed.append(result["instance"]) @@ -44,25 +45,10 @@ class SelectInvalidInstances(pyblish.api.Action): self.deselect() def select(self, instances): - if "nuke" in pyblish.api.registered_hosts(): - import avalon.nuke.lib - import nuke - avalon.nuke.lib.select_nodes( - [nuke.toNode(str(x)) for x in instances] - ) - - if "maya" in pyblish.api.registered_hosts(): - from maya import cmds - cmds.select(instances, replace=True, noExpand=True) + cmds.select(instances, replace=True, noExpand=True) def deselect(self): - if "nuke" in pyblish.api.registered_hosts(): - import avalon.nuke.lib - avalon.nuke.lib.reset_selection() - - if "maya" in pyblish.api.registered_hosts(): - from maya import cmds - cmds.select(deselect=True) + cmds.select(deselect=True) class RepairSelectInvalidInstances(pyblish.api.Action): @@ -92,23 +78,14 @@ class RepairSelectInvalidInstances(pyblish.api.Action): context_asset = context.data["assetEntity"]["name"] for instance in instances: - if "nuke" in pyblish.api.registered_hosts(): - import openpype.hosts.nuke.api as nuke_api - origin_node = instance[0] - nuke_api.lib.recreate_instance( - origin_node, avalon_data={"asset": context_asset} - ) - else: - self.set_attribute(instance, context_asset) + self.set_attribute(instance, context_asset) def set_attribute(self, instance, context_asset): - if "maya" in pyblish.api.registered_hosts(): - from maya import cmds - cmds.setAttr( - instance.data.get("name") + ".asset", - context_asset, - type="string" - ) + cmds.setAttr( + instance.data.get("name") + ".asset", + context_asset, + type="string" + ) class ValidateInstanceInContext(pyblish.api.InstancePlugin): @@ -124,7 +101,7 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin): order = openpype.api.ValidateContentsOrder label = "Instance in same Context" optional = True - hosts = ["maya", "nuke"] + hosts = ["maya"] actions = [SelectInvalidInstances, RepairSelectInvalidInstances] def process(self, instance): diff --git a/openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py b/openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py new file mode 100644 index 0000000000..ddf46a0873 --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +"""Validate if instance asset is the same as context asset.""" +from __future__ import absolute_import + +import nuke + +import pyblish.api +import openpype.api +import avalon.nuke.lib +import openpype.hosts.nuke.api as nuke_api + + +class SelectInvalidInstances(pyblish.api.Action): + """Select invalid instances in Outliner.""" + + label = "Select Instances" + icon = "briefcase" + on = "failed" + + def process(self, context, plugin): + """Process invalid validators and select invalid instances.""" + # Get the errored instances + failed = [] + for result in context.data["results"]: + if ( + result["error"] is None + or result["instance"] is None + or result["instance"] in failed + or result["plugin"] != plugin + ): + continue + + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + if instances: + self.log.info( + "Selecting invalid nodes: %s" % ", ".join( + [str(x) for x in instances] + ) + ) + self.select(instances) + else: + self.log.info("No invalid nodes found.") + self.deselect() + + def select(self, instances): + avalon.nuke.lib.select_nodes( + [nuke.toNode(str(x)) for x in instances] + ) + + def deselect(self): + avalon.nuke.lib.reset_selection() + + +class RepairSelectInvalidInstances(pyblish.api.Action): + """Repair the instance asset.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + # Get the errored instances + failed = [] + for result in context.data["results"]: + if ( + result["error"] is None + or result["instance"] is None + or result["instance"] in failed + or result["plugin"] != plugin + ): + continue + + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + context_asset = context.data["assetEntity"]["name"] + for instance in instances: + origin_node = instance[0] + nuke_api.lib.recreate_instance( + origin_node, avalon_data={"asset": context_asset} + ) + + +class ValidateInstanceInContext(pyblish.api.InstancePlugin): + """Validator to check if instance asset match context asset. + + When working in per-shot style you always publish data in context of + current asset (shot). This validator checks if this is so. It is optional + so it can be disabled when needed. + + Action on this validator will select invalid instances in Outliner. + """ + + order = openpype.api.ValidateContentsOrder + label = "Instance in same Context" + hosts = ["nuke"] + actions = [SelectInvalidInstances, RepairSelectInvalidInstances] + optional = True + + def process(self, instance): + asset = instance.data.get("asset") + context_asset = instance.context.data["assetEntity"]["name"] + msg = "{} has asset {}".format(instance.name, asset) + assert asset == context_asset, msg diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 3540c3eb29..13d417581e 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -156,6 +156,11 @@ "CollectMayaRender": { "sync_workfile_version": false }, + "ValidateInstanceInContext": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateContainers": { "enabled": true, "optional": true, diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index ac35349415..9254e0c8f6 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -38,6 +38,11 @@ "render" ] }, + "ValidateInstanceInContext": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateContainers": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 89cd30aed0..b5b035b9e6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -28,6 +28,16 @@ "type": "label", "label": "Validators" }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateInstanceInContext", + "label": "Validate Instance In Context" + } + ] + }, { "type": "schema_template", "name": "template_publish_plugin", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index c73453f8aa..74b2592d29 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -50,6 +50,16 @@ "type": "label", "label": "Validators" }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateInstanceInContext", + "label": "Validate Instance In Context" + } + ] + }, { "type": "schema_template", "name": "template_publish_plugin", From 55dfb0817c0874d5a87ac9bd53c10ebeac108dc4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 18:07:40 +0200 Subject: [PATCH 334/450] start stop timer plugins use settings from context --- openpype/plugins/publish/start_timer.py | 3 +-- openpype/plugins/publish/stop_timer.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/start_timer.py b/openpype/plugins/publish/start_timer.py index 6312294bf1..112d92bef0 100644 --- a/openpype/plugins/publish/start_timer.py +++ b/openpype/plugins/publish/start_timer.py @@ -1,6 +1,5 @@ import pyblish.api -from openpype.api import get_system_settings from openpype.lib import change_timer_to_current_context @@ -10,6 +9,6 @@ class StartTimer(pyblish.api.ContextPlugin): hosts = ["*"] def process(self, context): - modules_settings = get_system_settings()["modules"] + modules_settings = context.data["system_settings"]["modules"] if modules_settings["timers_manager"]["disregard_publishing"]: change_timer_to_current_context() diff --git a/openpype/plugins/publish/stop_timer.py b/openpype/plugins/publish/stop_timer.py index 5c939b5f1b..414e43a3c4 100644 --- a/openpype/plugins/publish/stop_timer.py +++ b/openpype/plugins/publish/stop_timer.py @@ -3,8 +3,6 @@ import requests import pyblish.api -from openpype.api import get_system_settings - class StopTimer(pyblish.api.ContextPlugin): label = "Stop Timer" @@ -12,7 +10,7 @@ class StopTimer(pyblish.api.ContextPlugin): hosts = ["*"] def process(self, context): - modules_settings = get_system_settings()["modules"] + modules_settings = context.data["system_settings"]["modules"] if modules_settings["timers_manager"]["disregard_publishing"]: webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url) From 7e925f94f4089b4cbfd040c3eb2a614c1719db3c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 18:08:05 +0200 Subject: [PATCH 335/450] moved sceneinventory import to method where is used --- openpype/plugins/publish/validate_containers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/validate_containers.py b/openpype/plugins/publish/validate_containers.py index 52df493451..784221c3b6 100644 --- a/openpype/plugins/publish/validate_containers.py +++ b/openpype/plugins/publish/validate_containers.py @@ -1,7 +1,5 @@ import pyblish.api - import openpype.lib -from avalon.tools import cbsceneinventory class ShowInventory(pyblish.api.Action): @@ -11,7 +9,9 @@ class ShowInventory(pyblish.api.Action): on = "failed" def process(self, context, plugin): - cbsceneinventory.show() + from avalon.tools import sceneinventory + + sceneinventory.show() class ValidateContainers(pyblish.api.ContextPlugin): From d6ebdd9280abe8b13748548cb58cbb54e6f3988c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 18:20:16 +0200 Subject: [PATCH 336/450] added active to validate version plugin --- openpype/plugins/publish/validate_version.py | 3 +++ openpype/settings/defaults/project_settings/global.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/validate_version.py b/openpype/plugins/publish/validate_version.py index 6701041541..927e024476 100644 --- a/openpype/plugins/publish/validate_version.py +++ b/openpype/plugins/publish/validate_version.py @@ -12,6 +12,9 @@ class ValidateVersion(pyblish.api.InstancePlugin): label = "Validate Version" hosts = ["nuke", "maya", "blender", "standalonepublisher"] + optional = False + active = True + def process(self, instance): version = instance.data.get("version") latest_version = instance.data.get("latestVersion") diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 0374ab9066..d311d9611e 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -6,7 +6,8 @@ }, "ValidateVersion": { "enabled": true, - "optional": false + "optional": false, + "active": true }, "ValidateIntent": { "enabled": false, From 639be45a113d1c669fb86136ea683eab72691808 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 19:30:45 +0200 Subject: [PATCH 337/450] added basics of settings validations --- openpype/settings/__init__.py | 7 +++++-- openpype/tools/tray/pype_tray.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index 74f2684b2a..9d7598a948 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -25,7 +25,8 @@ from .lib import ( ) from .entities import ( SystemSettings, - ProjectSettings + ProjectSettings, + DefaultsNotDefined ) @@ -51,6 +52,8 @@ __all__ = ( "get_anatomy_settings", "get_environments", "get_local_settings", + "SystemSettings", - "ProjectSettings" + "ProjectSettings", + "DefaultsNotDefined" ) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 35b254513f..61a938941c 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -17,6 +17,11 @@ from openpype.api import ( from openpype.lib import get_pype_execute_args from openpype.modules import TrayModulesManager from openpype import style +from openpype.settings import ( + SystemSettings, + ProjectSettings, + DefaultsNotDefined +) from .pype_info_widget import PypeInfoWidget @@ -114,6 +119,18 @@ class TrayManager: self.main_thread_timer = main_thread_timer + def _validate_settings_defaults(self): + valid = True + try: + SystemSettings() + ProjectSettings() + + except DefaultsNotDefined: + valid = False + + if valid: + return + def show_tray_message(self, title, message, icon=None, msecs=None): """Show tray message. From 9888f2750bdce56360924f32df8b3a524d85e638 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 19:31:05 +0200 Subject: [PATCH 338/450] run startup validations after tray manager initialization --- openpype/tools/tray/pype_tray.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 61a938941c..755154095d 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -119,6 +119,13 @@ class TrayManager: self.main_thread_timer = main_thread_timer + + self.execute_in_main_thread(self._startup_validations) + + def _startup_validations(self): + """Run possible startup validations.""" + self._validate_settings_defaults() + def _validate_settings_defaults(self): valid = True try: From c25f1307a7f05b982c9f44b1ecbc3720e7d98999 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 19:31:39 +0200 Subject: [PATCH 339/450] show messageboxwhen settings are not valid --- openpype/tools/tray/pype_tray.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 755154095d..279d71980f 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -119,6 +119,8 @@ class TrayManager: self.main_thread_timer = main_thread_timer + # For storing missing settings dialog + self._settings_validation_dialog = None self.execute_in_main_thread(self._startup_validations) @@ -138,6 +140,32 @@ class TrayManager: if valid: return + title = "Settings miss default values" + msg = ( + "Your OpenPype may not work as expected because have missing" + " default settings values. Please contact OpenPype team." + ) + msg_box = QtWidgets.QMessageBox( + QtWidgets.QMessageBox.Warning, + title, + msg, + QtWidgets.QMessageBox.Ok, + flags=QtCore.Qt.Dialog + ) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + msg_box.setWindowIcon(icon) + msg_box.setStyleSheet(style.load_stylesheet()) + msg_box.buttonClicked.connect(self._post_validate_settings_defaults) + + self._settings_validation_dialog = msg_box + + msg_box.show() + + def _post_validate_settings_defaults(self): + widget = self._settings_validation_dialog + self._settings_validation_dialog = None + widget.deleteLater() + def show_tray_message(self, title, message, icon=None, msecs=None): """Show tray message. From ae16ae25926d2f11e53ec5e0f4162c81f8bf1020 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Sep 2021 19:32:34 +0200 Subject: [PATCH 340/450] show menu at position where first click happened --- openpype/tools/tray/pype_tray.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 35b254513f..a7f055732c 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -236,6 +236,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): self._click_timer = click_timer self._doubleclick = False + self._click_pos = None def _click_timer_timeout(self): self._click_timer.stop() @@ -248,13 +249,17 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): self._show_context_menu() def _show_context_menu(self): - pos = QtGui.QCursor().pos() + pos = self._click_pos + self._click_pos = None + if pos is None: + pos = QtGui.QCursor().pos() self.contextMenu().popup(pos) def on_systray_activated(self, reason): # show contextMenu if left click if reason == QtWidgets.QSystemTrayIcon.Trigger: if self.tray_man.doubleclick_callback: + self._click_pos = QtGui.QCursor().pos() self._click_timer.start() else: self._show_context_menu() From 8b60c511cc7d187c6b8d8d76243db0ddaa2b4eff Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 28 Sep 2021 10:23:13 +0100 Subject: [PATCH 341/450] Fixed curves with modifiers in rigs --- .../hosts/blender/plugins/load/load_rig.py | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index 5573c081e1..c385dc237e 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -66,12 +66,16 @@ class BlendRigLoader(plugin.AssetLoader): objects = [] nodes = list(container.children) - for obj in nodes: - obj.parent = asset_group + allowed_types = ['ARMATURE', 'MESH'] for obj in nodes: - objects.append(obj) - nodes.extend(list(obj.children)) + if obj.type in allowed_types: + obj.parent = asset_group + + for obj in nodes: + if obj.type in allowed_types: + objects.append(obj) + nodes.extend(list(obj.children)) objects.reverse() @@ -126,7 +130,30 @@ class BlendRigLoader(plugin.AssetLoader): objects.reverse() - bpy.data.orphans_purge(do_local_ids=False) + curves = [obj for obj in data_to.objects if obj.type == 'CURVE'] + + for curve in curves: + local_obj = plugin.prepare_data(curve, group_name) + plugin.prepare_data(local_obj.data, group_name) + + local_obj.use_fake_user = True + + for mod in local_obj.modifiers: + mod_target_name = mod.object.name + mod.object = bpy.data.objects.get( + f"{group_name}:{mod_target_name}") + + if not local_obj.get(AVALON_PROPERTY): + local_obj[AVALON_PROPERTY] = dict() + + avalon_info = local_obj[AVALON_PROPERTY] + avalon_info.update({"container_name": group_name}) + + local_obj.parent = asset_group + objects.append(local_obj) + + while bpy.data.orphans_purge(do_local_ids=False): + pass bpy.ops.object.select_all(action='DESELECT') From 53c9c9c0eb8d60166e803ec883da5e45460f4175 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 11:42:32 +0200 Subject: [PATCH 342/450] return default deadline url if instance does not containe deadlineServers --- .../collect_deadline_server_from_instance.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/collect_deadline_server_from_instance.py b/openpype/modules/default_modules/deadline/plugins/publish/collect_deadline_server_from_instance.py index 784616615d..2e512add57 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/collect_deadline_server_from_instance.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/collect_deadline_server_from_instance.py @@ -46,24 +46,25 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): ["deadline"] ) - try: - default_servers = deadline_settings["deadline_urls"] - project_servers = ( - render_instance.context.data - ["project_settings"] - ["deadline"] - ["deadline_servers"] - ) - deadline_servers = { - k: default_servers[k] - for k in project_servers - if k in default_servers - } - - except AttributeError: - # Handle situation were we had only one url for deadline. - return render_instance.context.data["defaultDeadline"] + default_server = render_instance.context.data["defaultDeadline"] + instance_server = render_instance.data.get("deadlineServers") + if not instance_server: + return default_server + default_servers = deadline_settings["deadline_urls"] + project_servers = ( + render_instance.context.data + ["project_settings"] + ["deadline"] + ["deadline_servers"] + ) + deadline_servers = { + k: default_servers[k] + for k in project_servers + if k in default_servers + } + # This is Maya specific and may not reflect real selection of deadline + # url as dictionary keys in Python 2 are not ordered return deadline_servers[ list(deadline_servers.keys())[ int(render_instance.data.get("deadlineServers")) From d9cbbbd3681775a346204fb387c3f43757e34cad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:39:19 +0200 Subject: [PATCH 343/450] created idle threads in timers manager which are Qt based timers and threads --- .../timers_manager/idle_threads.py | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 openpype/modules/default_modules/timers_manager/idle_threads.py diff --git a/openpype/modules/default_modules/timers_manager/idle_threads.py b/openpype/modules/default_modules/timers_manager/idle_threads.py new file mode 100644 index 0000000000..73978ed7e8 --- /dev/null +++ b/openpype/modules/default_modules/timers_manager/idle_threads.py @@ -0,0 +1,158 @@ +import time +from Qt import QtCore +from pynput import mouse, keyboard + +from openpype.lib import PypeLogger + + +class IdleItem: + """Python object holds information if state of idle changed. + + This item is used to be independent from Qt objects. + """ + def __init__(self): + self.changed = False + + def reset(self): + self.changed = False + + def set_changed(self, changed=True): + self.changed = changed + + +class IdleManager(QtCore.QThread): + """ Measure user's idle time in seconds. + Idle time resets on keyboard/mouse input. + Is able to emit signals at specific time idle. + """ + time_signals = {} + idle_time = 0 + signal_reset_timer = QtCore.Signal() + + def __init__(self): + super(IdleManager, self).__init__() + self.log = PypeLogger.get_logger(self.__class__.__name__) + self.signal_reset_timer.connect(self._reset_time) + + self.idle_item = IdleItem() + + self._is_running = False + self._mouse_thread = None + self._keyboard_thread = None + + def add_time_signal(self, emit_time, signal): + """ If any module want to use IdleManager, need to use add_time_signal + + Args: + emit_time(int): Time when signal will be emitted. + signal(QtCore.Signal): Signal that will be emitted + (without objects). + """ + if emit_time not in self.time_signals: + self.time_signals[emit_time] = [] + self.time_signals[emit_time].append(signal) + + @property + def is_running(self): + return self._is_running + + def _reset_time(self): + self.idle_time = 0 + + def stop(self): + self._is_running = False + + def _on_mouse_destroy(self): + self._mouse_thread = None + + def _on_keyboard_destroy(self): + self._keyboard_thread = None + + def run(self): + self.log.info('IdleManager has started') + self._is_running = True + + thread_mouse = MouseThread(self.idle_item) + thread_keyboard = KeyboardThread(self.idle_item) + + thread_mouse.destroyed.connect(self._on_mouse_destroy) + thread_keyboard.destroyed.connect(self._on_keyboard_destroy) + + self._mouse_thread = thread_mouse + self._keyboard_thread = thread_keyboard + + thread_mouse.start() + thread_keyboard.start() + + # Main loop here is each second checked if idle item changed state + while self._is_running: + if self.idle_item.changed: + self.idle_item.reset() + self.signal_reset_timer.emit() + else: + self.idle_time += 1 + + if self.idle_time in self.time_signals: + for signal in self.time_signals[self.idle_time]: + signal.emit() + time.sleep(1) + + # Stop threads if still exist + if self._mouse_thread is not None: + self._mouse_thread.signal_stop.emit() + self._mouse_thread.terminate() + self._mouse_thread.wait() + + if self._keyboard_thread is not None: + self._keyboard_thread.signal_stop.emit() + self._keyboard_thread.terminate() + self._keyboard_thread.wait() + + self.log.info('IdleManager has stopped') + + +class MouseThread(QtCore.QThread): + """Listens user's mouse movement.""" + signal_stop = QtCore.Signal() + + def __init__(self, idle_item): + super(MouseThread, self).__init__() + self.signal_stop.connect(self.stop) + self.m_listener = None + self.idle_item = idle_item + + def stop(self): + if self.m_listener is not None: + self.m_listener.stop() + + def on_move(self, *args, **kwargs): + self.idle_item.set_changed() + + def run(self): + self.m_listener = mouse.Listener(on_move=self.on_move) + self.m_listener.start() + + +class KeyboardThread(QtCore.QThread): + """Listens user's keyboard input + """ + signal_stop = QtCore.Signal() + + def __init__(self, idle_item): + super(KeyboardThread, self).__init__() + self.signal_stop.connect(self.stop) + self.k_listener = None + self.idle_item = idle_item + + def stop(self): + if self.k_listener is not None: + listener = self.k_listener + self.k_listener = None + listener.stop() + + def on_press(self, *args, **kwargs): + self.idle_item.set_changed() + + def run(self): + self.k_listener = keyboard.Listener(on_press=self.on_press) + self.k_listener.start() From 84d21852c3c6782d9ae264db983423f752f4d959 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:45:02 +0200 Subject: [PATCH 344/450] modified UI initialization --- .../timers_manager/widget_user_idle.py | 151 ++++++++---------- 1 file changed, 64 insertions(+), 87 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index cefa6bb4fb..e130c8ea45 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -3,118 +3,95 @@ from openpype import resources, style class WidgetUserIdle(QtWidgets.QWidget): - SIZE_W = 300 SIZE_H = 160 def __init__(self, module): - super(WidgetUserIdle, self).__init__() self.bool_is_showed = False self.bool_not_stopped = True self.module = module + self.setWindowTitle("OpenPype - Stop timers") icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) + self.setWindowFlags( QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint ) - self._translate = QtCore.QCoreApplication.translate + msg_info = "You didn't work for a long time." + msg_question = "Would you like to stop Timers?" + msg_stopped = ( + "Your Timers were stopped. Do you want to start them again?" + ) - self.font = QtGui.QFont() - self.font.setFamily("DejaVu Sans Condensed") - self.font.setPointSize(9) - self.font.setBold(True) - self.font.setWeight(50) - self.font.setKerning(True) + lbl_info = QtWidgets.QLabel(msg_info, self) + lbl_info.setTextFormat(QtCore.Qt.RichText) + lbl_info.setWordWrap(True) + + lbl_question = QtWidgets.QLabel(msg_question, self) + lbl_question.setTextFormat(QtCore.Qt.RichText) + lbl_question.setWordWrap(True) + + lbl_stopped = QtWidgets.QLabel(msg_stopped, self) + lbl_stopped.setTextFormat(QtCore.Qt.RichText) + lbl_stopped.setWordWrap(True) + + lbl_rest_time = QtWidgets.QLabel(self) + lbl_rest_time.setTextFormat(QtCore.Qt.RichText) + lbl_rest_time.setWordWrap(True) + lbl_rest_time.setAlignment(QtCore.Qt.AlignCenter) + + form = QtWidgets.QFormLayout() + form.setContentsMargins(10, 15, 10, 5) + + form.addRow(lbl_info) + form.addRow(lbl_question) + form.addRow(lbl_stopped) + form.addRow(lbl_rest_time) + + btn_stop = QtWidgets.QPushButton("Stop timer", self) + btn_stop.setToolTip("Stop's All timers") + + btn_continue = QtWidgets.QPushButton("Continue", self) + btn_continue.setToolTip("Timer won't stop") + + btn_close = QtWidgets.QPushButton("Close", self) + btn_close.setToolTip("Close window") + + btn_restart = QtWidgets.QPushButton("Start timers", self) + btn_restart.setToolTip("Timer will be started again") + + group_layout = QtWidgets.QHBoxLayout() + group_layout.addStretch(1) + group_layout.addWidget(btn_continue) + group_layout.addWidget(btn_stop) + group_layout.addWidget(btn_restart) + group_layout.addWidget(btn_close) + + layout = QtWidgets.QVBoxLayout(self) + layout.addLayout(form) + layout.addLayout(group_layout) + + self.lbl_info = lbl_info + self.lbl_question = lbl_question + self.lbl_stopped = lbl_stopped + self.lbl_rest_time = lbl_rest_time + + self.btn_stop = btn_stop + self.btn_continue = btn_continue + self.btn_close = btn_close + self.btn_restart = btn_restart self.resize(self.SIZE_W, self.SIZE_H) self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) self.setStyleSheet(style.load_stylesheet()) - self.setLayout(self._main()) - self.refresh_context() - self.setWindowTitle('Pype - Stop timers') - - def _main(self): - self.main = QtWidgets.QVBoxLayout() - self.main.setObjectName('main') - - self.form = QtWidgets.QFormLayout() - self.form.setContentsMargins(10, 15, 10, 5) - self.form.setObjectName('form') - - msg_info = 'You didn\'t work for a long time.' - msg_question = 'Would you like to stop Timers?' - msg_stopped = ( - 'Your Timers were stopped. Do you want to start them again?' - ) - - self.lbl_info = QtWidgets.QLabel(msg_info) - self.lbl_info.setFont(self.font) - self.lbl_info.setTextFormat(QtCore.Qt.RichText) - self.lbl_info.setObjectName("lbl_info") - self.lbl_info.setWordWrap(True) - - self.lbl_question = QtWidgets.QLabel(msg_question) - self.lbl_question.setFont(self.font) - self.lbl_question.setTextFormat(QtCore.Qt.RichText) - self.lbl_question.setObjectName("lbl_question") - self.lbl_question.setWordWrap(True) - - self.lbl_stopped = QtWidgets.QLabel(msg_stopped) - self.lbl_stopped.setFont(self.font) - self.lbl_stopped.setTextFormat(QtCore.Qt.RichText) - self.lbl_stopped.setObjectName("lbl_stopped") - self.lbl_stopped.setWordWrap(True) - - self.lbl_rest_time = QtWidgets.QLabel("") - self.lbl_rest_time.setFont(self.font) - self.lbl_rest_time.setTextFormat(QtCore.Qt.RichText) - self.lbl_rest_time.setObjectName("lbl_rest_time") - self.lbl_rest_time.setWordWrap(True) - self.lbl_rest_time.setAlignment(QtCore.Qt.AlignCenter) - - self.form.addRow(self.lbl_info) - self.form.addRow(self.lbl_question) - self.form.addRow(self.lbl_stopped) - self.form.addRow(self.lbl_rest_time) - - self.group_btn = QtWidgets.QHBoxLayout() - self.group_btn.addStretch(1) - self.group_btn.setObjectName("group_btn") - - self.btn_stop = QtWidgets.QPushButton("Stop timer") - self.btn_stop.setToolTip('Stop\'s All timers') - self.btn_stop.clicked.connect(self.stop_timer) - - self.btn_continue = QtWidgets.QPushButton("Continue") - self.btn_continue.setToolTip('Timer won\'t stop') - self.btn_continue.clicked.connect(self.continue_timer) - - self.btn_close = QtWidgets.QPushButton("Close") - self.btn_close.setToolTip('Close window') - self.btn_close.clicked.connect(self.close_widget) - - self.btn_restart = QtWidgets.QPushButton("Start timers") - self.btn_restart.setToolTip('Timer will be started again') - self.btn_restart.clicked.connect(self.restart_timer) - - self.group_btn.addWidget(self.btn_continue) - self.group_btn.addWidget(self.btn_stop) - self.group_btn.addWidget(self.btn_restart) - self.group_btn.addWidget(self.btn_close) - - self.main.addLayout(self.form) - self.main.addLayout(self.group_btn) - - return self.main - def refresh_context(self): self.lbl_question.setVisible(self.bool_not_stopped) self.lbl_rest_time.setVisible(self.bool_not_stopped) From 15bb991c060c9912c2e5c008ae6ea4065af3cf83 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:46:08 +0200 Subject: [PATCH 345/450] refresh_context is private method --- .../timers_manager/widget_user_idle.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index e130c8ea45..ce2e5f406e 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -92,16 +92,6 @@ class WidgetUserIdle(QtWidgets.QWidget): self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) self.setStyleSheet(style.load_stylesheet()) - def refresh_context(self): - self.lbl_question.setVisible(self.bool_not_stopped) - self.lbl_rest_time.setVisible(self.bool_not_stopped) - self.lbl_stopped.setVisible(not self.bool_not_stopped) - - self.btn_continue.setVisible(self.bool_not_stopped) - self.btn_stop.setVisible(self.bool_not_stopped) - self.btn_restart.setVisible(not self.bool_not_stopped) - self.btn_close.setVisible(not self.bool_not_stopped) - def change_count_widget(self, time): str_time = str(time) self.lbl_rest_time.setText(str_time) @@ -127,12 +117,22 @@ class WidgetUserIdle(QtWidgets.QWidget): def close_widget(self): self.bool_is_showed = False self.bool_not_stopped = True - self.refresh_context() + self._refresh_context() self.hide() def showEvent(self, event): self.bool_is_showed = True + def _refresh_context(self): + self.lbl_question.setVisible(not self._timer_stopped) + self.lbl_rest_time.setVisible(not self._timer_stopped) + self.lbl_stopped.setVisible(self._timer_stopped) + + self.btn_continue.setVisible(not self._timer_stopped) + self.btn_stop.setVisible(not self._timer_stopped) + self.btn_restart.setVisible(self._timer_stopped) + self.btn_close.setVisible(self._timer_stopped) + class SignalHandler(QtCore.QObject): signal_show_message = QtCore.Signal() From 81d5953e58e2977db9162788eccd6e24c72f6ed1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:46:58 +0200 Subject: [PATCH 346/450] removed unused methods --- .../timers_manager/widget_user_idle.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index ce2e5f406e..a62f3c293c 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -92,21 +92,6 @@ class WidgetUserIdle(QtWidgets.QWidget): self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) self.setStyleSheet(style.load_stylesheet()) - def change_count_widget(self, time): - str_time = str(time) - self.lbl_rest_time.setText(str_time) - - def stop_timer(self): - self.module.stop_timers() - self.close_widget() - - def restart_timer(self): - self.module.restart_timers() - self.close_widget() - - def continue_timer(self): - self.close_widget() - def closeEvent(self, event): event.ignore() if self.bool_not_stopped is True: From dec46b3475593ac455c8312c5a77935ad0ea1d6b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:49:27 +0200 Subject: [PATCH 347/450] close_widget is private method --- .../timers_manager/widget_user_idle.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index a62f3c293c..a0c79d65fe 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -92,19 +92,6 @@ class WidgetUserIdle(QtWidgets.QWidget): self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) self.setStyleSheet(style.load_stylesheet()) - def closeEvent(self, event): - event.ignore() - if self.bool_not_stopped is True: - self.continue_timer() - else: - self.close_widget() - - def close_widget(self): - self.bool_is_showed = False - self.bool_not_stopped = True - self._refresh_context() - self.hide() - def showEvent(self, event): self.bool_is_showed = True @@ -118,6 +105,19 @@ class WidgetUserIdle(QtWidgets.QWidget): self.btn_restart.setVisible(self._timer_stopped) self.btn_close.setVisible(self._timer_stopped) + def _close_widget(self): + self._is_showed = False + self._timer_stopped = False + self._refresh_context() + self.hide() + + def closeEvent(self, event): + event.ignore() + if self._timer_stopped: + self._close_widget() + else: + self._on_continue_clicked() + class SignalHandler(QtCore.QObject): signal_show_message = QtCore.Signal() From d60b2451d40e704c694a98eee7e42db701c9b3a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:50:21 +0200 Subject: [PATCH 348/450] added missing attributes --- .../modules/default_modules/timers_manager/widget_user_idle.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index a0c79d65fe..04163c0a3d 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -23,6 +23,8 @@ class WidgetUserIdle(QtWidgets.QWidget): | QtCore.Qt.WindowMinimizeButtonHint ) + self._is_showed = False + self._timer_stopped = False msg_info = "You didn't work for a long time." msg_question = "Would you like to stop Timers?" msg_stopped = ( From 77852558364db3f705097879ad404bdf12c05229 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:51:14 +0200 Subject: [PATCH 349/450] modified showEvent method --- .../default_modules/timers_manager/widget_user_idle.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index 04163c0a3d..f0b86141de 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -94,8 +94,6 @@ class WidgetUserIdle(QtWidgets.QWidget): self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) self.setStyleSheet(style.load_stylesheet()) - def showEvent(self, event): - self.bool_is_showed = True def _refresh_context(self): self.lbl_question.setVisible(not self._timer_stopped) @@ -113,6 +111,12 @@ class WidgetUserIdle(QtWidgets.QWidget): self._refresh_context() self.hide() + def showEvent(self, event): + if not self._is_showed: + self._is_showed = True + self._refresh_context() + super(WidgetUserIdle, self).showEvent(event) + def closeEvent(self, event): event.ignore() if self._timer_stopped: From abf644ac42ab321a2304899e50739cba7f0017bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:52:03 +0200 Subject: [PATCH 350/450] added countdown and it's timer --- .../default_modules/timers_manager/widget_user_idle.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index f0b86141de..0fbf1f778b 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -25,6 +25,9 @@ class WidgetUserIdle(QtWidgets.QWidget): self._is_showed = False self._timer_stopped = False + self._countdown = 0 + self._countdown_start = 0 + msg_info = "You didn't work for a long time." msg_question = "Would you like to stop Timers?" msg_stopped = ( @@ -79,6 +82,9 @@ class WidgetUserIdle(QtWidgets.QWidget): layout.addLayout(form) layout.addLayout(group_layout) + count_timer = QtCore.QTimer() + count_timer.setInterval(1000) + self.lbl_info = lbl_info self.lbl_question = lbl_question self.lbl_stopped = lbl_stopped @@ -89,6 +95,8 @@ class WidgetUserIdle(QtWidgets.QWidget): self.btn_close = btn_close self.btn_restart = btn_restart + self._count_timer = count_timer + self.resize(self.SIZE_W, self.SIZE_H) self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) From 995544a17e4d7096fa7dda588fee474e9f6871b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:52:32 +0200 Subject: [PATCH 351/450] adde on stop btn click callback --- .../default_modules/timers_manager/widget_user_idle.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index 0fbf1f778b..81cef04ceb 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -85,6 +85,8 @@ class WidgetUserIdle(QtWidgets.QWidget): count_timer = QtCore.QTimer() count_timer.setInterval(1000) + btn_stop.clicked.connect(self._on_stop_clicked) + self.lbl_info = lbl_info self.lbl_question = lbl_question self.lbl_stopped = lbl_stopped @@ -113,6 +115,13 @@ class WidgetUserIdle(QtWidgets.QWidget): self.btn_restart.setVisible(self._timer_stopped) self.btn_close.setVisible(self._timer_stopped) + def _stop_timers(self): + self.module.stop_timers() + + def _on_stop_clicked(self): + self._stop_timers() + self._close_widget() + def _close_widget(self): self._is_showed = False self._timer_stopped = False From c826b10bebfe771544cd4af7e98510a193e29ade Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:53:08 +0200 Subject: [PATCH 352/450] adde on continue click callback --- .../default_modules/timers_manager/widget_user_idle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index 81cef04ceb..a03109ceee 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -86,6 +86,7 @@ class WidgetUserIdle(QtWidgets.QWidget): count_timer.setInterval(1000) btn_stop.clicked.connect(self._on_stop_clicked) + btn_continue.clicked.connect(self._on_continue_clicked) self.lbl_info = lbl_info self.lbl_question = lbl_question @@ -122,6 +123,9 @@ class WidgetUserIdle(QtWidgets.QWidget): self._stop_timers() self._close_widget() + def _on_continue_clicked(self): + self._close_widget() + def _close_widget(self): self._is_showed = False self._timer_stopped = False From 6c61f3101d39ad3ddc6507f3dd33c676a2508741 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:53:27 +0200 Subject: [PATCH 353/450] added close button callback --- .../modules/default_modules/timers_manager/widget_user_idle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index a03109ceee..5a2f0600be 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -87,6 +87,7 @@ class WidgetUserIdle(QtWidgets.QWidget): btn_stop.clicked.connect(self._on_stop_clicked) btn_continue.clicked.connect(self._on_continue_clicked) + btn_close.clicked.connect(self._close_widget) self.lbl_info = lbl_info self.lbl_question = lbl_question From cddf357daf7d1481524ade2097726cb470afc6c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:53:42 +0200 Subject: [PATCH 354/450] added restart btn callback --- .../default_modules/timers_manager/widget_user_idle.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index 5a2f0600be..d9af50a089 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -88,6 +88,7 @@ class WidgetUserIdle(QtWidgets.QWidget): btn_stop.clicked.connect(self._on_stop_clicked) btn_continue.clicked.connect(self._on_continue_clicked) btn_close.clicked.connect(self._close_widget) + btn_restart.clicked.connect(self._on_restart_clicked) self.lbl_info = lbl_info self.lbl_question = lbl_question @@ -124,6 +125,10 @@ class WidgetUserIdle(QtWidgets.QWidget): self._stop_timers() self._close_widget() + def _on_restart_clicked(self): + self.module.restart_timers() + self._close_widget() + def _on_continue_clicked(self): self._close_widget() From 4c8ae9d8a9218cc40941b21336533099bb03c2a5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:54:21 +0200 Subject: [PATCH 355/450] added method to update countdown label --- .../modules/default_modules/timers_manager/widget_user_idle.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index d9af50a089..9a1ebaffee 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -107,6 +107,8 @@ class WidgetUserIdle(QtWidgets.QWidget): self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) self.setStyleSheet(style.load_stylesheet()) + def _update_countdown_label(self): + self.lbl_rest_time.setText(str(self._countdown)) def _refresh_context(self): self.lbl_question.setVisible(not self._timer_stopped) From bc1ad2b7d24e1788482f4ea5d62ad7b5226c028c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:55:03 +0200 Subject: [PATCH 356/450] added public method to change timer stopped attribute --- .../default_modules/timers_manager/widget_user_idle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index 9a1ebaffee..d1d54d9cbb 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -107,6 +107,10 @@ class WidgetUserIdle(QtWidgets.QWidget): self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) self.setStyleSheet(style.load_stylesheet()) + def set_timer_stopped(self): + self._timer_stopped = True + self._refresh_context() + def _update_countdown_label(self): self.lbl_rest_time.setText(str(self._countdown)) From 6a8a244fd68fc2ccdb6b8531f11ef06f5fef29e7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:55:19 +0200 Subject: [PATCH 357/450] added countdown timer callbacks --- .../timers_manager/widget_user_idle.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index d1d54d9cbb..fbfb28beaf 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -89,6 +89,7 @@ class WidgetUserIdle(QtWidgets.QWidget): btn_continue.clicked.connect(self._on_continue_clicked) btn_close.clicked.connect(self._close_widget) btn_restart.clicked.connect(self._on_restart_clicked) + count_timer.timeout.connect(self._on_count_timeout) self.lbl_info = lbl_info self.lbl_question = lbl_question @@ -114,6 +115,18 @@ class WidgetUserIdle(QtWidgets.QWidget): def _update_countdown_label(self): self.lbl_rest_time.setText(str(self._countdown)) + def _on_count_timeout(self): + if self._timer_stopped or not self._is_showed: + self._count_timer.stop() + return + + if self._countdown <= 0: + self._stop_timers() + self.set_timer_stopped() + else: + self._countdown -= 1 + self._update_countdown_label() + def _refresh_context(self): self.lbl_question.setVisible(not self._timer_stopped) self.lbl_rest_time.setVisible(not self._timer_stopped) @@ -148,6 +161,9 @@ class WidgetUserIdle(QtWidgets.QWidget): if not self._is_showed: self._is_showed = True self._refresh_context() + + if not self._count_timer.isActive(): + self._count_timer.start() super(WidgetUserIdle, self).showEvent(event) def closeEvent(self, event): From 4ab065c366b851f361474dd28d094cab384f7e5e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:55:31 +0200 Subject: [PATCH 358/450] added more public methods --- .../timers_manager/widget_user_idle.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index fbfb28beaf..81c33ccb70 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -108,6 +108,18 @@ class WidgetUserIdle(QtWidgets.QWidget): self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) self.setStyleSheet(style.load_stylesheet()) + def set_countdown_start(self, countdown): + self._countdown_start = countdown + if not self.is_showed(): + self.reset_countdown() + + def reset_countdown(self): + self._countdown = self._countdown_start + self._update_countdown_label() + + def is_showed(self): + return self._is_showed + def set_timer_stopped(self): self._timer_stopped = True self._refresh_context() From b3a7cd8cfd57bd886e1474515a837bf1259056d8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:55:46 +0200 Subject: [PATCH 359/450] removed 'bool_*' attributes --- .../default_modules/timers_manager/widget_user_idle.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index 81c33ccb70..ecf3e4166b 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -9,10 +9,6 @@ class WidgetUserIdle(QtWidgets.QWidget): def __init__(self, module): super(WidgetUserIdle, self).__init__() - self.bool_is_showed = False - self.bool_not_stopped = True - - self.module = module self.setWindowTitle("OpenPype - Stop timers") icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) @@ -28,6 +24,8 @@ class WidgetUserIdle(QtWidgets.QWidget): self._countdown = 0 self._countdown_start = 0 + self.module = module + msg_info = "You didn't work for a long time." msg_question = "Would you like to stop Timers?" msg_stopped = ( From 88709e3ed6779f4eae27fb0f0c6308ac9d4c64a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:58:39 +0200 Subject: [PATCH 360/450] create idle manager in timers manager --- .../modules/default_modules/timers_manager/timers_manager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 47ba0b4059..025dfd58c4 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -110,14 +110,19 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): self.signal_handler = None self.widget_user_idle = None self.signal_handler = None + self._idle_manager = None self._connectors_by_module_id = {} self._modules_by_id = {} def tray_init(self): + from .idle_threads import IdleManager from .widget_user_idle import WidgetUserIdle, SignalHandler self.widget_user_idle = WidgetUserIdle(self) self.signal_handler = SignalHandler(self) + idle_manager = IdleManager() + + self._idle_manager = idle_manager def tray_start(self, *_a, **_kw): return From 7d0cc023c6c8da1781b166567b665aef0146faf9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:59:30 +0200 Subject: [PATCH 361/450] changed access to widget user --- .../timers_manager/timers_manager.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 025dfd58c4..1d3245300a 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -107,9 +107,8 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): self.last_task = None # Tray attributes - self.signal_handler = None - self.widget_user_idle = None - self.signal_handler = None + self._signal_handler = None + self._widget_user_idle = None self._idle_manager = None self._connectors_by_module_id = {} @@ -118,10 +117,14 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): def tray_init(self): from .idle_threads import IdleManager from .widget_user_idle import WidgetUserIdle, SignalHandler - self.widget_user_idle = WidgetUserIdle(self) - self.signal_handler = SignalHandler(self) - idle_manager = IdleManager() + signal_handler = SignalHandler(self) + idle_manager = IdleManager() + widget_user_idle = WidgetUserIdle(self) + widget_user_idle.set_countdown_start(self.time_show_message) + + self._signal_handler = signal_handler + self._widget_user_idle = widget_user_idle self._idle_manager = idle_manager def tray_start(self, *_a, **_kw): @@ -210,8 +213,8 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): if self.is_running is False: return - self.widget_user_idle.bool_not_stopped = False - self.widget_user_idle.refresh_context() + if self._widget_user_idle is not None: + self._widget_user_idle.set_timer_stopped() self.is_running = False self.timer_stopped(None) @@ -311,8 +314,9 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): def show_message(self): if self.is_running is False: return - if self.widget_user_idle.bool_is_showed is False: - self.widget_user_idle.show() + if not self._widget_user_idle.is_showed(): + self._widget_user_idle.reset_countdown() + self._widget_user_idle.show() # Webserver module implementation def webserver_initialization(self, server_manager): From f2b4b52cd055679a730c7b062d7a97ef04e35e02 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 12:59:53 +0200 Subject: [PATCH 362/450] TImersManager has tray start and trat exit for idle manager --- .../default_modules/timers_manager/timers_manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 1d3245300a..b649281cdc 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -128,11 +128,12 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): self._idle_manager = idle_manager def tray_start(self, *_a, **_kw): - return + if self._idle_manager: + self._idle_manager.start() def tray_exit(self): - """Nothing special for TimersManager.""" - return + if self._idle_manager: + self._idle_manager.stop() def start_timer(self, project_name, asset_name, task_name, hierarchy): """ From 2cd450fb10fcaf7478a6d2fc5e37ad8544d74681 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 13:10:41 +0200 Subject: [PATCH 363/450] removed unused methods --- .../timers_manager/timers_manager.py | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index b649281cdc..07efcd1ae5 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -253,65 +253,6 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): " for connector of module \"{}\"." ).format(module.name)) - def callbacks_by_idle_time(self): - """Implementation of IIdleManager interface.""" - # Time when message is shown - if not self.auto_stop: - return {} - - callbacks = collections.defaultdict(list) - callbacks[self.time_show_message].append(lambda: self.time_callback(0)) - - # Times when idle is between show widget and stop timers - show_to_stop_range = range( - self.time_show_message - 1, self.time_stop_timer - ) - for num in show_to_stop_range: - callbacks[num].append(lambda: self.time_callback(1)) - - # Times when widget is already shown and user restart idle - shown_and_moved_range = range( - self.time_stop_timer - self.time_show_message - ) - for num in shown_and_moved_range: - callbacks[num].append(lambda: self.time_callback(1)) - - # Time when timers are stopped - callbacks[self.time_stop_timer].append(lambda: self.time_callback(2)) - - return callbacks - - def time_callback(self, int_def): - if not self.signal_handler: - return - - if int_def == 0: - self.signal_handler.signal_show_message.emit() - elif int_def == 1: - self.signal_handler.signal_change_label.emit() - elif int_def == 2: - self.signal_handler.signal_stop_timers.emit() - - def change_label(self): - if self.is_running is False: - return - - if ( - not self.idle_manager - or self.widget_user_idle.bool_is_showed is False - ): - return - - if self.idle_manager.idle_time > self.time_show_message: - value = self.time_stop_timer - self.idle_manager.idle_time - else: - value = 1 + ( - self.time_stop_timer - - self.time_show_message - - self.idle_manager.idle_time - ) - self.widget_user_idle.change_count_widget(value) - def show_message(self): if self.is_running is False: return From d9cd2cc91ba1dd8bffd8b1a43f5bceb7393cfb91 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 13:10:59 +0200 Subject: [PATCH 364/450] added signal handler callbacks --- .../default_modules/timers_manager/timers_manager.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 07efcd1ae5..bb11082dfc 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -123,6 +123,16 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): widget_user_idle = WidgetUserIdle(self) widget_user_idle.set_countdown_start(self.time_show_message) + idle_manager.signal_reset_timer.connect( + widget_user_idle.reset_countdown + ) + idle_manager.add_time_signal( + self.time_show_message, signal_handler.signal_show_message + ) + idle_manager.add_time_signal( + self.time_stop_timer, signal_handler.signal_stop_timers + ) + self._signal_handler = signal_handler self._widget_user_idle = widget_user_idle self._idle_manager = idle_manager From 12d0aede85197412b1bba4820e7d28c14c278f1a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 13:11:17 +0200 Subject: [PATCH 365/450] do not prepare idle manager if auto stop is disabled --- .../modules/default_modules/timers_manager/timers_manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index bb11082dfc..f4d24da06b 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -115,6 +115,9 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): self._modules_by_id = {} def tray_init(self): + if not self.auto_stop: + return + from .idle_threads import IdleManager from .widget_user_idle import WidgetUserIdle, SignalHandler From 7fcd46c2bbd8a3ab1b78134efbce62b736a0f23e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 13:11:26 +0200 Subject: [PATCH 366/450] removed unused signal --- .../modules/default_modules/timers_manager/widget_user_idle.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index ecf3e4166b..8ccf6a0b31 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -186,12 +186,10 @@ class WidgetUserIdle(QtWidgets.QWidget): class SignalHandler(QtCore.QObject): signal_show_message = QtCore.Signal() - signal_change_label = QtCore.Signal() signal_stop_timers = QtCore.Signal() def __init__(self, module): super(SignalHandler, self).__init__() self.module = module self.signal_show_message.connect(module.show_message) - self.signal_change_label.connect(module.change_label) self.signal_stop_timers.connect(module.stop_timers) From 92db4b0b234da5c1a958cd4e5f9a84b09f126578 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 13:11:46 +0200 Subject: [PATCH 367/450] don't use IIdleManager --- .../modules/default_modules/timers_manager/timers_manager.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index f4d24da06b..4bbeb13f63 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -3,8 +3,7 @@ import collections from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITimersManager, - ITrayService, - IIdleManager + ITrayService ) from avalon.api import AvalonMongoDB @@ -68,7 +67,7 @@ class ExampleTimersManagerConnector: self._timers_manager_module.timer_stopped(self._module.id) -class TimersManager(OpenPypeModule, ITrayService, IIdleManager): +class TimersManager(OpenPypeModule, ITrayService): """ Handles about Timers. Should be able to start/stop all timers at once. From e3e933a1d65217e4050d147e6f844974b66578f6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 13:14:05 +0200 Subject: [PATCH 368/450] turn off autostop on MacOs --- .../modules/default_modules/timers_manager/timers_manager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 4bbeb13f63..b0aa336c55 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -1,5 +1,6 @@ import os import collections +import platform from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITimersManager, @@ -93,6 +94,10 @@ class TimersManager(OpenPypeModule, ITrayService): self.enabled = timers_settings["enabled"] auto_stop = timers_settings["auto_stop"] + # Turn of auto stop on MacOs because pynput requires root permissions + if platform.system().lower() == "darwin": + auto_stop = False + # When timer will stop if idle manager is running (minutes) full_time = int(timers_settings["full_time"] * 60) # How many minutes before the timer is stopped will popup the message From 8b1fa050fc4ee80b15a8e172974050ea2558a218 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 13:14:25 +0200 Subject: [PATCH 369/450] also turn of auto stop if full time is less than 1 --- .../default_modules/timers_manager/timers_manager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index b0aa336c55..8ce793012d 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -93,16 +93,16 @@ class TimersManager(OpenPypeModule, ITrayService): self.enabled = timers_settings["enabled"] - auto_stop = timers_settings["auto_stop"] - # Turn of auto stop on MacOs because pynput requires root permissions - if platform.system().lower() == "darwin": - auto_stop = False - # When timer will stop if idle manager is running (minutes) full_time = int(timers_settings["full_time"] * 60) # How many minutes before the timer is stopped will popup the message message_time = int(timers_settings["message_time"] * 60) + auto_stop = timers_settings["auto_stop"] + # Turn of auto stop on MacOs because pynput requires root permissions + if platform.system().lower() == "darwin" or full_time <= 0: + auto_stop = False + self.auto_stop = auto_stop self.time_show_message = full_time - message_time self.time_stop_timer = full_time From 0e2b8d55ec49776e371f2fbcafa09776bc699202 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 13:14:39 +0200 Subject: [PATCH 370/450] removed unused import --- .../modules/default_modules/timers_manager/timers_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 8ce793012d..6d08ad9dc5 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -1,5 +1,4 @@ import os -import collections import platform from openpype.modules import OpenPypeModule from openpype_interfaces import ( From 2a9a8bb60297683e889970524f156451f5f32e23 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 13:15:42 +0200 Subject: [PATCH 371/450] removed idle manager module --- .../default_modules/idle_manager/__init__.py | 8 -- .../idle_manager/idle_module.py | 79 --------------- .../idle_manager/idle_threads.py | 97 ------------------- .../idle_manager/interfaces.py | 26 ----- 4 files changed, 210 deletions(-) delete mode 100644 openpype/modules/default_modules/idle_manager/__init__.py delete mode 100644 openpype/modules/default_modules/idle_manager/idle_module.py delete mode 100644 openpype/modules/default_modules/idle_manager/idle_threads.py delete mode 100644 openpype/modules/default_modules/idle_manager/interfaces.py diff --git a/openpype/modules/default_modules/idle_manager/__init__.py b/openpype/modules/default_modules/idle_manager/__init__.py deleted file mode 100644 index 9d6e10bf39..0000000000 --- a/openpype/modules/default_modules/idle_manager/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .idle_module import ( - IdleManager -) - - -__all__ = ( - "IdleManager", -) diff --git a/openpype/modules/default_modules/idle_manager/idle_module.py b/openpype/modules/default_modules/idle_manager/idle_module.py deleted file mode 100644 index 1a6d71a961..0000000000 --- a/openpype/modules/default_modules/idle_manager/idle_module.py +++ /dev/null @@ -1,79 +0,0 @@ -import platform -import collections - -from openpype.modules import OpenPypeModule -from openpype_interfaces import ( - ITrayService, - IIdleManager -) - - -class IdleManager(OpenPypeModule, ITrayService): - """ Measure user's idle time in seconds. - Idle time resets on keyboard/mouse input. - Is able to emit signals at specific time idle. - """ - label = "Idle Service" - name = "idle_manager" - - def initialize(self, module_settings): - enabled = True - # Ignore on MacOs - # - pynput need root permissions and enabled access for application - if platform.system().lower() == "darwin": - enabled = False - self.enabled = enabled - - self.time_callbacks = collections.defaultdict(list) - self.idle_thread = None - - def tray_init(self): - return - - def tray_start(self): - if self.time_callbacks: - self.start_thread() - - def tray_exit(self): - self.stop_thread() - try: - self.time_callbacks = {} - except Exception: - pass - - def connect_with_modules(self, enabled_modules): - for module in enabled_modules: - if not isinstance(module, IIdleManager): - continue - - module.idle_manager = self - callbacks_items = module.callbacks_by_idle_time() or {} - for emit_time, callbacks in callbacks_items.items(): - if not isinstance(callbacks, (tuple, list, set)): - callbacks = [callbacks] - self.time_callbacks[emit_time].extend(callbacks) - - @property - def idle_time(self): - if self.idle_thread and self.idle_thread.is_running: - return self.idle_thread.idle_time - - def _create_thread(self): - from .idle_threads import IdleManagerThread - - return IdleManagerThread(self) - - def start_thread(self): - if self.idle_thread: - self.idle_thread.stop() - self.idle_thread.join() - self.idle_thread = self._create_thread() - self.idle_thread.start() - - def stop_thread(self): - if self.idle_thread: - self.idle_thread.stop() - self.idle_thread.join() - - def on_thread_stop(self): - self.set_service_failed_icon() diff --git a/openpype/modules/default_modules/idle_manager/idle_threads.py b/openpype/modules/default_modules/idle_manager/idle_threads.py deleted file mode 100644 index f19feddb77..0000000000 --- a/openpype/modules/default_modules/idle_manager/idle_threads.py +++ /dev/null @@ -1,97 +0,0 @@ -import time -import threading - -from pynput import mouse, keyboard - -from openpype.lib import PypeLogger - - -class MouseThread(mouse.Listener): - """Listens user's mouse movement.""" - - def __init__(self, callback): - super(MouseThread, self).__init__(on_move=self.on_move) - self.callback = callback - - def on_move(self, posx, posy): - self.callback() - - -class KeyboardThread(keyboard.Listener): - """Listens user's keyboard input.""" - - def __init__(self, callback): - super(KeyboardThread, self).__init__(on_press=self.on_press) - - self.callback = callback - - def on_press(self, key): - self.callback() - - -class IdleManagerThread(threading.Thread): - def __init__(self, module, *args, **kwargs): - super(IdleManagerThread, self).__init__(*args, **kwargs) - self.log = PypeLogger.get_logger(self.__class__.__name__) - self.module = module - self.threads = [] - self.is_running = False - self.idle_time = 0 - - def stop(self): - self.is_running = False - - def reset_time(self): - self.idle_time = 0 - - @property - def time_callbacks(self): - return self.module.time_callbacks - - def on_stop(self): - self.is_running = False - self.log.info("IdleManagerThread has stopped") - self.module.on_thread_stop() - - def run(self): - self.log.info("IdleManagerThread has started") - self.is_running = True - thread_mouse = MouseThread(self.reset_time) - thread_keyboard = KeyboardThread(self.reset_time) - thread_mouse.start() - thread_keyboard.start() - try: - while self.is_running: - if self.idle_time in self.time_callbacks: - for callback in self.time_callbacks[self.idle_time]: - thread = threading.Thread(target=callback) - thread.start() - self.threads.append(thread) - - for thread in tuple(self.threads): - if not thread.isAlive(): - thread.join() - self.threads.remove(thread) - - self.idle_time += 1 - time.sleep(1) - - except Exception: - self.log.warning( - 'Idle Manager service has failed', exc_info=True - ) - - # Threads don't have their attrs when Qt application already finished - try: - thread_mouse.stop() - thread_mouse.join() - except AttributeError: - pass - - try: - thread_keyboard.stop() - thread_keyboard.join() - except AttributeError: - pass - - self.on_stop() diff --git a/openpype/modules/default_modules/idle_manager/interfaces.py b/openpype/modules/default_modules/idle_manager/interfaces.py deleted file mode 100644 index 71cd17a64a..0000000000 --- a/openpype/modules/default_modules/idle_manager/interfaces.py +++ /dev/null @@ -1,26 +0,0 @@ -from abc import abstractmethod -from openpype.modules import OpenPypeInterface - - -class IIdleManager(OpenPypeInterface): - """Other modules interface to return callbacks by idle time in seconds. - - Expected output is dictionary with seconds as keys and callback/s - as value, value may be callback of list of callbacks. - EXAMPLE: - ``` - { - 60: self.on_minute_idle - } - ``` - """ - idle_manager = None - - @abstractmethod - def callbacks_by_idle_time(self): - pass - - @property - def idle_time(self): - if self.idle_manager: - return self.idle_manager.idle_time From 9a46920945a98a3fe87020b1c8eb576ce18c0590 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 28 Sep 2021 13:12:19 +0100 Subject: [PATCH 372/450] Packs images used by meshes in the blend file --- .../hosts/blender/plugins/publish/extract_blend.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index 6687c9fe76..e880b1bde0 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -28,6 +28,16 @@ class ExtractBlend(openpype.api.Extractor): for obj in instance: data_blocks.add(obj) + # Pack used images in the blend files. + if obj.type == 'MESH': + for material_slot in obj.material_slots: + mat = material_slot.material + if mat and mat.use_nodes: + tree = mat.node_tree + if tree.type == 'SHADER': + for node in tree.nodes: + if node.bl_idname == 'ShaderNodeTexImage': + node.image.pack() bpy.data.libraries.write(filepath, data_blocks) From d217827ce8eb7402725e817290e1a17e5e660563 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 14:34:44 +0200 Subject: [PATCH 373/450] fixed oiio tool path for linux machines --- start.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/start.py b/start.py index f3adabd942..919133d3df 100644 --- a/start.py +++ b/start.py @@ -403,15 +403,24 @@ def _validate_thirdparty_binaries(): raise RuntimeError(error_msg.format("FFmpeg")) # Validate existence of OpenImageIO (not on MacOs) - if low_platform != "darwin": + oiio_tool_path = None + if low_platform == "linux": + oiio_tool_path = os.path.join( + binary_vendors_dir, + "oiio", + low_platform, + "bin", + "oiiotool" + ) + elif low_platform == "windows": oiio_tool_path = os.path.join( binary_vendors_dir, "oiio", low_platform, "oiiotool" ) - if not is_tool(oiio_tool_path): - raise RuntimeError(error_msg.format("OpenImageIO")) + if oiio_tool_path is not None and not is_tool(oiio_tool_path): + raise RuntimeError(error_msg.format("OpenImageIO")) def _process_arguments() -> tuple: From 85c3e0b13b662473fba4a4a760e31c77314ce145 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 14:37:32 +0200 Subject: [PATCH 374/450] moved cleanup to separated method --- .../modules/default_modules/timers_manager/idle_threads.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/idle_threads.py b/openpype/modules/default_modules/timers_manager/idle_threads.py index 73978ed7e8..9ec27e659b 100644 --- a/openpype/modules/default_modules/timers_manager/idle_threads.py +++ b/openpype/modules/default_modules/timers_manager/idle_threads.py @@ -97,6 +97,10 @@ class IdleManager(QtCore.QThread): signal.emit() time.sleep(1) + self._post_run() + self.log.info('IdleManager has stopped') + + def _post_run(self): # Stop threads if still exist if self._mouse_thread is not None: self._mouse_thread.signal_stop.emit() @@ -108,8 +112,6 @@ class IdleManager(QtCore.QThread): self._keyboard_thread.terminate() self._keyboard_thread.wait() - self.log.info('IdleManager has stopped') - class MouseThread(QtCore.QThread): """Listens user's mouse movement.""" From eaedf21c077442a308aebca83dd7bb1c36924787 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 14:41:50 +0200 Subject: [PATCH 375/450] fix whitespaces --- .../modules/default_modules/timers_manager/widget_user_idle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/default_modules/timers_manager/widget_user_idle.py index 8ccf6a0b31..1ecea74440 100644 --- a/openpype/modules/default_modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/default_modules/timers_manager/widget_user_idle.py @@ -110,7 +110,7 @@ class WidgetUserIdle(QtWidgets.QWidget): self._countdown_start = countdown if not self.is_showed(): self.reset_countdown() - + def reset_countdown(self): self._countdown = self._countdown_start self._update_countdown_label() From d75a1062b9708629496d6c4e634408c0a1928cf0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 15:18:17 +0200 Subject: [PATCH 376/450] change variable from 'ffmpeg_cmd' to 'source_ffmpeg_cmd' --- openpype/scripts/otio_burnin.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 5b48a4f3f4..da9cab1290 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -69,7 +69,7 @@ def get_fps(str_value): return str(fps) -def _prores_codec_args(ffprobe_data, ffmpeg_cmd): +def _prores_codec_args(ffprobe_data, source_ffmpeg_cmd): output = [] tags = ffprobe_data.get("tags") or {} @@ -108,12 +108,12 @@ def _prores_codec_args(ffprobe_data, ffmpeg_cmd): return output -def _h264_codec_args(ffprobe_data, ffmpeg_cmd): +def _h264_codec_args(ffprobe_data, source_ffmpeg_cmd): output = [] output.extend(["-codec:v", "h264"]) - args = ffmpeg_cmd.split(" ") + args = source_ffmpeg_cmd.split(" ") crf = "" for count, arg in enumerate(args): if arg == "-crf": @@ -136,15 +136,15 @@ def _h264_codec_args(ffprobe_data, ffmpeg_cmd): return output -def get_codec_args(ffprobe_data, ffmpeg_cmd): +def get_codec_args(ffprobe_data, source_ffmpeg_cmd): codec_name = ffprobe_data.get("codec_name") # Codec "prores" if codec_name == "prores": - return _prores_codec_args(ffprobe_data, ffmpeg_cmd) + return _prores_codec_args(ffprobe_data, source_ffmpeg_cmd) # Codec "h264" if codec_name == "h264": - return _h264_codec_args(ffprobe_data, ffmpeg_cmd) + return _h264_codec_args(ffprobe_data, source_ffmpeg_cmd) output = [] if codec_name: @@ -478,7 +478,7 @@ def example(input_path, output_path): def burnins_from_data( input_path, output_path, data, codec_data=None, options=None, burnin_values=None, overwrite=True, - full_input_path=None, first_frame=None, ffmpeg_cmd=None + full_input_path=None, first_frame=None, source_ffmpeg_cmd=None ): """This method adds burnins to video/image file based on presets setting. @@ -656,7 +656,7 @@ def burnins_from_data( else: ffprobe_data = burnin._streams[0] - ffmpeg_args.extend(get_codec_args(ffprobe_data, ffmpeg_cmd)) + ffmpeg_args.extend(get_codec_args(ffprobe_data, source_ffmpeg_cmd)) # Use group one (same as `-intra` argument, which is deprecated) ffmpeg_args_str = " ".join(ffmpeg_args) @@ -680,6 +680,6 @@ if __name__ == "__main__": burnin_values=in_data.get("values"), full_input_path=in_data.get("full_input_path"), first_frame=in_data.get("first_frame"), - ffmpeg_cmd=in_data.get("ffmpeg_cmd") + source_ffmpeg_cmd=in_data.get("ffmpeg_cmd") ) print("* Burnin script has finished") From 2207f6f6a53f119d46e81e11dbc2efda7c96d3f9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Sep 2021 15:18:55 +0200 Subject: [PATCH 377/450] reuse more source arguments and skip using source bitrate --- openpype/scripts/otio_burnin.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index da9cab1290..184d7689e3 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -113,18 +113,19 @@ def _h264_codec_args(ffprobe_data, source_ffmpeg_cmd): output.extend(["-codec:v", "h264"]) - args = source_ffmpeg_cmd.split(" ") - crf = "" - for count, arg in enumerate(args): - if arg == "-crf": - crf = args[count + 1] - break - if crf: - output.extend(["-crf", crf]) - - bit_rate = ffprobe_data.get("bit_rate") - if bit_rate and not crf: - output.extend(["-b:v", bit_rate]) + # Use arguments from source if are available source arguments + if source_ffmpeg_cmd: + copy_args = ( + "-crf", + "-b:v", "-vb", + "-minrate", "-minrate:", + "-maxrate", "-maxrate:", + "-bufsize", "-bufsize:" + ) + args = source_ffmpeg_cmd.split(" ") + for idx, arg in enumerate(args): + if arg in copy_args: + output.extend([arg, args[idx + 1]]) pix_fmt = ffprobe_data.get("pix_fmt") if pix_fmt: From 5d59712e0d44b140dbdfd5fa91f6623aaab26283 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Tue, 28 Sep 2021 17:14:59 +0200 Subject: [PATCH 378/450] remove collector --- .../plugins/publish/collect_loaded_plugin.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py diff --git a/openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py deleted file mode 100644 index 7ee7021962..0000000000 --- a/openpype/hosts/maya/plugins/publish/collect_loaded_plugin.py +++ /dev/null @@ -1,17 +0,0 @@ -import pyblish.api -from maya import cmds - - -class CollectLoadedPlugin(pyblish.api.ContextPlugin): - """Collect loaded plugins""" - - order = pyblish.api.CollectorOrder - label = "Loaded Plugins" - hosts = ["maya"] - - def process(self, context): - - context.data["loadedPlugins"] = cmds.pluginInfo( - query=True, - listPlugins=True, - ) From 536d6f000ecdb21fa4dfa9af8ac9c5f9fd0c6edd Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Tue, 28 Sep 2021 17:50:35 +0200 Subject: [PATCH 379/450] add option for whitelist native maya plugins --- .../plugins/publish/validate_loaded_plugin.py | 13 ++++- .../defaults/project_settings/maya.json | 48 +------------------ .../schemas/schema_maya_publish.json | 5 ++ 3 files changed, 18 insertions(+), 48 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py index 01705e8b13..444aeb24c1 100644 --- a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py @@ -1,6 +1,7 @@ import pyblish.api import maya.cmds as cmds import openpype.api +import os class ValidateLoadedPlugin(pyblish.api.ContextPlugin): @@ -15,9 +16,16 @@ class ValidateLoadedPlugin(pyblish.api.ContextPlugin): def get_invalid(cls, context): invalid = [] + loaded_plugin = cmds.pluginInfo(query=True, listPlugins=True) + # get variable from OpenPype settings + whitelist_native_plugins = cls.whitelist_native_plugins + authorized_plugins = cls.authorized_plugins or [] - for plugin in context.data.get("loadedPlugins"): - if plugin not in cls.authorized_plugins: + for plugin in loaded_plugin: + if not whitelist_native_plugins and os.getenv('MAYA_LOCATION') \ + in cmds.pluginInfo(plugin, query=True, path=True): + continue + if plugin not in authorized_plugins: invalid.append(plugin) return invalid @@ -35,4 +43,5 @@ class ValidateLoadedPlugin(pyblish.api.ContextPlugin): """Unload forbidden plugins""" for plugin in cls.get_invalid(context): + cmds.pluginInfo(plugin, edit=True, autoload=False) cmds.unloadPlugin(plugin, force=True) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index b19d544fed..56496e05d0 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -171,52 +171,8 @@ }, "ValidateLoadedPlugin": { "enabled": false, - "authorized_plugins": [ - "stereoCamera", - "svgFileTranslator", - "invertShape", - "mayaHIK", - "GamePipeline", - "curveWarp", - "tiffFloatReader", - "MASH", - "poseInterpolator", - "ATFPlugin", - "hairPhysicalShader", - "cacheEvaluator", - "ikSpringSolver", - "ik2Bsolver", - "xgenToolkit", - "AbcExport", - "retargeterNodes", - "gameFbxExporter", - "VectorRender", - "OpenEXRLoader", - "lookdevKit", - "Unfold3D", - "Type", - "mayaCharacterization", - "meshReorder", - "modelingToolkit", - "MayaMuscle", - "rotateHelper", - "dx11Shader", - "matrixNodes", - "AbcImport", - "autoLoader", - "deformerEvaluator", - "sceneAssembly", - "gpuCache", - "OneClick", - "shaderFXPlugin", - "objExport", - "renderSetup", - "GPUBuiltInDeformer", - "ArubaTessellator", - "quatNodes", - "fbxmaya", - "Turtle" - ] + "whitelist_native_plugins": false, + "authorized_plugins": [] }, "ValidateRenderSettings": { "arnold_render_attributes": [], diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index e2df6654f2..8379f04556 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -94,6 +94,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "whitelist_native_plugins", + "label": "Whitelist Maya Native Plugins" + }, { "type": "list", "key": "authorized_plugins", From cd05b01f053f5c491452142e6d9bc9bab6d1f4e8 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Tue, 28 Sep 2021 17:54:56 +0200 Subject: [PATCH 380/450] flake8 E125 correction --- openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py index 444aeb24c1..9306d8ce15 100644 --- a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py @@ -23,7 +23,7 @@ class ValidateLoadedPlugin(pyblish.api.ContextPlugin): for plugin in loaded_plugin: if not whitelist_native_plugins and os.getenv('MAYA_LOCATION') \ - in cmds.pluginInfo(plugin, query=True, path=True): + in cmds.pluginInfo(plugin, query=True, path=True): continue if plugin not in authorized_plugins: invalid.append(plugin) From 09a3e9e83caa263e7e839f1fdeb1aab53d0b17bb Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 29 Sep 2021 03:39:34 +0000 Subject: [PATCH 381/450] [Automated] Bump version --- CHANGELOG.md | 18 +++++++++--------- openpype/version.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99c994f13d..3d260509bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.5.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.5.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...HEAD) @@ -17,7 +17,10 @@ **🐛 Bug fixes** - Fix Sync Queue when project disabled [\#2063](https://github.com/pypeclub/OpenPype/pull/2063) -- TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) + +**Merged pull requests:** + +- Nuke UI scaling [\#2077](https://github.com/pypeclub/OpenPype/pull/2077) ## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23) @@ -35,6 +38,7 @@ - Settings UI: Deffered set value on entity [\#2044](https://github.com/pypeclub/OpenPype/pull/2044) - Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043) - Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042) +- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) - Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) @@ -69,7 +73,6 @@ **🚀 Enhancements** -- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) - Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) - Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) @@ -89,6 +92,7 @@ - Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959) - CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) - Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949) +- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) **🐛 Bug fixes** @@ -115,13 +119,9 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) -**🚀 Enhancements** - -- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) - **🐛 Bug fixes** -- Maya: Menu actions fix [\#1945](https://github.com/pypeclub/OpenPype/pull/1945) +- TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) - standalone: editorial shared object problem [\#1941](https://github.com/pypeclub/OpenPype/pull/1941) ## [3.3.0](https://github.com/pypeclub/OpenPype/tree/3.3.0) (2021-08-17) @@ -138,9 +138,9 @@ **🐛 Bug fixes** +- Maya: Menu actions fix [\#1945](https://github.com/pypeclub/OpenPype/pull/1945) - Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) - Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) -- Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index f8ed9c7c2f..0ddfdff5fe 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.5.0-nightly.1" +__version__ = "3.5.0-nightly.2" From b866f2ed78e1865e4c10a412e368e0dc18d9373e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Sep 2021 07:59:53 +0000 Subject: [PATCH 382/450] Bump pywin32 from 300 to 301 Bumps [pywin32](https://github.com/mhammond/pywin32) from 300 to 301. - [Release notes](https://github.com/mhammond/pywin32/releases) - [Changelog](https://github.com/mhammond/pywin32/blob/main/CHANGES.txt) - [Commits](https://github.com/mhammond/pywin32/commits) --- updated-dependencies: - dependency-name: pywin32 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 47 +++++++++++++++++++++++++++++++++++------------ pyproject.toml | 2 +- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/poetry.lock b/poetry.lock index bc308d63e9..c30631340a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1090,7 +1090,7 @@ python-versions = "*" [[package]] name = "pywin32" -version = "300" +version = "301" description = "Python for Window Extensions" category = "main" optional = false @@ -1499,7 +1499,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "63ab0f15fa9d40931622f71ad6e8d5810e1f9b7ef5d27e1d9a8d00caad767c1d" +content-hash = "ed6430d2ba01f108a15e585629bf7b01ab47fed4217b93d336e9877273ab29e7" [metadata.files] acre = [] @@ -1904,12 +1904,22 @@ log4mongo = [ {file = "log4mongo-1.7.0.tar.gz", hash = "sha256:dc374617206162a0b14167fbb5feac01dbef587539a235dadba6200362984a68"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1918,14 +1928,21 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1935,6 +1952,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2211,6 +2231,7 @@ pynput = [ ] pyobjc-core = [ {file = "pyobjc-core-7.3.tar.gz", hash = "sha256:5081aedf8bb40aac1a8ad95adac9e44e148a882686ded614adf46bb67fd67574"}, + {file = "pyobjc_core-7.3-1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a1f1e6b457127cbf2b5bd2b94520a7c89fb590b739911eadb2b0499a3a5b0e6f"}, {file = "pyobjc_core-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e93ad769a20b908778fe950f62a843a6d8f0fa71996e5f3cc9fab5ae7d17771"}, {file = "pyobjc_core-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f63fd37bbf3785af4ddb2f86cad5ca81c62cfc7d1c0099637ca18343c3656c1"}, {file = "pyobjc_core-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9b1311f72f2e170742a7ee3a8149f52c35158dc024a21e88d6f1e52ba5d718b"}, @@ -2219,6 +2240,7 @@ pyobjc-core = [ ] pyobjc-framework-cocoa = [ {file = "pyobjc-framework-Cocoa-7.3.tar.gz", hash = "sha256:b18d05e7a795a3455ad191c3e43d6bfa673c2a4fd480bb1ccf57191051b80b7e"}, + {file = "pyobjc_framework_Cocoa-7.3-1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1e31376806e5de883a1d7c7c87d9ff2a8b09fc05d267e0dfce6e42409fb70c67"}, {file = "pyobjc_framework_Cocoa-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9edffdfa6dd1f71f21b531c3e61fdd3e4d5d3bf6c5a528c98e88828cd60bac11"}, {file = "pyobjc_framework_Cocoa-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:35a6340437a4e0109a302150b7d1f6baf57004ccf74834f9e6062fcafe2fd8d7"}, {file = "pyobjc_framework_Cocoa-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c3886f2608ab3ed02482f8b2ebf9f782b324c559e84b52cfd92dba8a1109872"}, @@ -2227,6 +2249,7 @@ pyobjc-framework-cocoa = [ ] pyobjc-framework-quartz = [ {file = "pyobjc-framework-Quartz-7.3.tar.gz", hash = "sha256:98812844c34262def980bdf60923a875cd43428a8375b6fd53bd2cd800eccf0b"}, + {file = "pyobjc_framework_Quartz-7.3-1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1139bc6874c0f8b58f0b8602015e0994198bc506a6bcec1071208de32b55ed26"}, {file = "pyobjc_framework_Quartz-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ef18f5a16511ded65980bf4f5983ea5d35c88224dbad1b3112abd29c60413ea"}, {file = "pyobjc_framework_Quartz-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b41eec8d4b10c7c7e011e2f9051367f5499ef315ba52dfbae573c3a2e05469c"}, {file = "pyobjc_framework_Quartz-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c65456ed045dfe1711d0298734e5a3ad670f8c770f7eb3b19979256c388bdd2"}, @@ -2300,16 +2323,16 @@ pytz = [ {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] pywin32 = [ - {file = "pywin32-300-cp35-cp35m-win32.whl", hash = "sha256:1c204a81daed2089e55d11eefa4826c05e604d27fe2be40b6bf8db7b6a39da63"}, - {file = "pywin32-300-cp35-cp35m-win_amd64.whl", hash = "sha256:350c5644775736351b77ba68da09a39c760d75d2467ecec37bd3c36a94fbed64"}, - {file = "pywin32-300-cp36-cp36m-win32.whl", hash = "sha256:a3b4c48c852d4107e8a8ec980b76c94ce596ea66d60f7a697582ea9dce7e0db7"}, - {file = "pywin32-300-cp36-cp36m-win_amd64.whl", hash = "sha256:27a30b887afbf05a9cbb05e3ffd43104a9b71ce292f64a635389dbad0ed1cd85"}, - {file = "pywin32-300-cp37-cp37m-win32.whl", hash = "sha256:d7e8c7efc221f10d6400c19c32a031add1c4a58733298c09216f57b4fde110dc"}, - {file = "pywin32-300-cp37-cp37m-win_amd64.whl", hash = "sha256:8151e4d7a19262d6694162d6da85d99a16f8b908949797fd99c83a0bfaf5807d"}, - {file = "pywin32-300-cp38-cp38-win32.whl", hash = "sha256:fbb3b1b0fbd0b4fc2a3d1d81fe0783e30062c1abed1d17c32b7879d55858cfae"}, - {file = "pywin32-300-cp38-cp38-win_amd64.whl", hash = "sha256:60a8fa361091b2eea27f15718f8eb7f9297e8d51b54dbc4f55f3d238093d5190"}, - {file = "pywin32-300-cp39-cp39-win32.whl", hash = "sha256:638b68eea5cfc8def537e43e9554747f8dee786b090e47ead94bfdafdb0f2f50"}, - {file = "pywin32-300-cp39-cp39-win_amd64.whl", hash = "sha256:b1609ce9bd5c411b81f941b246d683d6508992093203d4eb7f278f4ed1085c3f"}, + {file = "pywin32-301-cp35-cp35m-win32.whl", hash = "sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7"}, + {file = "pywin32-301-cp35-cp35m-win_amd64.whl", hash = "sha256:9635df6998a70282bd36e7ac2a5cef9ead1627b0a63b17c731312c7a0daebb72"}, + {file = "pywin32-301-cp36-cp36m-win32.whl", hash = "sha256:c866f04a182a8cb9b7855de065113bbd2e40524f570db73ef1ee99ff0a5cc2f0"}, + {file = "pywin32-301-cp36-cp36m-win_amd64.whl", hash = "sha256:dafa18e95bf2a92f298fe9c582b0e205aca45c55f989937c52c454ce65b93c78"}, + {file = "pywin32-301-cp37-cp37m-win32.whl", hash = "sha256:98f62a3f60aa64894a290fb7494bfa0bfa0a199e9e052e1ac293b2ad3cd2818b"}, + {file = "pywin32-301-cp37-cp37m-win_amd64.whl", hash = "sha256:fb3b4933e0382ba49305cc6cd3fb18525df7fd96aa434de19ce0878133bf8e4a"}, + {file = "pywin32-301-cp38-cp38-win32.whl", hash = "sha256:88981dd3cfb07432625b180f49bf4e179fb8cbb5704cd512e38dd63636af7a17"}, + {file = "pywin32-301-cp38-cp38-win_amd64.whl", hash = "sha256:8c9d33968aa7fcddf44e47750e18f3d034c3e443a707688a008a2e52bbef7e96"}, + {file = "pywin32-301-cp39-cp39-win32.whl", hash = "sha256:595d397df65f1b2e0beaca63a883ae6d8b6df1cdea85c16ae85f6d2e648133fe"}, + {file = "pywin32-301-cp39-cp39-win_amd64.whl", hash = "sha256:87604a4087434cd814ad8973bd47d6524bd1fa9e971ce428e76b62a5e0860fdf"}, ] pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, diff --git a/pyproject.toml b/pyproject.toml index baa897cc81..158b969095 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ speedcopy = "^2.1" six = "^1.15" semver = "^2.13.0" # for version resolution wsrpc_aiohttp = "^3.1.1" # websocket server -pywin32 = { version = "300", markers = "sys_platform == 'win32'" } +pywin32 = { version = "301", markers = "sys_platform == 'win32'" } jinxed = [ { version = "^1.0.1", markers = "sys_platform == 'darwin'" }, { version = "^1.0.1", markers = "sys_platform == 'linux'" } From 6f4d0572788e36cdb0e104782366fc4766f87ae4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 29 Sep 2021 12:50:04 +0200 Subject: [PATCH 383/450] Added choosing different dirmap mapping if workfile synched locally --- openpype/hosts/maya/api/__init__.py | 76 ++++++++++++++++++- .../sync_server/sync_server_module.py | 12 +++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 3412803714..3768b84d91 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -64,14 +64,23 @@ def process_dirmap(project_settings): # type: (dict) -> None """Go through all paths in Settings and set them using `dirmap`. + If artists has Site Sync enabled, take dirmap mapping directly from + Local Settings when artist is syncing workfile locally. + Args: project_settings (dict): Settings for current project. """ - if not project_settings["maya"].get("maya-dirmap"): + local_mapping = _get_local_sync_dirmap(project_settings) + if not project_settings["maya"].get("maya-dirmap") and not local_mapping: return - mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {} - mapping_enabled = project_settings["maya"]["maya-dirmap"]["enabled"] + + mapping = local_mapping or \ + project_settings["maya"]["maya-dirmap"]["paths"] \ + or {} + mapping_enabled = project_settings["maya"]["maya-dirmap"]["enabled"] \ + or bool(local_mapping) + if not mapping or not mapping_enabled: return if mapping.get("source-path") and mapping_enabled is True: @@ -94,6 +103,67 @@ def process_dirmap(project_settings): continue +def _get_local_sync_dirmap(project_settings): + """ + Returns dirmap if synch to local project is enabled. + + Only valid mapping is from roots of remote site to local site set in + Local Settings. + + Args: + project_settings (dict) + Returns: + dict : { "source-path": [XXX], "destination-path": [YYYY]} + """ + import json + mapping = {} + + if not project_settings["global"]["sync_server"]["enabled"]: + log.debug("Site Sync not enabled") + return mapping + + from openpype.settings.lib import get_site_local_overrides + from openpype.modules import ModulesManager + + manager = ModulesManager() + sync_module = manager.modules_by_name["sync_server"] + + project_name = os.getenv("AVALON_PROJECT") + sync_settings = sync_module.get_sync_project_setting( + os.getenv("AVALON_PROJECT"), exclude_locals=False, cached=False) + log.debug(json.dumps(sync_settings, indent=4)) + + active_site = sync_module.get_local_normalized_site( + sync_module.get_active_site(project_name)) + remote_site = sync_module.get_local_normalized_site( + sync_module.get_remote_site(project_name)) + log.debug("active {} - remote {}".format(active_site, remote_site)) + + if active_site == "local" \ + and project_name in sync_module.get_enabled_projects()\ + and active_site != remote_site: + overrides = get_site_local_overrides(os.getenv("AVALON_PROJECT"), + active_site) + for root_name, value in overrides.items(): + if os.path.isdir(value): + try: + mapping["destination-path"] = [value] + mapping["source-path"] = [sync_settings["sites"]\ + [remote_site]\ + ["root"]\ + [root_name]] + except IndexError: + # missing corresponding destination path + log.debug("overrides".format(overrides)) + log.error( + ("invalid dirmap mapping, missing corresponding" + " destination directory.")) + break + + log.debug("local sync mapping:: {}".format(mapping)) + return mapping + + def uninstall(): pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 7dabd45bae..f2e9237542 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -398,6 +398,18 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return remote_site + def get_local_normalized_site(self, site_name): + """ + Return 'site_name' or 'local' if 'site_name' is local id. + + In some places Settings or Local Settings require 'local' instead + of real site name. + """ + if site_name == get_local_site_id(): + site_name = self.LOCAL_SITE + + return site_name + # Methods for Settings UI to draw appropriate forms @classmethod def get_system_settings_schema(cls): From de075189ca3d49354322d0f67fa0a43510a07504 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 29 Sep 2021 13:14:21 +0200 Subject: [PATCH 384/450] pass context data in dynamic data for creation --- openpype/hosts/tvpaint/api/plugin.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index 3eb9a5be31..e148e44a27 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -3,4 +3,17 @@ from avalon.tvpaint import pipeline class Creator(PypeCreatorMixin, pipeline.Creator): - pass + @classmethod + def get_dynamic_data(cls, *args, **kwargs): + dynamic_data = super(Creator, cls).get_dynamic_data(*args, **kwargs) + + # Change asset and name by current workfile context + workfile_context = pipeline.get_current_workfile_context() + asset_name = workfile_context.get("asset") + task_name = workfile_context.get("task") + if "asset" not in dynamic_data and asset_name: + dynamic_data["asset"] = asset_name + + if "task" not in dynamic_data and task_name: + dynamic_data["task"] = task_name + return dynamic_data From b1cd7f9b7a7f929f6a4a763f77cee9b9f7c137d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Sep 2021 17:13:29 +0200 Subject: [PATCH 385/450] message text changes Still not sure it it will work for a studio who is self maintaining --- openpype/tools/tray/pype_tray.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 279d71980f..fde43980cc 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -142,8 +142,9 @@ class TrayManager: title = "Settings miss default values" msg = ( - "Your OpenPype may not work as expected because have missing" - " default settings values. Please contact OpenPype team." + "Your OpenPype will not work as expected! \n" + "Some default values in settigs are missing. \n\n" + "Please contact OpenPype team." ) msg_box = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Warning, From a6e334c16d32c480235ca36c735064da755761c3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 29 Sep 2021 18:50:50 +0200 Subject: [PATCH 386/450] Added running configurable disk mapping command before start of OP --- .../defaults/system_settings/general.json | 5 ++ .../schemas/system_schema/schema_general.json | 68 +++++++++++++++++++ openpype/settings/handlers.py | 2 +- start.py | 34 ++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/system_settings/general.json b/openpype/settings/defaults/system_settings/general.json index d03fedf3c9..f54e8b2b16 100644 --- a/openpype/settings/defaults/system_settings/general.json +++ b/openpype/settings/defaults/system_settings/general.json @@ -7,6 +7,11 @@ "global": [] } }, + "disk_mapping": { + "windows": [], + "linux": [], + "darwin": [] + }, "openpype_path": { "windows": [], "darwin": [], diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index fe5a8d8203..81d38dc668 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -40,6 +40,74 @@ { "type": "splitter" }, + { + "type": "dict", + "key": "disk_mapping", + "label": "Disk mapping", + "collapsible": false, + "children": [ + { + "key": "windows", + "label": "Windows", + "type": "list", + "object_type": { + "type": "list-strict", + "key": "item", + "object_types": [ + { + "label": "Source", + "type": "path" + }, + { + "label": "Destination", + "type": "path" + } + ] + } + }, + { + "key": "linux", + "label": "Linux", + "type": "list", + "object_type": { + "type": "list-strict", + "key": "item", + "object_types": [ + { + "label": "Source", + "type": "path" + }, + { + "label": "Destination", + "type": "path" + } + ] + } + }, + { + "key": "darwin", + "label": "MacOS", + "type": "list", + "object_type": { + "type": "list-strict", + "key": "item", + "object_types": [ + { + "label": "Source", + "type": "path" + }, + { + "label": "Destination", + "type": "path" + } + ] + } + } + ] + }, + { + "type": "splitter" + }, { "type": "path", "key": "openpype_path", diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 288fc76801..c59e2bc542 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -168,7 +168,7 @@ class CacheValues: class MongoSettingsHandler(SettingsHandler): """Settings handler that use mongo for storing and loading of settings.""" - global_general_keys = ("openpype_path", "admin_password") + global_general_keys = ("openpype_path", "admin_password", "disk_mapping") def __init__(self): # Get mongo connection diff --git a/start.py b/start.py index f3adabd942..49500fcf8e 100644 --- a/start.py +++ b/start.py @@ -102,6 +102,8 @@ import subprocess import site from pathlib import Path +from igniter.tools import get_openpype_global_settings + # OPENPYPE_ROOT is variable pointing to build (or code) directory # WARNING `OPENPYPE_ROOT` must be defined before igniter import @@ -275,6 +277,35 @@ def run(arguments: list, env: dict = None) -> int: return p.returncode +def run_disk_mapping_commands(mongo_url): + """ Run disk mapping command + + Used to map shared disk for OP to pull codebase. + """ + settings = get_openpype_global_settings(mongo_url) + + low_platform = platform.system().lower() + disk_mapping = settings.get("disk_mapping") + if not disk_mapping: + return + + for mapping in disk_mapping.get(low_platform): + source, destination = mapping + + args = ["subst", destination.rstrip('/'), source.rstrip('/')] + _print("disk mapping args:: {}".format(args)) + try: + output = subprocess.Popen(args) + if output.returncode != 0: + exc_msg = "Executing arguments was not successful: \"{}\"".format( + args) + + raise RuntimeError(exc_msg) + except TypeError: + _print("Error in mapping drive") + raise + + def set_avalon_environments(): """Set avalon specific environments. @@ -886,6 +917,9 @@ def boot(): os.environ["OPENPYPE_MONGO"] = openpype_mongo os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" # name of Pype database + _print(">>> run disk mapping command ...") + run_disk_mapping_commands(openpype_mongo) + # Get openpype path from database and set it to environment so openpype can # find its versions there and bootstrap them. openpype_path = get_openpype_path_from_db(openpype_mongo) From 97b0a72ca9e2755977234645afc08c4dc6cb17f1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 29 Sep 2021 18:57:35 +0200 Subject: [PATCH 387/450] Fixed returncode for repeatable starts --- start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.py b/start.py index 49500fcf8e..6ab920c853 100644 --- a/start.py +++ b/start.py @@ -296,7 +296,7 @@ def run_disk_mapping_commands(mongo_url): _print("disk mapping args:: {}".format(args)) try: output = subprocess.Popen(args) - if output.returncode != 0: + if output.returncode and output.returncode != 0: exc_msg = "Executing arguments was not successful: \"{}\"".format( args) From 021f1a7ccd62e4bbac20bc661346e5bd460ee064 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 29 Sep 2021 19:03:45 +0200 Subject: [PATCH 388/450] Fixed possibility of multiple drives --- start.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/start.py b/start.py index 6ab920c853..e772dfc6b3 100644 --- a/start.py +++ b/start.py @@ -292,18 +292,18 @@ def run_disk_mapping_commands(mongo_url): for mapping in disk_mapping.get(low_platform): source, destination = mapping - args = ["subst", destination.rstrip('/'), source.rstrip('/')] - _print("disk mapping args:: {}".format(args)) - try: - output = subprocess.Popen(args) - if output.returncode and output.returncode != 0: - exc_msg = "Executing arguments was not successful: \"{}\"".format( - args) + args = ["subst", destination.rstrip('/'), source.rstrip('/')] + _print("disk mapping args:: {}".format(args)) + try: + output = subprocess.Popen(args) + if output.returncode and output.returncode != 0: + exc_msg = "Executing args was not successful: \"{}\"".format( + args) - raise RuntimeError(exc_msg) - except TypeError: - _print("Error in mapping drive") - raise + raise RuntimeError(exc_msg) + except TypeError: + _print("Error in mapping drive") + raise def set_avalon_environments(): From 8c53b3f1ce0129ee91f6d2430138cbb8e91f4c0c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Sep 2021 12:12:32 +0200 Subject: [PATCH 389/450] removing `still` family for integration keeping only `image` family --- openpype/hosts/nuke/plugins/load/load_image.py | 2 +- openpype/hosts/nuke/plugins/publish/extract_render_local.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_writes.py | 3 +++ openpype/settings/defaults/project_settings/nuke.json | 3 +-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index afd1a173b6..9b8bc43d12 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -12,7 +12,7 @@ from openpype.hosts.nuke.api.lib import ( class LoadImage(api.Loader): """Load still image into Nuke""" - families = ["render", "source", "plate", "review", "image", "still"] + families = ["render", "source", "plate", "review", "image"] representations = ["exr", "dpx", "jpg", "jpeg", "png", "psd", "tiff"] label = "Load Image" diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index 253fc5e6a3..bc7b41c733 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -100,7 +100,7 @@ class NukeRenderLocal(openpype.api.Extractor): families.remove('prerender.local') families.insert(0, "prerender") elif "still.local" in families: - instance.data['family'] = 'still' + instance.data['family'] = 'image' families.remove('still.local') instance.data["families"] = families diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index 4d9bf26457..189f28f7c6 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -102,6 +102,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): if collected_frames_len == 1: representation['files'] = collected_frames.pop() + if "still" in _families_test: + instance.data['family'] = 'image' + instance.data["families"].remove('still') else: representation['files'] = collected_frames instance.data["representations"].append(representation) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 0ea6c47027..aa59c5bcfd 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -119,8 +119,7 @@ "render", "prerender", "review", - "image", - "still" + "image" ], "representations": [ "exr", From df154cb8a013f846e311ac5d2efb47d9818c726e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Sep 2021 12:14:41 +0200 Subject: [PATCH 390/450] remove `still` family from integrate new --- openpype/plugins/publish/integrate_new.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 13815d5dd5..3bff3ff79c 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -86,7 +86,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "source", "matchmove", "image", - "still", "source", "assembly", "fbx", From d3ccbded827aa052e6c8ce76f1b7db79a3d47b51 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 30 Sep 2021 14:21:52 +0200 Subject: [PATCH 391/450] Fixed label wrapper --- .../settings/entities/schemas/system_schema/schema_general.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index 81d38dc668..31cd997d14 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -44,6 +44,7 @@ "type": "dict", "key": "disk_mapping", "label": "Disk mapping", + "use_label_wrap": false, "collapsible": false, "children": [ { From 9db9190d047a152f30391af72f210af7b4888910 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 30 Sep 2021 14:22:08 +0200 Subject: [PATCH 392/450] Updated documentation --- website/docs/admin_settings_system.md | 3 +++ .../settings/settings_system_general.png | Bin 17313 -> 33586 bytes 2 files changed, 3 insertions(+) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 80154356af..6057ed0830 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -21,6 +21,9 @@ as a naive barier to prevent artists from accidental setting changes. **`Environment`** - Globally applied environment variables that will be appended to any OpenPype process in the studio. +**`Disk mapping`** - Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up. +Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume). + **`Versions Repository`** - Location where automatic update mechanism searches for zip files with OpenPype update packages. To read more about preparing OpenPype for automatic updates go to [Admin Distribute docs](admin_distribute#2-openpype-codebase) diff --git a/website/docs/assets/settings/settings_system_general.png b/website/docs/assets/settings/settings_system_general.png index 4c1452d0d474b8605c6e0438641e2f54593cd906..d04586205d24ee30445a80ce54330b2c3c03e77c 100644 GIT binary patch literal 33586 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 literal 17313 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
    Date: Thu, 30 Sep 2021 15:59:09 +0200 Subject: [PATCH 393/450] Fix - safer handling of no presets --- .../modules/default_modules/sync_server/providers/sftp.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/sftp.py b/openpype/modules/default_modules/sync_server/providers/sftp.py index 3b480be8b6..c7a6e9a620 100644 --- a/openpype/modules/default_modules/sync_server/providers/sftp.py +++ b/openpype/modules/default_modules/sync_server/providers/sftp.py @@ -42,17 +42,18 @@ class SFTPHandler(AbstractProvider): self.project_name = project_name self.site_name = site_name self.root = None + self.conn = None self.presets = presets if not self.presets: - log.info("Sync Server: There are no presets for {}.". - format(site_name)) + log.warning("Sync Server: There are no presets for {}.". + format(site_name)) return provider_presets = self.presets.get(self.CODE) if not provider_presets: msg = "Sync Server: No provider presets for {}".format(self.CODE) - log.info(msg) + log.warning(msg) return # store to instance for reconnect From 772492ecf40d679db297ae0dc3e90632ef469d90 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 30 Sep 2021 16:22:37 +0200 Subject: [PATCH 394/450] Revert unwanted --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 2aed427da6..a0f0356e5c 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 2aed427da67f63806455e3fb0d91b80cda38b685 +Subproject commit a0f0356e5c65a25532fe73caa4d471f95d386d49 From 1549deba939877b876a4363f8c542200c5a2b8ea Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 30 Sep 2021 16:25:25 +0200 Subject: [PATCH 395/450] Revert unwanted second try --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index a0f0356e5c..8aee68fa10 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit a0f0356e5c65a25532fe73caa4d471f95d386d49 +Subproject commit 8aee68fa10ab4d79be1a91e7728a609748e7c3c6 From 8eab5e09b1ae2e9078a2519348517d439def741b Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 30 Sep 2021 18:47:25 +0200 Subject: [PATCH 396/450] Fix docstring on extract review --- openpype/plugins/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index f5d6789dd4..a60dbfcf8b 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -30,8 +30,8 @@ class ExtractReview(pyblish.api.InstancePlugin): otherwise the representation is ignored. All new representations are created and encoded by ffmpeg following - presets found in `pype-config/presets/plugins/global/ - publish.json:ExtractReview:outputs`. + presets found in OpenPype Settings interface at + `project_settings/global/publish/ExtractReview/profiles:outputs`. """ label = "Extract Review" From d543203344055a525c11924da73d1fbf304d7a3c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 11:15:38 +0200 Subject: [PATCH 397/450] use template_publish_plugin for validate version --- .../schemas/schema_global_publish.json | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 8613743ef2..c50f383f02 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -25,22 +25,12 @@ ] }, { - "type": "dict", - "collapsible": true, - "checkbox_key": "enabled", - "key": "ValidateVersion", - "label": "Validate Version", - "is_group": true, - "children": [ + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "boolean", - "key": "optional", - "label": "Optional" + "key": "ValidateVersion", + "label": "Validate Version" } ] }, From f8037602131b3751df6430b55df3936617fa0d95 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Fri, 1 Oct 2021 11:25:45 +0200 Subject: [PATCH 398/450] Disable Relative paths --- openpype/hosts/nuke/api/lib.py | 3 +- openpype/hosts/nuke/startup/write_to_read.py | 41 +++++++++++--------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 8948cb4d78..a7c4ec6f41 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -288,7 +288,8 @@ def script_name(): def add_button_write_to_read(node): name = "createReadNode" label = "Create Read From Rendered" - value = "import write_to_read;write_to_read.write_to_read(nuke.thisNode())" + value = "import write_to_read;\ + write_to_read.write_to_read(nuke.thisNode(), allow_relative=False)" knob = nuke.PyScript_Knob(name, label, value) knob.clearFlag(nuke.STARTLINE) node.addKnob(knob) diff --git a/openpype/hosts/nuke/startup/write_to_read.py b/openpype/hosts/nuke/startup/write_to_read.py index 295a6e3c85..f5cf66b357 100644 --- a/openpype/hosts/nuke/startup/write_to_read.py +++ b/openpype/hosts/nuke/startup/write_to_read.py @@ -9,7 +9,9 @@ SINGLE_FILE_FORMATS = ['avi', 'mp4', 'mxf', 'mov', 'mpg', 'mpeg', 'wmv', 'm4v', 'm2v'] -def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame): +def evaluate_filepath_new( + k_value, k_eval, project_dir, first_frame, allow_relative): + # get combined relative path combined_relative_path = None if k_eval is not None and project_dir is not None: @@ -26,8 +28,9 @@ def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame): combined_relative_path = None try: - k_value = k_value % first_frame - if os.path.exists(k_value): + # k_value = k_value % first_frame + if os.path.isdir(os.path.basename(k_value)): + # doesn't check for file, only parent dir filepath = k_value elif os.path.exists(k_eval): filepath = k_eval @@ -37,10 +40,12 @@ def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame): filepath = os.path.abspath(filepath) except Exception as E: - log.error("Cannot create Read node. Perhaps it needs to be rendered first :) Error: `{}`".format(E)) + log.error("Cannot create Read node. Perhaps it needs to be \ + rendered first :) Error: `{}`".format(E)) return None filepath = filepath.replace('\\', '/') + # assumes last number is a sequence counter current_frame = re.findall(r'\d+', filepath)[-1] padding = len(current_frame) basename = filepath[: filepath.rfind(current_frame)] @@ -51,11 +56,13 @@ def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame): pass else: # Image sequence needs hashes + # to do still with no number not handled filepath = basename + '#' * padding + '.' + filetype # relative path? make it relative again - if not isinstance(project_dir, type(None)): - filepath = filepath.replace(project_dir, '.') + if allow_relative: + if (not isinstance(project_dir, type(None))) and project_dir != "": + filepath = filepath.replace(project_dir, '.') # get first and last frame from disk frames = [] @@ -95,41 +102,40 @@ def create_read_node(ndata, comp_start): return -def write_to_read(gn): +def write_to_read(gn, + allow_relative=False): + comp_start = nuke.Root().knob('first_frame').value() - comp_end = nuke.Root().knob('last_frame').value() project_dir = nuke.Root().knob('project_directory').getValue() if not os.path.exists(project_dir): project_dir = nuke.Root().knob('project_directory').evaluate() group_read_nodes = [] - with gn: height = gn.screenHeight() # get group height and position new_xpos = int(gn.knob('xpos').value()) new_ypos = int(gn.knob('ypos').value()) + height + 20 group_writes = [n for n in nuke.allNodes() if n.Class() == "Write"] - print("__ group_writes: {}".format(group_writes)) if group_writes != []: # there can be only 1 write node, taking first n = group_writes[0] if n.knob('file') is not None: - file_path_new = evaluate_filepath_new( + myfile, firstFrame, lastFrame = evaluate_filepath_new( n.knob('file').getValue(), n.knob('file').evaluate(), project_dir, - comp_start + comp_start, + allow_relative ) - if not file_path_new: + if not myfile: return - myfiletranslated, firstFrame, lastFrame = file_path_new # get node data ndata = { - 'filepath': myfiletranslated, - 'firstframe': firstFrame, - 'lastframe': lastFrame, + 'filepath': myfile, + 'firstframe': int(firstFrame), + 'lastframe': int(lastFrame), 'new_xpos': new_xpos, 'new_ypos': new_ypos, 'colorspace': n.knob('colorspace').getValue(), @@ -139,7 +145,6 @@ def write_to_read(gn): } group_read_nodes.append(ndata) - # create reads in one go for oneread in group_read_nodes: # create read node From 31ce3f9fec4946c1628e8ad970709b92574de3bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 14:48:51 +0200 Subject: [PATCH 399/450] get rid of decompose_url and compose_url --- igniter/tools.py | 107 ++++------------------------------------------- 1 file changed, 7 insertions(+), 100 deletions(-) diff --git a/igniter/tools.py b/igniter/tools.py index c934289064..4e31601665 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -22,89 +22,6 @@ from pymongo.errors import ( ) -def decompose_url(url: str) -> Dict: - """Decompose mongodb url to its separate components. - - Args: - url (str): Mongodb url. - - Returns: - dict: Dictionary of components. - - """ - components = { - "scheme": None, - "host": None, - "port": None, - "username": None, - "password": None, - "auth_db": None - } - - result = urlparse(url) - if result.scheme is None: - _url = "mongodb://{}".format(url) - result = urlparse(_url) - - components["scheme"] = result.scheme - components["host"] = result.hostname - try: - components["port"] = result.port - except ValueError: - raise RuntimeError("invalid port specified") - components["username"] = result.username - components["password"] = result.password - - try: - components["auth_db"] = parse_qs(result.query)['authSource'][0] - except KeyError: - # no auth db provided, mongo will use the one we are connecting to - pass - - return components - - -def compose_url(scheme: str = None, - host: str = None, - username: str = None, - password: str = None, - port: int = None, - auth_db: str = None) -> str: - """Compose mongodb url from its individual components. - - Args: - scheme (str, optional): - host (str, optional): - username (str, optional): - password (str, optional): - port (str, optional): - auth_db (str, optional): - - Returns: - str: mongodb url - - """ - - url = "{scheme}://" - - if username and password: - url += "{username}:{password}@" - - url += "{host}" - if port: - url += ":{port}" - - if auth_db: - url += "?authSource={auth_db}" - - return url.format(**{ - "scheme": scheme, - "host": host, - "username": username, - "password": password, - "port": port, - "auth_db": auth_db - }) def validate_mongo_connection(cnx: str) -> (bool, str): @@ -121,12 +38,14 @@ def validate_mongo_connection(cnx: str) -> (bool, str): if parsed.scheme not in ["mongodb", "mongodb+srv"]: return False, "Not mongodb schema" + kwargs = { + "serverSelectionTimeoutMS": 2000 + } try: - client = MongoClient( - cnx, - serverSelectionTimeoutMS=2000 - ) + client = MongoClient(cnx, **kwargs) client.server_info() + with client.start_session(): + pass client.close() except ServerSelectionTimeoutError as e: return False, f"Cannot connect to server {cnx} - {e}" @@ -195,21 +114,9 @@ def get_openpype_global_settings(url: str) -> dict: Returns: dict: With settings data. Empty dictionary is returned if not found. """ - try: - components = decompose_url(url) - except RuntimeError: - return {} - mongo_kwargs = { - "host": compose_url(**components), - "serverSelectionTimeoutMS": 2000 - } - port = components.get("port") - if port is not None: - mongo_kwargs["port"] = int(port) - try: # Create mongo connection - client = MongoClient(**mongo_kwargs) + client = MongoClient(url) # Access settings collection col = client["openpype"]["settings"] # Query global settings From 6cad9d50eac6646f64a548af334773fb064af6d6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 14:52:09 +0200 Subject: [PATCH 400/450] skip scheme validation which happens in validate_mongo_connection --- igniter/tools.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/igniter/tools.py b/igniter/tools.py index 4e31601665..f465d43597 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -71,10 +71,7 @@ def validate_mongo_string(mongo: str) -> (bool, str): """ if not mongo: return True, "empty string" - parsed = urlparse(mongo) - if parsed.scheme in ["mongodb", "mongodb+srv"]: - return validate_mongo_connection(mongo) - return False, "not valid mongodb schema" + return validate_mongo_connection(mongo) def validate_path_string(path: str) -> (bool, str): From 76dd29daa72d48972a0fbb85b41fb7f2389e97c0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 14:55:40 +0200 Subject: [PATCH 401/450] implemented add_certificate_path_to_mongo_url which adds certificate path to mongo url --- igniter/tools.py | 58 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/igniter/tools.py b/igniter/tools.py index f465d43597..a8ff708f90 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -1,18 +1,12 @@ # -*- coding: utf-8 -*- -"""Tools used in **Igniter** GUI. - -Functions ``compose_url()`` and ``decompose_url()`` are the same as in -``openpype.lib`` and they are here to avoid importing OpenPype module before its -version is decided. - -""" -import sys +"""Tools used in **Igniter** GUI.""" import os -from typing import Dict, Union -from urllib.parse import urlparse, parse_qs +from typing import Union +from urllib.parse import urlparse, parse_qs, ParseResult from pathlib import Path import platform +import certifi from pymongo import MongoClient from pymongo.errors import ( ServerSelectionTimeoutError, @@ -22,6 +16,50 @@ from pymongo.errors import ( ) +def add_certificate_path_to_mongo_url(mongo_url): + """Check if should add ca certificate to mongo url. + + Since 30.9.2021 cloud mongo requires newer certificates that are not + available on most of workstation. This adds path to certifi certificate + which is valid for it. To add the certificate path url must have scheme + 'mongodb+srv' or has 'ssl=true' or 'tls=true' in url query. + """ + parsed = urlparse(mongo_url) + query = parse_qs(parsed.query) + lowered_query_keys = set(key.lower() for key in query.keys()) + add_certificate = False + # Check if url 'ssl' or 'tls' are set to 'true' + for key in ("ssl", "tls"): + if key in query and "true" in query["ssl"]: + add_certificate = True + break + + # Check if url contains 'mongodb+srv' + if not add_certificate and parsed.scheme == "mongodb+srv": + add_certificate = True + + # Check if url does already contain certificate path + if add_certificate and "tlscafile" in lowered_query_keys: + add_certificate = False + + # Add certificate path to mongo url + if add_certificate: + path = parsed.path + if not path: + path = "/admin" + query = parsed.query + tls_query = "tlscafile={}".format(certifi.where()) + if not query: + query = tls_query + else: + query = "&".join((query, tls_query)) + new_url = ParseResult( + parsed.scheme, parsed.netloc, path, + parsed.params, query, parsed.fragment + ) + mongo_url = new_url.geturl() + + return mongo_url def validate_mongo_connection(cnx: str) -> (bool, str): From be191b0c932f575ff772a8ab3b31a3c12edcb0ea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 14:55:57 +0200 Subject: [PATCH 402/450] use 'add_certificate_path_to_mongo_url' on openpype start --- igniter/tools.py | 3 +++ start.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/igniter/tools.py b/igniter/tools.py index a8ff708f90..ae680bf1f1 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -79,6 +79,9 @@ def validate_mongo_connection(cnx: str) -> (bool, str): kwargs = { "serverSelectionTimeoutMS": 2000 } + # Add certificate path if should be required + cnx = add_certificate_path_to_mongo_url(cnx) + try: client = MongoClient(cnx, **kwargs) client.server_info() diff --git a/start.py b/start.py index 689efbdac1..3c345200c3 100644 --- a/start.py +++ b/start.py @@ -193,6 +193,7 @@ import igniter # noqa: E402 from igniter import BootstrapRepos # noqa: E402 from igniter.tools import ( get_openpype_path_from_db, + add_certificate_path_to_mongo_url, validate_mongo_connection ) # noqa from igniter.bootstrap_repos import OpenPypeVersion # noqa: E402 @@ -585,7 +586,7 @@ def _determine_mongodb() -> str: except ValueError: raise RuntimeError("Missing MongoDB url") - return openpype_mongo + return add_certificate_path_to_mongo_url(openpype_mongo) def _initialize_environment(openpype_version: OpenPypeVersion) -> None: From d684cac9bf7e267e6293feeee0d296574f95f6b7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 14:56:28 +0200 Subject: [PATCH 403/450] added session check to ftrack mongo connection --- .../default_modules/ftrack/ftrack_server/event_server_cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py index 075694d8f6..7061b34ab3 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py @@ -40,6 +40,8 @@ def check_mongo_url(mongo_uri, log_error=False): # Force connection on a request as the connect=True parameter of # MongoClient seems to be useless here client.server_info() + with client.start_session(): + pass client.close() except pymongo.errors.ServerSelectionTimeoutError as err: if log_error: From 4fd4b8e2e8bfbac7218050b7f15dce9bd4d51038 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 16:33:06 +0200 Subject: [PATCH 404/450] fix import of `get_openpype_global_settings` --- start.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/start.py b/start.py index 3c345200c3..6861355600 100644 --- a/start.py +++ b/start.py @@ -102,9 +102,6 @@ import subprocess import site from pathlib import Path -from igniter.tools import get_openpype_global_settings - - # OPENPYPE_ROOT is variable pointing to build (or code) directory # WARNING `OPENPYPE_ROOT` must be defined before igniter import # - igniter changes cwd which cause that filepath of this script won't lead @@ -192,6 +189,7 @@ else: import igniter # noqa: E402 from igniter import BootstrapRepos # noqa: E402 from igniter.tools import ( + get_openpype_global_settings, get_openpype_path_from_db, add_certificate_path_to_mongo_url, validate_mongo_connection From 0b9160c2fef1e16c6250aa1fdc85a77186403657 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Fri, 1 Oct 2021 17:27:59 +0200 Subject: [PATCH 405/450] check if files exists by path with hashes --- openpype/lib/delivery.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index 943cd9fcaf..5735cbc99d 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -1,9 +1,11 @@ """Functions useful for delivery action or loader""" import os import shutil +import glob import clique import collections + def collect_frames(files): """ Returns dict of source path and its frame, if from sequence @@ -228,7 +230,16 @@ def process_sequence( Returns: (collections.defaultdict , int) """ - if not os.path.exists(src_path): + + def hash_path_exist(myPath): + res = myPath.replace('#', '*') + glob_search_results = glob.glob(res) + if len(glob_search_results) > 0: + return True + else: + return False + + if not hash_path_exist(src_path): msg = "{} doesn't exist for {}".format(src_path, repre["_id"]) report_items["Source file was not found"].append(msg) From b7581c4c7dab7d78d6bf0d3954d7eec535c2fdec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:14:09 +0200 Subject: [PATCH 406/450] event server cli uses validate mongo url from openpype.lib --- .../ftrack/ftrack_server/event_server_cli.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py index 7061b34ab3..1a76905b38 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py @@ -17,7 +17,8 @@ from openpype.lib import ( get_pype_execute_args, OpenPypeMongoConnection, get_openpype_version, - get_build_version + get_build_version, + validate_mongo_connection ) from openpype_modules.ftrack import FTRACK_MODULE_DIR from openpype_modules.ftrack.lib import credentials @@ -36,13 +37,15 @@ class MongoPermissionsError(Exception): def check_mongo_url(mongo_uri, log_error=False): """Checks if mongo server is responding""" try: - client = pymongo.MongoClient(mongo_uri) - # Force connection on a request as the connect=True parameter of - # MongoClient seems to be useless here - client.server_info() - with client.start_session(): - pass - client.close() + validate_mongo_connection(mongo_uri) + + except pymongo.errors.InvalidURI as err: + if log_error: + print("Can't connect to MongoDB at {} because: {}".format( + mongo_uri, err + )) + return False + except pymongo.errors.ServerSelectionTimeoutError as err: if log_error: print("Can't connect to MongoDB at {} because: {}".format( From 06c3076c993b0414824fae7be8f31c7d2c44ef02 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:15:06 +0200 Subject: [PATCH 407/450] ssl certificate filepath is not added to mongo connection string but is added as argument to MongoClient --- igniter/tools.py | 31 +++++++++---------------------- start.py | 3 +-- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/igniter/tools.py b/igniter/tools.py index ae680bf1f1..cb06fdaee2 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -16,7 +16,7 @@ from pymongo.errors import ( ) -def add_certificate_path_to_mongo_url(mongo_url): +def should_add_certificate_path_to_mongo_url(mongo_url): """Check if should add ca certificate to mongo url. Since 30.9.2021 cloud mongo requires newer certificates that are not @@ -41,25 +41,7 @@ def add_certificate_path_to_mongo_url(mongo_url): # Check if url does already contain certificate path if add_certificate and "tlscafile" in lowered_query_keys: add_certificate = False - - # Add certificate path to mongo url - if add_certificate: - path = parsed.path - if not path: - path = "/admin" - query = parsed.query - tls_query = "tlscafile={}".format(certifi.where()) - if not query: - query = tls_query - else: - query = "&".join((query, tls_query)) - new_url = ParseResult( - parsed.scheme, parsed.netloc, path, - parsed.params, query, parsed.fragment - ) - mongo_url = new_url.geturl() - - return mongo_url + return add_certificate def validate_mongo_connection(cnx: str) -> (bool, str): @@ -80,7 +62,8 @@ def validate_mongo_connection(cnx: str) -> (bool, str): "serverSelectionTimeoutMS": 2000 } # Add certificate path if should be required - cnx = add_certificate_path_to_mongo_url(cnx) + if should_add_certificate_path_to_mongo_url(cnx): + kwargs["ssl_ca_certs"] = certifi.where() try: client = MongoClient(cnx, **kwargs) @@ -152,9 +135,13 @@ def get_openpype_global_settings(url: str) -> dict: Returns: dict: With settings data. Empty dictionary is returned if not found. """ + kwargs = {} + if should_add_certificate_path_to_mongo_url(url): + kwargs["ssl_ca_certs"] = certifi.where() + try: # Create mongo connection - client = MongoClient(url) + client = MongoClient(url, **kwargs) # Access settings collection col = client["openpype"]["settings"] # Query global settings diff --git a/start.py b/start.py index 6861355600..ada613b4eb 100644 --- a/start.py +++ b/start.py @@ -191,7 +191,6 @@ from igniter import BootstrapRepos # noqa: E402 from igniter.tools import ( get_openpype_global_settings, get_openpype_path_from_db, - add_certificate_path_to_mongo_url, validate_mongo_connection ) # noqa from igniter.bootstrap_repos import OpenPypeVersion # noqa: E402 @@ -584,7 +583,7 @@ def _determine_mongodb() -> str: except ValueError: raise RuntimeError("Missing MongoDB url") - return add_certificate_path_to_mongo_url(openpype_mongo) + return openpype_mongo def _initialize_environment(openpype_version: OpenPypeVersion) -> None: From 53fe16fffcea8225eaa949ced837fa31b05f85da Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:15:30 +0200 Subject: [PATCH 408/450] created copy of 'should_add_certificate_path_to_mongo_url' in openpype.lib.mongo --- openpype/lib/mongo.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index 8bfaba75d6..054f40a5b0 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -3,6 +3,7 @@ import sys import time import logging import pymongo +import certifi if sys.version_info[0] == 2: from urlparse import urlparse, parse_qs @@ -93,6 +94,35 @@ def extract_port_from_url(url): return parsed_url.port +def should_add_certificate_path_to_mongo_url(mongo_url): + """Check if should add ca certificate to mongo url. + + Since 30.9.2021 cloud mongo requires newer certificates that are not + available on most of workstation. This adds path to certifi certificate + which is valid for it. To add the certificate path url must have scheme + 'mongodb+srv' or has 'ssl=true' or 'tls=true' in url query. + """ + parsed = urlparse(mongo_url) + query = parse_qs(parsed.query) + lowered_query_keys = set(key.lower() for key in query.keys()) + add_certificate = False + # Check if url 'ssl' or 'tls' are set to 'true' + for key in ("ssl", "tls"): + if key in query and "true" in query["ssl"]: + add_certificate = True + break + + # Check if url contains 'mongodb+srv' + if not add_certificate and parsed.scheme == "mongodb+srv": + add_certificate = True + + # Check if url does already contain certificate path + if add_certificate and "tlscafile" in lowered_query_keys: + add_certificate = False + + return add_certificate + + def validate_mongo_connection(mongo_uri): """Check if provided mongodb URL is valid. From 5782d6d801b67afe62ea3427291e3a4f484728af Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:17:27 +0200 Subject: [PATCH 409/450] added ability to change retry times of connecting to mongo --- openpype/lib/mongo.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index 054f40a5b0..3630b1320f 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -192,7 +192,7 @@ class OpenPypeMongoConnection: return connection @classmethod - def create_connection(cls, mongo_url, timeout=None): + def create_connection(cls, mongo_url, timeout=None, retry_attempts=None): if timeout is None: timeout = int(os.environ.get("AVALON_TIMEOUT") or 1000) @@ -206,8 +206,13 @@ class OpenPypeMongoConnection: kwargs["port"] = int(port) mongo_client = pymongo.MongoClient(**kwargs) + if retry_attempts is None: + retry_attempts = 3 - for _retry in range(3): + elif not retry_attempts: + retry_attempts = 1 + + for _retry in range(retry_attempts): try: t1 = time.time() mongo_client.server_info() From 23cc014f3299f4f71923f214f2d15b39edb7fe9b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:25:45 +0200 Subject: [PATCH 410/450] removed useless extract_port_from_url --- openpype/lib/mongo.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index 3630b1320f..7527fce3b9 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -86,14 +86,6 @@ def get_default_components(): return decompose_url(mongo_url) -def extract_port_from_url(url): - parsed_url = urlparse(url) - if parsed_url.scheme is None: - _url = "mongodb://{}".format(url) - parsed_url = urlparse(_url) - return parsed_url.port - - def should_add_certificate_path_to_mongo_url(mongo_url): """Check if should add ca certificate to mongo url. @@ -201,9 +193,6 @@ class OpenPypeMongoConnection: "serverSelectionTimeoutMS": timeout } - port = extract_port_from_url(mongo_url) - if port is not None: - kwargs["port"] = int(port) mongo_client = pymongo.MongoClient(**kwargs) if retry_attempts is None: From 0c00a50417220d21cdb9008fcc9f1413df08f59a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:25:59 +0200 Subject: [PATCH 411/450] added validation of mongo url --- openpype/lib/mongo.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index 7527fce3b9..64c46eac68 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -185,6 +185,14 @@ class OpenPypeMongoConnection: @classmethod def create_connection(cls, mongo_url, timeout=None, retry_attempts=None): + parsed = urlparse(mongo_url) + # Force validation of scheme + if parsed.scheme not in ["mongodb", "mongodb+srv"]: + raise pymongo.errors.InvalidURI(( + "Invalid URI scheme:" + " URI must begin with 'mongodb://' or 'mongodb+srv://'" + )) + if timeout is None: timeout = int(os.environ.get("AVALON_TIMEOUT") or 1000) From 3448e1ce78803b89980bd295c1f77ca2d4322984 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:26:51 +0200 Subject: [PATCH 412/450] moved time start out of loop --- openpype/lib/mongo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index 64c46eac68..cadf81b63b 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -209,9 +209,9 @@ class OpenPypeMongoConnection: elif not retry_attempts: retry_attempts = 1 + t1 = time.time() for _retry in range(retry_attempts): try: - t1 = time.time() mongo_client.server_info() except Exception: From 2714c644f25ad1d9fe4a85e730d6feb6c076ab93 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:27:16 +0200 Subject: [PATCH 413/450] pass mongo url as argument instead of kwarg --- openpype/lib/mongo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index cadf81b63b..a192427c81 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -197,12 +197,11 @@ class OpenPypeMongoConnection: timeout = int(os.environ.get("AVALON_TIMEOUT") or 1000) kwargs = { - "host": mongo_url, "serverSelectionTimeoutMS": timeout } + mongo_client = pymongo.MongoClient(mongo_url, **kwargs) - mongo_client = pymongo.MongoClient(**kwargs) if retry_attempts is None: retry_attempts = 3 From 95de5d15f55fb640d7d1bbf1ac100f4df2562559 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:37:10 +0200 Subject: [PATCH 414/450] store exception and reraise it when connection is not successful --- openpype/lib/mongo.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index a192427c81..fcfc4a62f3 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -208,23 +208,27 @@ class OpenPypeMongoConnection: elif not retry_attempts: retry_attempts = 1 + last_exc = None + valid = False t1 = time.time() - for _retry in range(retry_attempts): + for attempt in range(1, retry_attempts + 1): try: mongo_client.server_info() - - except Exception: - cls.log.warning("Retrying...") - time.sleep(1) - timeout *= 1.5 - - else: + with mongo_client.start_session(): + pass + valid = True break - else: - raise IOError(( - "ERROR: Couldn't connect to {} in less than {:.3f}ms" - ).format(mongo_url, timeout)) + except Exception as exc: + last_exc = exc + if attempt < retry_attempts: + cls.log.warning( + "Attempt {} failed. Retrying... ".format(attempt) + ) + time.sleep(1) + + if not valid: + raise last_exc cls.log.info("Connected to {}, delay {:.3f}s".format( mongo_url, time.time() - t1 From 4d4c01519ebb6a6b0e5912fe777922f2c97c1f42 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:37:28 +0200 Subject: [PATCH 415/450] validate mongo connection uses `create_connection` --- openpype/lib/mongo.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index fcfc4a62f3..d27ec99aa2 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -128,26 +128,9 @@ def validate_mongo_connection(mongo_uri): passed so probably couldn't connect to mongo server. """ - parsed = urlparse(mongo_uri) - # Force validation of scheme - if parsed.scheme not in ["mongodb", "mongodb+srv"]: - raise pymongo.errors.InvalidURI(( - "Invalid URI scheme:" - " URI must begin with 'mongodb://' or 'mongodb+srv://'" - )) - # we have mongo connection string. Let's try if we can connect. - components = decompose_url(mongo_uri) - mongo_args = { - "host": compose_url(**components), - "serverSelectionTimeoutMS": 1000 - } - port = components.get("port") - if port is not None: - mongo_args["port"] = int(port) - - # Create connection - client = pymongo.MongoClient(**mongo_args) - client.server_info() + client = OpenPypeMongoConnection.create_connection( + mongo_uri, retry_attempts=1 + ) client.close() From f4e58771a2c8cdeec7052be44ad89f80a28531fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:37:39 +0200 Subject: [PATCH 416/450] add ssla ca certificate if should --- openpype/lib/mongo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index d27ec99aa2..c758f0d73c 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -182,6 +182,8 @@ class OpenPypeMongoConnection: kwargs = { "serverSelectionTimeoutMS": timeout } + if should_add_certificate_path_to_mongo_url(mongo_url): + kwargs["ssl_ca_certs"] = certifi.where() mongo_client = pymongo.MongoClient(mongo_url, **kwargs) From 0dec31288b2c33afd51d7ef3a542546aa83a2f4c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:48:26 +0200 Subject: [PATCH 417/450] added session validation to get_mongo_connection --- openpype/lib/mongo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index c758f0d73c..0fd4517b5b 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -156,6 +156,8 @@ class OpenPypeMongoConnection: # Naive validation of existing connection try: connection.server_info() + with connection.start_session(): + pass except Exception: connection = None From 39f8e1dc4200fbf560d1fadb5bd2f1d407e92d31 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 1 Oct 2021 18:48:43 +0200 Subject: [PATCH 418/450] removed unused import --- igniter/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/igniter/tools.py b/igniter/tools.py index cb06fdaee2..04d7451335 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -2,7 +2,7 @@ """Tools used in **Igniter** GUI.""" import os from typing import Union -from urllib.parse import urlparse, parse_qs, ParseResult +from urllib.parse import urlparse, parse_qs from pathlib import Path import platform From dbfc44ec6d36c7488ff68531c8884cc5e576bb0f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Oct 2021 18:55:46 +0200 Subject: [PATCH 419/450] Added documentation --- .../site_sync_project_sftp_settings.png | Bin 0 -> 16385 bytes ...e_sync_sftp_project_setting_not_forced.png | Bin 0 -> 21718 bytes .../assets/site_sync_sftp_settings_local.png | Bin 0 -> 7415 bytes website/docs/assets/site_sync_sftp_system.png | Bin 0 -> 13112 bytes website/docs/module_site_sync.md | 30 ++++++++++++++++++ 5 files changed, 30 insertions(+) create mode 100644 website/docs/assets/site_sync_project_sftp_settings.png create mode 100644 website/docs/assets/site_sync_sftp_project_setting_not_forced.png create mode 100644 website/docs/assets/site_sync_sftp_settings_local.png create mode 100644 website/docs/assets/site_sync_sftp_system.png diff --git a/website/docs/assets/site_sync_project_sftp_settings.png b/website/docs/assets/site_sync_project_sftp_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..79267ba1d77e682c8fa53e3164c6509575fa5894 GIT binary patch literal 16385 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

    >HeTp&T@%pixR$fb_oca+tpcCM zszNK;N$hs_YXJY4tJw(c^q{_?ZQ&IQrvI3Py6o{x$3k4 zTzA@%W|0Tp{JL?|{H7jSO{~^9@cceC&2{ax&|9SLH6j;I*OuLQG#fVv zv(TP_&6BTS-;;e9pXyk#H0$fr(P|iLe?zvG%YhGWnc4sNpWBrvMHuc zK1o1N>0naYRUTcL`CEqVTb7{su%+3k?jWbfGt{7k9C)yuZmjt)|48782k$?EQqkN* zR-|)r^42X~>RoIIPkc9=VOmlDoTzxNJ@V z4_$^_<4a7h|5{|XH)t56;rGA?sdFh7Ejx)v$8*gq#zjkU(gdS}PmH3yxyJ-kHTAjy zdQ>uezAjI6Nap_QjTxs;w$U&GJ0f80Th3ivcW{Et9wNDJ3)-8N4{!qIf93>$()zbL zO|HmqX4v#3=cueTaV7*`472sWdbAI>-8&B9WDGf~bc*U3Z&+GD;%-xT%bykWZu0Hf zWO!$1*Y6^&@Fc+up{qMJ2pBLTEI3}4 z9HQ$)()VW~j$*PU%+&m+I)cBax2B{{oxX0u6*&|_G zGO(p5%(6Ob?Y0ncI0FP$iNmr$SSjn<_V$SPK{tcFM5t3TY57z<_8!3H(AwO{JM-aS zGoPa^?Vf?upJLCJRB&`E4&T>P8><%^#aZfD54@;*+N-ZSz{n!Rs%L}iropTN*Anny zh8M?HORi7_B_lz6&{2W9jd(-Pbpcfy%Vcye_NAqWqc_@%fy7dewX3N_Lu_}sPvHtVw!c>RJ!_Q2xW{ui_fh%aAS>6kRpKAluV&w{?t~l|$+Mw(06nvdJ zSGGqzFAK$ldu7Y2^nnO)jI!aWCZo2MKL&G`w%#2j5qeeD`1Su{ zs0r-^cPzoUlSs`?)Pc(|W&Q3k!zA}c5vmK@G7YT$t)pG;PSG$B|zgPakvmT z|Iz7}kZ497{qe|poqa*kT$cRcJWm%&vz|Wobo!*qR(W-r7j*WON1TZR(Fw8_=~Fa> zUA!V!$ITyD85HbJVsurxdwbJ%2vdo`SK zy<{y`xfor;Ecj&dEj&K(*=8oLHVdc9{VH#fUW z!+TGY{=zkpJLXOyx+`^{g=&B`L{~1I`4Mw>{zk~g!&?s1wZ;6~k)xyleR)lFi8ocX zkN$2|+tImrs8EvhG(R_dFL?^w2&gXr&+0C3wB_Y@G!Q&lK|7`SNk}pqUW|MN?fG z0Ip;o3RlpvCK35<6uTyOFNL_P0juatPx=QXNnKyyUYE~~S~dFZ2%bk?XG!FoB`*+- zpmbWGbp~TbR{r+EHdoPxy_z*Z%jEU}O~3NPPH<-(dx33?Slx>W>@|n+F4DSlR3boV zSlX$72UHBea*|P)Z|E(oIo4qTJTcC~W`OG)OViliPsw;wv=l#mK%|8zBX~20rb6`Qo4^^U7G0$V0rRAznDPFRU5^^w$a*w2eyyti1b#UU6pYP~W>dX|9`8*>ND=d~B}w>* zF0izO3pBj1S$YLTaXZD3h=)#EXI z6st86nDy!zF{(@JVhkhQ%pljigiEB0qRz*n*mEVf1FB)Xz^%h?!Y48#-O;@4N1+h)q20Y_MA;V#ttNHn z;k5pni;4Mal=dF9_bz;nM)2>22oUA*IP9+#mXb&!4d%9nimAL?oLDt z!AU?kAoSBNvzhvi301sNbS~r@_sZt){H8}-Onteg&Ny{zo~W@86Z)kw5^Pzg*J5dy z)A^;;W=U;a#7rbUip*mk)~+0oeP_0b(<_?{+b?(pTe)$hR@Haf^dR`1BAai<5^4>0 z8y{(M)(Dt3d!J^ok+*Y*bos?)D#T?!_(Cju0xWnB4SDs_?*cr4!l;+JQSfV?>$m7o zbHjlPF0y|w0%M=r(_V1%vx3S%Y7IEU`6E2Z8~b1{97}~NG27D-yvMV+Pizld93SMK ztiEk}R_(U=1fl6Z-Ja&wNV0q{<6|MzP)Xb#SHq&;mk3=hSf7Q1@K_CNKr2}x=+fCF z@X)#u&8vZT`4oS3X8<|s@~3u7!!!Fy!6=hqKPn7_PmS?nZ~aaGD`p)5r|+P79?m=OKp`@vI21 z`3Wx{qViB{6#$|EU`@5{Ck=bI$h{OR2iLwDA~8}==X}l87u6`ybg;}V$A@_IplCFC zaA~u3dJXD)=NDBwvmZb!wkl>?d z8iPv;fQHuZ*B>;XA5~F1J!p z1XZkv*#XBxPt09t(oKuhuFdsS4K5|q%b|1hjtVj;H6RaFT_IN!ouJr82K_dolL8JE88Kbf~#{$W#4VVI75p1!(4&uF;0 zVa=neV%70%UeR;A(R9PBE8lNSp5Sh*Bx*ODs@;*Cv9^gm4)gfS)*-3t3*&d49v7}v z=v5sDM(s%gdxp%Qlz?x&{ z&Yw93%=*IOm=j$`8b5+#S#wv+1rxdZASN4NLe6~7>vAQy&NQIaRLoc%jpN%z5`J5& zql%{BnpvpTQb00CC5IG9d&QFg`(duBhh5pX^2Fx~6|Eh)qjzCeTTw#Hm37mXv&_O> zBpqLt;7yE~r4}{nqrEo`1Ums#$O}+V@*cNUr2s#;13b|P*_m1M%_}%6YY5!hXNFuZ z6Rt_=j+#xpzgGr(v}q6a<6aj5ztM<~*4LVO0e~?Q2=s|tk>G)Yi$=~-!BLcoIZbOF zq3hshTwHcnYW*Da8b+khG$@XP;r4}qRx%553|smXGlkyrm`C;u6* z_;-nL4(RtF>qtO0Y&*(-_Iqa(^?N;{}+9Rds(bVnz1VJKY7 zQWJhy9S}U07LGT3FyR!31>4y^)5D_>Ku-ek|Gw?Xt3}XL`w;HEX~G~T72d)!KR@rj z)x76yY3ix=s@zoXF0|B$wWFg07o`}pAanGc^nr-~C3on5v$5X;fSp(n+xcmzvC-YuMz>io^VbtM zujZyx^YzX31NtDHzhpr;kOg&4*WKA<&UrjSPSj?I&3ylWm)@#3rN5I%Zc#s4`=xKO z0nmIYfqhSE|3OU~*K>t*7%#BKFUCWdx-|K!O0;wgZHve^R_? z{fhka%OMc=qAoD8>{XzsRu>?^Be!wY!U7_|*2%fmGg(6vr@nisWOe0wQPsI_+w)H? zQ+CYr{Lw`k=shrHGsT{g$N7-sKo?M_~pEa@U0h-#<}mJe@waae#zn0Uu99ZSf%A^rJ1 z71U#FycyPSk{80K`%6H}rI?nntp-SZMu7OT)~sN^*6cHvJhrtdFuZ7^ScXI=*uQNbc20Mry}TQjKD7);&xUh6eVJ}-W*L+$QEn%(z3Np zb(Q3_Njtp?Jm_hz+W5xcw~%(h#HQo%wpv6mwTp*p*kH-t#PI&$6OCOl!p`P9nIb*L zr&90Su7Fg4+-5WaCj!K%3?zeUQt7Ev!n?kLYtSESS_{CeiFZ6=`<_i^vF}*>Dfw1+G($bXK9I>ojp^hcG zwE>E&m>~h!;${N+d}#U^4gs!nsP`VSbE1CY-i5L$=AAoE4~+l!(hvK{R#abEjhbQ8 z>W4_}iZAJH%)!f*W+!a#eN7_up9+6#>{pOdTDXoKpQ~5$o?7KvoV`Ry_mk%#`YBy3 zv_nHdD>+lAx+j)7h^q|nUx>#J%-!YNj~nf(*8`>9KU*e@!9(mFiXARai_y_B*oK!? zej6(4ALHMli4b%nE|V7yaO1MT6%ppXwL!DKdeKd6o4wu2Eh|EB0Cco5Cn8FD44q%S#EACQ zVMrIc&$GMa+*Pn@M(0e3Zkr=NCMjS)%nC8@uP6hlGyYEb?>2ia{#PmL0CqH;uz|$w z>YJ}=x#Jsur=3BA7;wS80@R~Sg8v@si2F*oq)N=l$Igk z=d0>gQ4V0&uL;1xW`%`4ItZU@)l*ed{o^#i>N~JM5n+1R?}~xwsVz`qY;|;X7k=;R z)iG|27bxOz?n~iG(F%O&b_vuLNPGJ>Ac_Emp6-8l3@X@T*7jZ#{AKnl&hReC<;ovu d_gum$;}XKuvbFRFGw?A;MNv}$CTIEb{{hx`)R6!H literal 0 HcmV?d00001 diff --git a/website/docs/assets/site_sync_sftp_project_setting_not_forced.png b/website/docs/assets/site_sync_sftp_project_setting_not_forced.png new file mode 100644 index 0000000000000000000000000000000000000000..7bd735b46c8d3dce973fd2e5b288fd333f3a0c73 GIT binary patch literal 21718 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

    d{z2ghwSm-mLz1XxlZAlF|8 z^#6%VS#6YRsk69SJrH(PKB(?77ssMkVJz5TlhITVCLk%dWKXBnNFV3 z_NOEL>B6C))`Q`;)e5Ov=GmNs&T@KSy_~7{U9K`9WLW)~qXuf)lLxfeS- zuX_1M9Z2(ZkK25npyyI>(~opbbk?aFe9@>Y?Sw8mD4rXT4M;Z0O3#kGWrp46mbN~f z>f+DH?elKK{f`x8;Pjp46tr>rc24>9^=xe&?XyAAe`u!Cyq3OwOj#))UEBt4n9vdb zeCgGE7}G4^g8Zkp-T#RZ@js#-z|`ktQsjIqGSKZYPpyuz=3rXT;s0$1%YVUU0B3Vno-!9voWKK5EuMg45x=I=_qptMnwEw<>T z3(&*>Y|qMBX%84q%j<5@suv67`2G$gAaQA5Jtay2;w`eF*F1yF+V3(rakrdV8UzhF z`~xY;%uPMH?u!dE#@>3x#kGf_TCj6%ZqMbRjdgGrZ@n}4H9yYjl-*83zn5)G>5{t9 z{AB)b)y0P>x9RvGOD8t;4N~Kl*JBuESTr1g0?gu7fZ4bZAa!NBF)lBvU#|dChwaN! zgoeJ%J8A8$IH&H76PD!*SLA44!Gx4n9|MKIL!jAYK<;ueP$=MWn&LjEo?TysGg{i`)2au?3-gE^U6KdfOINVdF0C;J;S=<=aF7zD?htIMAEWjl}^b=mUHm;EJL@|809F zsi|##IpHKcWpDxcKiG<@o`F9EiTCHy&l(C4z4sj z2i^CP28>ppS!*@W3~43j|Ua^s;%ob178-c%_P(S4c$%pgXsFF))#&++i$WsIf-hlT+{K! z%s?KI;XL!<;|#@ASvpsY)S zwbsvCAiPU>KeVIaqas0vZ-)@^Rc8(2?U@?u508yzGtce}8u)E2>j(I@)X{Wi0iCW6 z!3}k;(?w%xRKLcxrKeNdUv{QajE6GJ2MsYNqvz9X<5-ly@`P(}2j3R!sTlI|2l6y7 z6)N%cQ1~0QRmOFS6vs)&^f9kbN!gIP)qmA~_Ud27^ZTy_A>$?9Btxh>7U+wI507EB z8;bRsAQraB^{JXodVH8Yrcr%}g}4ZPp{jlektdyPu{&)HEu$BRl(88;hut)3 zqUVLlh0}wVQlMgu3%3VuFh_aN!;=rEn{tBNZ?3K;uFX!gwE?xNSIr*_T#^DU4pQdp zqW5~{Y?rjlW{SZf!4HsVN00DSwnY8?avY}$g1;$vLm!vBOmfl4Z`D~`EEb>VwI$sS zP+(Eh1x%c4r{_JKT!>dk8i8AM?EQ~>5*w>+VuBC*nZRu99|53v$A%dk8N^M=++#7H zHphIAXVg6q7N`-6Kd-jDx@`hM)xEF}jWrty_bFxkyu9#<^-eXcE0L^5lq@HVw8(o} zs7J!Be*#{^{ZsqpX2585J%ri4sDy*x&JZxm}T zrWQY{yP0q)cin%fIYdIg?%g<#scDT~Vw&-4{%e�eCp;G}@S~S?~0#8RFN@Yr=8v zHYuU{HE-OxxD9IVPQOe~u#N?EoYN)G1RW`4w7hzi{NzjpH39k=aH*>;3$+t@GLAn@ znfUG7_a01-ep^Dzb54hii{l?^u%!5Uf@tsl<@DPiz|-^TQ$=q)vByuHC3j@tv&F+6 zFn6Dh@sc`di{;exCe*n^zWPdi)0k$;WYr_f!ly0xjmpPH6B?W-T-V$54}p|S7z4(fm4@({ti6e6;)6)Rdt-WPD!K{S7Hz! z`+d(~f$>1>%$e!eXUME?I;Ss-?*cdE%w5JeZ3>Wz=0e9m*{T^B2G>RRBieKxN?v;v zGRzoRnf?7mVzQBAf68=_#p`xpk29@-G}1v2*zNy@Khy&bbvrQF@bY#>CmJ~JI&yQp z%8L5?>aZ9dWlR2NAICOl%3w_5HABGCR^E>c|9_pFc|4SD-^RI%9!a^mDT)?b*~h+w zvLs_kG2DzTvX7BHQ&|dAT0Dp`Ci{|M?E5G}5@YO)ZL$=@jFB*wu|3z^PtWps-@o4X z|7-i4*LfY+_jmq|<8=EQ_{`wHDMWF~`Y-1wWLA>hPz;zUVdMRk_W&)q*Ru9{KZMLL zT&5`@m1G2D{|>%`BOkD0Ipc#cvQSSXOOza3E&wNe%&vtf$Q@Qjcp;@mOCAAMSC)}= zc^J*gfjCzfY?1MnFX;DwfOmA#C234#Dwsgtn;*}C+6%G5;5b$k9FL)ycV*N3u{xifEHAMg&7D9w^!_~B$FbKY6EaJo^` ze`kymLTX{bSsmqFgp5qH-Jc5{CCQ*wv#W)sOzpx>h!JOTC&X@Ws}RF4CQDbD8$DKLo^of$dX)iNM2;i zY!pLu;Gk1$P!*W~SG98)?+BjgpbSX4eYq7yI4w5i8tcNQlv2<3H<;dz63#}(R{G;O?Wl+CR`#dA+S0F*NBdVsO&k|g zn?^=yfyouHYKQC6Ql4=5y0pp18GyE_mrP_&AY? z3OEv70m&c7%X%d#=lCSm>nc|KtOwRLUbgU1#-ax2XJ6N)yZvU`t)$*}R=rBjbb9(L zgSo>`O97SK)7V@%9f?ETNlN)}R(|+>-HyQiYDeio9qFcm-asMY>4GNzYZzs<4wWQO zII-hQ8YV97E*H~0qaUZsq>K-F;;sLAV5R+vj8b0gd`#Up* z{zC-Dao^0AZ4wz7gjRv-IWBNBb7c)T==UfbtVWgZ%?5xQK|-Q8!C zxO3`EJU}a!is`ewHr5WfwYB4~gpzj^^w^&MewXW7EAU$4_<;qSe}|g?aMt+7_p8UI zt#*h!%K>;v@N@gfgofL|QTZ~=K~(eb4voa$mvd`|)#SRbeh%Ox-JF&WSIhb!=YxWx zataocOy!qg`x+~f$-o_eEl@5l8wYJtX9AWmEu!=VHUrb219`>(zOu#cI*it4 z$5K8&kTWGj{n4ks;nR@;g>?;GnVPusY1eAzYoDhd^c}1cx4HCW8Pou#9RW-Z`vI=S zn`G;)B7=cnLDMWg9kFE0jP7RKToxk>=s}$ym3)$`+&kV&zWyveVW5SYeIS0W7#0<7 z&TNHumpiXoNVNhKnn$a(I=un6vnr z&bD@ciZjv0$#Ut+XtI9_)u%ckN@RM_fht$Oi^uQ4A~B>J)RmGdl}Yqt|AD*YkR>j4 zu?$U%)SxfBXV5CLBS6Y}YzokPAa}?!LDzrDqa2zk6lfC%h`$8j#|*DHh(AGlS6cnP z!))j(viUkNUo+RzXTXg3(&kU5j4G}xU45tW-J0c1$r$tn z!DU)vwvRiBFtP0U+Pf`5e*f}-=h89B{&|}(pg_AkO4j)7XKu^!Y=Sh&%Ya0YkQxuE z(Qi31Y6s-6WjrzTDV6>gEd4RC@{_nSfSyou&gdT*Da&>Hyk6EVo;EWAGk76Xo6~hv zQQ=wmt`_cm!)o=C3|-#R{Vrw%7V@QpHw7V(_co)ETjvJ8R)+CjaH&ypjKl^zOGuwhA4Pd}2NLa;%mZQ(jpIQ2N?3bEl^$*$ z>@;MLZ5BG$c{X>KUqU4di}XA8!{IJgs&iEwGr95*fux=`RC$H|s66nhQ1SW|&tvc= zi|)-7$;;>{qpV+-H*DaNcR6C*AMB2qpF=X9mJIsmUl|xJTAfqv5a~ZvY)nltbVjty z{jq`!W z*>+=aw`DX!Ym|rc)x1ZS%c4u%OVw)9LY{O(SU)KoX#HQ~dc=wUt?etW>S!^qc$opQ zIr6W=>r1>52fo|bvbFaW49LiaHGN#Bmf~Gs)1sEa!m}r7 z^C!Y;-L;VK6B4icmIUt`MDS02GtD*rsmwi*z?iWnvP*BKC?}U`9X}x_GDB0+YvQ66$sLb^N@xLwz7v_t+Jv{9`{LKYd|)EdDlg!{9}dfOyj@M?ohA_Y4sr z##n%8?!d4i1b`)iFGItorxvLXuucK3v(ysZ!4aZoQ}NJE@?!`u0@seWpU;UOwjlb3 zLNY($CEui#Kc@nZ^;{IhK0n*QBMnEF95p(x{0Y|7@wVMj9v1?^Nj*foG!ssx_Ja9g zaL1w!0FyZBp%*{L32FD8(3CL%V&+Lk6OC8@j5La442{}-buIMHG?)gA7}vz1=4-oq z?|HSAf4{uE7d}#e_+HBA4<_bq1orfBORN4=W)NM_Jn|mQ#OyN^4xx5jWUc=C(AI#_ zYq!G7BkIIP>RUbJVdGuEyyHY z(%JqF)`7=mYnEO3r%^r>-}Q1?3qDK&Qnbmci4`%rtP1!lEWE_w6fG)(SH(AcB*&8C z@BeINawwK;40h3iKS?UpZ|n-z(ezkHBQ&I+aUk~aNkqRboeu^K?n~*P&1TSxH>}dK z-&7x|ca(D91@pf#`ijIR=Y<99=&sTZ>`YjSB=7y z>GDzeA-ScFZRYHqI5>%W_5fVhRc^{ln*}MQ)wp=)Du?-&n8?e?Np6qvCS$2?M_%jM zv|N}%HyLRgnENDq5Lbj7>_>eJUf4+$IjHv7JSGS$_x&2Toi*k~ld$O-Boa_1sDdq$ zdDPpQq}_LEhaEz| P_{L^pWMNpQ@ABjyq*vRV literal 0 HcmV?d00001 diff --git a/website/docs/assets/site_sync_sftp_settings_local.png b/website/docs/assets/site_sync_sftp_settings_local.png new file mode 100644 index 0000000000000000000000000000000000000000..45125da1a820673400b4368fa5bca2709a4d0247 GIT binary patch literal 7415 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 YO1-W3v*=EN4FQ15m+UNR%zbbE8)?L-;Q#;t literal 0 HcmV?d00001 diff --git a/website/docs/assets/site_sync_sftp_system.png b/website/docs/assets/site_sync_sftp_system.png new file mode 100644 index 0000000000000000000000000000000000000000..6e8e125f9578165e44a37f4f5d88563865576155 GIT binary patch literal 13112 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 HgM|DK Date: Fri, 1 Oct 2021 18:56:30 +0200 Subject: [PATCH 420/450] Fix - updated authentication via ssh key --- .../modules/default_modules/sync_server/providers/sftp.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/sftp.py b/openpype/modules/default_modules/sync_server/providers/sftp.py index c7a6e9a620..ce63d35c8c 100644 --- a/openpype/modules/default_modules/sync_server/providers/sftp.py +++ b/openpype/modules/default_modules/sync_server/providers/sftp.py @@ -4,6 +4,7 @@ import time import sys import six import threading +import platform from openpype.api import Logger from openpype.api import get_system_settings @@ -406,8 +407,9 @@ class SFTPHandler(AbstractProvider): } if self.sftp_pass and self.sftp_pass.strip(): conn_params['password'] = self.sftp_pass - if self.sftp_key: - conn_params['private_key'] = self.sftp_key + if self.sftp_key: # expects .pem format, not .ppk! + conn_params['private_key'] = \ + self.sftp_key[platform.system().lower()] if self.sftp_key_pass: conn_params['private_key_pass'] = self.sftp_key_pass From 1df04401d1907da54d7bb41ad59f8d485d0692d7 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 2 Oct 2021 03:38:43 +0000 Subject: [PATCH 421/450] [Automated] Bump version --- CHANGELOG.md | 50 ++++++++++++++++++--------------------------- openpype/version.py | 2 +- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d260509bf..8d8b094d3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,41 @@ # Changelog -## [3.5.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.5.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...HEAD) **🆕 New features** +- Added running configurable disk mapping command before start of OP [\#2091](https://github.com/pypeclub/OpenPype/pull/2091) +- SFTP provider [\#2073](https://github.com/pypeclub/OpenPype/pull/2073) - Maya: Validate setdress top group [\#2068](https://github.com/pypeclub/OpenPype/pull/2068) - Maya: Enable publishing render attrib sets \(e.g. V-Ray Displacement\) with model [\#1955](https://github.com/pypeclub/OpenPype/pull/1955) **🚀 Enhancements** +- Added choosing different dirmap mapping if workfile synched locally [\#2088](https://github.com/pypeclub/OpenPype/pull/2088) +- General: Remove IdleManager module [\#2084](https://github.com/pypeclub/OpenPype/pull/2084) +- Tray UI: Message box about missing settings defaults [\#2080](https://github.com/pypeclub/OpenPype/pull/2080) +- Tray UI: Show menu where first click happened [\#2079](https://github.com/pypeclub/OpenPype/pull/2079) +- Global: add global validators to settings [\#2078](https://github.com/pypeclub/OpenPype/pull/2078) +- Use CRF for burnin when available [\#2070](https://github.com/pypeclub/OpenPype/pull/2070) - Project manager: Filter first item after selection of project [\#2069](https://github.com/pypeclub/OpenPype/pull/2069) +- Nuke: Adding `still` image family workflow [\#2064](https://github.com/pypeclub/OpenPype/pull/2064) +- Maya: validate authorized loaded plugins [\#2062](https://github.com/pypeclub/OpenPype/pull/2062) - Tools: add support for pyenv on windows [\#2051](https://github.com/pypeclub/OpenPype/pull/2051) +- SyncServer: Dropbox Provider [\#1979](https://github.com/pypeclub/OpenPype/pull/1979) **🐛 Bug fixes** +- Global: Fix docstring on publish plugin extract review [\#2097](https://github.com/pypeclub/OpenPype/pull/2097) +- Blender: fix texture missing when publishing blend files [\#2085](https://github.com/pypeclub/OpenPype/pull/2085) +- General: Startup validations oiio tool path fix on linux [\#2083](https://github.com/pypeclub/OpenPype/pull/2083) +- Blender: fixed Curves with modifiers in Rigs [\#2081](https://github.com/pypeclub/OpenPype/pull/2081) - Fix Sync Queue when project disabled [\#2063](https://github.com/pypeclub/OpenPype/pull/2063) **Merged pull requests:** +- Bump pywin32 from 300 to 301 [\#2086](https://github.com/pypeclub/OpenPype/pull/2086) - Nuke UI scaling [\#2077](https://github.com/pypeclub/OpenPype/pull/2077) ## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23) @@ -38,13 +54,12 @@ - Settings UI: Deffered set value on entity [\#2044](https://github.com/pypeclub/OpenPype/pull/2044) - Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043) - Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042) -- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) - Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) -- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024) - Feature Maya import asset from scene inventory [\#2018](https://github.com/pypeclub/OpenPype/pull/2018) +- Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009) **🐛 Bug fixes** @@ -65,7 +80,6 @@ ### 📖 Documentation - Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) -- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) **🆕 New features** @@ -73,12 +87,13 @@ **🚀 Enhancements** +- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) +- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) - Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) - General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) - Ftrack: Sync to avalon actions have jobs [\#2015](https://github.com/pypeclub/OpenPype/pull/2015) -- Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009) - Settings UI: Number with configurable steps [\#2001](https://github.com/pypeclub/OpenPype/pull/2001) - Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996) - Configurable items for providers without Settings [\#1987](https://github.com/pypeclub/OpenPype/pull/1987) @@ -91,8 +106,6 @@ - Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962) - Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959) - CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) -- Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949) -- OpenPype: Add version validation and `--headless` mode and update progress 🔄 [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) **🐛 Bug fixes** @@ -119,33 +132,10 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) -**🐛 Bug fixes** - -- TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) -- standalone: editorial shared object problem [\#1941](https://github.com/pypeclub/OpenPype/pull/1941) - ## [3.3.0](https://github.com/pypeclub/OpenPype/tree/3.3.0) (2021-08-17) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.0-nightly.11...3.3.0) -**🆕 New features** - -- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) - -**🚀 Enhancements** - -- Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940) - -**🐛 Bug fixes** - -- Maya: Menu actions fix [\#1945](https://github.com/pypeclub/OpenPype/pull/1945) -- Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) -- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) - -**Merged pull requests:** - -- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937) - ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.7...3.2.0) diff --git a/openpype/version.py b/openpype/version.py index 0ddfdff5fe..ac62442f9b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.5.0-nightly.2" +__version__ = "3.5.0-nightly.3" From 1e6cdd784f3fe497efbeaa920af2aaf7cd4cef87 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Oct 2021 11:14:51 +0200 Subject: [PATCH 422/450] updating avalon-core submodul repo --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 8aee68fa10..4b80f81e66 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 8aee68fa10ab4d79be1a91e7728a609748e7c3c6 +Subproject commit 4b80f81e66aca593784be8b299110a0b6541276f From 5be0af5b42f2a565db253c6d6323b056b3ad7206 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Oct 2021 11:14:51 +0200 Subject: [PATCH 423/450] updating avalon-core submodul repo --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 8aee68fa10..4b80f81e66 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 8aee68fa10ab4d79be1a91e7728a609748e7c3c6 +Subproject commit 4b80f81e66aca593784be8b299110a0b6541276f From 768dd94af96e321648b20b15499b92e2a800cb0e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 4 Oct 2021 13:11:35 +0200 Subject: [PATCH 424/450] Fix - broken import --- .../modules/default_modules/sync_server/providers/sftp.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/sftp.py b/openpype/modules/default_modules/sync_server/providers/sftp.py index ce63d35c8c..afcc89767c 100644 --- a/openpype/modules/default_modules/sync_server/providers/sftp.py +++ b/openpype/modules/default_modules/sync_server/providers/sftp.py @@ -8,9 +8,7 @@ import platform from openpype.api import Logger from openpype.api import get_system_settings -from openpype.modules.default_modules.sync_server.providers.abstract_provider \ - import AbstractProvider - +from .abstract_provider import AbstractProvider log = Logger().get_logger("SyncServer") try: From da23042db96482991cfccd6257ed98946dad1249 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 4 Oct 2021 12:32:40 +0100 Subject: [PATCH 425/450] Fix NoneType error when animationdata is missing for a rig --- openpype/hosts/blender/plugins/load/load_rig.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index c385dc237e..6062c293df 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -111,7 +111,8 @@ class BlendRigLoader(plugin.AssetLoader): if action is not None: local_obj.animation_data.action = action - elif local_obj.animation_data.action is not None: + elif (local_obj.animation_data and + local_obj.animation_data.action is not None): plugin.prepare_data( local_obj.animation_data.action, group_name) From d8c3535b6b3d3c3983995d314322828ecbd5c1c5 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 4 Oct 2021 18:06:23 +0200 Subject: [PATCH 426/450] fix djv view for openpype3 --- openpype/plugins/load/open_djv.py | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index 39b54364d9..5b49bb58d0 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -1,26 +1,28 @@ import os -import subprocess from avalon import api +from openpype.api import ApplicationManager def existing_djv_path(): - djv_paths = os.environ.get("DJV_PATH") or "" - for path in djv_paths.split(os.pathsep): - if os.path.exists(path): - return path - return None + app_manager = ApplicationManager() + djv_list = [] + for app_name, app in app_manager.applications.items(): + if 'djv' in app_name and app.find_executable(): + djv_list.append(app_name) + + return djv_list class OpenInDJV(api.Loader): """Open Image Sequence with system default""" - djv_path = existing_djv_path() - families = ["*"] if djv_path else [] + djv_list = existing_djv_path() + families = ["*"] if djv_list else [] representations = [ "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", - "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img" + "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img", "h264", ] label = "Open in DJV" @@ -41,20 +43,18 @@ class OpenInDJV(api.Loader): ) if not remainder: - seqeunce = collections[0] - first_image = list(seqeunce)[0] + sequence = collections[0] + first_image = list(sequence)[0] else: first_image = self.fname filepath = os.path.normpath(os.path.join(directory, first_image)) self.log.info("Opening : {}".format(filepath)) - cmd = [ - # DJV path - os.path.normpath(self.djv_path), - # PATH TO COMPONENT - os.path.normpath(filepath) - ] + last_djv_version = sorted(self.djv_list)[-1] - # Run DJV with these commands - subprocess.Popen(cmd) + app_manager = ApplicationManager() + djv = app_manager.applications.get(last_djv_version) + djv.arguments.append(filepath) + + app_manager.launch(last_djv_version) From ebdd47a47a9b6650438361e4aab47eeb85727ece Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 5 Oct 2021 09:43:14 +0200 Subject: [PATCH 427/450] Fixed avalon-core branch commit --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index b90847b476..4b80f81e66 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit b90847b4763cb571be9324759c041f1b32b35752 +Subproject commit 4b80f81e66aca593784be8b299110a0b6541276f From a2a350b434fbc9c0984be611c28812a22cf83412 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 5 Oct 2021 10:19:25 +0200 Subject: [PATCH 428/450] use constant for "intent" custom attribute --- .../ftrack/event_handlers_user/action_create_cust_attrs.py | 3 ++- openpype/modules/default_modules/ftrack/lib/__init__.py | 3 ++- openpype/modules/default_modules/ftrack/lib/constants.py | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py index 3869d8ad08..0bd243ab4c 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py @@ -10,6 +10,7 @@ from openpype_modules.ftrack.lib import ( CUST_ATTR_GROUP, CUST_ATTR_TOOLS, CUST_ATTR_APPLICATIONS, + CUST_ATTR_INTENT, default_custom_attributes_definition, app_definitions_from_app_manager, @@ -431,7 +432,7 @@ class CustomAttributes(BaseAction): intent_custom_attr_data = { "label": "Intent", - "key": "intent", + "key": CUST_ATTR_INTENT, "type": "enumerator", "entity_type": "assetversion", "group": CUST_ATTR_GROUP, diff --git a/openpype/modules/default_modules/ftrack/lib/__init__.py b/openpype/modules/default_modules/ftrack/lib/__init__.py index 433a1f7881..80b4db9dd6 100644 --- a/openpype/modules/default_modules/ftrack/lib/__init__.py +++ b/openpype/modules/default_modules/ftrack/lib/__init__.py @@ -3,7 +3,8 @@ from .constants import ( CUST_ATTR_AUTO_SYNC, CUST_ATTR_GROUP, CUST_ATTR_TOOLS, - CUST_ATTR_APPLICATIONS + CUST_ATTR_APPLICATIONS, + CUST_ATTR_INTENT ) from .settings import ( get_ftrack_event_mongo_info diff --git a/openpype/modules/default_modules/ftrack/lib/constants.py b/openpype/modules/default_modules/ftrack/lib/constants.py index 73d5112e6d..e6e2013d2b 100644 --- a/openpype/modules/default_modules/ftrack/lib/constants.py +++ b/openpype/modules/default_modules/ftrack/lib/constants.py @@ -10,3 +10,5 @@ CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" CUST_ATTR_APPLICATIONS = "applications" # Environment tools custom attribute CUST_ATTR_TOOLS = "tools_env" +# Intent custom attribute name +CUST_ATTR_INTENT = "intent" From 253f7f97c4839293d5f60cdf55297c558e472578 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 5 Oct 2021 10:19:56 +0200 Subject: [PATCH 429/450] raise not found custom attributes only for attributes that can be set on ftrack --- .../default_modules/ftrack/ftrack_module.py | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index c73f9b100d..1a45b1e4b7 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -230,7 +230,13 @@ class FtrackModule( return import ftrack_api - from openpype_modules.ftrack.lib import get_openpype_attr + from openpype_modules.ftrack.lib import ( + get_openpype_attr, + default_custom_attributes_definition, + CUST_ATTR_TOOLS, + CUST_ATTR_APPLICATIONS, + CUST_ATTR_INTENT + ) try: session = self.create_ftrack_session() @@ -255,6 +261,15 @@ class FtrackModule( project_id = project_entity["id"] + ca_defs = default_custom_attributes_definition() + hierarchical_attrs = ca_defs.get("is_hierarchical") or {} + project_attrs = ca_defs.get("show") or {} + ca_keys = ( + set(hierarchical_attrs.keys()) + + set(project_attrs.keys()) + + {CUST_ATTR_TOOLS, CUST_ATTR_APPLICATIONS, CUST_ATTR_INTENT} + ) + cust_attr, hier_attr = get_openpype_attr(session) cust_attr_by_key = {attr["key"]: attr for attr in cust_attr} hier_attrs_by_key = {attr["key"]: attr for attr in hier_attr} @@ -266,10 +281,11 @@ class FtrackModule( if not configuration: configuration = cust_attr_by_key.get(key) if not configuration: - self.log.warning( - "Custom attribute \"{}\" was not found.".format(key) - ) - missing[key] = value + if key in ca_keys: + self.log.warning( + "Custom attribute \"{}\" was not found.".format(key) + ) + missing[key] = value continue # TODO add add permissions check From 39f009ed7c3efb9fb3bde86019fa58f788daf85c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 5 Oct 2021 10:21:57 +0200 Subject: [PATCH 430/450] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/lib/applications.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 05ed67a6f0..b9badedb69 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1344,8 +1344,8 @@ def _prepare_last_workfile(data, workdir, workfile_template_key): ) # Last workfile path - last_workfile_path = "" - if not data.get("last_workfile_path"): # to inject explicitly + last_workfile_path = data.get("last_workfile_path") or "" + if not last_workfile_path: extensions = avalon.api.HOST_WORKFILE_EXTENSIONS.get(app.host_name) if extensions: @@ -1361,8 +1361,6 @@ def _prepare_last_workfile(data, workdir, workfile_template_key): last_workfile_path = avalon.api.last_workfile( workdir, file_template, workdir_data, extensions, True ) - else: - last_workfile_path = data.get("last_workfile_path") if os.path.exists(last_workfile_path): log.debug(( From c03ba80974bccf2085de207d9944eb3e9b6219ac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 5 Oct 2021 10:22:55 +0200 Subject: [PATCH 431/450] fix set adding --- openpype/modules/default_modules/ftrack/ftrack_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 1a45b1e4b7..bfcecabafe 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -266,8 +266,8 @@ class FtrackModule( project_attrs = ca_defs.get("show") or {} ca_keys = ( set(hierarchical_attrs.keys()) - + set(project_attrs.keys()) - + {CUST_ATTR_TOOLS, CUST_ATTR_APPLICATIONS, CUST_ATTR_INTENT} + | set(project_attrs.keys()) + | {CUST_ATTR_TOOLS, CUST_ATTR_APPLICATIONS, CUST_ATTR_INTENT} ) cust_attr, hier_attr = get_openpype_attr(session) From c55ca5af2d2b4d6050b2b6bcd2d913028aa6ceee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 5 Oct 2021 10:26:17 +0200 Subject: [PATCH 432/450] skip keys before they're checked --- .../modules/default_modules/ftrack/ftrack_module.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index bfcecabafe..9cbf979239 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -277,15 +277,17 @@ class FtrackModule( failed = {} missing = {} for key, value in attributes_changes.items(): + if key not in ca_keys: + continue + configuration = hier_attrs_by_key.get(key) if not configuration: configuration = cust_attr_by_key.get(key) if not configuration: - if key in ca_keys: - self.log.warning( - "Custom attribute \"{}\" was not found.".format(key) - ) - missing[key] = value + self.log.warning( + "Custom attribute \"{}\" was not found.".format(key) + ) + missing[key] = value continue # TODO add add permissions check From a199a850b07c4d178d21954ece9b4a2560994225 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 5 Oct 2021 10:41:07 +0200 Subject: [PATCH 433/450] project model has it's refresh and use constants for roles --- openpype/tools/settings/settings/constants.py | 16 +++ openpype/tools/settings/settings/widgets.py | 123 +++++++++++------- 2 files changed, 90 insertions(+), 49 deletions(-) create mode 100644 openpype/tools/settings/settings/constants.py diff --git a/openpype/tools/settings/settings/constants.py b/openpype/tools/settings/settings/constants.py new file mode 100644 index 0000000000..5c20bf1afe --- /dev/null +++ b/openpype/tools/settings/settings/constants.py @@ -0,0 +1,16 @@ +from Qt import QtCore + + +DEFAULT_PROJECT_LABEL = "< Default >" +PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 +PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2 +PROJECT_IS_SELECTED_ROLE = QtCore.Qt.UserRole + 3 + + +__all__ = ( + "DEFAULT_PROJECT_LABEL", + + "PROJECT_NAME_ROLE", + "PROJECT_IS_ACTIVE_ROLE", + "PROJECT_IS_SELECTED_ROLE" +) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index a461f3e675..a94621c8d3 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -7,6 +7,12 @@ from avalon.mongodb import ( ) from openpype.settings.lib import get_system_settings +from .constants import ( + DEFAULT_PROJECT_LABEL, + PROJECT_NAME_ROLE, + PROJECT_IS_ACTIVE_ROLE, + PROJECT_IS_SELECTED_ROLE +) class SettingsLineEdit(QtWidgets.QLineEdit): @@ -602,10 +608,63 @@ class NiceCheckbox(QtWidgets.QFrame): return super(NiceCheckbox, self).mouseReleaseEvent(event) -class ProjectListModel(QtGui.QStandardItemModel): - sort_role = QtCore.Qt.UserRole + 10 - filter_role = QtCore.Qt.UserRole + 11 - selected_role = QtCore.Qt.UserRole + 12 +class ProjectModel(QtGui.QStandardItemModel): + def __init__(self, only_active, *args, **kwargs): + super(ProjectModel, self).__init__(*args, **kwargs) + + self.dbcon = None + + self._only_active = only_active + self._default_item = None + self._items_by_name = {} + + def set_dbcon(self, dbcon): + self.dbcon = dbcon + + def refresh(self): + new_items = [] + if self._default_item is None: + item = QtGui.QStandardItem(DEFAULT_PROJECT_LABEL) + item.setData(None, PROJECT_NAME_ROLE) + item.setData(True, PROJECT_IS_ACTIVE_ROLE) + item.setData(False, PROJECT_IS_SELECTED_ROLE) + new_items.append(item) + self._default_item = item + + project_names = set() + if self.dbcon is not None: + for project_doc in self.dbcon.projects( + projection={"name": 1, "data.active": 1}, + only_active=self._only_active + ): + project_name = project_doc["name"] + project_names.add(project_name) + if project_name in self._items_by_name: + item = self._items_by_name[project_name] + else: + item = QtGui.QStandardItem(project_name) + + self._items_by_name[project_name] = item + new_items.append(item) + + is_active = project_doc.get("data", {}).get("active", True) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) + item.setData(False, PROJECT_IS_SELECTED_ROLE) + + if not is_active: + font = item.font() + font.setItalic(True) + item.setFont(font) + + root_item = self.invisibleRootItem() + for project_name in tuple(self._items_by_name.keys()): + if project_name not in project_names: + item = self._items_by_name.pop(project_name) + root_item.removeRow(item.row()) + + if new_items: + root_item.appendRows(new_items) class ProjectListView(QtWidgets.QListView): @@ -619,7 +678,6 @@ class ProjectListView(QtWidgets.QListView): class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): - def __init__(self, *args, **kwargs): super(ProjectListSortFilterProxy, self).__init__(*args, **kwargs) self._enable_filter = True @@ -630,7 +688,7 @@ class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): index = self.sourceModel().index(source_row, 0, source_parent) is_active = bool(index.data(self.filterRole())) - is_selected = bool(index.data(ProjectListModel.selected_role)) + is_selected = bool(index.data(PROJECT_IS_SELECTED_ROLE)) return is_active or is_selected @@ -643,7 +701,6 @@ class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): class ProjectListWidget(QtWidgets.QWidget): - default = "< Default >" project_changed = QtCore.Signal() def __init__(self, parent, only_active=False): @@ -657,13 +714,10 @@ class ProjectListWidget(QtWidgets.QWidget): label_widget = QtWidgets.QLabel("Projects") project_list = ProjectListView(self) - project_model = ProjectListModel() + project_model = ProjectModel(only_active) project_proxy = ProjectListSortFilterProxy() - project_proxy.setFilterRole(ProjectListModel.filter_role) - project_proxy.setSortRole(ProjectListModel.sort_role) - project_proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) - + project_proxy.setFilterRole(PROJECT_IS_ACTIVE_ROLE) project_proxy.setSourceModel(project_model) project_list.setModel(project_proxy) @@ -693,13 +747,14 @@ class ProjectListWidget(QtWidgets.QWidget): project_list.left_mouse_released_at.connect(self.on_item_clicked) + self._default_project_item = None + self.project_list = project_list self.project_proxy = project_proxy self.project_model = project_model self.inactive_chk = inactive_chk self.dbcon = None - self._only_active = only_active def on_item_clicked(self, new_index): new_project_name = new_index.data(QtCore.Qt.DisplayRole) @@ -746,12 +801,12 @@ class ProjectListWidget(QtWidgets.QWidget): return not self._parent.entity.has_unsaved_changes def project_name(self): - if self.current_project == self.default: + if self.current_project == DEFAULT_PROJECT_LABEL: return None return self.current_project def select_default_project(self): - self.select_project(self.default) + self.select_project(DEFAULT_PROJECT_LABEL) def select_project(self, project_name): model = self.project_model @@ -759,10 +814,10 @@ class ProjectListWidget(QtWidgets.QWidget): found_items = model.findItems(project_name) if not found_items: - found_items = model.findItems(self.default) + found_items = model.findItems(DEFAULT_PROJECT_LABEL) index = model.indexFromItem(found_items[0]) - model.setData(index, True, ProjectListModel.selected_role) + model.setData(index, True, PROJECT_IS_SELECTED_ROLE) index = proxy.mapFromSource(index) @@ -777,9 +832,6 @@ class ProjectListWidget(QtWidgets.QWidget): selected_project = index.data(QtCore.Qt.DisplayRole) break - model = self.project_model - model.clear() - mongo_url = os.environ["OPENPYPE_MONGO"] # Force uninstall of whole avalon connection if url does not match @@ -797,35 +849,8 @@ class ProjectListWidget(QtWidgets.QWidget): self.dbcon = None self.current_project = None - items = [(self.default, True)] - - if self.dbcon: - - for doc in self.dbcon.projects( - projection={"name": 1, "data.active": 1}, - only_active=self._only_active - ): - items.append( - (doc["name"], doc.get("data", {}).get("active", True)) - ) - - for project_name, is_active in items: - - row = QtGui.QStandardItem(project_name) - row.setData(is_active, ProjectListModel.filter_role) - row.setData(False, ProjectListModel.selected_role) - - if is_active: - row.setData(project_name, ProjectListModel.sort_role) - - else: - row.setData("~" + project_name, ProjectListModel.sort_role) - - font = row.font() - font.setItalic(True) - row.setFont(font) - - model.appendRow(row) + self.project_model.set_dbcon(self.dbcon) + self.project_model.refresh() self.project_proxy.sort(0) From 134bae90d39689bf4acaddbe0c6d724831f61e0f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 5 Oct 2021 10:41:56 +0200 Subject: [PATCH 434/450] implement custom sort method for project sorting --- openpype/tools/settings/settings/widgets.py | 24 ++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index a94621c8d3..710884e9e5 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -677,11 +677,29 @@ class ProjectListView(QtWidgets.QListView): super(ProjectListView, self).mouseReleaseEvent(event) -class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): +class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): - super(ProjectListSortFilterProxy, self).__init__(*args, **kwargs) + super(ProjectSortFilterProxy, self).__init__(*args, **kwargs) self._enable_filter = True + def lessThan(self, left_index, right_index): + if left_index.data(PROJECT_NAME_ROLE) is None: + return True + + if right_index.data(PROJECT_NAME_ROLE) is None: + return False + + left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE) + right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE) + if right_is_active == left_is_active: + return super(ProjectSortFilterProxy, self).lessThan( + left_index, right_index + ) + + if left_is_active: + return True + return False + def filterAcceptsRow(self, source_row, source_parent): if not self._enable_filter: return True @@ -715,7 +733,7 @@ class ProjectListWidget(QtWidgets.QWidget): project_list = ProjectListView(self) project_model = ProjectModel(only_active) - project_proxy = ProjectListSortFilterProxy() + project_proxy = ProjectSortFilterProxy() project_proxy.setFilterRole(PROJECT_IS_ACTIVE_ROLE) project_proxy.setSourceModel(project_model) From 4830120abbc5760ad794c5211053a53755e22a7c Mon Sep 17 00:00:00 2001 From: karimmozilla Date: Tue, 5 Oct 2021 11:42:35 +0200 Subject: [PATCH 435/450] add mayaAscii in loading --- openpype/hosts/maya/plugins/create/create_mayaascii.py | 2 +- openpype/hosts/maya/plugins/load/load_reference.py | 3 ++- .../hosts/maya/plugins/publish/extract_maya_scene_raw.py | 3 ++- openpype/plugins/publish/collect_resources_path.py | 1 + openpype/plugins/publish/integrate_new.py | 1 + .../schemas/projects_schema/schemas/schema_maya_load.json | 5 +++++ website/docs/pype2/admin_presets_plugins.md | 1 + 7 files changed, 13 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_mayaascii.py b/openpype/hosts/maya/plugins/create/create_mayaascii.py index 5ce634cec4..7be867e2d5 100644 --- a/openpype/hosts/maya/plugins/create/create_mayaascii.py +++ b/openpype/hosts/maya/plugins/create/create_mayaascii.py @@ -5,7 +5,7 @@ class CreateMayaScene(plugin.Creator): """Raw Maya Ascii file export""" name = "mayaScene" - label = "Maya Ascii" + label = "Maya Scene" family = "mayaScene" icon = "file-archive-o" defaults = ['Main'] diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 544544a823..c2b07ea373 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -12,6 +12,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): families = ["model", "pointcache", "animation", + "mayaAscii", "mayaScene", "setdress", "layout", @@ -71,7 +72,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): except: # noqa: E722 pass - if family not in ["layout", "setdress", "mayaScene"]: + if family not in ["layout", "setdress", "mayaAscii" , "mayaScene"]: for root in roots: root.setParent(world=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index ccae7351dc..e7fb5bc8cb 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -16,7 +16,8 @@ class ExtractMayaSceneRaw(openpype.api.Extractor): label = "Maya Scene (Raw)" hosts = ["maya"] - families = ["mayaScene", + families = ["mayaAscii", + "mayaScene", "setdress", "layout", "camerarig", diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index 4f15a391c7..c21f09ab8d 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -25,6 +25,7 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "camera", "animation", "model", + "mayaAscii", "mayaScene", "setdress", "layout", diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 1c3a8adb78..6275973cdf 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -62,6 +62,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "camera", "animation", "model", + "mayaAscii", "mayaScene", "setdress", "layout", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json index a21f59c8e5..7c87644817 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json @@ -45,6 +45,11 @@ "label": "FBX:", "key": "fbx" }, + { + "type": "color", + "label": "Maya Ascii:", + "key": "mayaAscii" + }, { "type": "color", "label": "Maya Scene:", diff --git a/website/docs/pype2/admin_presets_plugins.md b/website/docs/pype2/admin_presets_plugins.md index eb97a1262f..9c838d4a64 100644 --- a/website/docs/pype2/admin_presets_plugins.md +++ b/website/docs/pype2/admin_presets_plugins.md @@ -468,6 +468,7 @@ maya outliner colours for various families "ass": [1.0, 0.332, 0.312], "camera": [0.447, 0.312, 1.0], "fbx": [1.0, 0.931, 0.312], + "mayaAscii": [0.312, 1.0, 0.747], "mayaScene": [0.312, 1.0, 0.747], "setdress": [0.312, 1.0, 0.747], "layout": [0.312, 1.0, 0.747], From 502d5d56479e052909fcedad92be1339c8bad445 Mon Sep 17 00:00:00 2001 From: karimmozilla Date: Tue, 5 Oct 2021 11:52:51 +0200 Subject: [PATCH 436/450] edit comments --- openpype/hosts/maya/plugins/create/create_mayaascii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_mayaascii.py b/openpype/hosts/maya/plugins/create/create_mayaascii.py index 7be867e2d5..8bbdf107c6 100644 --- a/openpype/hosts/maya/plugins/create/create_mayaascii.py +++ b/openpype/hosts/maya/plugins/create/create_mayaascii.py @@ -2,7 +2,7 @@ from openpype.hosts.maya.api import plugin class CreateMayaScene(plugin.Creator): - """Raw Maya Ascii file export""" + """Raw Maya Scene file export""" name = "mayaScene" label = "Maya Scene" From ea949146d1b65f76f050980d235dc7e6b67ec7cb Mon Sep 17 00:00:00 2001 From: karimmozilla Date: Tue, 5 Oct 2021 11:56:56 +0200 Subject: [PATCH 437/450] add color --- openpype/settings/defaults/project_settings/maya.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 6bda996eef..b464149966 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -473,6 +473,12 @@ 255, 255 ], + "mayaAscii": [ + 67, + 174, + 255, + 255 + ], "mayaScene": [ 67, 174, From 0d2d878e8a50d9f5787d2cc8e6aef892e74b4b13 Mon Sep 17 00:00:00 2001 From: karimmozilla <82811760+karimmozilla@users.noreply.github.com> Date: Tue, 5 Oct 2021 11:59:55 +0200 Subject: [PATCH 438/450] delete white space --- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index c2b07ea373..6dd161cf98 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -72,7 +72,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): except: # noqa: E722 pass - if family not in ["layout", "setdress", "mayaAscii" , "mayaScene"]: + if family not in ["layout", "setdress", "mayaAscii", "mayaScene"]: for root in roots: root.setParent(world=True) From c014b698a693a7409b49a9a08ceb5877643d3630 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 5 Oct 2021 15:29:45 +0200 Subject: [PATCH 439/450] Fix - import pysftp only when necessary Blender 2.93 has issue with conflicting libraries, pysftp is actually not needed in provider running in a host, do not import it or explode when its not necessary --- .../sync_server/providers/sftp.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/sftp.py b/openpype/modules/default_modules/sync_server/providers/sftp.py index afcc89767c..b1eacb32a7 100644 --- a/openpype/modules/default_modules/sync_server/providers/sftp.py +++ b/openpype/modules/default_modules/sync_server/providers/sftp.py @@ -11,11 +11,15 @@ from openpype.api import get_system_settings from .abstract_provider import AbstractProvider log = Logger().get_logger("SyncServer") +pysftp = None try: - import pysftp + import pysftp as _pysftp + + pysftp = _pysftp except (ImportError, SyntaxError): - if six.PY3: - six.reraise(*sys.exc_info()) + pass + # if six.PY3: + # six.reraise(*sys.exc_info()) # handle imports from Python 2 hosts - in those only basic methods are used log.warning("Import failed, imported from Python 2, operations will fail.") @@ -41,7 +45,7 @@ class SFTPHandler(AbstractProvider): self.project_name = project_name self.site_name = site_name self.root = None - self.conn = None + self._conn = None self.presets = presets if not self.presets: @@ -63,11 +67,17 @@ class SFTPHandler(AbstractProvider): self.sftp_key = provider_presets["sftp_key"] self.sftp_key_pass = provider_presets["sftp_key_pass"] - self.conn = self._get_conn() - self._tree = None self.active = True + @property + def conn(self): + """SFTP connection, cannot be used in all places though.""" + if not self._conn: + self._conn = self._get_conn() + + return self._conn + def is_active(self): """ Returns True if provider is activated, eg. has working credentials. @@ -321,7 +331,8 @@ class SFTPHandler(AbstractProvider): if not self.file_path_exists(path): raise FileNotFoundError("File {} to be deleted doesn't exist." .format(path)) - self.conn.remove(path) + conn = self._get_conn() + conn.remove(path) def list_folder(self, folder_path): """ @@ -394,6 +405,9 @@ class SFTPHandler(AbstractProvider): Returns: pysftp.Connection """ + if not pysftp: + raise ImportError + cnopts = pysftp.CnOpts() cnopts.hostkeys = None From 2c6efcc01737c4c0c8f8d41fc1e1e2bb8ce279ee Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 5 Oct 2021 15:36:39 +0200 Subject: [PATCH 440/450] Small refactor --- .../modules/default_modules/sync_server/providers/sftp.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/sftp.py b/openpype/modules/default_modules/sync_server/providers/sftp.py index b1eacb32a7..3363ed40a5 100644 --- a/openpype/modules/default_modules/sync_server/providers/sftp.py +++ b/openpype/modules/default_modules/sync_server/providers/sftp.py @@ -13,9 +13,7 @@ log = Logger().get_logger("SyncServer") pysftp = None try: - import pysftp as _pysftp - - pysftp = _pysftp + import pysftp except (ImportError, SyntaxError): pass # if six.PY3: From 7c61510f302d798f3a984718182ee4e918b01872 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 5 Oct 2021 15:38:06 +0200 Subject: [PATCH 441/450] Small refactor --- openpype/modules/default_modules/sync_server/providers/sftp.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/sftp.py b/openpype/modules/default_modules/sync_server/providers/sftp.py index 3363ed40a5..9036493d2a 100644 --- a/openpype/modules/default_modules/sync_server/providers/sftp.py +++ b/openpype/modules/default_modules/sync_server/providers/sftp.py @@ -16,8 +16,6 @@ try: import pysftp except (ImportError, SyntaxError): pass - # if six.PY3: - # six.reraise(*sys.exc_info()) # handle imports from Python 2 hosts - in those only basic methods are used log.warning("Import failed, imported from Python 2, operations will fail.") From 5f52935485c28f2d1ee43308043019820af88d85 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 5 Oct 2021 15:44:42 +0200 Subject: [PATCH 442/450] Small refactor for file deletion --- .../modules/default_modules/sync_server/providers/sftp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/sftp.py b/openpype/modules/default_modules/sync_server/providers/sftp.py index 9036493d2a..07450265e2 100644 --- a/openpype/modules/default_modules/sync_server/providers/sftp.py +++ b/openpype/modules/default_modules/sync_server/providers/sftp.py @@ -327,8 +327,8 @@ class SFTPHandler(AbstractProvider): if not self.file_path_exists(path): raise FileNotFoundError("File {} to be deleted doesn't exist." .format(path)) - conn = self._get_conn() - conn.remove(path) + + self.conn.remove(path) def list_folder(self, folder_path): """ From 86deaebea87bd5ab71c1978ff74e93b58d19f551 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 5 Oct 2021 17:03:26 +0200 Subject: [PATCH 443/450] added second variant of "loop" behavior - "repeat" --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 36f0b0c954..c45ff53c3c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -606,7 +606,7 @@ class ExtractSequence(pyblish.api.Extractor): self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath - elif pre_behavior == "loop": + elif pre_behavior in ("loop", "repeat"): # Loop backwards from last frame of layer for frame_idx in reversed(range(mark_in_index, frame_start_index)): eq_frame_idx_offset = ( @@ -678,7 +678,7 @@ class ExtractSequence(pyblish.api.Extractor): self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath - elif post_behavior == "loop": + elif post_behavior in ("loop", "repeat"): # Loop backwards from last frame of layer for frame_idx in range(frame_end_index + 1, mark_out_index + 1): eq_frame_idx = frame_idx % frame_count From dd7ed45af2601ce4df2fb70e01873f9818d0b353 Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Tue, 5 Oct 2021 16:18:38 -0700 Subject: [PATCH 444/450] Add startup script for Houdini Core. This is equivalent to 123.py for HoudiniFX. --- openpype/hosts/houdini/startup/scripts/houdinicore.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 openpype/hosts/houdini/startup/scripts/houdinicore.py diff --git a/openpype/hosts/houdini/startup/scripts/houdinicore.py b/openpype/hosts/houdini/startup/scripts/houdinicore.py new file mode 100644 index 0000000000..4233d68c15 --- /dev/null +++ b/openpype/hosts/houdini/startup/scripts/houdinicore.py @@ -0,0 +1,9 @@ +from avalon import api, houdini + + +def main(): + print("Installing OpenPype ...") + api.install(houdini) + + +main() From af75c9873fa259d9b17042c43c3dee6bf36b772f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 6 Oct 2021 03:38:39 +0000 Subject: [PATCH 445/450] [Automated] Bump version --- CHANGELOG.md | 26 ++++++++++++-------------- openpype/version.py | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8b094d3f..32943834d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.5.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.5.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...HEAD) @@ -9,10 +9,10 @@ - Added running configurable disk mapping command before start of OP [\#2091](https://github.com/pypeclub/OpenPype/pull/2091) - SFTP provider [\#2073](https://github.com/pypeclub/OpenPype/pull/2073) - Maya: Validate setdress top group [\#2068](https://github.com/pypeclub/OpenPype/pull/2068) -- Maya: Enable publishing render attrib sets \(e.g. V-Ray Displacement\) with model [\#1955](https://github.com/pypeclub/OpenPype/pull/1955) **🚀 Enhancements** +- Settings UI: Project model refreshing and sorting [\#2104](https://github.com/pypeclub/OpenPype/pull/2104) - Added choosing different dirmap mapping if workfile synched locally [\#2088](https://github.com/pypeclub/OpenPype/pull/2088) - General: Remove IdleManager module [\#2084](https://github.com/pypeclub/OpenPype/pull/2084) - Tray UI: Message box about missing settings defaults [\#2080](https://github.com/pypeclub/OpenPype/pull/2080) @@ -27,7 +27,12 @@ **🐛 Bug fixes** +- TVPaint: Behavior name of loop also accept repeat [\#2109](https://github.com/pypeclub/OpenPype/pull/2109) +- Ftrack: Project settings save custom attributes skip unknown attributes [\#2103](https://github.com/pypeclub/OpenPype/pull/2103) +- Fix broken import in sftp provider [\#2100](https://github.com/pypeclub/OpenPype/pull/2100) - Global: Fix docstring on publish plugin extract review [\#2097](https://github.com/pypeclub/OpenPype/pull/2097) +- General: Cloud mongo ca certificate issue [\#2095](https://github.com/pypeclub/OpenPype/pull/2095) +- TVPaint: Creator use context from workfile [\#2087](https://github.com/pypeclub/OpenPype/pull/2087) - Blender: fix texture missing when publishing blend files [\#2085](https://github.com/pypeclub/OpenPype/pull/2085) - General: Startup validations oiio tool path fix on linux [\#2083](https://github.com/pypeclub/OpenPype/pull/2083) - Blender: fixed Curves with modifiers in Rigs [\#2081](https://github.com/pypeclub/OpenPype/pull/2081) @@ -35,6 +40,7 @@ **Merged pull requests:** +- Delivery Action Files Sequence fix [\#2096](https://github.com/pypeclub/OpenPype/pull/2096) - Bump pywin32 from 300 to 301 [\#2086](https://github.com/pypeclub/OpenPype/pull/2086) - Nuke UI scaling [\#2077](https://github.com/pypeclub/OpenPype/pull/2077) @@ -54,12 +60,13 @@ - Settings UI: Deffered set value on entity [\#2044](https://github.com/pypeclub/OpenPype/pull/2044) - Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043) - Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042) +- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) - Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) +- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024) - Feature Maya import asset from scene inventory [\#2018](https://github.com/pypeclub/OpenPype/pull/2018) -- Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009) **🐛 Bug fixes** @@ -68,6 +75,7 @@ - Differentiate jpg sequences from thumbnail [\#2056](https://github.com/pypeclub/OpenPype/pull/2056) - FFmpeg: Split command to list does not work [\#2046](https://github.com/pypeclub/OpenPype/pull/2046) - Removed shell flag in subprocess call [\#2045](https://github.com/pypeclub/OpenPype/pull/2045) +- Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033) **Merged pull requests:** @@ -87,32 +95,24 @@ **🚀 Enhancements** -- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) -- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) - Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) - General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) - Ftrack: Sync to avalon actions have jobs [\#2015](https://github.com/pypeclub/OpenPype/pull/2015) +- Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009) - Settings UI: Number with configurable steps [\#2001](https://github.com/pypeclub/OpenPype/pull/2001) - Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996) - Configurable items for providers without Settings [\#1987](https://github.com/pypeclub/OpenPype/pull/1987) - Global: Example addons [\#1986](https://github.com/pypeclub/OpenPype/pull/1986) - Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982) - Settings UI: Number sliders [\#1978](https://github.com/pypeclub/OpenPype/pull/1978) -- Workfiles: Support more workfile templates [\#1966](https://github.com/pypeclub/OpenPype/pull/1966) -- Launcher: Fix crashes on action click [\#1964](https://github.com/pypeclub/OpenPype/pull/1964) -- Settings: Minor fixes in UI and missing default values [\#1963](https://github.com/pypeclub/OpenPype/pull/1963) -- Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962) -- Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959) -- CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) **🐛 Bug fixes** - Workfiles tool: Task selection [\#2040](https://github.com/pypeclub/OpenPype/pull/2040) - Ftrack: Delete old versions missing settings key [\#2037](https://github.com/pypeclub/OpenPype/pull/2037) - Nuke: typo on a button [\#2034](https://github.com/pypeclub/OpenPype/pull/2034) -- Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033) - FFmpeg: Subprocess arguments as list [\#2032](https://github.com/pypeclub/OpenPype/pull/2032) - General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016) - Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006) @@ -125,8 +125,6 @@ - Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974) - Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972) - Deadline: Houdini plugins in different hierarchy [\#1970](https://github.com/pypeclub/OpenPype/pull/1970) -- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) -- Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961) ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) diff --git a/openpype/version.py b/openpype/version.py index ac62442f9b..99ba3fd543 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.5.0-nightly.3" +__version__ = "3.5.0-nightly.4" From 81c6987dbf84cb6d721f98eb1aa6296915e530ac Mon Sep 17 00:00:00 2001 From: karimmozilla Date: Wed, 6 Oct 2021 12:02:04 +0200 Subject: [PATCH 446/450] rename collect_maya_scene --- .../publish/{collect_mayaascii.py => collect_maya_scene.py} | 2 +- .../plugins/publish/{collect_scene.py => collect_workfile.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename openpype/hosts/maya/plugins/publish/{collect_mayaascii.py => collect_maya_scene.py} (95%) rename openpype/hosts/maya/plugins/publish/{collect_scene.py => collect_workfile.py} (97%) diff --git a/openpype/hosts/maya/plugins/publish/collect_mayaascii.py b/openpype/hosts/maya/plugins/publish/collect_maya_scene.py similarity index 95% rename from openpype/hosts/maya/plugins/publish/collect_mayaascii.py rename to openpype/hosts/maya/plugins/publish/collect_maya_scene.py index 199fb4197c..eb21b17989 100644 --- a/openpype/hosts/maya/plugins/publish/collect_mayaascii.py +++ b/openpype/hosts/maya/plugins/publish/collect_maya_scene.py @@ -4,7 +4,7 @@ import pyblish.api class CollectMayaScene(pyblish.api.InstancePlugin): - """Collect May Ascii Data + """Collect Maya Scene Data """ diff --git a/openpype/hosts/maya/plugins/publish/collect_scene.py b/openpype/hosts/maya/plugins/publish/collect_workfile.py similarity index 97% rename from openpype/hosts/maya/plugins/publish/collect_scene.py rename to openpype/hosts/maya/plugins/publish/collect_workfile.py index be2a294f26..ee676f50d0 100644 --- a/openpype/hosts/maya/plugins/publish/collect_scene.py +++ b/openpype/hosts/maya/plugins/publish/collect_workfile.py @@ -4,7 +4,7 @@ import os from maya import cmds -class CollectMayaScene(pyblish.api.ContextPlugin): +class CollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" order = pyblish.api.CollectorOrder - 0.01 From 9761b0a79f628bf5c460e91547820aee1d966992 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 6 Oct 2021 13:27:53 +0200 Subject: [PATCH 447/450] Small fixes --- .../hosts/maya/test_publish_in_maya.py | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index c178a6687e..1babf30029 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -8,6 +8,8 @@ from tests.lib.testing_classes import PublishTest class TestPublishInMaya(PublishTest): """Basic test case for publishing in Maya + Shouldnt be running standalone only via 'runtests' pype command! (??) + Uses generic TestCase to prepare fixtures for test data, testing DBs, env vars. @@ -61,38 +63,39 @@ class TestPublishInMaya(PublishTest): "startup") original_pythonpath = os.environ.get("PYTHONPATH") monkeypatch_session.setenv("PYTHONPATH", - "{};{}".format(original_pythonpath, - startup_path)) + "{}{}{}".format(startup_path, + os.pathsep, + original_pythonpath)) def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") - assert 5 == dbcon.find({"type": "version"}).count(), \ + assert 5 == dbcon.count_documents({"type": "version"}), \ "Not expected no of versions" - assert 0 == dbcon.find({"type": "version", - "name": {"$ne": 1}}).count(), \ + assert 0 == dbcon.count_documents({"type": "version", + "name": {"$ne": 1}}), \ "Only versions with 1 expected" - assert 1 == dbcon.find({"type": "subset", - "name": "modelMain"}).count(), \ + assert 1 == dbcon.count_documents({"type": "subset", + "name": "modelMain"}), \ "modelMain subset must be present" - assert 1 == dbcon.find({"type": "subset", - "name": "workfileTest_task"}).count(), \ + assert 1 == dbcon.count_documents({"type": "subset", + "name": "workfileTest_task"}), \ "workfileTest_task subset must be present" - assert 11 == dbcon.find({"type": "representation"}).count(), \ + assert 11 == dbcon.count_documents({"type": "representation"}), \ "Not expected no of representations" - assert 2 == dbcon.find({"type": "representation", - "context.subset": "modelMain", - "context.ext": "abc"}).count(), \ + assert 2 == dbcon.count_documents({"type": "representation", + "context.subset": "modelMain", + "context.ext": "abc"}), \ "Not expected no of representations with ext 'abc'" - assert 2 == dbcon.find({"type": "representation", - "context.subset": "modelMain", - "context.ext": "ma"}).count(), \ + assert 2 == dbcon.count_documents({"type": "representation", + "context.subset": "modelMain", + "context.ext": "ma"}), \ "Not expected no of representations with ext 'abc'" From 2681caacd7058de35bbae60e75be5740024c0621 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 6 Oct 2021 13:47:42 +0200 Subject: [PATCH 448/450] swapped order of CollectDefaultDeadlineServerand and CollectDeadlineServerFromInstance --- .../plugins/publish/collect_deadline_server_from_instance.py | 2 +- .../deadline/plugins/publish/collect_default_deadline_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/collect_deadline_server_from_instance.py b/openpype/modules/default_modules/deadline/plugins/publish/collect_deadline_server_from_instance.py index 2e512add57..968bffd890 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/collect_deadline_server_from_instance.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/collect_deadline_server_from_instance.py @@ -11,7 +11,7 @@ import pyblish.api class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): """Collect Deadline Webservice URL from instance.""" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder + 0.01 label = "Deadline Webservice from the Instance" families = ["rendering"] diff --git a/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py index 53231bd7e4..afb8583069 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -6,7 +6,7 @@ import pyblish.api class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): """Collect default Deadline Webservice URL.""" - order = pyblish.api.CollectorOrder + 0.01 + order = pyblish.api.CollectorOrder label = "Default Deadline Webservice" def process(self, context): From aefbfa638aef9106d7b0d74657efeec031edbaf1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 6 Oct 2021 16:30:01 +0200 Subject: [PATCH 449/450] remove unused assignment --- openpype/hosts/maya/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index b198052c93..4983109d58 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -393,7 +393,7 @@ class ARenderProducts: self.layer_data, force_aov_name=product.productName, force_ext=product.ext, - force_cameras=[product.camera] or None + force_cameras=[product.camera] ) def get_renderable_cameras(self): From 8312412cd6105fd18a38a3193cfbc7fe237ca89f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 9 Oct 2021 03:38:41 +0000 Subject: [PATCH 450/450] [Automated] Bump version --- CHANGELOG.md | 19 ++++++++++++------- openpype/version.py | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32943834d0..c88f5c9ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ # Changelog -## [3.5.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.5.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...HEAD) +**Deprecated:** + +- Maya: Change mayaAscii family to mayaScene [\#2106](https://github.com/pypeclub/OpenPype/pull/2106) + **🆕 New features** - Added running configurable disk mapping command before start of OP [\#2091](https://github.com/pypeclub/OpenPype/pull/2091) @@ -13,6 +17,7 @@ **🚀 Enhancements** - Settings UI: Project model refreshing and sorting [\#2104](https://github.com/pypeclub/OpenPype/pull/2104) +- Create Read From Rendered - Disable Relative paths by default [\#2093](https://github.com/pypeclub/OpenPype/pull/2093) - Added choosing different dirmap mapping if workfile synched locally [\#2088](https://github.com/pypeclub/OpenPype/pull/2088) - General: Remove IdleManager module [\#2084](https://github.com/pypeclub/OpenPype/pull/2084) - Tray UI: Message box about missing settings defaults [\#2080](https://github.com/pypeclub/OpenPype/pull/2080) @@ -27,6 +32,7 @@ **🐛 Bug fixes** +- Add startup script for Houdini Core. [\#2110](https://github.com/pypeclub/OpenPype/pull/2110) - TVPaint: Behavior name of loop also accept repeat [\#2109](https://github.com/pypeclub/OpenPype/pull/2109) - Ftrack: Project settings save custom attributes skip unknown attributes [\#2103](https://github.com/pypeclub/OpenPype/pull/2103) - Fix broken import in sftp provider [\#2100](https://github.com/pypeclub/OpenPype/pull/2100) @@ -35,11 +41,14 @@ - TVPaint: Creator use context from workfile [\#2087](https://github.com/pypeclub/OpenPype/pull/2087) - Blender: fix texture missing when publishing blend files [\#2085](https://github.com/pypeclub/OpenPype/pull/2085) - General: Startup validations oiio tool path fix on linux [\#2083](https://github.com/pypeclub/OpenPype/pull/2083) +- Deadline: Collect deadline server does not check existence of deadline key [\#2082](https://github.com/pypeclub/OpenPype/pull/2082) - Blender: fixed Curves with modifiers in Rigs [\#2081](https://github.com/pypeclub/OpenPype/pull/2081) +- Maya: Fix multi-camera renders [\#2065](https://github.com/pypeclub/OpenPype/pull/2065) - Fix Sync Queue when project disabled [\#2063](https://github.com/pypeclub/OpenPype/pull/2063) **Merged pull requests:** +- Blender: Fix NoneType error when animation\_data is missing for a rig [\#2101](https://github.com/pypeclub/OpenPype/pull/2101) - Delivery Action Files Sequence fix [\#2096](https://github.com/pypeclub/OpenPype/pull/2096) - Bump pywin32 from 300 to 301 [\#2086](https://github.com/pypeclub/OpenPype/pull/2086) - Nuke UI scaling [\#2077](https://github.com/pypeclub/OpenPype/pull/2077) @@ -60,7 +69,6 @@ - Settings UI: Deffered set value on entity [\#2044](https://github.com/pypeclub/OpenPype/pull/2044) - Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043) - Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042) -- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) - Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) @@ -75,7 +83,6 @@ - Differentiate jpg sequences from thumbnail [\#2056](https://github.com/pypeclub/OpenPype/pull/2056) - FFmpeg: Split command to list does not work [\#2046](https://github.com/pypeclub/OpenPype/pull/2046) - Removed shell flag in subprocess call [\#2045](https://github.com/pypeclub/OpenPype/pull/2045) -- Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033) **Merged pull requests:** @@ -95,6 +102,7 @@ **🚀 Enhancements** +- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) - Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) - Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) @@ -113,6 +121,7 @@ - Workfiles tool: Task selection [\#2040](https://github.com/pypeclub/OpenPype/pull/2040) - Ftrack: Delete old versions missing settings key [\#2037](https://github.com/pypeclub/OpenPype/pull/2037) - Nuke: typo on a button [\#2034](https://github.com/pypeclub/OpenPype/pull/2034) +- Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033) - FFmpeg: Subprocess arguments as list [\#2032](https://github.com/pypeclub/OpenPype/pull/2032) - General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016) - Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006) @@ -121,10 +130,6 @@ - nuke, resolve, hiero: precollector order lest then 0.5 [\#1984](https://github.com/pypeclub/OpenPype/pull/1984) - Last workfile with multiple work templates [\#1981](https://github.com/pypeclub/OpenPype/pull/1981) - Collectors order [\#1977](https://github.com/pypeclub/OpenPype/pull/1977) -- Stop timer was within validator order range. [\#1975](https://github.com/pypeclub/OpenPype/pull/1975) -- Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974) -- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972) -- Deadline: Houdini plugins in different hierarchy [\#1970](https://github.com/pypeclub/OpenPype/pull/1970) ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) diff --git a/openpype/version.py b/openpype/version.py index 99ba3fd543..f6ace59d7d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.5.0-nightly.4" +__version__ = "3.5.0-nightly.5"