From 7c302631014f85efa05cf809679f55260a0d9593 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 7 Jul 2021 18:50:53 +0200 Subject: [PATCH 001/213] #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/213] #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/213] #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/213] #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 zcmeHOO^h5z6)yJ-pyYzYfdg;=E{Fp^;s^*3NPqwdMT&?J7nCChBJsVduAb^i?`9`) z#Ij_iFx^$Js@|*j-uHgmyz)`TZfiPi_N7;?AOH06g`d>E_OnYyydH3NJ7X*e{p-9H zGsCm{oP8K0F^}S=8ZNZ$*k-R`*l79f9D>_jXF0UP^>hWBXt_>s#$)@iNIah(#NuUN zCx|dce{&}$Iw1~x%4@TK6hJ55F2;5k%lDq`wOuhy;q!C5w&`}Z?pUG<_Fvh za6dR7FOEZ|RjZ9;a*XCilQkt_cT^~K$4*R~h4w*1HJ92i=^J72i`GFF7jvY+Tz6Vcs zC*~pVbfBh)Xy^pXWS>llK7^-%7joMzS4))|-U?3N?sCUxFGEiSAv*gWUG)t=Tju?ca!`&Wmk3#6a@mI^JMS z83P^Vc-@PE-Qo;vFwAm0WJcty#hiPmocJt^noK9U@boEW2y=VOXvg(!L8x!x`dwN} zL-W|x_n5Kj@EbhL?ilw5{bkQ~+{`d5guKhw&UnEfj@VU_$&t_J@C<7_jUPkM2u)o5 zV#zK&f3kGNEX`dz9JqGOFYwNhsmBf_J*8pBF>F#UrT$ae10%-y|}@(KEIO zE9i4i-tsxx&^SMT=6o~pL$d6w!drtV_WML5?PBp=sr~GwrHtm`1WxRaD9Q2S*^GV+ zj99hoP>#a$3Erb@2yOCNzGKbw|P}T%yNSSFH%~KKqG`A+;tfU*?OXNZ;%{J87g5fw& zZL(}A08X*kpLDezq&-X#hBUPBFRSs9OihYZy@Qy-1TLTfom&@V(4&U+Wn}&%n2CjrjmRPrI7}))HI>e3KIl z;k+WSLL!kdorCQV!S@k+;2{#Y*=5>QMOJe4`J?3|bArhyLG~jUFF%Xytk0ve2*`$v zxF4g5z89CHo*nYxLhK&f(MrVrMh5*f_ju^EWa5B?BaE8@AIM_5GzXfK$Q?y8rp=Q# zQ%=bW&j1W@VmFth-*!Q=#Mk0M$a5)Tz6Of3zQ5Aut1?B>jv?i`m;=cLrwe?}KPE)} z$O=!joj7*~P7^r9UiXn8mazu<2FHQ?rq2!+=hodic;a+qioD#r_*0x9|!A( zCJI6g>j+sXgWgIKw@aKZ+6Z^KTm)i<73P8Ze&mRKM8d0`M3+WU0Y8P9_eF9NvA;uQ zg(QiK*bA7PKpsM2qZ!z3B(hybEE>rmJAjwfjpXg9d2Hjv2>Iq_QeC9$$tVm_a0uQv z#G_a$o)jGxCKmpEvA}mJv_6UF4RV3239u+>|~GlDOVH_Jp? z-XMG4#L1MyK0({4l~h?>+iWJ(O-a~HCE0wUxq*wKWN7o))CQ~+y9gh;hjuy95uD+) z6SOsPodkBIMdqpk!56Bp5le*v(^4 zOo4GK26rf2!W4T5z+gz(*Co42tzbj*9$xvV zm%g1*XGKFn8bkb~_5`=9FkjV|c`fP7JUh%bg?Iu!ASyL$DAXnINw0#k%(Pb_revo= zv}_D)UkngZJp^l!eB8O}L<8Gh6k{s7K8igM_@a0T;Kc67+psmHf-c%Ena)jKLq+Sd zXuX&GHjTKLxAc03&7G0f5Vez33rX)SjrMSoCK0ui#NTaF`b^)89ZGNg_0=~%|4OH~ zf5}*pB4-jIZShb!1mH67AlmfSgae3l8EMo}8LI7hLCmE_pUhIfiNmC6zP|R` zUwozDUf+lxP~1|&mRg)+JFpuUR%!V>ehgP*IxqrGOb}W}KL$3}>QoL*E?at08ku4FQYQU&|tdME%&s}O-8#+L`5-630H?Pfb>Kl+rv0WWi=Svdq*gm1^~-Wg&&CjSDOP)M4d2;Aj-1A6Bq3X8I~K7wVQ$^bny$<|QT$ z8Rmos@s@N}Lr&BTomGOsP8=fVC5DF8NLo;^mveM}NOAh^lGukwli0^_)))-$<(yXk z*9N$iq(4Z7BfT+pC?_sGs?>5TGu}hPjF((XTGo?bLi+s;@G?%uD^0H@k@MGPTLNHD{W0wRae0p8J)Syq0r7;>ddl z5_gj1>K}!?4;9+y66rjf&0AOt-}iq96~CZ*rf;o^}TxR`to;R468zQ4L#fPz@ri>2)z$|lpD3Frsp> z-zDE&$_gXKEL`bpg!-PON`V9n9>LVG5~MN{rRyCUJsxw*)xM@r#h<14duQdU+NL~O zu2L!XHcam25|y#K7BJK;Rn%qQiXv2PY7L7}`YI`l(0N?H^i=$NlCg?VWpoYopsa8E zLUfc``%JnAOVN9Ay}PIs%{agM+>(nR`Q@nin!UsCEcD@fo6vEbZpLI(m5xGTRT|`% zVUYS4x3?`vm1&8O*O5samF}hZZRV$nnZuNsf6I9L3uIYesK@=I%0y3mb+I cYjUn))vGhJH|x{W^_jVGW61H}Xl3bt05iqOQ~&?~ 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/213] #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/213] #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/213] #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/213] 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/213] 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/213] 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/213] 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/213] 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/213] 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/213] #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/213] 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/213] #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/213] 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/213] 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/213] 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 90d80dcfabeb758eec9ffa212deac9a1c68815cb Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 22 Aug 2021 04:36:51 +0800 Subject: [PATCH 020/213] 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 021/213] 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 022/213] 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 023/213] 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 024/213] 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 025/213] 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 026/213] 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 027/213] 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 028/213] 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 029/213] 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 030/213] 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 031/213] 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 032/213] 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 9887e00859850cf0044452b0413db8406624f051 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 1 Sep 2021 09:36:20 +0200 Subject: [PATCH 033/213] 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 034/213] 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 035/213] 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 036/213] 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 037/213] 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 038/213] 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 039/213] 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 8be9838e011f187641ff4cc78c087c2fc1e513a6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Sep 2021 18:47:03 +0200 Subject: [PATCH 040/213] #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 041/213] #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 042/213] #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 043/213] 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 7e088d6b977f02560001cfef82650b8a47d16684 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Sep 2021 17:45:32 +0200 Subject: [PATCH 044/213] #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 045/213] #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 046/213] #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: Wed, 8 Sep 2021 05:57:12 +0800 Subject: [PATCH 047/213] 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 84ca6a591c5aa1d15cb16cde3e05318eb5b71915 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 8 Sep 2021 23:19:23 +0800 Subject: [PATCH 048/213] 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 049/213] 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 777facc69c792356ace880d5c19a7f8f9cf1da51 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 18:46:56 +0200 Subject: [PATCH 050/213] #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 2167671d8e8adf8f2ea4eeaf2745266899494f2b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 14:18:13 +0200 Subject: [PATCH 051/213] #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 f96555d9e95d34d9e7f3160f674a11e3276d4955 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 17 Sep 2021 02:27:52 +0800 Subject: [PATCH 052/213] 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 053/213] 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 1cd8aec44c07c75c264110b0a9bfecb035e093d2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 22 Sep 2021 03:07:20 +0200 Subject: [PATCH 054/213] 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 7d2d621c9ab589d1b34638fec182e2939290f77c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 22 Sep 2021 18:07:48 +0200 Subject: [PATCH 055/213] 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 0c8af1f7b6e7b3e27e0b9230abb4ecddc92d4bf2 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 23 Sep 2021 11:10:36 +0200 Subject: [PATCH 056/213] 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 057/213] 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 058/213] 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 059/213] 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 060/213] 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 fd7438c2aca6e8effcbf5b46e54c2d8e1c95eeb4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 14:02:30 +0200 Subject: [PATCH 061/213] 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 062/213] 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 063/213] 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 064/213] 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 065/213] _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 066/213] 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 067/213] 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 068/213] 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 069/213] 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 070/213] 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 071/213] 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 072/213] =?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 073/213] =?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 da7dda6b6a8b7d8cd240292c73bb7eec641490e5 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 24 Sep 2021 02:35:59 +0800 Subject: [PATCH 074/213] 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 075/213] 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 076/213] 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 077/213] 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 078/213] 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 079/213] [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 080/213] 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 081/213] 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 082/213] 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 083/213] 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 084/213] 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 085/213] 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 086/213] 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 087/213] 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 088/213] 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 089/213] 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 090/213] 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 091/213] 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 092/213] 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 093/213] 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 094/213] 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 095/213] 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 096/213] 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 097/213] 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 098/213] 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 099/213] 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 100/213] 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 101/213] 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 102/213] 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 103/213] 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 104/213] 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 105/213] 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 106/213] 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 107/213] 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 108/213] 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 109/213] 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 110/213] 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 111/213] 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 112/213] 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 113/213] 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 114/213] 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 115/213] 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 116/213] 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 117/213] 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 118/213] 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 119/213] 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 120/213] 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 121/213] 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 122/213] 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 123/213] 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 124/213] 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 125/213] 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 126/213] 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 127/213] 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 128/213] 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 129/213] 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 130/213] 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 131/213] 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 132/213] 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 133/213] 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 134/213] 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 135/213] 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 136/213] 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 137/213] [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 138/213] 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 139/213] 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 140/213] 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 141/213] 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 142/213] 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 143/213] 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 144/213] 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 145/213] 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 146/213] 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 147/213] 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 148/213] 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 zcmeIZXIxX;w=RsLpaNn?r0LczDgq)(FLrbzVxtofQHl@&Tj`56Vj4^$D z^@@eurahY^BqZc6T|9SPLPC-%A+e@>gB0+j&P!Vo_}`kK>lSAu(5=c7z>jsFr>#y) zNR%YXEW51-es6?abO@4=*i01vw}#~Z;f{pFS-nf=PTz#Num(hxzu$#x!;mQ_XQi7{ z9efA{j~$YocWOV`Dh&EQjU?R-8uW^{>W};?{X~O+CF9}qxtYuR75i6uUik_ zZ1p;|uXosMi_V^zh65V?W?kD>R$D#dy8J!A{w+`2HRQ((p$WV8tI_}*|hRD z^Ica2BXnA)6F(X{RHmIiQ%9%0vrv<(T%O=0@NWJ?4`q3EaFLqueD9o6o5E>9XPexV z>LZr6iRu+R_c5ej(>mq`2X>@D&|@8aL-qdg^tm5|X_T7ju9aoA6~FSoRrT?ea}lgT zBd4_U2x=l)!DY9mm94Mww*b3vmXL_tZZ1S>B$y&T(KvNgg+pX}dEyAIlQJ&pe>}Ph%zMbDJOn0|lM$JF5 zQ&zlh>g-SvWD<1jiQ%ND-J|{T-Pl(@&+Xo`BV%u)@1=q_hc=uNx(a(lR(iqE3acAk zeTYT*m7~l4wcMIwcFgHs!bAyVQd3Z$i)314cW*IUDpZROaTU23jJA6c`-N?f=&mbQ zdgt|Jstc+&{N)W>cEFB&F*^&DrPArbsVDo18mQ{qkWSV74PP#OQ9bI~o`Pk%r#svJ zRPM)7JMu{gscAhkj2K!l>C0IZn#t~2f25!%Oz?GU=desMQ6ER z-(>@nz!gH8wztJrnD?roMA@G8j1`OqM4mXG(lXsA`ytLTc5ic|`H|)wlk5F{+CwMJ zj%)}a{UOP_{fK4X)i*dVKyGVAEL0NloQR_Zc){ahHhE~HCrikhZjW`5^)#P8W5D{P z785eGlmTgiBGn7vG{O1dmB}GtpBLwHg%or5XYvEXO=Sn1llI z3$7T7KGJd!(t{eN~NF1srEoCB8Kz zqI$%cEZoD$`-uaNE;d&(HSZkmIze~1=k90gA1T0?FAyPP+19Z)R3mG#w4pnLEeBoeuBG z+3?}j+(L=w#{4cd$yA|S_fdnGq&;BdFg@{=7PP!!#RvJ#t6UE7d-{XFp$pN_Tz1u} ztmSRqabFU$gED(F1QWh8&L@UvEnhKMOtX%8QER$kFws2OM$MqYl+Y zZ&=&*S|u~?ceUuD#k5*QR+mjmascT_{Vl|WDvUK4Uv6|}{G3zG$We&w+tFI0(Nmpr zJDm%j(%funIip@+55!*b*k}vYc zvJyqVEq9dD@EZo`$u>QnPO)odnssA!OSZGlFQ{;g2P~V^&w4s?M@Qz9qt(9b{{BQz zSzbBi6#q5^$BfuR{#sE8_i|*Vw&|TM`V}{%uhV6}gFo6R->`dw0!tgQ?-gk;_gU9} zHNS6CEmR+p6EQhv#>M8&GwSpr;S%F?-QYrt`KDZi07^8@61gp~hnD9*+|9fv)wGg- z9~o*Rjg+mq3HESVja-M{cfLg*febAro6GqiIqk(`iM2{7(V{Nxk@+&Kcy?&z(}xgy zhOoOWHd267i(#zrM5=`uxtjxf6W*5d2n_pJi(xH~uj;K<|ClfD2t*`6E?c|tU2Arj zEpm|C%oclUmC!59WTNZl?)USvqG>*DNlmCm(-$p^lnYZTH{15A5GsxMs*F)3EIH^% z%D~@H_x`3OXZ_sYL8a5}-wmSw>8Sh92XtzzP((E3Mlp#Q*@j&sA@N3ftv>K;?$j{A zY#yj?09cO1wnndB9lh4($x`>H1^{2h0V*wbH&1fq#oecRUp-@PB+iMxtdUjF{f ze@7KCi+yPlco;Qu8EW!a#6~VhPn*}yxW>3HC=_ihG8y^uyEaM}rH{gg#R9WKVD{aS zf@D<0*{zO9+MlCa%hN`%(SE?J#E#O<)NAR7Tci6%e?yr=Nx;{|)tZ938fTXag9rcl zlMI;>vK1QzEPX3%J2v?NRRtg}bu$Mc+G}ve;+>3wk3+J8ZLp%~WMapA3-l)S6O|6% zylM1A6Gbz`bu>6&bnR5&3%on9hW+54NJ0Jd;^{9d%hHX^R>J%sBov(Htvpf(uixqB zzhn`mz~1CJigEg42vu1iL_=dVbsAbVi6>h~whAY3cb1l_2 z5l-)P3#n>3y$=Fxr{HFvB3qzstm&Y?O_EorU`y7KRaj%PaWFuXs2~{g#L~@dh9(f% z$t6Ag7v6^7ro=cu086p^1FKCp3_1CsPMbN{trK|dcpBHox|4QB_FDq06f7x^+!t{K zq}4ro7WqqmPSd7Tx&v!>Be{?BBUDe8u@+i|#F_LTRMlmk@rLox<@J?N^^=Ne68NRYpw}n3J+POCR9Isi&c9u}BI4KFE zG=~%xf_H_ldcd1~g16puMe5g-smFUk)l=0}vy@L-^-Esxok>h>^CFid#34zjQMO8Q zE?gd3;*60KypE9PL$bJnhS@-rWWQHexW%w-`9phoh1u}* z*MN2G-}nCVjg3^xQ~!@%U^VQr_HVID<;BNSykq04k4r`>0l)5FUB|vL6m(%U!9UCx z_epkPvXp0HRZ9fji(y?{g$%=!#Lxp`bFE8H zbu(VYlv`I?id9xuIhOl>o^BwRmrKkvW^WERTa1hl3vuEEF*Nv_BwsO0`BC3)FUuak zjhrvO&@Zdt)cipalO44iJG8wOAG*7=sA}3YJ49`RLQhh%gjmQ2ihw}D-`4VtwW`g` zv*ArikN`!jbY;;K%FCv2PUu=*2W{ksfz~`;RuNd*QpP^i8KsT3?wm|P9eB`+3@efPEwQiLaQUfmW~~OvYpnm zdHg3mJOwclzrF@2{6@{Yh{0l&Q4efK%6KXKjBL{lDfSdWbB$4D;_BRyUNJ1MNj{`) zJ0_bNjtz>$r@p_^VnobnS~ngjIKKSPCzEEK?f9PjEcvv)z>Ia^sRHx%G-xjb%BF0^ z#;$=cPfV;Huk>F(6`CD@RZ)UZJKU6Fbe@RB`wTfhK!d~K*gwR_d}}jsq$)pF&ptNm z|9q(bS8e3lRzUe;Qiy#l5o@v^TS_kY(%0jK|NVV`?2QiOs-8!Zj86F*neD&27 zA|8LG*#=krJERD*ib)ac*bT)VLA^MiAp2K7PC1($hs|hDiup8+`blTN?&^+XO4mYV z8=+Jc-pjnF)q1|v3S(dAj$VYL_psU1Y12JDtyVrt%+N<66QR=W=Y~rOX5ebFywoEjLEH5OU{Hd+fW z8Mx)kgvNW~$RSabd_9(`n&%r_OXEA!mD6qmJgQE|tn)&mOhp~2*)MsG@lImsaVDrW zLjP>A9}Z_7gzwi=iA0pQC8eoXg5X~X?Nx7UlBIN(HAa`IUL*3I(zgDe%RO7ZIy=SK&e&x{BLNeHRk57QH zVJA46m~mo_pSWCnexJ_f!?un{PW~u{T;Nh zC*U~N1-dfpMus&t_kH5-dLK$f0-(b;ZtK^xBd?vZ@{EcF>q@cz$~qvcPZ?p2t(+hB zPjR>+(D1~ikv<%72did>7^)hx^4*W_pRY4Lw&~+U{B$w6u?{r%qB-fGhJgJ1j7$6v zD>BXqZj!m!iDm@J0NCdqTU#LDm3_K9o~{GY_x!U1GsF9QG|;`#cqjkkTCP_T9)n&i zol%s8w)ZT^%T<^)ft3!fh3389sujhEkHi~Vrej`Js(UML-2h(PE@W7kfqT4+jQOp@6dE(OB5gzjExWy$^3?GVdrMAbAYiB^6N1XPr^cJ>xTdWYeT>Tj zSYwrHW?Ubmv~UwHL6P-Xk#jDf;KUYrzUJA`b`ekRE2d0}-58FI8)Zwl$WWt(9m7ST zvhOZQpg2M37BkO&l5Y3>v~%;RWU$RQaI=?nGfdr)IP-y%)}~aXok%IM4#M$N*|!}e zqyZVq9(rKiFS22mLbl$q0B1iH~0qkCB+Pux1klsZsRY6gKdo! z1!xsFX0t9e04K}cfA@j^wJaKHAC7*v>-=k#iuZ2Na(2Jvo=D8=FjWjoWnFKcrX0aL z3slyu;)c^>$8t9Hw3-lAl4EJL%Kkft0WIt*ez;b@?1Qv8)_MF6c`=_%q_t*Ij?3S3{2`VZO*Ez`3H8T*=51`cUyI2#5`cua3RuI>XSJ9{$X>-SxJxG zucC+c_BAg&aZA;!*H?*>P7Co}3UaRS=EIaFjr9v#{Ce)S>S!xthZHd;)!M#>`+9%? zQFnSXRjI!FG>lq>sP-G+skngx!naED*G%lD0I!I}AeUsXV;F}-ZE=`YzAmi~A#Jgj zdH#O)xfdy;hhiE$yqY!j`QCnZl$`xjyC|cLdiRv(Dg(kT&B3G(EPl#qFRe<&OKtDB zMTySPj|#^3Sd@w8_CV*%zspiR2bKe2OVI!pE%)#BO-z=Ncijp^g7dvfB2B)J7O-H! z-q<@a#-f?=<|)ph;_kJf6(+JzyW)fUZ3p~(%MccpUlNQO^){6*B8*V&RBb5As=-Y2 zQ|)Yo5t>w2b$Yz0wQ$x*wrefl5Gp4bQWD|P8~D{&TIAt@T`z{*CSZ$_*;^c2>qBGO zFL)Jb>PLB(X%70a%{!(7!eK3{`M#EkI_-MrhuNEmGAjA3`#UJ__aP#g$G)jzEyeNW zRC1IW_J)qL?I-`nx&hf7Z>O;E->UUIBJuCs)9xxy=%fm)l&};JEFRV6>*JF6Lw@5~ z-F58WoD*Hm5G=z{BQf`R)ACl1S|O3XG5h>g=Ucn*`Gq*q4qd#v$|qb{0ml6VKkXm{ znh=oW-jzfT)!auoZwg5)B-|PD!r*_0)I#hGG(fumgm|!Xa#)HzZ#>C)rnUBmfpa5Y zvBx7zmqmA=-an>k)@LmDseyIkoBZYGE$UT`wlYO6qbOi!OxeKx^TVpK(<4#c;$VI&+USceRcA+NbpdVz zE*o>r{&H>NeEL$bvCeFRTI3>TqTp~&7{ns0r;zxh@&)8bRr>osbHc-}T~ep1&I@>Eivd|q>v z?&(IR6;xJ(!3ImOZTh6etYyFj**y81-RMu3-1;5Zpg|Jk(5p_dgnn;Uix; za{dOPR(k#$sT$P$L?gWdqm>^czi$EFU*@#f7F9w~~ zjugDIP!{LxvG1KXx2}?*TT(dXbo%e3|K6Z~#g6_Z()@P^eM1hrlo?J@D+~*oDoV-= zhPLvk0npwu_*nwH&mMq~J?SJBJhAa4jz{b}ZVYXc7aBhGVXhd&`)+We&n~)g(-FIk zI?a6$-D@_dE=ocpzH1>rTK8`44l(`MHuK1wjJXR@?yW*#0U%iltU_NG<{nr~{NqAD z$?bh?T?U>l+>Y%CDpF;13>JNDv*zkL&-HawObN~5rl=bExG>Yr?~2UWiRUNE5(NP% zrg|PbkZh=*V;b0aYJR19Tg&3YB~mW~Mp-gD)N`l_l-pbtrKkEJ%0B?eskeJ@)#e(j zqJ#%hrDCpq`5;}pa3@PkU2vkY>Rt!i(LDBKq~O@GFt+sEZnW?yV})3L@jenU9x!D& z5a~=g%Z)I7WyU5E7fVVWs_~^;(t8%JiV9T9=?wG>l6rBea(P`%N%~#OvsY&xxwh2H z-4PuDwrc~SZpEM)Z^$6}AS%c;2PpB~);n#V%&M}TCQ}_Ql3Wi7{CF=^b%p!S>>#=L zxn@{|ZO-;dml_}8jT5|$uT%(gzWO|?@o>il<)J}Viwj*S1gL4%hj>rvyAG?0oNqd+O3nuypre@5f^u_d zUl|2i)HOl&Ws}Zg4tj#) z6EHdbhSpjPwX+?rY0;`t-r?;%-Y?+6nT%U$>(#=)a{uZ>=mjOlV&>=Rqpk4Ups*UB z9<6(*8u}vUEh0XNWF1Bgto@tl_LFXySR8DdXjs@ouJGB~Q>jw>4bhSo$Ult`H42p~ zOtq8g%EIuyd3?l892Tb%a=EEz?>3NVL8zqTMs|kY}Hs-}bCv>Dq{q?U1Fc z=zgT~o;3$52}{=J+$pIBm^`@hSYT~&(@LJJ)F>gFc~dnP0}3D( zO=9IhLMvZ=qoPK$zR~E$cB0))>yF4|GGpB9zbI%bHyvfx7$QB&Y6I^v5FAFYxZ;T0ntuxb~e1 zNXs8;ic9tY&OsMeuZ4J)h|v}~?#cQb=Q?vYywp|k^7naMh|FO@5yI_Ge=6fxiFM%S zLgCIHW0ubX+3Wb5gv73p^nTWef&*1=Fe?FJ0e{usdKhd;a&hV4YPU32DUwY4T=_62 zL(e%`>BeExPzny?64ehsVZ!ca1xyi31;NTX4_}r~KPgAm+|6${drIEk5)m zY1viMd86F#6wvp83f2q7@M=^=iPnJ$_e^*i7)NMgwuiJ-VaP80F~AW%b|?Hmm3<3o zVZ8vccUuHyZA@VrlvtC(?s6&>1?i)(7I4Stl)VR5?}n+A5~KTM<{1)q7gb-~6fsNv zqXTW-m}JbC6dFFMq@ZpTDj9wwS@&>L!Q9Xw5nAUxDN&GsP}q#LIM{QHg=Z~*da51u zk2C4|(2OcH2<-fIX(p5skh6r7FgTHPVOT3O!u{gzKZt#I%y0NAWx?FTcPf*#RnXBn zf5EE(a8R^OrRdoR4m!4Fmw;r?;Djf;L4}pKzGZ-`Jc|S|F5OghpF6wm8EPV#7ee|Nqg?WSc82SgaLw}N% zdLQsnjaD8x}uq8`)r?y^Z}l5+)m&{h?SZHb3piPyn(cU3*Z_*^~e*?7F8 z;%kQG4LUUyt~}o6<7cST(dNNe;x2YAHD!jA=i}XdQ71u}SSc00Co@2a(iT`pqK$bhATe5dT^E)6yP>lS*+EKaG{<0e7VZkDM@z-7%50Eb-FBRwhmkZW zPr5j)S|PWiwpfXIkf-E5pl5cNq(0EDoqoc^ruOAXAb5!TB4p_tNGSc+eu z>qc$pd*ZWC-c+YpmGA5c$BL#+nu2x<*WPoEZ6*9Q#v7gaa)z<}J!1#yrLjwxRfmMV zlRh1*F7R=VZR8PMzn8#|U;J#=S%ddDP560cX7_-PEMX~0|GTIPd0!EE!OETMg#eaR49Hn(WOn5mkBNaTu>d{?i_ zQ9gvAj+i^j**)Wq` zsE@BLe{t$s%3aEc0&?5srqM_@uP4^LiNI1T`lTw^$Q41;2kFwwLlxi`uA3X?_soL^ z{ao^U^>i?wbTA|eH`&U>Hmrp_+OCt@^L8reE&V1XvF&a|{3QU|?X`+r_xzS0ki&Qp z^Vxx9+TM-bJj#2;W_E`lF&RERCqJ~RJJ?0fL~`kObkO7?hBj7JzZ+L&ZE%^qsB&t!NF&GpBhAQA38VLZhEFb|aN zQvkHNhS!n=lN08*lT<4je4ehzqA7zjv()h-SbF@Dk0}80AaWrPfnVq>?w*1*pDZ8n z*PZX%?asP?q5+&{9+eHpHy&@PNEy>HFiHFopdSFwHE&y=Y|*dA2b-sXnUtdQY^wY+ z!)w9W0Tk8$nwf)GKCyI>!>4f@!430AD0u-PRBgis1b1fy9sIqV~dy?jtEwJI>Z3#P=38|4WKe4I0SwhZsJH{#H=&YXFPe4gb^2~2C>&Ng_|nKc}z zK9W}yEGQXCzHaIC-hfk@wM6boqg+ug3j=@0d@%82RZCwG*w8gzl2nD)Gq9Uwl-0}N zCkIip9FyD`a~qrRu&e~mi?>|lIuBL;8z6&w2j7uO^7Ewxh);rla<^L1lKiC*q#nsY z5aKXS4dT0%p!FKhoy2$rBj5MD1K0M4SR(Z3OTk&exb|J7R|1@(CA#giFN|Jdo9Q1k zg_}+L7AU4bem$WX^xb&0Ll8bM5mt?XJEE;_*df7;E zh1Rxw_XNXB;c;_}Qkc^f9GvOy)#Rhw<$jF!%7mQ?f5{9Ko~Q(Q;Q}Sh)5_3o83Ge? ziwQy$_sX*9_e#*Lqm4jb0InHQ5>6?v`<|z%V%P6UfgK?-UrG(oboRI(1-t<9mPPRL z!UNIO6W#F{(E0?7%=$h$w=e-jSaj=9h=lal5h=gTx}(T9j%$gfHkmzVL@Bi4C;T|U zor5i+`0r{xOVeX3AOzL3?)W}P2YNY<>&BM>S_)5|q`bvA?3`JSogEG+lsBHYJGclh zR>rnH82#B4_|-@Jx6+yacxF-rDS``2+~zl`$5wERfHdlXV)bI74^b)6X}b%&v!rje zIP0(Sh*c@8dI0D%UIY>w56GNOK9I5*nTk^~-_`QXf?Wz#Qe3o|`0V_OEUp8qi?gz3 z#`Gqtk6vsBUN6=Lr`MQ+z4Eitn{;W|1;}EmjHet9T~@m|?DBXYNL&OTADG!dJ%5U- z5;|A{ly+ZuYZrc=j@T)7k-@2J!buEj4@cB>NUD$rQd_s1{pVnQD! zM8V0JLgvuTIUQ%!X#VjC)XG4wuXUo$WUn5o!o=kz3K}uRU3we>d}~V4@8o+QwS#xJ znkxE(BaG{-9)qQun4M?DV!UoUzJbae{gD81_ED0WxNQhlTi<*ny>YLxJsOih4xj{P zwez;KxS==PDZkxeU^FpG2{qkAF~udaJuM-luqPqJpdp;kj?@mHb`ySi(HJfml)LNK z))B+4>%R40_A`v4P%(-}C`oA|3^*&*i?Rjd;raOmcWiBhUZ=X?Gu?OS>bfO2dlc!C zM**ersc|yqAL2cir?w<}SyVVMJ&KPe2ffv;q%K#21)b-dOd&laXS7=D@nmK!(9-Z* zDCNTNoBFvXbaE3tfSjCc{b=6JwSm{d4f!XLV8d5D!pR}t_P$aiZ_4-4v05-CH;|H~ z`$dOpTlO`DEC0jgp^KCLxH}_9?kgcXg>@Ugv;q{r@4r=Kqeo@Mm62O!mc|gzC^w_D z^b`lJ$}GKLJrS5<*5##vGENP<;;%qSXso=~fx4GI8Xw{Zsu&*^XD8(W@w%2YW3@qd zED9)6aGwVP3;ydS&3|d7{|`Dlex<8EyjW1MS|$x64Y*l%@Z=U*3flyf)hdVPB_CoV zXtkz+SuM5QGSfH);cQ?Af;AqBJ5iA04ODLmy2Xvo3mn7C=IFc|RKcLFF`(N0Q;&5i zqq0!&&^1v*Nh&;{+cWV7yKKKNwLH92&xBn?#bgIq4I!(9UvA;uTwhd4@{Nf}v96n~ zYo`OTy!1~qhR~grS%`%v#YT8jaRF;Tq^JM+El|wlOz2GM?Cu9ZX~QX+@*5;qpm+9u zzgh^!EW0x0dOf_z)fuFSp%E$|& z{iAv~xcnWds{BF>HJx@Pm{(#DX-o*NhH*m6ti%}i0nnzCU9U>?SO)&NkPJSEvGORV z2m0AoQ}saXdp<{d28p1fiH#Mf)VeOi`v(6y2@%(O1@%B5%$Xv-R^ZyJfJuKUzRzm! zV0&xWkFX1+S4bWxK;2n21fN#z)Vupqd97z9p&@PvCf`Ww@>g3ioc3V;X2r5mVsDx}zv-9@4qRdEP-QfFmr zH_SUMF3QPl$9{D@1YtZ3@!0}I9{b33ex+4wp^KlqtmSvO%J1^T#NFi#0UJHEk$NX& zwF$B7H^__C4rjB)@IlK}piiu`N)j8_F}29Fqd#G6t-ixAg!tQN0Wns4?Xz34Usv1j zWBdPSqW_eRn411hN+>fA8{TQaTuxQ?sFhWH`PKXNkC;uJ=Dp~Sbu}5O=#!QQ&NfWP zHw!isPEf8<| zM+L1mz+ayR02g-25LobEUf%lekuI?U{&k0aVkou6jR9br4H_`U;bSME!!8xx(B2mt zZ!j@KDxm=UI6+TszF;3m!qVBr>Lnp?54@QT1NW;3{-Q5O8Yei6PQzOFa5jWv{0)i) z^kW;T7HR?vggpkPjaY+J2r!0Sy}Q6wb_h0uD-PmkLRKuaHggahL7BjcE=MZdsMa5N z3A>!CRL=@m+a$XofFsyD?iw|vHjtZM-0Do1vx>T|8K3yjZs*j! z)9|ys_UqfR=&e(^EcG7G0OM;@=9@EGlD}FTuPh-{=q(8!Lo*YmjhHz7GxxfZY&iHT zj+8K-LzIi!o29Hh*s4-#l-mf)4* zt61vZ>3i>}IXJML`9vVPEe9*yYhqhga_Lrv_i_g&B!lsJ-jdQ3BBPS4?0+8*T9SSq zLLs}$^E3qsbj_kmVbx;+9iuQ~Q@=9J0d3RdIVwFg%Du5fju;lG)j`dT+e?U#=ucs# zGyQNHzRTZ|d?`8dt~>my;Z?xHG}r&vsj>Z_gZB;cwEPX}7-T zk)n3lh%SA((x|h!Msk#!O0r5$qqBf?JDD3F5z>r>o5c27glMHO?dB^DMx@_0@O>Hm z9^8c$1tN2>yb7bm6ZY~EFXg_M!Ip7q)^mWHA61Flfwedta$>(B;xG``FYBE=>-5*S zHs%%OGtx(RfQIR_wyd?uN|@_676%`799E6Vi};dXBVA>$ zx*mPum{Wzfg(l2KH?2W$J_Sf;ytA`{4Jn>D&|$x0#H!`bSSl#KkQ5@8_W&~z=cu!15MBMh`zN4XkBZ!j^OZM0}x-j zk6sttD0iXDmD}+p2we7MK4`)dt!b8EqVShzz$n|4_Sb2V_Yi>E-%DVTc-32hFI5-W4?;8XxF`ZfValCTS_R;5iAksoUK{}N&s5j`)8q^)6mLkg< zh+$Bc-7m?u&YSDqB>AXT;?mRIWm(%PW3BIxRd-jo?}BSWgu5J;Gg*K%I84(eyJ&$L z?g+qtlnj~av%~~w#CwciTMfWHgs)`U%NC|FPQZiFB-Iztz957>Y^o zHo=G*M#3>c;8%y&LZ|lgUZLYSHK?Jo3}EP=@Xv(rVkmomb~ux?O^!OBj0xIH;Qk1r zeLnr6rNSm>1694ySl49k^Gbh}Mdp38^q@hDvVHe=DVZ;nOi$lR|EZFFkh6`g1L5^y zYB;Y_&6p_#m(A7;*?MS@Kz?P3HzMqxsLJABCc!f7NQW~C zpOFSmyTIPwWT`(idkQ-YvEl$5Bueh_jtE*rHnTFen>-=*YmJLM63 zP!R(GeVAVU_9#M-+x8+lB(TBM{B|sM; zbd^5fO_n=nqX^JD0)SMAMc!4+AV?EKDygr)K2NmwBPV0cy!g1j7b2Hf>gBfB~PHC z8n#tFQCh2a4~B&;-O!yVcV_0S^^`Wj(-<+|b2wLRK|O}Cgjo2>c*5yJxe8BOi#_4( z>dof=jbVsy-27V?(f^BFCatfqPdRk(XHOa|tYSrlK&Lh^pNHr=)w+MPnDX>uR zHpS`O9Z|gOHo_LOfCffAM#U~h7dgKS0nX^r_Mwmq2Aus0(QRzP`wb-hf!PX3XM1fr zE4xJ+2@i)&g=@#;7u9M;j8&x(qbZ?}_{&Yw^@a@i3E$cXJM0QZP%l@>kqaj*&!>nj zf3r$tTHnsdaEo4p4PR^)#L21hqam!B)UX>&we4JwXI@WC0=^MUbeN=7ow%GVwA~jG zOpV%FY*}PD*$ZLVscp7{(=fi_9f}-d{VBnUr5ok0od@Mc2`5~2*%r%1)wr?tVzuwg zCRq}<4~Z^e$4m1|g~0Jz#?`ef#2iR7CdxxGi7=}Y~fNRv5*fprKpS1e*?7{ zy@?cHYZOB}y#@6NfAT~!rGcVzqB430Qk0hD$;N$MV-6|O}nE|%+340{Fw?Zi&e zhp|#49HPPm!N5cPfiR@cDpv1>=;9A3laf&!)DuF1BzHu8U17f zxy7k}Y=`+?O~BTB^x{bT+TAYvoUV$H5)O&L&-Q(q*+6d-!u}FR`H3yq6wn@293|yg z27zysLGzRAzEK?;euiuox3|@gB!7`y(K7Nq15Ydh4>~$Q+0m4GAhM;(wU=Qhmna99 ze&SVHUq$rT9k>o2M6foN8_j-d!c}DlW;xa-0RAcOVL4O_VnPENRy{S-C+$2s%hEgu z%=P|(+hQ800p)B|kXy3;g?(v#R$H;;Vw2?^GeC7}UX|^&ql(Ns5uu>lPsc1tzxy4& zYiT3p+J7~eY@j~n_O2+zar#lA*;$Lg45n4b`>aM}8U7I|q5Mg1!1$jQ2H4D5ZrZi6=5gn$kG8QUAa-Ug$ z#B@((EPyh%u}e#odQUXo&>0K1fVCAx>=pAYB99D>X8Pl`sSv?nF@x-yg#%D%_}fDX9ty0A+Bpifh+w#EIBFgt&S9+c(Z&_4Nf``o z6tv0-2%_t~p?$CVcsHj6nbNg8{i@yl?Tt6EW%{Z|zfXVWBe!W)P38F_#boz4NJpag zTK--ye~`IjEyAMrM$A-qok}uroqZGmCfT3U@kK6|=9hs%BjkEMm46FU7|fR(I_l9F zM5`j#xQart^MoSLU$iF;Q65b@ZUT*-L}NZ|U>MhTgD&0Toc@nuc}L>z=YmaCNqFr` z6Cj(2XbS}ribM`lry8IyKFr%giExfZ-(J%=QkH;wZZ!Y^tu9~?1GsEWXaUn@`Mj0} z0Zx!gLzPjulHUWtW!PW_d)g^w$KGh2-xmYXeuY?o`}|I^O}QOo43Jfv0-_qjUOv5{ zcBNa}6HvP!7VfXp`VWcS2f~Nc&^Q{;SG+kPE{!dlTYz*0gQmiD*7B{OgXTYT#tqbO zBRS!OUE-ogAIAHK+1u*iSyTB(GG_NL>0PwWUU)Qvho1qeYe(;{)h{=g9CzI;b^;7o z%;9rsUwTxM!EtbNopED@oLn*0@9r0;QYV0U#gqt0Hq2gvnKR!Aoah!O^AxZ~1A((N zsb3aZMJ8ICnvaKD{47sw!Md#pXNMnihLS;jn4E>X7I*cCHLh1i-0kL$??^xTX|te(gx^Ggm Date: Thu, 30 Sep 2021 15:59:09 +0200 Subject: [PATCH 149/213] 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 150/213] 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 151/213] 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 152/213] 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 153/213] 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 154/213] 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 155/213] 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 156/213] 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 157/213] 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 158/213] 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 159/213] 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 160/213] 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 161/213] 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 162/213] 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 163/213] 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 164/213] 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 165/213] 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 166/213] 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 167/213] 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 168/213] 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 169/213] 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 170/213] 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 171/213] 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 172/213] 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 173/213] 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 174/213] 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 175/213] 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 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 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 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 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 176/213] 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 177/213] [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 178/213] 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 179/213] 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 180/213] 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 181/213] 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 182/213] 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 183/213] 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 184/213] 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 185/213] 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 186/213] 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 187/213] 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 188/213] 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 189/213] 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 190/213] 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 191/213] 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 192/213] 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 193/213] 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 194/213] 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 195/213] 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 196/213] 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 197/213] 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 198/213] 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 199/213] 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 200/213] 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 201/213] [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 202/213] 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 203/213] 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 204/213] 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 205/213] 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 94d4926084a594cfd1170c7f7cbaaf77a16a1fea Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 7 Oct 2021 12:51:50 +0200 Subject: [PATCH 206/213] PYPE-1218 - changed namespace to contain subset name --- openpype/hosts/maya/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 448cb814d9..fdad0e0989 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -123,7 +123,7 @@ class ReferenceLoader(api.Loader): count = options.get("count") or 1 for c in range(0, count): namespace = namespace or lib.unique_namespace( - asset["name"] + "_", + "{}_{}_".format(asset["name"], context["subset"]["name"]), prefix="_" if asset["name"][0].isdigit() else "", suffix="_", ) From dd71de1b70776541f41b1a9977a402a29ee94c43 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 7 Oct 2021 16:53:17 +0200 Subject: [PATCH 207/213] get path for the first time publish --- openpype/plugins/publish/collect_resources_path.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index c21f09ab8d..fa181301ee 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -68,6 +68,12 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "representation": "TEMP" }) + # For the first time publish + if instance.data.get("hierarchy"): + template_data.update({ + "hierarchy": instance.data["hierarchy"] + }) + anatomy_filled = anatomy.format(template_data) if "folder" in anatomy.templates["publish"]: From 8312412cd6105fd18a38a3193cfbc7fe237ca89f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 9 Oct 2021 03:38:41 +0000 Subject: [PATCH 208/213] [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" From d9e63d0a69e086067b241f3735e614656d81fde2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 11 Oct 2021 11:35:58 +0200 Subject: [PATCH 209/213] PYPE-1218 - update to namespace name for groups --- openpype/hosts/maya/plugins/load/load_reference.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 6dd161cf98..cfe8149218 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -41,14 +41,13 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): family = "model" with maya.maintained_selection(): - - groupName = "{}:{}".format(namespace, name) + groupName = "{}:_GRP".format(namespace) cmds.loadPlugin("AbcImport.mll", quiet=True) nodes = cmds.file(self.fname, namespace=namespace, sharedReferenceFile=False, groupReference=True, - groupName="{}:{}".format(namespace, name), + groupName=groupName, reference=True, returnNewNodes=True) From 330d61cf00658e591b35fc4c8f48d4543df030a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 11 Oct 2021 17:40:41 +0200 Subject: [PATCH 210/213] make sure that mapping is set to iterable --- start.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/start.py b/start.py index ada613b4eb..a4c58b8fc7 100644 --- a/start.py +++ b/start.py @@ -287,9 +287,8 @@ def run_disk_mapping_commands(mongo_url): if not disk_mapping: return - for mapping in disk_mapping.get(low_platform): - source, destination = mapping - + mappings = disk_mapping.get(low_platform) or [] + for source, destination in mappings: args = ["subst", destination.rstrip('/'), source.rstrip('/')] _print("disk mapping args:: {}".format(args)) try: From 7a956f495f7d37ee71c368bc65aee7416ce6e9b4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 11 Oct 2021 17:40:55 +0200 Subject: [PATCH 211/213] disk mapping is now group --- .../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 31cd997d14..51a58a6e27 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", + "is_group": true, "use_label_wrap": false, "collapsible": false, "children": [ From a1cc57d8ef8e2914e7e8a1726f65787a662fb989 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 13 Oct 2021 03:39:34 +0000 Subject: [PATCH 212/213] [Automated] Bump version --- CHANGELOG.md | 9 ++++----- openpype/version.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c88f5c9ca0..7f92fdc9f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.5.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.5.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...HEAD) @@ -28,10 +28,11 @@ - 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** +- General: Disk mapping group [\#2120](https://github.com/pypeclub/OpenPype/pull/2120) +- Hiero: publishing effect first time makes wrong resources path [\#2115](https://github.com/pypeclub/OpenPype/pull/2115) - 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) @@ -65,7 +66,6 @@ - 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) @@ -102,6 +102,7 @@ **🚀 Enhancements** +- Ftrack: Removed ftrack interface [\#2049](https://github.com/pypeclub/OpenPype/pull/2049) - 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) @@ -114,7 +115,6 @@ - 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) **🐛 Bug fixes** @@ -129,7 +129,6 @@ - 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) ## [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 f6ace59d7d..3a589bac75 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.5" +__version__ = "3.5.0-nightly.6" From 3f74b6b3854005ec238a8e3b3d551eed43bb187b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 Oct 2021 14:16:16 +0200 Subject: [PATCH 213/213] make sure CollectDefaultDeadlineServer is after CollectModules --- .../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 968bffd890..1bc4eaa067 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 + 0.01 + order = pyblish.api.CollectorOrder + 0.02 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 afb8583069..53231bd7e4 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 + order = pyblish.api.CollectorOrder + 0.01 label = "Default Deadline Webservice" def process(self, context):