From c30b917e6ca5c4e754bc60450de9495482051ebd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Jun 2021 17:53:09 +0200 Subject: [PATCH 001/716] remove PyQt5 and add PySide2 --- openpype/lib/import_utils.py | 15 +++++++ poetry.lock | 75 ++++++--------------------------- pyproject.toml | 1 - tools/fetch_thirdparty_libs.ps1 | 3 ++ 4 files changed, 31 insertions(+), 63 deletions(-) diff --git a/openpype/lib/import_utils.py b/openpype/lib/import_utils.py index 4e72618803..9f459bbb51 100644 --- a/openpype/lib/import_utils.py +++ b/openpype/lib/import_utils.py @@ -2,6 +2,7 @@ import os import sys import importlib from .log import PypeLogger as Logger +from pathlib import Path log = Logger().get_logger(__name__) @@ -23,3 +24,17 @@ def discover_host_vendor_module(module_name): sys.path.insert(1, module_path) return importlib.import_module(module_name) + + +def get_pyside2_location(): + """Get location of PySide2 and its dependencies. + + Returned path can be used with `site.addsitedir()` + + Returns: + str: path to PySide2 + + """ + path = Path(os.getenv("OPENPYPE_ROOT")) + path = path / "vendor/python/PySide2" + return str(path) diff --git a/poetry.lock b/poetry.lock index 48e6f95469..869e953060 100644 --- a/poetry.lock +++ b/poetry.lock @@ -941,34 +941,6 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "pyqt5" -version = "5.15.4" -description = "Python bindings for the Qt cross platform application toolkit" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -PyQt5-Qt5 = ">=5.15" -PyQt5-sip = ">=12.8,<13" - -[[package]] -name = "pyqt5-qt5" -version = "5.15.2" -description = "The subset of a Qt installation needed by PyQt5." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pyqt5-sip" -version = "12.9.0" -description = "The sip module support for PyQt5" -category = "main" -optional = false -python-versions = ">=3.5" - [[package]] name = "pyrsistent" version = "0.17.3" @@ -1466,7 +1438,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 = "70c5951f20ded8f10757ea030f7a99a49c1ea6773ad944f922533b692a3c5166" [metadata.files] acre = [] @@ -1582,24 +1554,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"}, @@ -1976,7 +1960,6 @@ pillow = [ {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"}, {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"}, {file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8b56553c0345ad6dcb2e9b433ae47d67f95fc23fe28a0bde15a120f25257e291"}, {file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"}, ] pluggy = [ @@ -2177,38 +2160,6 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] -pyqt5 = [ - {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-abi3-macosx_10_13_intel.whl", hash = "sha256:8c0848ba790a895801d5bfd171da31cad3e551dbcc4e59677a3b622de2ceca98"}, - {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:883a549382fc22d29a0568f3ef20b38c8e7ab633a59498ac4eb63a3bf36d3fd3"}, - {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-none-win32.whl", hash = "sha256:a88526a271e846e44779bb9ad7a738c6d3c4a9d01e15a128ecfc6dd4696393b7"}, - {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:213bebd51821ed89b4d5b35bb10dbe67564228b3568f463a351a08e8b1677025"}, - {file = "PyQt5-5.15.4.tar.gz", hash = "sha256:2a69597e0dd11caabe75fae133feca66387819fc9bc050f547e5551bce97e5be"}, -] -pyqt5-qt5 = [ - {file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"}, - {file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"}, - {file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"}, - {file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"}, -] -pyqt5-sip = [ - {file = "PyQt5_sip-12.9.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:d85002238b5180bce4b245c13d6face848faa1a7a9e5c6e292025004f2fd619a"}, - {file = "PyQt5_sip-12.9.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:83c3220b1ca36eb8623ba2eb3766637b19eb0ce9f42336ad8253656d32750c0a"}, - {file = "PyQt5_sip-12.9.0-cp36-cp36m-win32.whl", hash = "sha256:d8b2bdff7bbf45bc975c113a03b14fd669dc0c73e1327f02706666a7dd51a197"}, - {file = "PyQt5_sip-12.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:69a3ad4259172e2b1aa9060de211efac39ddd734a517b1924d9c6c0cc4f55f96"}, - {file = "PyQt5_sip-12.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42274a501ab4806d2c31659170db14c282b8313d2255458064666d9e70d96206"}, - {file = "PyQt5_sip-12.9.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a8701892a01a5a2a4720872361197cc80fdd5f49c8482d488ddf38c9c84f055"}, - {file = "PyQt5_sip-12.9.0-cp37-cp37m-win32.whl", hash = "sha256:ac57d796c78117eb39edd1d1d1aea90354651efac9d3590aac67fa4983f99f1f"}, - {file = "PyQt5_sip-12.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4347bd81d30c8e3181e553b3734f91658cfbdd8f1a19f254777f906870974e6d"}, - {file = "PyQt5_sip-12.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c446971c360a0a1030282a69375a08c78e8a61d568bfd6dab3dcc5cf8817f644"}, - {file = "PyQt5_sip-12.9.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fc43f2d7c438517ee33e929e8ae77132749c15909afab6aeece5fcf4147ffdb5"}, - {file = "PyQt5_sip-12.9.0-cp38-cp38-win32.whl", hash = "sha256:055581c6fed44ba4302b70eeb82e979ff70400037358908f251cd85cbb3dbd93"}, - {file = "PyQt5_sip-12.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:c5216403d4d8d857ec4a61f631d3945e44fa248aa2415e9ee9369ab7c8a4d0c7"}, - {file = "PyQt5_sip-12.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a25b9843c7da6a1608f310879c38e6434331aab1dc2fe6cb65c14f1ecf33780e"}, - {file = "PyQt5_sip-12.9.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:dd05c768c2b55ffe56a9d49ce6cc77cdf3d53dbfad935258a9e347cbfd9a5850"}, - {file = "PyQt5_sip-12.9.0-cp39-cp39-win32.whl", hash = "sha256:4f8e05fe01d54275877c59018d8e82dcdd0bc5696053a8b830eecea3ce806121"}, - {file = "PyQt5_sip-12.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:b09f4cd36a4831229fb77c424d89635fa937d97765ec90685e2f257e56a2685a"}, - {file = "PyQt5_sip-12.9.0.tar.gz", hash = "sha256:d3e4489d7c2b0ece9d203ae66e573939f7f60d4d29e089c9f11daa17cfeaae32"}, -] pyrsistent = [ {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, ] diff --git a/pyproject.toml b/pyproject.toml index e376986606..c9580b1601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ Pillow = "^8.1" # only used for slates prototype pyblish-base = "^1.8.8" pynput = "^1.7.2" # idle manager in tray pymongo = "^3.11.2" -pyqt5 = "^5.12.2" # ideally should be replaced with PySide2 "Qt.py" = "^1.3.3" speedcopy = "^2.1" six = "^1.15" diff --git a/tools/fetch_thirdparty_libs.ps1 b/tools/fetch_thirdparty_libs.ps1 index 23f0b50c7a..7ece9ee10e 100644 --- a/tools/fetch_thirdparty_libs.ps1 +++ b/tools/fetch_thirdparty_libs.ps1 @@ -36,6 +36,9 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { } else { Write-Host "OK" -ForegroundColor Green } +Write-Host ">>> " -NoNewline -ForegroundColor Green +Write-Host "Installing PySide2 ... " +& "$($env:POETRY_HOME)\bin\poetry.bat" run python -m pip install PySide2 -t "$($openpype_root)\vendor\python\PySide2" & poetry run python "$($openpype_root)\tools\fetch_thirdparty_libs.py" Set-Location -Path $current_dir From fbc7ccab31613d8766d5ef5408a3b2f8718ca720 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Jun 2021 18:08:05 +0200 Subject: [PATCH 002/716] linux version --- tools/fetch_thirdparty_libs.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/fetch_thirdparty_libs.sh b/tools/fetch_thirdparty_libs.sh index 31f109ba68..e5c79b81dc 100755 --- a/tools/fetch_thirdparty_libs.sh +++ b/tools/fetch_thirdparty_libs.sh @@ -99,6 +99,9 @@ main () { pushd "$openpype_root" > /dev/null || return > /dev/null + echo -e "${BIGreen}>>>${RST} Installing PySide2 ..." + "$POETRY_HOME/bin/poetry" run python -m pip install PySide2 -t "$openpype_root/vendor/python/PySide2" + echo -e "${BIGreen}>>>${RST} Running Pype tool ..." poetry run python "$openpype_root/tools/fetch_thirdparty_libs.py" } From 09486d7e66cd5d946164605f25442642dbdf5ac1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Jun 2021 18:09:57 +0200 Subject: [PATCH 003/716] add vendor/python to gitignore --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 07c1c151ce..c4d0d4a9d0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -142,5 +142,6 @@ cython_debug/ .poetry/ .github/ vendor/bin/ +vendor/python/ docs/ website/ From 6c06eefaefa86314b524724eeb2d1809477d23b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 10:24:11 +0200 Subject: [PATCH 004/716] added python vendor to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 221a2f2241..fa3fae1ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ Temporary Items /dist/ /vendor/bin/* +/vendor/python/* /.venv /venv/ From 25a18e1bc1b74da8c05521411e50d405bd9a7fc6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 10:24:35 +0200 Subject: [PATCH 005/716] do not install PySide2 into subfolder --- tools/fetch_thirdparty_libs.ps1 | 2 +- tools/fetch_thirdparty_libs.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/fetch_thirdparty_libs.ps1 b/tools/fetch_thirdparty_libs.ps1 index 7ece9ee10e..f87ce3e724 100644 --- a/tools/fetch_thirdparty_libs.ps1 +++ b/tools/fetch_thirdparty_libs.ps1 @@ -38,7 +38,7 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { } Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Installing PySide2 ... " -& "$($env:POETRY_HOME)\bin\poetry.bat" run python -m pip install PySide2 -t "$($openpype_root)\vendor\python\PySide2" +& "$($env:POETRY_HOME)\bin\poetry.bat" run python -m pip install PySide2 -t "$($openpype_root)\vendor\python" & poetry run python "$($openpype_root)\tools\fetch_thirdparty_libs.py" Set-Location -Path $current_dir diff --git a/tools/fetch_thirdparty_libs.sh b/tools/fetch_thirdparty_libs.sh index e5c79b81dc..f619bc9f01 100755 --- a/tools/fetch_thirdparty_libs.sh +++ b/tools/fetch_thirdparty_libs.sh @@ -100,7 +100,7 @@ main () { pushd "$openpype_root" > /dev/null || return > /dev/null echo -e "${BIGreen}>>>${RST} Installing PySide2 ..." - "$POETRY_HOME/bin/poetry" run python -m pip install PySide2 -t "$openpype_root/vendor/python/PySide2" + "$POETRY_HOME/bin/poetry" run python -m pip install PySide2 -t "$openpype_root/vendor/python" echo -e "${BIGreen}>>>${RST} Running Pype tool ..." poetry run python "$openpype_root/tools/fetch_thirdparty_libs.py" From 64a1839c9d0f1632db45ccb5038816acc10046aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 10:24:59 +0200 Subject: [PATCH 006/716] add vendor directory to sys.path in start.py --- start.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/start.py b/start.py index 8e7c195e95..905526480f 100644 --- a/start.py +++ b/start.py @@ -124,6 +124,10 @@ else: paths.append(frozen_libs) os.environ["PYTHONPATH"] = os.pathsep.join(paths) +# Vendored python modules that must not be in PYTHONPATH environment but +# are required for OpenPype processes +vendor_python_path = os.path.join(OPENPYPE_ROOT, "vendor", "python") +sys.path.insert(0, vendor_python_path) import blessed # noqa: E402 import certifi # noqa: E402 From 39bf35429b7403735c3ff02025e78a7fc46aed5a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 10:25:34 +0200 Subject: [PATCH 007/716] removed get_pyside2_location as is not needed inside openpype --- openpype/lib/import_utils.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/openpype/lib/import_utils.py b/openpype/lib/import_utils.py index 9f459bbb51..4e72618803 100644 --- a/openpype/lib/import_utils.py +++ b/openpype/lib/import_utils.py @@ -2,7 +2,6 @@ import os import sys import importlib from .log import PypeLogger as Logger -from pathlib import Path log = Logger().get_logger(__name__) @@ -24,17 +23,3 @@ def discover_host_vendor_module(module_name): sys.path.insert(1, module_path) return importlib.import_module(module_name) - - -def get_pyside2_location(): - """Get location of PySide2 and its dependencies. - - Returned path can be used with `site.addsitedir()` - - Returns: - str: path to PySide2 - - """ - path = Path(os.getenv("OPENPYPE_ROOT")) - path = path / "vendor/python/PySide2" - return str(path) From ea8a3e891d68806384a08857c9f55b1fb70c4c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 23 Jun 2021 10:50:47 +0200 Subject: [PATCH 008/716] changes in ignore files --- .dockerignore | 2 +- .gitignore | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index c4d0d4a9d0..9c506b9964 100644 --- a/.dockerignore +++ b/.dockerignore @@ -87,7 +87,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. diff --git a/.gitignore b/.gitignore index 221a2f2241..d2c682b1e5 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,5 @@ website/.docusaurus .poetry/ .python-version + +vendor/python/PySide2 From 2e35e30ead8f06ee79dbe576d382b5a73a09678d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 11:04:17 +0200 Subject: [PATCH 009/716] removed duplicated ignore directory --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 63d311f033..fa3fae1ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -100,5 +100,3 @@ website/.docusaurus .poetry/ .python-version - -vendor/python/PySide2 From 5937461cf6c53f29d48153a14527bb771031846f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 11:26:05 +0200 Subject: [PATCH 010/716] fix receivers discovery --- openpype/tools/settings/settings/window.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index a60a2a1d88..54f8ec0a11 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -141,7 +141,10 @@ class MainWidget(QtWidgets.QWidget): # Don't show dialog if there are not registered slots for # `trigger_restart` signal. # - For example when settings are runnin as standalone tool - if self.receivers(self.trigger_restart) < 1: + # - PySide2 and PyQt5 compatible way how to find out + method_index = self.metaObject().indexOfMethod("trigger_restart()") + method = self.metaObject().method(method_index) + if not self.isSignalConnected(method): return dialog = RestartDialog(self) From 60445b705cf2dd9591aa12ec9c7969e441a9de90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 11:56:56 +0200 Subject: [PATCH 011/716] fix log viewer stylesheet of qtoolbutton --- openpype/modules/log_viewer/tray/app.py | 8 ++++---- openpype/modules/log_viewer/tray/widgets.py | 5 ++--- openpype/style/style.css | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/modules/log_viewer/tray/app.py b/openpype/modules/log_viewer/tray/app.py index 9aab37cd20..1e8d6483cd 100644 --- a/openpype/modules/log_viewer/tray/app.py +++ b/openpype/modules/log_viewer/tray/app.py @@ -7,12 +7,13 @@ class LogsWindow(QtWidgets.QWidget): def __init__(self, parent=None): super(LogsWindow, self).__init__(parent) - self.setStyleSheet(style.load_stylesheet()) + self.setWindowTitle("Logs viewer") + self.resize(1400, 800) log_detail = OutputWidget(parent=self) logs_widget = LogsWidget(log_detail, parent=self) - main_layout = QtWidgets.QHBoxLayout() + main_layout = QtWidgets.QHBoxLayout(self) log_splitter = QtWidgets.QSplitter(self) log_splitter.setOrientation(QtCore.Qt.Horizontal) @@ -24,5 +25,4 @@ class LogsWindow(QtWidgets.QWidget): self.logs_widget = logs_widget self.log_detail = log_detail - self.setLayout(main_layout) - self.setWindowTitle("Logs") + self.setStyleSheet(style.load_stylesheet()) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index b9a8499a4c..d906a1b6ad 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -76,13 +76,12 @@ class CustomCombo(QtWidgets.QWidget): toolbutton.setMenu(toolmenu) toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) + toolbutton.setProperty("popup_mode", "1") - layout = QtWidgets.QHBoxLayout() + layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(toolbutton) - self.setLayout(layout) - toolmenu.selection_changed.connect(self.selection_changed) self.toolbutton = toolbutton diff --git a/openpype/style/style.css b/openpype/style/style.css index c57b9a8da6..8391fcd0ae 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -97,7 +97,7 @@ QToolButton:disabled { background: {color:bg-buttons-disabled}; } -QToolButton[popupMode="1"] { +QToolButton[popupMode="1"], QToolButton[popup_mode="1"] { /* make way for the popup button */ padding-right: 20px; border: 1px solid {color:bg-buttons}; From 7b45e69f99dba951c9f9a13ee3cea73061aa187e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 12:11:17 +0200 Subject: [PATCH 012/716] added comment --- openpype/modules/log_viewer/tray/widgets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index d906a1b6ad..669acf4b67 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -76,6 +76,9 @@ class CustomCombo(QtWidgets.QWidget): toolbutton.setMenu(toolmenu) toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) + + # Fake popupMenu property as PySide2 does not store it's value as + # integer but as enum object toolbutton.setProperty("popup_mode", "1") layout = QtWidgets.QHBoxLayout(self) From 25cb349b7a0f1de801831c638e12933b0d71e672 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 12:38:50 +0200 Subject: [PATCH 013/716] fixed popupMode property --- openpype/modules/log_viewer/tray/widgets.py | 4 ---- openpype/style/style.css | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index 669acf4b67..0f77a7f111 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -77,10 +77,6 @@ class CustomCombo(QtWidgets.QWidget): toolbutton.setMenu(toolmenu) toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) - # Fake popupMenu property as PySide2 does not store it's value as - # integer but as enum object - toolbutton.setProperty("popup_mode", "1") - layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(toolbutton) diff --git a/openpype/style/style.css b/openpype/style/style.css index 8391fcd0ae..8dffd98e43 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -97,7 +97,7 @@ QToolButton:disabled { background: {color:bg-buttons-disabled}; } -QToolButton[popupMode="1"], QToolButton[popup_mode="1"] { +QToolButton[popupMode="1"], QToolButton[popupMode="MenuButtonPopup"] { /* make way for the popup button */ padding-right: 20px; border: 1px solid {color:bg-buttons}; From bc30890b88ee23cce7c3908d47fab215b5d128c6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 23 Jun 2021 14:58:31 +0200 Subject: [PATCH 014/716] get PySide2 version from pyproject.toml --- pyproject.toml | 6 ++++++ tools/fetch_thirdparty_libs.ps1 | 3 --- tools/fetch_thirdparty_libs.py | 21 +++++++++++++++++++-- tools/fetch_thirdparty_libs.sh | 3 --- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c9580b1601..1e797130db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,12 @@ build-backend = "poetry.core.masonry.api" [openpype] +[openpype.pyside2] +# note: in here we can use pip version specifiers as this is installed with pip until +# Poetry will support custom location (-t flag for pip) +# https://pip.pypa.io/en/stable/cli/pip_install/#requirement-specifiers +version = "==5.15.2" + [openpype.thirdparty.ffmpeg.windows] url = "https://distribute.openpype.io/thirdparty/ffmpeg-4.4-windows.zip" hash = "dd51ba29d64ee238e7c4c3c7301b19754c3f0ee2e2a729c20a0e2789e72db925" diff --git a/tools/fetch_thirdparty_libs.ps1 b/tools/fetch_thirdparty_libs.ps1 index f87ce3e724..23f0b50c7a 100644 --- a/tools/fetch_thirdparty_libs.ps1 +++ b/tools/fetch_thirdparty_libs.ps1 @@ -36,9 +36,6 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { } else { Write-Host "OK" -ForegroundColor Green } -Write-Host ">>> " -NoNewline -ForegroundColor Green -Write-Host "Installing PySide2 ... " -& "$($env:POETRY_HOME)\bin\poetry.bat" run python -m pip install PySide2 -t "$($openpype_root)\vendor\python" & poetry run python "$($openpype_root)\tools\fetch_thirdparty_libs.py" Set-Location -Path $current_dir diff --git a/tools/fetch_thirdparty_libs.py b/tools/fetch_thirdparty_libs.py index 75ee052950..60392d782c 100644 --- a/tools/fetch_thirdparty_libs.py +++ b/tools/fetch_thirdparty_libs.py @@ -20,6 +20,7 @@ import hashlib import tarfile import zipfile import time +import subprocess term = blessed.Terminal() @@ -65,11 +66,27 @@ def _print(msg: str, message_type: int = 0) -> None: print("{}{}".format(header, msg)) - -_print("Processing third-party dependencies ...") start_time = time.time_ns() openpype_root = Path(os.path.dirname(__file__)).parent pyproject = toml.load(openpype_root / "pyproject.toml") +_print("Handling PySide2 Qt framework ...") +pyside2_version = None +try: + pyside2_version = pyproject["openpype"]["pyside2"]["version"] +except AttributeError: + _print("No PySide2 version was specified, using latest available.", 2) + +pyside2_arg = "PySide2" if pyside2_version else "PySide2{}".format(pyside2_version) # noqa: E501 +try: + subprocess.run( + [sys.executable, "-m", "pip", "install", "--upgrade", + pyside2_arg, "-t", str(openpype_root / "vendor/python")], check=True) +except subprocess.CalledProcessError as e: + _print("Error during PySide2 installation.", 1) + _print(str(e), 1) + sys.exit(1) + +_print("Processing third-party dependencies ...") platform_name = platform.system().lower() try: diff --git a/tools/fetch_thirdparty_libs.sh b/tools/fetch_thirdparty_libs.sh index f619bc9f01..31f109ba68 100755 --- a/tools/fetch_thirdparty_libs.sh +++ b/tools/fetch_thirdparty_libs.sh @@ -99,9 +99,6 @@ main () { pushd "$openpype_root" > /dev/null || return > /dev/null - echo -e "${BIGreen}>>>${RST} Installing PySide2 ..." - "$POETRY_HOME/bin/poetry" run python -m pip install PySide2 -t "$openpype_root/vendor/python" - echo -e "${BIGreen}>>>${RST} Running Pype tool ..." poetry run python "$openpype_root/tools/fetch_thirdparty_libs.py" } From 4362668b66f22fec09b26a65ec8ae767353eed79 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 10:15:49 +0200 Subject: [PATCH 015/716] use parenting to skip style set --- .../tools/standalonepublish/widgets/widget_component_item.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/standalonepublish/widgets/widget_component_item.py b/openpype/tools/standalonepublish/widgets/widget_component_item.py index 186c8024db..de3cde50cd 100644 --- a/openpype/tools/standalonepublish/widgets/widget_component_item.py +++ b/openpype/tools/standalonepublish/widgets/widget_component_item.py @@ -1,7 +1,6 @@ import os from Qt import QtCore, QtGui, QtWidgets from .resources import get_resource -from avalon import style class ComponentItem(QtWidgets.QFrame): @@ -61,7 +60,7 @@ class ComponentItem(QtWidgets.QFrame): name="menu", size=QtCore.QSize(22, 22) ) - self.action_menu = QtWidgets.QMenu() + self.action_menu = QtWidgets.QMenu(self.btn_action_menu) expanding_sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding @@ -229,7 +228,6 @@ class ComponentItem(QtWidgets.QFrame): if not self.btn_action_menu.isVisible(): self.btn_action_menu.setVisible(True) self.btn_action_menu.clicked.connect(self.show_actions) - self.action_menu.setStyleSheet(style.load_stylesheet()) def set_repre_name_valid(self, valid): self.has_valid_repre = valid From d28f8cd1d433b002bff2ded970b7950bd21fc120 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 30 Jun 2021 18:13:11 +0200 Subject: [PATCH 016/716] fix linux scripts, add patchelf --- Dockerfile | 46 ++++++++++++++++------------------ tools/build.sh | 5 ++-- tools/create_env.sh | 4 +-- tools/docker_build.sh | 2 +- tools/fetch_thirdparty_libs.py | 5 ++-- tools/fetch_thirdparty_libs.sh | 4 +-- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2d8ed27b15..99b9743de0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build Pype docker image -FROM centos:7 AS builder +FROM centos:7 AS system_builder ARG OPENPYPE_PYTHON_VERSION=3.7.10 LABEL org.opencontainers.image.name="pypeclub/openpype" @@ -22,6 +22,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n which \ git \ devtoolset-7-gcc* \ + gcc-c++ \ make \ cmake \ curl \ @@ -35,13 +36,19 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n openssl-devel \ tk-devel libffi-devel \ qt5-qtbase-devel \ - patchelf \ + autoconf \ + automake \ && yum clean all -RUN mkdir /opt/openpype -# RUN useradd -m pype -# RUN chown pype /opt/openpype -# USER pype +# we need to build our own patchelf +WORKDIR /temp-patchelf +RUN git clone https://github.com/NixOS/patchelf.git . \ + && source scl_source enable devtoolset-7 \ + && ./bootstrap.sh \ + && ./configure \ + && make \ + && make install + RUN curl https://pyenv.run | bash ENV PYTHON_CONFIGURE_OPTS --enable-shared @@ -49,27 +56,21 @@ ENV PYTHON_CONFIGURE_OPTS --enable-shared RUN echo 'export PATH="$HOME/.pyenv/bin:$PATH"'>> $HOME/.bashrc \ && echo 'eval "$(pyenv init -)"' >> $HOME/.bashrc \ && echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/.bashrc \ - && echo 'eval "$(pyenv init --path)"' >> $HOME/.bashrc -RUN source $HOME/.bashrc && pyenv install ${OPENPYPE_PYTHON_VERSION} - -COPY . /opt/openpype/ -RUN rm -rf /openpype/.poetry || echo "No Poetry installed yet." -# USER root -# RUN chown -R pype /opt/openpype -RUN chmod +x /opt/openpype/tools/create_env.sh && chmod +x /opt/openpype/tools/build.sh - -# USER pype + && echo 'eval "$(pyenv init --path)"' >> $HOME/.bashrc \ + && source $HOME/.bashrc \ + && pyenv install ${OPENPYPE_PYTHON_VERSION} WORKDIR /opt/openpype - -RUN cd /opt/openpype \ +COPY . /opt/openpype/ +RUN rm -rf /opt/openpype/.poetry || echo "No Poetry installed yet." \ + && chmod +x /opt/openpype/tools/create_env.sh \ + && chmod +x /opt/openpype/tools/build.sh \ && source $HOME/.bashrc \ && pyenv local ${OPENPYPE_PYTHON_VERSION} RUN source $HOME/.bashrc \ - && ./tools/create_env.sh - -RUN source $HOME/.bashrc \ + && ./tools/create_env.sh \ + && source $HOME/.bashrc \ && ./tools/fetch_thirdparty_libs.sh RUN source $HOME/.bashrc \ @@ -77,6 +78,3 @@ RUN source $HOME/.bashrc \ && cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.7/lib \ && cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.7/lib \ && cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.7/lib - -RUN cd /opt/openpype \ - rm -rf ./vendor/bin diff --git a/tools/build.sh b/tools/build.sh index aa8f0121ea..4343431c2b 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -58,7 +58,7 @@ BICyan='\033[1;96m' # Cyan BIWhite='\033[1;97m' # White args=$@ -disable_submodule_update = 0 +disable_submodule_update=0 while :; do case $1 in --no-submodule-update) @@ -122,7 +122,7 @@ clean_pyc () { local path path=$openpype_root echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c" - find "$path" -path ./build -prune -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete + find "$path" -path ./build -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete echo -e "${BIGreen}DONE${RST}" } @@ -228,3 +228,4 @@ if [ "$disable_submodule_update" == 1 ]; then } main +exit $? diff --git a/tools/create_env.sh b/tools/create_env.sh index 226a26e199..f93b8e32e6 100755 --- a/tools/create_env.sh +++ b/tools/create_env.sh @@ -126,7 +126,7 @@ clean_pyc () { local path path=$openpype_root echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c" - find "$path" -path ./build -prune -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete + find "$path" -path ./build -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete echo -e "${BIGreen}DONE${RST}" } @@ -177,7 +177,7 @@ main () { echo -e "${BIGreen}>>>${RST} Installing dependencies ..." fi - poetry install --no-root $poetry_verbosity || { echo -e "${BIRed}!!!${RST} Poetry environment installation failed"; return; } + poetry install --no-root --ansi $poetry_verbosity || { echo -e "${BIRed}!!!${RST} Poetry environment installation failed"; return; } echo -e "${BIGreen}>>>${RST} Cleaning cache files ..." clean_pyc diff --git a/tools/docker_build.sh b/tools/docker_build.sh index 7600fe044b..a6df2a099e 100755 --- a/tools/docker_build.sh +++ b/tools/docker_build.sh @@ -32,7 +32,7 @@ main () { openpype_version="$(python3 <<< ${version_command})" echo -e "${BIGreen}>>>${RST} Running docker build ..." - docker build --pull --no-cache -t pypeclub/openpype:$openpype_version . + docker build --pull -t pypeclub/openpype:$openpype_version . if [ $? -ne 0 ] ; then echo -e "${BIRed}!!!${RST} Docker build failed." return 1 diff --git a/tools/fetch_thirdparty_libs.py b/tools/fetch_thirdparty_libs.py index 60392d782c..1ded907576 100644 --- a/tools/fetch_thirdparty_libs.py +++ b/tools/fetch_thirdparty_libs.py @@ -73,14 +73,15 @@ _print("Handling PySide2 Qt framework ...") pyside2_version = None try: pyside2_version = pyproject["openpype"]["pyside2"]["version"] + _print("We'll install PySide2{}".format(pyside2_version)) except AttributeError: _print("No PySide2 version was specified, using latest available.", 2) -pyside2_arg = "PySide2" if pyside2_version else "PySide2{}".format(pyside2_version) # noqa: E501 +pyside2_arg = "PySide2" if not pyside2_version else "PySide2{}".format(pyside2_version) # noqa: E501 try: subprocess.run( [sys.executable, "-m", "pip", "install", "--upgrade", - pyside2_arg, "-t", str(openpype_root / "vendor/python")], check=True) + pyside2_arg, "-t", str(openpype_root / "vendor/python")], check=True, stdout=subprocess.DEVNULL) except subprocess.CalledProcessError as e: _print("Error during PySide2 installation.", 1) _print(str(e), 1) diff --git a/tools/fetch_thirdparty_libs.sh b/tools/fetch_thirdparty_libs.sh index 31f109ba68..12116d9e9e 100755 --- a/tools/fetch_thirdparty_libs.sh +++ b/tools/fetch_thirdparty_libs.sh @@ -99,8 +99,8 @@ main () { pushd "$openpype_root" > /dev/null || return > /dev/null - echo -e "${BIGreen}>>>${RST} Running Pype tool ..." + echo -e "${BIGreen}>>>${RST} Fetching third party dependencies ..." poetry run python "$openpype_root/tools/fetch_thirdparty_libs.py" } -main \ No newline at end of file +main From f460fad23f992d3ed34a5135c0eb286ef24ca2c5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 30 Jun 2021 18:15:43 +0200 Subject: [PATCH 017/716] fix hound --- tools/fetch_thirdparty_libs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/fetch_thirdparty_libs.py b/tools/fetch_thirdparty_libs.py index 1ded907576..803d092186 100644 --- a/tools/fetch_thirdparty_libs.py +++ b/tools/fetch_thirdparty_libs.py @@ -81,7 +81,8 @@ pyside2_arg = "PySide2" if not pyside2_version else "PySide2{}".format(pyside2_v try: subprocess.run( [sys.executable, "-m", "pip", "install", "--upgrade", - pyside2_arg, "-t", str(openpype_root / "vendor/python")], check=True, stdout=subprocess.DEVNULL) + pyside2_arg, "-t", str(openpype_root / "vendor/python")], + check=True, stdout=subprocess.DEVNULL) except subprocess.CalledProcessError as e: _print("Error during PySide2 installation.", 1) _print(str(e), 1) From fa78f9805b2135df3ea9bef8ab6adf282623156a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 1 Jul 2021 10:41:23 +0200 Subject: [PATCH 018/716] add ncurses --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 99b9743de0..74ab06a114 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n which \ git \ devtoolset-7-gcc* \ - gcc-c++ \ + gcc-c++ \ make \ cmake \ curl \ @@ -36,8 +36,9 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n openssl-devel \ tk-devel libffi-devel \ qt5-qtbase-devel \ - autoconf \ - automake \ + autoconf \ + automake \ + ncurses-libs \ && yum clean all # we need to build our own patchelf From 7c302631014f85efa05cf809679f55260a0d9593 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 7 Jul 2021 18:50:53 +0200 Subject: [PATCH 019/716] #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 020/716] #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 021/716] #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 022/716] #1784 - refactored names Removed unneeded test files, are being downloaded from GDrive Slight refactoring of fixtures --- tests/lib/{DBHandler.py => db_handler.py} | 1 + tests/lib/{FileHandler.py => file_handler.py} | 12 +-- .../sync_server/fixture/openpype/logs.bson | Bin 2824 -> 0 bytes .../fixture/openpype/logs.metadata.json | 1 - .../fixture/openpype/settings.bson | Bin 623 -> 0 bytes .../fixture/openpype/settings.metadata.json | 1 - .../fixture/test_db/test_project.bson | Bin 13295 -> 0 bytes .../test_db/test_project.metadata.json | 1 - .../sync_server/test_site_operations.py | 102 ++++++++++++------ 9 files changed, 74 insertions(+), 44 deletions(-) rename tests/lib/{DBHandler.py => db_handler.py} (98%) rename tests/lib/{FileHandler.py => file_handler.py} (96%) delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/logs.bson delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/settings.bson delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.bson delete mode 100644 tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json diff --git a/tests/lib/DBHandler.py b/tests/lib/db_handler.py similarity index 98% rename from tests/lib/DBHandler.py rename to tests/lib/db_handler.py index 258ff67df7..4f134e4b66 100644 --- a/tests/lib/DBHandler.py +++ b/tests/lib/db_handler.py @@ -69,6 +69,7 @@ class DBHandler(): print("{} doesn't exist".format(db_name)) return + print("Dropping {} database".format(db_name)) self.client.drop_database(db_name) def backup_to_dump(self, db_name, dump_dir, overwrite=False): diff --git a/tests/lib/FileHandler.py b/tests/lib/file_handler.py similarity index 96% rename from tests/lib/FileHandler.py rename to tests/lib/file_handler.py index e90eac34c1..79f86b5cf9 100644 --- a/tests/lib/FileHandler.py +++ b/tests/lib/file_handler.py @@ -264,9 +264,9 @@ class RemoteFileHandler: return match.group("id") -url = "https://drive.google.com/file/d/1LOVnao6WLW7FpbQELKawzjd19GKx-HH_/view?usp=sharing" # readme -url = "https://drive.google.com/file/d/1SYTZGRVjJUwMUGgZjmOjhDljMzyGaWcv/view?usp=sharing" - - -RemoteFileHandler.download_url(url, root="c:/projects/", filename="temp.zip") -RemoteFileHandler.unzip("c:/projects/temp.zip") \ No newline at end of file +# url = "https://drive.google.com/file/d/1LOVnao6WLW7FpbQELKawzjd19GKx-HH_/view?usp=sharing" # readme +# url = "https://drive.google.com/file/d/1SYTZGRVjJUwMUGgZjmOjhDljMzyGaWcv/view?usp=sharing" +# +# +# RemoteFileHandler.download_url(url, root="c:/projects/", filename="temp.zip") +# RemoteFileHandler.unzip("c:/projects/temp.zip") \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.bson b/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.bson deleted file mode 100644 index 37efb8a4a8dd94738bd7800df8e7a6d0f4c010c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2824 zcmdUw&ui2`6vxM)*7^gAJtf+z9Nn@3+}vNbEQ71~3DWoPofFEjJz^WMwHLxha@EF|~7 zz5P7?bL&O>*~`kPW-%yDGe0;#+Nv}}9ze$b*au1j$r0Q&2}FC_p`a5 z&MQfuIM#=p#67H=kzS=XU4nieav4{FdqDRC+0v>Up#-gb^%J zj*z8wa9dQlm_1TMIE40ek#~;oUDWr*m*G+T*q+5R4>mJZo^YCAd2I*rVH9MC57E7f zwi42PS3tL#qkCZwx=-|9>0(L~o;g zUqpM*u-kcd$b%fg#Rq4}s4(jpl`TGBo|ombo3b%gOE+UxR=` diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json b/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json deleted file mode 100644 index 8c7a16261d..0000000000 --- a/tests/unit/openpype/modules/sync_server/fixture/openpype/logs.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{"capped":true,"size":{"$numberDouble":"1.073741824E+09"},"max":{"$numberInt":"5000"}},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"f982c4d7baf54d03b88aaa540c9ced8e","collectionName":"logs"} \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.bson b/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.bson deleted file mode 100644 index dbfe2e88c6e63f08fccd8a06a280b29dd9bf5bbc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 623 zcmbV~Jx&8L5QS&6AXcIY5+#SAqo4%|3JN6p!MmPKO#HL*%p&bg=(!1J0nWidfUr@D zkir^Sp7;6LZ=MT)19TSd-(GHRpW6A!-NoxfhK^tg06efzKqe6vXAGq^Vj!rf49WJM z81Qq`N;b^`&QwxSG_@xSu!oo)DQ9OX;(`W7gk$ZPoI<7Lw+~8jfihz-(ab3CjNu_R z>NV@7V@9#P6-&l?7ikRm$6HuTw8p1MU0-|0bCi_)t~mXtb6x8Zy{{sg`BWRf9?`yv z0IvT?<1*dL!=>~{kGh;5sF97@$}w7MP45K?xY_`kt~~sJG(SmU_#Trl4`a>;Ny@BQ diff --git a/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json b/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json deleted file mode 100644 index dafcd98d52..0000000000 --- a/tests/unit/openpype/modules/sync_server/fixture/openpype/settings.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"8329d557adfe48018cd533dc648e3b7f","collectionName":"settings"} \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.bson b/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.bson deleted file mode 100644 index c81a0bd315f5c02f4ef86523e34f8b4098ae31cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13295 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 diff --git a/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json b/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json deleted file mode 100644 index b43f27f459..0000000000 --- a/tests/unit/openpype/modules/sync_server/fixture/test_db/test_project.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"bfe11cd230d041438b288f7d6ad8e70f","collectionName":"test_project"} \ No newline at end of file diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index 7e1c994456..cea201e0c8 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -1,15 +1,34 @@ -"""Test file for Sync Server, tests site operations add_site, remove_site""" +"""Test file for Sync Server, tests site operations add_site, remove_site. + + File: + creates temporary directory and downloads .zip file from GDrive + unzips .zip file + uses content of .zip file (MongoDB's dumps) to import to new databases + with use of 'monkeypatch_session' modifies required env vars + temporarily + runs battery of tests checking that site operation for Sync Server + module are working + removes temporary folder + removes temporary databases (?) +""" import os import pytest +import tempfile +import shutil from bson.objectid import ObjectId -from tests.lib.DBHandler import DBHandler +from tests.lib.db_handler import DBHandler +from tests.lib.file_handler import RemoteFileHandler TEST_DB_NAME = "test_db" TEST_PROJECT_NAME = "test_project" TEST_OPENPYPE_NAME = "test_openpype" REPRESENTATION_ID = "60e578d0c987036c6a7b741d" +TEST_FILES = [ + ("1eCwPljuJeOI8A3aisfOIBKKjcmIycTEt", "test_site_operations.zip", "") +] + @pytest.fixture(scope='session') def monkeypatch_session(): @@ -21,21 +40,36 @@ def monkeypatch_session(): @pytest.fixture(scope="module") -def db_init(monkeypatch_session): - backup_dir = os.path.abspath( - os.path.join( - os.path.dirname(__file__), - 'fixture' - ) - ) +def download_test_data(): + tmpdir = tempfile.mkdtemp() + for test_file in TEST_FILES: + file_id, file_name, md5 = test_file + + f_name, ext = os.path.splitext(file_name) + + RemoteFileHandler.download_file_from_google_drive(file_id, + str(tmpdir), + file_name) + + if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: + RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) + + + yield tmpdir + shutil.rmtree(tmpdir) + + +@pytest.fixture(scope="module") +def db(monkeypatch_session, download_test_data): + backup_dir = download_test_data uri = os.environ.get("OPENPYPE_MONGO") or "mongodb://localhost:27017" - db = DBHandler(uri) - db.setup_from_dump(TEST_DB_NAME, backup_dir, True, - db_name_out=TEST_DB_NAME) + db_handler = DBHandler(uri) + db_handler.setup_from_dump(TEST_DB_NAME, backup_dir, True, + db_name_out=TEST_DB_NAME) - db.setup_from_dump("openpype", backup_dir, True, - db_name_out=TEST_OPENPYPE_NAME) + db_handler.setup_from_dump("openpype", backup_dir, True, + db_name_out=TEST_OPENPYPE_NAME) # set needed env vars temporarily for tests monkeypatch_session.setenv("OPENPYPE_MONGO", uri) @@ -46,17 +80,15 @@ def db_init(monkeypatch_session): monkeypatch_session.setenv("AVALON_PROJECT", TEST_PROJECT_NAME) monkeypatch_session.setenv("PYPE_DEBUG", "3") - -@pytest.fixture(scope="module") -def setup_avalon_db(db_init): - """Connect to Avalon, only after 'db_init' sets env vars.""" from avalon.api import AvalonMongoDB db = AvalonMongoDB() yield db + db_handler.teardown(TEST_DB_NAME) + db_handler.teardown(TEST_OPENPYPE_NAME) @pytest.fixture(scope="module") -def setup_sync_server_module(db_init): +def setup_sync_server_module(db): """Get sync_server_module from ModulesManager""" from openpype.modules import ModulesManager @@ -65,25 +97,25 @@ def setup_sync_server_module(db_init): yield sync_server -@pytest.mark.usefixtures("setup_avalon_db") -def test_project_created(setup_avalon_db): - assert ['test_project'] == setup_avalon_db.database.collection_names(False) +@pytest.mark.usefixtures("db") +def test_project_created(db): + assert ['test_project'] == db.database.collection_names(False) -@pytest.mark.usefixtures("setup_avalon_db") -def test_objects_imported(setup_avalon_db): - count_obj = len(list(setup_avalon_db.database[TEST_PROJECT_NAME].find({}))) +@pytest.mark.usefixtures("db") +def test_objects_imported(db): + count_obj = len(list(db.database[TEST_PROJECT_NAME].find({}))) assert 15 == count_obj @pytest.mark.usefixtures("setup_sync_server_module") -def test_add_site(setup_avalon_db, setup_sync_server_module): +def test_add_site(db, setup_sync_server_module): """Adds 'test_site', checks that added, checks that doesn't duplicate.""" query = { "_id": ObjectId(REPRESENTATION_ID) } - ret = setup_avalon_db.database[TEST_PROJECT_NAME].find(query) + ret = db.database[TEST_PROJECT_NAME].find(query) assert 1 == len(list(ret)), \ "Single {} must be in DB".format(REPRESENTATION_ID) @@ -91,7 +123,7 @@ def test_add_site(setup_avalon_db, setup_sync_server_module): setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, site_name='test_site') - ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + ret = list(db.database[TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(REPRESENTATION_ID) @@ -102,7 +134,7 @@ def test_add_site(setup_avalon_db, setup_sync_server_module): @pytest.mark.usefixtures("setup_sync_server_module") -def test_add_site_again(setup_avalon_db, setup_sync_server_module): +def test_add_site_again(db, setup_sync_server_module): """Depends on test_add_site, must throw exception.""" with pytest.raises(ValueError): setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, @@ -110,7 +142,7 @@ def test_add_site_again(setup_avalon_db, setup_sync_server_module): @pytest.mark.usefixtures("setup_sync_server_module") -def test_add_site_again_force(setup_avalon_db, setup_sync_server_module): +def test_add_site_again_force(db, setup_sync_server_module): """Depends on test_add_site, must not throw exception.""" setup_sync_server_module.add_site(TEST_PROJECT_NAME, REPRESENTATION_ID, site_name='test_site', force=True) @@ -119,14 +151,14 @@ def test_add_site_again_force(setup_avalon_db, setup_sync_server_module): "_id": ObjectId(REPRESENTATION_ID) } - ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + ret = list(db.database[TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(REPRESENTATION_ID) @pytest.mark.usefixtures("setup_sync_server_module") -def test_remove_site(setup_avalon_db, setup_sync_server_module): +def test_remove_site(db, setup_sync_server_module): """Depends on test_add_site, must remove 'test_site'.""" setup_sync_server_module.remove_site(TEST_PROJECT_NAME, REPRESENTATION_ID, site_name='test_site') @@ -135,7 +167,7 @@ def test_remove_site(setup_avalon_db, setup_sync_server_module): "_id": ObjectId(REPRESENTATION_ID) } - ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + ret = list(db.database[TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(REPRESENTATION_ID) @@ -147,7 +179,7 @@ def test_remove_site(setup_avalon_db, setup_sync_server_module): @pytest.mark.usefixtures("setup_sync_server_module") -def test_remove_site_again(setup_avalon_db, setup_sync_server_module): +def test_remove_site_again(db, setup_sync_server_module): """Depends on test_add_site, must trow exception""" with pytest.raises(ValueError): setup_sync_server_module.remove_site(TEST_PROJECT_NAME, @@ -158,7 +190,7 @@ def test_remove_site_again(setup_avalon_db, setup_sync_server_module): "_id": ObjectId(REPRESENTATION_ID) } - ret = list(setup_avalon_db.database[TEST_PROJECT_NAME].find(query)) + ret = list(db.database[TEST_PROJECT_NAME].find(query)) assert 1 == len(ret), \ "Single {} must be in DB".format(REPRESENTATION_ID) From bafba8dae019e13dec3865a72b729c4d44b8d883 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 13 Jul 2021 15:33:51 +0200 Subject: [PATCH 023/716] #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 024/716] #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 025/716] #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 026/716] 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 027/716] 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 028/716] 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 029/716] 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 030/716] 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 031/716] 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 3e87997b401da0ba7e57ab0707b78ada9168ff2c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 10:54:05 +0200 Subject: [PATCH 032/716] modified how default settings are loaded --- openpype/settings/lib.py | 49 ++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 4a363910b8..04d8753869 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -329,6 +329,41 @@ def reset_default_settings(): _DEFAULT_SETTINGS = None +def _get_default_settings(): + from openpype.modules import get_module_settings_defs + + defaults = load_openpype_default_settings() + + module_settings_defs = get_module_settings_defs() + for module_settings_def_cls in module_settings_defs: + module_settings_def = module_settings_def_cls() + system_defaults = module_settings_def.get_system_defaults() + for path, value in system_defaults.items(): + if not path: + continue + + subdict = defaults["system_settings"] + path_items = list(path.split("/")) + last_key = path_items.pop(-1) + for key in path_items: + subdict = subdict[key] + subdict[last_key] = value + + project_defaults = module_settings_def.get_project_defaults() + for path, value in project_defaults.items(): + if not path: + continue + + subdict = defaults["project_settings"] + path_items = list(path.split("/")) + last_key = path_items.pop(-1) + for key in path_items: + subdict = subdict[key] + subdict[last_key] = value + + return defaults + + def get_default_settings(): """Get default settings. @@ -339,11 +374,11 @@ def get_default_settings(): dict: Loaded default settings. """ # TODO add cacher - return load_openpype_default_settings() - # global _DEFAULT_SETTINGS - # if _DEFAULT_SETTINGS is None: - # _DEFAULT_SETTINGS = load_jsons_from_dir(DEFAULTS_DIR) - # return copy.deepcopy(_DEFAULT_SETTINGS) + + global _DEFAULT_SETTINGS + if _DEFAULT_SETTINGS is None: + _DEFAULT_SETTINGS = _get_default_settings() + return copy.deepcopy(_DEFAULT_SETTINGS) def load_json_file(fpath): @@ -380,8 +415,8 @@ def load_jsons_from_dir(path, *args, **kwargs): "data1": "CONTENT OF FILE" }, "folder2": { - "data1": { - "subfolder1": "CONTENT OF FILE" + "subfolder1": { + "data2": "CONTENT OF FILE" } } } From aa2f5d85701fa56ed8eb5b1a568dceced3c2ead1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 10:54:31 +0200 Subject: [PATCH 033/716] defined class which defined base settings --- openpype/modules/__init__.py | 13 ++++++- openpype/modules/base.py | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index 81853faa38..261d65d2ee 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -1,16 +1,25 @@ # -*- coding: utf-8 -*- from .base import ( OpenPypeModule, + OpenPypeAddOn, OpenPypeInterface, + ModulesManager, - TrayModulesManager + TrayModulesManager, + + ModuleSettingsDef, + get_module_settings_defs ) __all__ = ( "OpenPypeModule", + "OpenPypeAddOn", "OpenPypeInterface", "ModulesManager", - "TrayModulesManager" + "TrayModulesManager", + + "ModuleSettingsDef", + "get_module_settings_defs" ) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index d43d5635d1..18bbb75cec 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -920,3 +920,74 @@ class TrayModulesManager(ModulesManager): ), exc_info=True ) + + +def get_module_settings_defs(): + load_modules() + + import openpype_modules + + settings_defs = [] + + log = PypeLogger.get_logger("ModuleSettingsLoad") + + for raw_module in openpype_modules: + for attr_name in dir(raw_module): + attr = getattr(raw_module, attr_name) + if ( + not inspect.isclass(attr) + or attr is ModuleSettingsDef + or not issubclass(attr, ModuleSettingsDef) + ): + continue + + if inspect.isabstract(attr): + # Find missing implementations by convetion on `abc` module + not_implemented = [] + for attr_name in dir(attr): + attr = getattr(attr, attr_name, None) + abs_method = getattr( + attr, "__isabstractmethod__", None + ) + if attr and abs_method: + not_implemented.append(attr_name) + + # Log missing implementations + log.warning(( + "Skipping abstract Class: {} in module {}." + " Missing implementations: {}" + ).format( + attr_name, raw_module.__name__, ", ".join(not_implemented) + )) + continue + + settings_defs.append(attr) + + return settings_defs + + +@six.add_metaclass(ABCMeta) +class ModuleSettingsDef: + @abstractmethod + def get_system_schemas(self): + pass + + @abstractmethod + def get_project_schemas(self): + pass + + @abstractmethod + def save_system_defaults(self, data): + pass + + @abstractmethod + def save_project_defaults(self, data): + pass + + @abstractmethod + def get_system_defaults(self): + pass + + @abstractmethod + def get_project_defaults(self): + pass From 08ceabd441d5762aaa1788ba6e9212a37ea6f5ed Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Aug 2021 12:01:41 +0200 Subject: [PATCH 034/716] #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 035/716] 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 036/716] #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 037/716] 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 038/716] 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 f76b5b08679f1e327d1047b5e5217a3e662dc37f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 14:28:12 +0200 Subject: [PATCH 039/716] use constants for schema keys --- openpype/settings/entities/lib.py | 3 +++ openpype/settings/entities/root_entities.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index e58281644a..307792edc9 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -23,6 +23,9 @@ TEMPLATE_METADATA_KEYS = ( DEFAULT_VALUES_KEY, ) +SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema" +SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema" + template_key_pattern = re.compile(r"(\{.*?[^{0]*\})") diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 00677480e8..39b5cb5096 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -9,6 +9,8 @@ from .base_entity import BaseItemEntity from .lib import ( NOT_SET, WRAPPER_TYPES, + SCHEMA_KEY_SYSTEM_SETTINGS, + SCHEMA_KEY_PROJECT_SETTINGS, OverrideState, SchemasHub ) @@ -468,7 +470,7 @@ class SystemSettings(RootEntity): ): if schema_hub is None: # Load system schemas - schema_hub = SchemasHub("system_schema") + schema_hub = SchemasHub(SCHEMA_KEY_SYSTEM_SETTINGS) super(SystemSettings, self).__init__(schema_hub, reset) @@ -599,7 +601,7 @@ class ProjectSettings(RootEntity): if schema_hub is None: # Load system schemas - schema_hub = SchemasHub("projects_schema") + schema_hub = SchemasHub(SCHEMA_KEY_PROJECT_SETTINGS) super(ProjectSettings, self).__init__(schema_hub, reset) From 50e2fce229992d8f1ca5800b3dcaff3796604cd4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 14:28:19 +0200 Subject: [PATCH 040/716] remove TODO --- openpype/settings/lib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 04d8753869..04e8bffd8f 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -373,8 +373,6 @@ def get_default_settings(): Returns: dict: Loaded default settings. """ - # TODO add cacher - global _DEFAULT_SETTINGS if _DEFAULT_SETTINGS is None: _DEFAULT_SETTINGS = _get_default_settings() From 65b60d6b5dd6a32e296a7b81e3d2451ee409a5ff Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 9 Aug 2021 16:56:20 +0100 Subject: [PATCH 041/716] 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 5db15b273e2a6a36309c51067f432d7d784b369e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 18:20:08 +0200 Subject: [PATCH 042/716] settings def has id --- openpype/modules/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 18bbb75cec..8b575bc8cd 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -968,6 +968,14 @@ def get_module_settings_defs(): @six.add_metaclass(ABCMeta) class ModuleSettingsDef: + _id = None + + @property + def id(self): + if self._id is None: + self._id = uuid4() + return self._id + @abstractmethod def get_system_schemas(self): pass From 9d7f0db6d8177860930e533d082bf7e549f2e074 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 18:25:07 +0200 Subject: [PATCH 043/716] changed how schemas are get from openpype --- openpype/modules/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 8b575bc8cd..3d3d7ae6cb 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -977,11 +977,11 @@ class ModuleSettingsDef: return self._id @abstractmethod - def get_system_schemas(self): + def get_settings_schemas(self, schema_type): pass @abstractmethod - def get_project_schemas(self): + def get_dynamic_schemas(self, schema_type): pass @abstractmethod From 5099f5f853d96e5846c7eab31aa0557c3c2ff4da Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 10:33:28 +0200 Subject: [PATCH 044/716] small condition modifications --- openpype/settings/entities/base_entity.py | 2 +- openpype/settings/entities/dict_conditional.py | 2 +- openpype/settings/entities/dict_mutable_keys_entity.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index b4ebe885f5..d9dcf633e5 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -253,7 +253,7 @@ class BaseItemEntity(BaseEntity): # Validate that env group entities will be stored into file. # - env group entities must store metadata which is not possible if # metadata would be outside of file - if not self.file_item and self.is_env_group: + if self.file_item is None and self.is_env_group: reason = ( "Environment item is not inside file" " item so can't store metadata for defaults." diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d275d8ac3d..fc7cbfdee5 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -469,7 +469,7 @@ class DictConditionalEntity(ItemEntity): output = {} for key, child_obj in children_items: child_value = child_obj.settings_value() - if not child_obj.is_file and not child_obj.file_item: + if not child_obj.is_file and child_obj.file_item is None: for _key, _value in child_value.items(): new_key = "/".join([key, _key]) output[new_key] = _value diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index c3df935269..f75fb23d82 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -261,7 +261,7 @@ class DictMutableKeysEntity(EndpointEntity): raise EntitySchemaError(self, reason) # TODO Ability to store labels should be defined with different key - if self.collapsible_key and not self.file_item: + if self.collapsible_key and self.file_item is None: reason = ( "Modifiable dictionary with collapsible keys is not under" " file item so can't store metadata." From 13720249575860a6aa01c1b7b2e1f21d3ccfefc6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 10:34:26 +0200 Subject: [PATCH 045/716] added loading of setttings modules definitions in SchemaHub --- openpype/settings/entities/lib.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 307792edc9..a72908967f 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -117,14 +117,27 @@ class SchemasHub: # It doesn't make sence to reload types on each reset as they can't be # changed self._load_types() + # Attributes for modules settings + self._modules_settings_defs_by_id = {} + self._dynamic_schemas_by_module_id = {} # Trigger reset if reset: self.reset() def reset(self): + self._load_modules_settings_defs() self._load_schemas() + def _load_modules_settings_defs(self): + from openpype.modules import get_module_settings_defs + + module_settings_defs = get_module_settings_defs() + for module_settings_def_cls in module_settings_defs: + module_settings_def = module_settings_def_cls() + def_id = module_settings_def.id + self._modules_settings_defs_by_id[def_id] = module_settings_def + @property def gui_types(self): return self._gui_types From 2e9e0ba09ff17393b90d18717b0f4fc94e09cb27 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 10:34:52 +0200 Subject: [PATCH 046/716] use constant for extending schema types --- openpype/settings/entities/lib.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index a72908967f..4b6ed5a365 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -26,6 +26,10 @@ TEMPLATE_METADATA_KEYS = ( SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema" SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema" +SCHEMA_EXTEND_TYPES = ( + "schema", "template", "schema_template", "dynamic_schema" +) + template_key_pattern = re.compile(r"(\{.*?[^{0]*\})") @@ -217,7 +221,7 @@ class SchemasHub: list: Resolved schema data. """ schema_type = schema_data["type"] - if schema_type not in ("schema", "template", "schema_template"): + if schema_type not in SCHEMA_EXTEND_TYPES: return [schema_data] if schema_type == "schema": From 2bb74c68e9b8ba41de9268f95c0264f4201bc98a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 10:35:48 +0200 Subject: [PATCH 047/716] added resolving of dynamic module items --- openpype/settings/entities/lib.py | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 4b6ed5a365..dd216f4d90 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -146,6 +146,23 @@ class SchemasHub: def gui_types(self): return self._gui_types + def resolve_dynamic_schema(self, dynamic_key): + output = [] + for def_id, def_keys in self._dynamic_schemas_by_module_id.items(): + if dynamic_key in def_keys: + def_schema = def_keys[dynamic_key] + if not def_schema: + continue + + if isinstance(def_schema, dict): + def_schema = [def_schema] + + for item in def_schema: + item["_module_id"] = def_id + item["_module_store_key"] = dynamic_key + output.extend(def_schema) + return output + def get_schema(self, schema_name): """Get schema definition data by it's name. @@ -229,6 +246,9 @@ class SchemasHub: self.get_schema(schema_data["name"]) ) + if schema_type == "dynamic_schema": + return self.resolve_dynamic_schema(schema_data["name"]) + template_name = schema_data["name"] template_def = self.get_template(template_name) @@ -329,6 +349,7 @@ class SchemasHub: self._crashed_on_load = {} self._loaded_templates = {} self._loaded_schemas = {} + self._dynamic_schemas_by_module_id = {} dirpath = os.path.join( os.path.dirname(os.path.abspath(__file__)), @@ -337,6 +358,7 @@ class SchemasHub: ) loaded_schemas = {} loaded_templates = {} + dynamic_schemas_by_module_id = {} for root, _, filenames in os.walk(dirpath): for filename in filenames: basename, ext = os.path.splitext(filename) @@ -386,8 +408,31 @@ class SchemasHub: ) loaded_schemas[basename] = schema_data + defs_iter = self._modules_settings_defs_by_id.items() + for def_id, module_settings_def in defs_iter: + dynamic_schemas_by_module_id[def_id] = ( + module_settings_def.get_dynamic_schemas(self._schema_subfolder) + ) + module_schemas = module_settings_def.get_settings_schemas( + self._schema_subfolder + ) + for key, schema_data in module_schemas.items(): + if isinstance(schema_data, list): + if key in loaded_templates: + raise KeyError( + "Duplicated template key \"{}\"".format(key) + ) + loaded_templates[key] = schema_data + else: + if key in loaded_schemas: + raise KeyError( + "Duplicated schema key \"{}\"".format(key) + ) + loaded_schemas[key] = schema_data + self._loaded_templates = loaded_templates self._loaded_schemas = loaded_schemas + self._dynamic_schemas_by_module_id = dynamic_schemas_by_module_id def _fill_template(self, child_data, template_def): """Fill template based on schema definition and template definition. From b648dd7dc325029906cf69dd8a6887ee31e567ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 10:35:58 +0200 Subject: [PATCH 048/716] load types on each reset --- openpype/settings/entities/lib.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index dd216f4d90..91e66eec8e 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -118,9 +118,6 @@ class SchemasHub: self._loaded_templates = {} self._loaded_schemas = {} - # It doesn't make sence to reload types on each reset as they can't be - # changed - self._load_types() # Attributes for modules settings self._modules_settings_defs_by_id = {} self._dynamic_schemas_by_module_id = {} @@ -131,6 +128,7 @@ class SchemasHub: def reset(self): self._load_modules_settings_defs() + self._load_types() self._load_schemas() def _load_modules_settings_defs(self): From 32011838b3e4404c09249b2b60c69e533d63b300 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:10:33 +0200 Subject: [PATCH 049/716] renamed variable --- openpype/settings/entities/lib.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 91e66eec8e..b87845b95e 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -120,7 +120,7 @@ class SchemasHub: # Attributes for modules settings self._modules_settings_defs_by_id = {} - self._dynamic_schemas_by_module_id = {} + self._dynamic_schemas_def_by_id = {} # Trigger reset if reset: @@ -146,7 +146,7 @@ class SchemasHub: def resolve_dynamic_schema(self, dynamic_key): output = [] - for def_id, def_keys in self._dynamic_schemas_by_module_id.items(): + for def_id, def_keys in self._dynamic_schemas_def_by_id.items(): if dynamic_key in def_keys: def_schema = def_keys[dynamic_key] if not def_schema: @@ -347,7 +347,7 @@ class SchemasHub: self._crashed_on_load = {} self._loaded_templates = {} self._loaded_schemas = {} - self._dynamic_schemas_by_module_id = {} + self._dynamic_schemas_def_by_id = {} dirpath = os.path.join( os.path.dirname(os.path.abspath(__file__)), @@ -356,7 +356,7 @@ class SchemasHub: ) loaded_schemas = {} loaded_templates = {} - dynamic_schemas_by_module_id = {} + dynamic_schemas_def_by_id = {} for root, _, filenames in os.walk(dirpath): for filename in filenames: basename, ext = os.path.splitext(filename) @@ -408,7 +408,7 @@ class SchemasHub: defs_iter = self._modules_settings_defs_by_id.items() for def_id, module_settings_def in defs_iter: - dynamic_schemas_by_module_id[def_id] = ( + dynamic_schemas_def_by_id[def_id] = ( module_settings_def.get_dynamic_schemas(self._schema_subfolder) ) module_schemas = module_settings_def.get_settings_schemas( @@ -430,7 +430,7 @@ class SchemasHub: self._loaded_templates = loaded_templates self._loaded_schemas = loaded_schemas - self._dynamic_schemas_by_module_id = dynamic_schemas_by_module_id + self._dynamic_schemas_def_by_id = dynamic_schemas_def_by_id def _fill_template(self, child_data, template_def): """Fill template based on schema definition and template definition. From f38c7a462e96755d6dce8255becef57810de9b06 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:15:21 +0200 Subject: [PATCH 050/716] added few attributes for dynamic schemas --- openpype/settings/entities/base_entity.py | 12 ++++++++++++ openpype/settings/entities/lib.py | 3 +-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index d9dcf633e5..832c8ab854 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -104,6 +104,12 @@ class BaseItemEntity(BaseEntity): self.is_group = False # Entity's value will be stored into file with name of it's key self.is_file = False + # Default values are not stored to an openpype file + # - these must not be set through schemas directly + self.dynamic_schema_id = None + self.is_dynamic_schema_node = False + self.is_in_dynamic_schema_node = False + # Reference to parent entity which has `is_group` == True # - stays as None if none of parents is group self.group_item = None @@ -800,6 +806,12 @@ class ItemEntity(BaseItemEntity): self.is_dynamic_item = is_dynamic_item self.is_file = self.schema_data.get("is_file", False) + # These keys have underscore as they must not be set in schemas + self.dynamic_schema_id = self.schema_data.get( + "_dynamic_schema_id", None + ) + self.is_dynamic_schema_node = self.dynamic_schema_id is not None + self.is_group = self.schema_data.get("is_group", False) self.is_in_dynamic_item = bool( not self.is_dynamic_item diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index b87845b95e..2a1bbaa115 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -156,8 +156,7 @@ class SchemasHub: def_schema = [def_schema] for item in def_schema: - item["_module_id"] = def_id - item["_module_store_key"] = dynamic_key + item["_dynamic_schema_id"] = def_id output.extend(def_schema) return output From 5541b3fd0cc8fd5fe60cb11a8c22dfbc8f4911db Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:16:57 +0200 Subject: [PATCH 051/716] added few conditions so it is possbile to load dynamic schemas --- openpype/settings/entities/base_entity.py | 25 ++++++++++++++++---- openpype/settings/entities/input_entities.py | 6 ++++- openpype/settings/entities/item_entities.py | 7 +++++- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 832c8ab854..bea90882a7 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -841,10 +841,20 @@ class ItemEntity(BaseItemEntity): self._require_restart_on_change = require_restart_on_change # File item reference - if self.parent.is_file: - self.file_item = self.parent - elif self.parent.file_item: - self.file_item = self.parent.file_item + if not self.is_dynamic_schema_node: + self.is_in_dynamic_schema_node = ( + self.parent.is_dynamic_schema_node + or self.parent.is_in_dynamic_schema_node + ) + + if ( + not self.is_dynamic_schema_node + and not self.is_in_dynamic_schema_node + ): + if self.parent.is_file: + self.file_item = self.parent + elif self.parent.file_item: + self.file_item = self.parent.file_item # Group item reference if self.parent.is_group: @@ -903,7 +913,12 @@ class ItemEntity(BaseItemEntity): ) raise EntitySchemaError(self, reason) - if self.is_file and self.file_item is not None: + if ( + not self.is_dynamic_schema_node + and not self.is_in_dynamic_schema_node + and self.is_file + and self.file_item is not None + ): reason = ( "Entity has set `is_file` to true but" " it's parent is already marked as file item." diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 6952529963..b65c1c440e 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -116,7 +116,11 @@ class InputEntity(EndpointEntity): def schema_validations(self): # Input entity must have file parent. - if not self.file_item: + if ( + not self.is_dynamic_schema_node + and not self.is_in_dynamic_schema_node + and self.file_item is None + ): raise EntitySchemaError(self, "Missing parent file entity.") super(InputEntity, self).schema_validations() diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index 7e84f8c801..1e4f1025cc 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -215,7 +215,12 @@ class ListStrictEntity(ItemEntity): def schema_validations(self): # List entity must have file parent. - if not self.file_item and not self.is_file: + if ( + not self.is_dynamic_schema_node + and not self.is_in_dynamic_schema_node + and not self.is_file + and self.file_item is None + ): raise EntitySchemaError( self, "Missing file entity in hierarchy." ) From eaa1499510566f5d6ebc8308db50b4c5d8780c5a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:17:17 +0200 Subject: [PATCH 052/716] conditional dict does not care about paths as it must be group --- openpype/settings/entities/dict_conditional.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index fc7cbfdee5..8a944e5fdc 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -468,13 +468,7 @@ class DictConditionalEntity(ItemEntity): output = {} for key, child_obj in children_items: - child_value = child_obj.settings_value() - if not child_obj.is_file and child_obj.file_item is None: - for _key, _value in child_value.items(): - new_key = "/".join([key, _key]) - output[new_key] = _value - else: - output[key] = child_value + output[key] = child_obj.settings_value() return output if self.is_group: From f65dee0a0e8b40f42eb8485faf1377d01542f52a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:22:53 +0200 Subject: [PATCH 053/716] added method which collects dynamic schema entities --- openpype/settings/entities/base_entity.py | 22 ++++++++++++++++++- .../settings/entities/dict_conditional.py | 4 ++++ .../entities/dict_immutable_keys_entity.py | 7 ++++++ openpype/settings/entities/input_entities.py | 4 ++++ openpype/settings/entities/item_entities.py | 7 ++++++ openpype/settings/entities/lib.py | 9 ++++++++ openpype/settings/entities/root_entities.py | 13 ++++++++++- 7 files changed, 64 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index bea90882a7..0d2923f9e0 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -476,7 +476,15 @@ class BaseItemEntity(BaseEntity): @abstractmethod def settings_value(self): - """Value of an item without key.""" + """Value of an item without key without dynamic items.""" + pass + + @abstractmethod + def collect_dynamic_schema_entities(self): + """Collect entities that are on top of dynamically added schemas. + + This method make sence only when defaults are saved. + """ pass @abstractmethod @@ -905,6 +913,18 @@ class ItemEntity(BaseItemEntity): def root_key(self): return self.root_item.root_key + @abstractmethod + def collect_dynamic_schema_entities(self, collector): + """Collect entities that are on top of dynamically added schemas. + + This method make sence only when defaults are saved. + + Args: + collector(DynamicSchemaValueCollector): Object where dynamic + entities are stored. + """ + pass + def schema_validations(self): if not self.label and self.use_label_wrap: reason = ( diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 8a944e5fdc..44775e9113 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -455,6 +455,10 @@ class DictConditionalEntity(ItemEntity): return True return False + def collect_dynamic_schema_entities(self, collector): + if self.is_dynamic_schema_node: + collector.add_entity(self) + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index bde5304787..24cd9401b9 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -318,6 +318,13 @@ class DictImmutableKeysEntity(ItemEntity): return True return False + def collect_dynamic_schema_entities(self, collector): + for child_obj in self.non_gui_children.values(): + child_obj.collect_dynamic_schema_entities(collector) + + if self.is_dynamic_schema_node: + collector.add_entity(self) + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index b65c1c440e..469fdee310 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -49,6 +49,10 @@ class EndpointEntity(ItemEntity): super(EndpointEntity, self).schema_validations() + def collect_dynamic_schema_entities(self, collector): + if self.is_dynamic_schema_node: + collector.add_entity(self) + @abstractmethod def _settings_value(self): pass diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index 1e4f1025cc..3823a25c60 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -112,6 +112,9 @@ class PathEntity(ItemEntity): def set(self, value): self.child_obj.set(value) + def collect_dynamic_schema_entities(self, *args, **kwargs): + self.child_obj.collect_dynamic_schema_entities(*args, **kwargs) + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET @@ -251,6 +254,10 @@ class ListStrictEntity(ItemEntity): for idx, item in enumerate(new_value): self.children[idx].set(item) + def collect_dynamic_schema_entities(self, collector): + if self.is_dynamic_schema_node: + collector.add_entity(self) + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 2a1bbaa115..98dede39e8 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -663,3 +663,12 @@ class SchemasHub: if found_idx is not None: metadata_item = template_def.pop(found_idx) return metadata_item + + +class DynamicSchemaValueCollector: + def __init__(self, schema_hub): + self._schema_hub = schema_hub + self._dynamic_entities = [] + + def add_entity(self, entity): + self._dynamic_entities.append(entity) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 39b5cb5096..2c88016344 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -12,7 +12,8 @@ from .lib import ( SCHEMA_KEY_SYSTEM_SETTINGS, SCHEMA_KEY_PROJECT_SETTINGS, OverrideState, - SchemasHub + SchemasHub, + DynamicSchemaValueCollector ) from .exceptions import ( SchemaError, @@ -259,6 +260,16 @@ class RootEntity(BaseItemEntity): output[key] = child_obj.value return output + def collect_dynamic_schema_entities(self): + output = DynamicSchemaValueCollector(self.schema_hub) + if self._override_state is not OverrideState.DEFAULTS: + return output + + for child_obj in self.non_gui_children.values(): + child_obj.collect_dynamic_schema_entities(output) + + return output + def settings_value(self): """Value for current override state with metadata. From 6282e8d1ad57e85e7c8c8e11ddb201b4bf56b21a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:23:23 +0200 Subject: [PATCH 054/716] skip dynamic schema entities in settings values method --- openpype/settings/entities/dict_immutable_keys_entity.py | 3 +++ openpype/settings/entities/root_entities.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index 24cd9401b9..a81a64c183 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -332,6 +332,9 @@ class DictImmutableKeysEntity(ItemEntity): if self._override_state is OverrideState.DEFAULTS: output = {} for key, child_obj in self.non_gui_children.items(): + if child_obj.is_dynamic_schema_node: + continue + child_value = child_obj.settings_value() if not child_obj.is_file and not child_obj.file_item: for _key, _value in child_value.items(): diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 2c88016344..b178e3fa36 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -281,6 +281,8 @@ class RootEntity(BaseItemEntity): if self._override_state is not OverrideState.DEFAULTS: output = {} for key, child_obj in self.non_gui_children.items(): + if child_obj.is_dynamic_schema_node: + continue value = child_obj.settings_value() if value is not NOT_SET: output[key] = value From 37ef6d022355f7f325b933c6192665c5811cde6c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:23:39 +0200 Subject: [PATCH 055/716] ignore file handling for dynamic schema nodes --- openpype/settings/entities/dict_immutable_keys_entity.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index a81a64c183..8871a3a3d9 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -330,13 +330,20 @@ class DictImmutableKeysEntity(ItemEntity): return NOT_SET if self._override_state is OverrideState.DEFAULTS: + is_dynamic_schema_node = ( + self.is_dynamic_schema_node or self.is_in_dynamic_schema_node + ) output = {} for key, child_obj in self.non_gui_children.items(): if child_obj.is_dynamic_schema_node: continue child_value = child_obj.settings_value() - if not child_obj.is_file and not child_obj.file_item: + if ( + not is_dynamic_schema_node + and not child_obj.is_file + and not child_obj.file_item + ): for _key, _value in child_value.items(): new_key = "/".join([key, _key]) output[new_key] = _value From cf9114b0f1dfbea06bb8b688dbfa3627a885d41c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:23:57 +0200 Subject: [PATCH 056/716] added schema validation of dynamic schemas --- openpype/settings/entities/base_entity.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 0d2923f9e0..f5f5b4d761 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -253,9 +253,18 @@ class BaseItemEntity(BaseEntity): ) # Group item can be only once in on hierarchy branch. - if self.is_group and self.group_item: + if self.is_group and self.group_item is not None: raise SchemeGroupHierarchyBug(self) + # Group item can be only once in on hierarchy branch. + if self.group_item is not None and self.is_dynamic_schema_node: + reason = ( + "Dynamic schema is inside grouped item {}." + " Change group hierarchy or remove dynamic" + " schema to be able work properly." + ).format(self.group_item.path) + raise EntitySchemaError(self, reason) + # Validate that env group entities will be stored into file. # - env group entities must store metadata which is not possible if # metadata would be outside of file From fff590f7f88aaf0130adfc7cf6acd98cc6dac05c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:43:16 +0200 Subject: [PATCH 057/716] add getter method for dynamic schema definitions --- openpype/settings/entities/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 98dede39e8..3877b49648 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -431,6 +431,9 @@ class SchemasHub: self._loaded_schemas = loaded_schemas self._dynamic_schemas_def_by_id = dynamic_schemas_def_by_id + def get_dynamic_schema_def(self, schema_def_id): + return self._dynamic_schemas_def_by_id.get(schema_def_id) + def _fill_template(self, child_data, template_def): """Fill template based on schema definition and template definition. From b507b4e9d33b6a65003954545f7fa271030d995d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 13:02:08 +0200 Subject: [PATCH 058/716] modified dynamic schemas attributes --- openpype/settings/entities/lib.py | 36 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 3877b49648..457468b18b 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -108,8 +108,8 @@ class OverrideState: class SchemasHub: - def __init__(self, schema_subfolder, reset=True): - self._schema_subfolder = schema_subfolder + def __init__(self, schema_type, reset=True): + self._schema_type = schema_type self._loaded_types = {} self._gui_types = tuple() @@ -119,13 +119,17 @@ class SchemasHub: self._loaded_schemas = {} # Attributes for modules settings - self._modules_settings_defs_by_id = {} - self._dynamic_schemas_def_by_id = {} + self._dynamic_schemas_defs_by_id = {} + self._dynamic_schemas_by_id = {} # Trigger reset if reset: self.reset() + @property + def schema_type(self): + return self._schema_type + def reset(self): self._load_modules_settings_defs() self._load_types() @@ -138,7 +142,7 @@ class SchemasHub: for module_settings_def_cls in module_settings_defs: module_settings_def = module_settings_def_cls() def_id = module_settings_def.id - self._modules_settings_defs_by_id[def_id] = module_settings_def + self._dynamic_schemas_defs_by_id[def_id] = module_settings_def @property def gui_types(self): @@ -146,7 +150,7 @@ class SchemasHub: def resolve_dynamic_schema(self, dynamic_key): output = [] - for def_id, def_keys in self._dynamic_schemas_def_by_id.items(): + for def_id, def_keys in self._dynamic_schemas_by_id.items(): if dynamic_key in def_keys: def_schema = def_keys[dynamic_key] if not def_schema: @@ -346,16 +350,16 @@ class SchemasHub: self._crashed_on_load = {} self._loaded_templates = {} self._loaded_schemas = {} - self._dynamic_schemas_def_by_id = {} + self._dynamic_schemas_by_id = {} dirpath = os.path.join( os.path.dirname(os.path.abspath(__file__)), "schemas", - self._schema_subfolder + self.schema_type ) loaded_schemas = {} loaded_templates = {} - dynamic_schemas_def_by_id = {} + dynamic_schemas_by_id = {} for root, _, filenames in os.walk(dirpath): for filename in filenames: basename, ext = os.path.splitext(filename) @@ -405,13 +409,13 @@ class SchemasHub: ) loaded_schemas[basename] = schema_data - defs_iter = self._modules_settings_defs_by_id.items() + defs_iter = self._dynamic_schemas_defs_by_id.items() for def_id, module_settings_def in defs_iter: - dynamic_schemas_def_by_id[def_id] = ( - module_settings_def.get_dynamic_schemas(self._schema_subfolder) + dynamic_schemas_by_id[def_id] = ( + module_settings_def.get_dynamic_schemas(self.schema_type) ) module_schemas = module_settings_def.get_settings_schemas( - self._schema_subfolder + self.schema_type ) for key, schema_data in module_schemas.items(): if isinstance(schema_data, list): @@ -429,10 +433,10 @@ class SchemasHub: self._loaded_templates = loaded_templates self._loaded_schemas = loaded_schemas - self._dynamic_schemas_def_by_id = dynamic_schemas_def_by_id + self._dynamic_schemas_by_id = dynamic_schemas_by_id - def get_dynamic_schema_def(self, schema_def_id): - return self._dynamic_schemas_def_by_id.get(schema_def_id) + def get_dynamic_modules_settings_defs(self, schema_def_id): + return self._dynamic_schemas_defs_by_id.get(schema_def_id) def _fill_template(self, child_data, template_def): """Fill template based on schema definition and template definition. From 90076d519f389a8a5a50e1df5f9771294a2128b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 13:02:45 +0200 Subject: [PATCH 059/716] removed project_settings getter --- openpype/settings/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 04e8bffd8f..d7684082f3 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -354,7 +354,7 @@ def _get_default_settings(): if not path: continue - subdict = defaults["project_settings"] + subdict = defaults path_items = list(path.split("/")) last_key = path_items.pop(-1) for key in path_items: From 9d31ec70116589ab0d00bf6a6c0d840420999924 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 13:03:43 +0200 Subject: [PATCH 060/716] implemented save for dynamic schemas --- openpype/settings/entities/lib.py | 21 +++++++++++++++++++++ openpype/settings/entities/root_entities.py | 3 +++ 2 files changed, 24 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 457468b18b..13037ac373 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -3,6 +3,7 @@ import re import json import copy import inspect +import collections from .exceptions import ( SchemaTemplateMissingKeys, @@ -679,3 +680,23 @@ class DynamicSchemaValueCollector: def add_entity(self, entity): self._dynamic_entities.append(entity) + + def create_hierarchy(self): + output = collections.defaultdict(dict) + for entity in self._dynamic_entities: + output[entity.dynamic_schema_id][entity.path] = ( + entity.settings_value() + ) + return output + + def save_values(self): + hierarchy = self.create_hierarchy() + + for schema_def_id, schema_def_value in hierarchy.items(): + schema_def = self._schema_hub.get_dynamic_modules_settings_defs( + schema_def_id + ) + if self._schema_hub.schema_type == SCHEMA_KEY_SYSTEM_SETTINGS: + schema_def.save_system_defaults(schema_def_value) + elif self._schema_hub.schema_type == SCHEMA_KEY_PROJECT_SETTINGS: + schema_def.save_project_defaults(schema_def_value) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index b178e3fa36..6f444d5394 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -428,6 +428,9 @@ class RootEntity(BaseItemEntity): with open(output_path, "w") as file_stream: json.dump(value, file_stream, indent=4) + dynamic_values_item = self.collect_dynamic_schema_entities() + dynamic_values_item.save_values() + @abstractmethod def _save_studio_values(self): """Save studio override values.""" From 8cfe9bb270e2859c09a0ec17554b11aac4860a87 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 13:04:04 +0200 Subject: [PATCH 061/716] added first dynamic_schema item in schemas --- .../entities/schemas/projects_schema/schema_main.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index 4a8a9d496e..058ff492f3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -121,6 +121,10 @@ { "type": "schema", "name": "schema_project_unreal" + }, + { + "type": "dynamic_schema", + "name": "project_settings/global" } ] } From 1aa3d42704813ce67722f3053bd1a9462a90247c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 13:06:43 +0200 Subject: [PATCH 062/716] reset defaults on save defaults --- openpype/settings/entities/root_entities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 6f444d5394..78e8aad47f 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -31,6 +31,7 @@ from openpype.settings.lib import ( DEFAULTS_DIR, get_default_settings, + reset_default_settings, get_studio_system_settings_overrides, save_studio_settings, @@ -381,6 +382,7 @@ class RootEntity(BaseItemEntity): if self._override_state is OverrideState.DEFAULTS: self._save_default_values() + reset_default_settings() elif self._override_state is OverrideState.STUDIO: self._save_studio_values() From db622cf9898c3ea87b96703b9f39162020dfd7f9 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 19 Aug 2021 22:00:40 +0200 Subject: [PATCH 063/716] Nuke Quick Start / Tutorial First attempt to make a hands on doc for artists. --- website/docs/artist_hosts_nuke_tut.md | 166 ++++++++++++++++++ .../nuke_tut/nuke_AnatomyAppsVersions.png | Bin 0 -> 29588 bytes .../docs/assets/nuke_tut/nuke_AssetLoader.png | Bin 0 -> 49946 bytes website/docs/assets/nuke_tut/nuke_Context.png | Bin 0 -> 5480 bytes website/docs/assets/nuke_tut/nuke_Create.png | Bin 0 -> 9903 bytes website/docs/assets/nuke_tut/nuke_Creator.png | Bin 0 -> 15493 bytes website/docs/assets/nuke_tut/nuke_Load.png | Bin 0 -> 9928 bytes .../docs/assets/nuke_tut/nuke_NukeColor.png | Bin 0 -> 42347 bytes website/docs/assets/nuke_tut/nuke_Publish.png | Bin 0 -> 9940 bytes .../nuke_tut/nuke_PyblishDialogNuke.png | Bin 0 -> 52744 bytes .../nuke_tut/nuke_RunNukeFtrackAction.png | Bin 0 -> 150648 bytes .../nuke_tut/nuke_RunNukeFtrackAction_p2.png | Bin 0 -> 87912 bytes .../assets/nuke_tut/nuke_RunNukeLauncher.png | Bin 0 -> 38738 bytes .../nuke_tut/nuke_RunNukeLauncher_p2.png | Bin 0 -> 31166 bytes .../nuke_tut/nuke_WorkFileNamingAnatomy.png | Bin 0 -> 18795 bytes .../assets/nuke_tut/nuke_WorkFileSaveAs.png | Bin 0 -> 26227 bytes .../nuke_tut/nuke_WorkfileOnStartup.png | Bin 0 -> 22326 bytes .../docs/assets/nuke_tut/nuke_WriteNode.png | Bin 0 -> 19061 bytes .../assets/nuke_tut/nuke_WriteSettings.png | Bin 0 -> 41188 bytes .../docs/assets/nuke_tut/nuke_versionless.png | Bin 0 -> 5034 bytes website/sidebars.js | 1 + 21 files changed, 167 insertions(+) create mode 100644 website/docs/artist_hosts_nuke_tut.md create mode 100644 website/docs/assets/nuke_tut/nuke_AnatomyAppsVersions.png create mode 100644 website/docs/assets/nuke_tut/nuke_AssetLoader.png create mode 100644 website/docs/assets/nuke_tut/nuke_Context.png create mode 100644 website/docs/assets/nuke_tut/nuke_Create.png create mode 100644 website/docs/assets/nuke_tut/nuke_Creator.png create mode 100644 website/docs/assets/nuke_tut/nuke_Load.png create mode 100644 website/docs/assets/nuke_tut/nuke_NukeColor.png create mode 100644 website/docs/assets/nuke_tut/nuke_Publish.png create mode 100644 website/docs/assets/nuke_tut/nuke_PyblishDialogNuke.png create mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction.png create mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png create mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeLauncher.png create mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeLauncher_p2.png create mode 100644 website/docs/assets/nuke_tut/nuke_WorkFileNamingAnatomy.png create mode 100644 website/docs/assets/nuke_tut/nuke_WorkFileSaveAs.png create mode 100644 website/docs/assets/nuke_tut/nuke_WorkfileOnStartup.png create mode 100644 website/docs/assets/nuke_tut/nuke_WriteNode.png create mode 100644 website/docs/assets/nuke_tut/nuke_WriteSettings.png create mode 100644 website/docs/assets/nuke_tut/nuke_versionless.png diff --git a/website/docs/artist_hosts_nuke_tut.md b/website/docs/artist_hosts_nuke_tut.md new file mode 100644 index 0000000000..5743c8c756 --- /dev/null +++ b/website/docs/artist_hosts_nuke_tut.md @@ -0,0 +1,166 @@ +--- +id: artist_hosts_nuke_tut +title: Nuke QuickStart +sidebar_label: Nuke QuickStart +--- + +# Nuke QuickStart +- [Nuke QuickStart](#nuke-quickstart) + - [Run Nuke - Shot and Task Context](#run-nuke---shot-and-task-context) + - [Save Nuke script – the Work File](#save-nuke-script--the-work-file) + - [Load plate – Asset Loader](#load-plate--asset-loader) + - [Create Write Node – Instance Creator](#create-write-node--instance-creator) + - [What Nuke Publish Does](#what-nuke-publish-does) + - [Publish steps](#publish-steps) + - [Pyblish Note and Intent](#pyblish-note-and-intent) + - [Pyblish Checkbox](#pyblish-checkbox) + - [Pyblish Dialog](#pyblish-dialog) + - [Review](#review) + - [Render and Publish](#render-and-publish) + - [Version-less Render](#version-less-render) + - [Fixing Validate Containers](#fixing-validate-containers) + - [Fixing Validate Version](#fixing-validate-version) + +This QuickStart is just a small introduction to what OpenPype can do for you. It attempts to make an overview for compositing artists, and simplifies processes that are better described in specific parts of the documentation. + +## Run Nuke - Shot and Task Context +OpenPype has to know what shot and task you are working on. You need to run Nuke in context of the task, using Ftrack Action or OpenPype Launcher to select the task and run Nuke. + +![Run Nuke From Ftrack](assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png) +![Run Nuke From Launcher](assets/nuke_tut/nuke_RunNukeLauncher_p2.png) + + +OpenPype menu shows the current context - shot and task. + +If you use Ftrack, executing Nuke with context stops your timer, and starts the Ftrack clock on the shot and task you picked. + +![Context](assets/nuke_tut/nuke_Context.png) + +:::tip Admin Tip - Nuke version +You can [configure](admin_settings_project_anatomy.md#Attributes) which version(s) will be available for current project in **Studio Settings β†’ Project β†’ Anatomy β†’ Attributes β†’ Applications** +::: + +## Save Nuke script – the Work File +Use OpenPype - Work files menu to create a new Nuke script. Openpype offers you the preconfigured naming. +![Context](assets/nuke_tut/nuke_AssetLoader.png) + +Openpype makes initial setup for your Nuke script. It is the same as running [Apply All Settings](artist_hosts_nuke.md#apply-all-settings) from the OpenPype menu. +Reads frame range and resolution from Avalon database, sets it in Nuke Project Settings +Creates Viewer node, sets it’s range and indicates handles by In and Out points +Reads Color settings from the project configuration, and sets it in Nuke Project Settings and Viewer +Sets project directory in the Nuke Project Settings to the Nuke Script Directory + +:::tip Admin Tip - Workfile Naming +The [workfile naming](admin_settings_project_anatomy#templates) is configured in anatomy, see **Studio Settings β†’ Project β†’ Anatomy β†’ Templates β†’ Work** +::: + +:::tip Admin Tip - Open Workfile +You can [configure](project_settings/settings_project_nuke#create-first-workfile) Nuke to automatically open the last version, or create a file on startup. See **Studio Settings β†’ Project β†’ Global β†’ Tools β†’ Workfiles** +::: + +:::tip Admin Tip - Nuke Color Settings +[Color setting](project_settings/settings_project_nuke) for Nuke can be found in **Studio Settings β†’ Project β†’ Anatomy β†’ Color Management and Output Formats β†’ Nuke** +::: + +## Load plate – Asset Loader +If your IO or editorial prepared plates and references, or your CG team has a render to be composited, we need to load it. + +![OpenPype Load](assets/nuke_tut/nuke_Load.png) +![Asset Load](assets/nuke_tut/nuke_AssetLoader.png) + +Pick the plate asset, right click and choose Load Image Sequence to create a Read node in Nuke. Note that the Read node created by OpenPype is green and has an OpenPype Tab. Green color indicates the highest version available. + +## Create Write Node – Instance Creator +To create OpenPype managed Write node, select the Read node you just created, from OpenPype menu, pick Create. +In the Instance Creator, pick Create Write Render, and Create. + +![OpenPype Create](assets/nuke_tut/nuke_Create.png) +![OpenPype Create](assets/nuke_tut/nuke_Creator.png) + +This will create a Group with a Write node inside. + +:::tip Admin Tip - Configuring write node +You can configure write node parameters in **Studio Settings β†’ Project β†’ Anatomy β†’ Color Management and Output Formats β†’ Nuke β†’ Nodes** +::: + +## What Nuke Publish Does +From Artist perspective, Nuke publish gathers all the stuff found in the Nuke script with Publish checkbox set to on, exports stuff and raises the Nuke script (workfile) version. +The Pyblish dialog shows the progress of the process. + +![OpenPype Publish](assets/nuke_tut/nuke_Publish.png) + +![OpenPype Publish](assets/nuke_tut/nuke_PyblishDialogNuke.png) + +#### Publish steps +- gathers all the stuff found in the Nuke script with Publish checkbox set to on (1) +- collects all the info (from the script, database…) (2) +- validates components to be published (checks render range and resolution...) (3) +- extracts data from the script (generates thumbnail, creates reviews like h264 ...) (4) +- copies and renames components (render, reviews, thumbnail, Nuke script...) to publish folder +- integrates components (writes to database, sends preview of the render to Ftrack ...) (5) +- increments Nuke script version, cleans up the render directory (6) + +#### Pyblish Note and Intent +Artist can add Note and Intent before firing the publish (7) button. The Note and Intent is ment for easy communication between artist and supervisor. After publish, Note and Intent can be seen in Ftrack notes. + +#### Pyblish Checkbox +Some may say Pyblish Dialog looks unnecessarily complex; it just tries to put a lot of info in a small area. One of the more tricky parts is that it uses non-standard checkboxes. Some squares can be turned on and off by the artist, some are mandatory. + +#### Pyblish Dialog +The left column of the dialog shows what will be published. If you run the publish and decide to not publish the Nuke script, you can turn it off right in the Pyblish dialog by clicking on the checkbox. If you decide to render and publish the shot in lower resolution to speed up the turnaround, you have to turn off the Write Resolution validator. If you want to use an older version of the asset (older version of the plate...), you have to turn off the Validate containers, and so on. + +Time wise, gathering all the info and validating usually takes just a few seconds, but creating reviews for long, high resolution shots can take too much time on the artist machine. + +More info about [Using Pyblish](artist_tools#using-pyblish) + +:::tip Admin Tip - Configuring validators +You can configure Nuke validators like Output Resolution in **Studio Settings β†’ Project β†’ Nuke β†’ Publish plugins** +::: + +## Review +When you turn the review checkbox on in your OpenPype write node, here is what happens: +- OpenPype uses the current Nuke script to + - Load the render + - Optionally apply LUT + - Render Prores 4444 with the same resolution as your render +- Use Ffmpeg to convert the Prores to whatever review(s) you defined +- Use Ffmpeg to add (optional) burnin to the review(s) from previous step + +Creating reviews is a part of the publishing process. If you choose to do a local publish or to use existing frames, review will be processed also on the artist's machine. +If you choose to publish on the farm, you will render and do reviews on the farm. + +So far there is no option for using existing frames (from your local / check render) and just do the review on the farm. + +More info about [configuring reviews](pype2/admin_presets_plugins#extractreview). + +:::tip Admin Tip - Configuring Reviews +You can configure reviewsin **Studio Settings β†’ Project β†’ Global β†’ Publish plugins β†’ ExtractReview / ExtractBurnin** +Reviews can be configured separately for each host, task, or family. For example Maya can produce different review to Nuke, animation task can have different burnin then modelling, and plate can have different review then model. +::: + +## Render and Publish +Let’s say you want to render and publish the shot right now, with only a Read and Write node. You need to decide if you want to render, check the render and then publish it, or you want to execute the render and publish in one go. + +If you wish to check your render before publishing, you can use your local machine or your farm to render the write node as you would do without OpenPype, load and check your render (OpenPype Write has a convenience button for that), and if happy, use publish with Use existing frames option selected in the write node to generate the review on your local machine. + +If you want to render and publish on the farm in one go, run publish with On farm option selected in the write node to render and make the review on farm. + +![OpenPype Create](assets/nuke_tut/nuke_WriteNode.png) + + +## Version-less Render +OpenPype is configured so your render file names have no version number. The main advantage is that you can keep the render from the previous version and re-render only part of the shot. With care, this is handy. + +Main disadvantage of this approach is that you can render only one version of your shot at one time. Otherwise you risk to partially overwrite your shot render before publishing copies and renames the rendered files to the properly versioned publish folder. + +When making quick farm publishes, like making two versions with different color correction, care must be taken to let the first job (first version) completely finish before the second version starts rendering. + +![Versionless](assets/nuke_tut/nuke_versionless.png) + +## Fixing Validate Containers +If your Pyblish dialog fails on Validate Containers, you might have an old asset loaded. Use OpenPype - Manage... to switch the asset(s) to the latest greatest version. + +## Fixing Validate Version +If your Pyblish dialog fails on Validate Version, you might be trying to publish already published version. Rise your version in the OpenPype WorkFiles SaveAs. + +Or maybe you accidentaly copied write node from different shot to your current one. Check the write publishes on the left side of the Pyblish dialog. Typically you publish only one write. Locate and delete the stray write from other shot. \ No newline at end of file diff --git a/website/docs/assets/nuke_tut/nuke_AnatomyAppsVersions.png b/website/docs/assets/nuke_tut/nuke_AnatomyAppsVersions.png new file mode 100644 index 0000000000000000000000000000000000000000..92e1b4dad7371ffac83828170d260fcf4de1d95b GIT binary patch literal 29588 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 ge+Jk87m1D=oTAV;SN>=Gi>*$IZat;^IQ@!a{HB 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 S+fal&TS{DBtVmSf?|%WTv)8%+ literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_Context.png b/website/docs/assets/nuke_tut/nuke_Context.png new file mode 100644 index 0000000000000000000000000000000000000000..65bb288764912c14afe73ebfd1aeb401df0d67be GIT binary patch literal 5480 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 B?4JMt literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_Creator.png b/website/docs/assets/nuke_tut/nuke_Creator.png new file mode 100644 index 0000000000000000000000000000000000000000..3cb7f01666ac8bfebca6c19c0f69c087e3f24f95 GIT binary patch literal 15493 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_Load.png b/website/docs/assets/nuke_tut/nuke_Load.png new file mode 100644 index 0000000000000000000000000000000000000000..2a345dc69f9b79dd48f4c52225e53ebcfebee4d7 GIT binary patch literal 9928 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_NukeColor.png b/website/docs/assets/nuke_tut/nuke_NukeColor.png new file mode 100644 index 0000000000000000000000000000000000000000..5c4f9a15e0253d59de517113cd366193f5f07a76 GIT binary patch literal 42347 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 HtGE6OI0r}; literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_Publish.png b/website/docs/assets/nuke_tut/nuke_Publish.png new file mode 100644 index 0000000000000000000000000000000000000000..b53b6cc06c14c7871fcf5dea12834778c453aa42 GIT binary patch literal 9940 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
  • 8jh8c%*RhB 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 Q6b&HpuQI}Ag1UbH3#et^U;qFB literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction.png b/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction.png new file mode 100644 index 0000000000000000000000000000000000000000..75faaec5725287b33426edff95ff09c024a3d760 GIT binary patch literal 150648 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

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

    `7$lRiZg)#TM$ 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png b/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png new file mode 100644 index 0000000000000000000000000000000000000000..47c8b61824e0428424c6b6fb4f8ab97fd74899db GIT binary patch literal 87912 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�`ul7l497JN6CRceF>W#qXx`GjE-u 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 E8}T?SegFUf literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_RunNukeLauncher.png b/website/docs/assets/nuke_tut/nuke_RunNukeLauncher.png new file mode 100644 index 0000000000000000000000000000000000000000..a42ee6d7b97eb323825b652cc37bdb772a3c7b20 GIT binary patch literal 38738 zcma&N1z1&G(>K16I`jc)q(hJn>GBYg(n@#Np&O|KNGl>J(ka~?(n^AbPBl#zRAH? zj{yKwZ5vH(H*IAlVKcY`hlx4d)Plpy!3m@TfT)C*lZly~g&U2jg_Vt?7~NjeXF3`i zb1}LXkCnNUoun+RZRC7hEHr$cX`1=inF*QGNr+>LdI^IJ94y>SXuKTk9bJXJ#OUt$ z3WL9IUvtvY+%<8t6Qk2sR;7`GyI9aX=6KA(MJJ9)BkE#qDXcCn`*&yXNsP|g&CN-e zlhf1Flf#pj1MXtQ$t@%##L30O$-~1AwqSSlc62lGVs~_+?x!FH?JGr~q-*GlK#qA9{t_I-L8wU zs*RV0y|%OsxTvn+Zivx6=HdB2s5<{n<>LDnRSIqochLaV&_axk`|kDa+6l`$m{?i7 zurYVD{uj4@5tJ+(t?np)bGoD4N@wqWs}5R@HsE~z65)=d{V$proe)2$h_^~|N7DIU zN&nmn;XC=1;pU)tym!6+MUt12Qgwk_+Sr3MSM{eb8hIHher_Rtes&&?zor6ioG{qe z#?o8b#0_l4!^OkR&LzmsEvU&YD9pzr{FslGi$|F2FL~}-gXhQG#LeXY)&BMr(1@Cw z3A@?2*<0K_{dFpy*t`Ap^w*=k&FxVTwl{IK5~K5CH@C1fakqD)6IXY4eCBRvp=9Fj zXl8BUBFcGtg6wVnW_0)T4^E<-|1pdxD5kQqu$+yn8{EbF-$vH3aQ@Gc?+zjj&7FD) zo0#41j2NAh3*6k@%)KdwcT+2#c%X089clq`Sn!JkM`gbbRR$H{f9QbJ+e(ATg7|@jHCbhmYW%Rd#v&b@y+h zV#_}%<;^Lv`n4I?^Iu-d`}0VMF;*Bfj? zBEyS!I@zy==xli2pGvdK9p!W-Y|4}AM0(uZ98bs{p(;#lho~MJ>u{zn&jt>=_xmi> z6%QkSY7DRP6AG}wLo<{y;Xex@FwdXGgD)z%x&M-wy?+YPkh5 zd!|18@>r-jbTiU#0@Z#`GAZSR%@0Y|+G=WCNirV1)-aspbX)-dx9j!~=^ZOB1pv?h z^3qQE`L-dWoUn9SC2NKL9*CQL0$%P~kEvXX4?=$rasiMmPt_@%tZ z!=~QDD=#09r4Qa427^OYdxx#OoHb4>uqRM}76JWfRiVB4#t|6?$+SnTAkiXJ2}!A- zFo;ahzdvNgd~TTA_PzSr>uTT__w@IpYS?yA9-#vFW&sOhSSV39kvEz%BIu7N>UZc*Iy(O-ze;tw0bh_O){7=h4^rdwhY3 zXzVz;IM#d_6RNs6n|vAL&9k(%UOp4IY%Ly%6yeC`>9|k{12HDg^K4Z1n3aEefROEb9kHTnij(fG1F=6W*AI6+E zMyl!9)Ax?`=5p4~1poWaOjW?k-I{l-FK`=XQC6O)6Go^LQlbizySZ7|wAT?o3o!28 zDah^4m^cqXWR2G{57E|vYdzwWkeF!bxm(kY-$6}L+H^d-(pAXdgI9geM`2VgBcBKd z2N0t&N>V{8A+FXPv3U|jKMtYgiw~xk)-^?Dc#xd<k|W#MaEEz4o;vfr z#cV*8N1&W6!)&iV$XP{?TOP0Xf)X%XJ8hR-+hLQ}f~$Q`XNHQTm%w;T(3a+b}^!;q<>T;>eP{7eqA7k-`J zD!rLBDy_D`Y3Mmkl+-;t{M?t&K5!oZ9t=)LKw-?Rh!}y>Dy7b7XvAB^_-AieFs&oA zwvziIFP~}c&tQ^s%`?d1LtgoSvi={&7 zuSA=-W-10hr{~f)SU&jvh;iK*S^$@v;slM+S7Ci9wf1*O3`Ki7O=PzDub3C^_Sl*v zlTRQ#yM+`Rt(Pu0I{5~n*`tt5Y_+rFI_+A=g+`aPzH+YRIH+EoBc%&RY~<(nn{U5J zoNqV0bu3BCss{d8pfbiiYeE4CWg5;n!$tTwR3f(16bsAv3j8S`plunb&}PH%!IKqZ zC$s1U66iE4)s4TM_OnEejS>QZ_s}rYF=L^yY~|p6rypHgE!A#@ia{`<;N!U;dD!;f ziJvVl`n~LvUg-G#o6L~`G#fRH5WM@qPZ#)F81~Q^-|~(yfZ9rrcLRwFQj--7n7-d7KdT2ZrWhU>|TRTYbU4>ES)Z?;{6QgcYm0TwnPTA(3%%E#qP0f9r80q3SvQj)DcqFwcVKWl)tYq6(1){lH;Hu(l6~%((ku0ho^EklH_}? z2J`5A9_vt8h$gx3zGlPux|@kTsdJ^;uaDzv1um#0jaGs+xCYn1zQ3_0f-z$;{t{E= z)6(R71|M)eI?mCoVNj%sQ#_DwB`Di=Z3!V#Pms+Xw|;6pjf})xMI=V*p6YWmcPSN` z5RNA16M^izV9w91`rbFUT&+aWfYyv&nOB9 zV?EYQWm6POO)Z!V8~fC>)BIcY$+Vy}+$!lpDLs#pUI5mRU)*uwZh?C<~!6H}R7 zXWQebdxrgqORq8Cxmo9kDxo#!H>oFbnX=it+q+aQsm$!FqBB;?Q`5wD{ z$4SB}n?lhEI-BJbmW=*(Ko;L)owC5i<#x zwVNW8>eI@ggcfsm(q5(k?F%Laq-<5WlNCzuN24iYwlXdKWA}+^SLraU+aNK}$VXLE z)&*@8WJcHyXKyM@dj4Q}v|ew7;i&BRT@9y~eW0v$--_@47*JKwT+HUGOu4Wrt~*Bn znr}&t1lcShNalmrGy62#S(khh!Z3p8ym?0Z-`=Y1%>SN^CL2dG?up(CIRB9tL{s6` zYZ@@U50{b3+j=#op^5EPh74nF#DAYnAgY(wTx@>`PcLG|+ao>=>53Kb;4r8ZRP#sF z%xI4`cJ!eqJ)XAJEA9{Jr^B=R#2B)7HbNf8Q2$JA6}4!B1GlR0k33yO4oQqA9@7;R z2H0$gt!}I-rwE#y9$^#Fq6Ai;aN0<-VIy&UlphGW?2@%JFlf1%_*q)FMW0n&2Q%w4JiA zVC+U?7$rbJ$s ztD|%~jT#iK4tg&Og3yLQngvy@k)ZJ@s z=*c!6R=`;f`x$u{6&0FkXUoZzDT5g@tu&$DDRGb)dlyHfaRnEh(bPR`C@&I#HP!a} z$UR66=$IuZZEbyq0?=UhAK?)}JVfx=tmr*%UZI)@eMI#FtdW4lM}T(y?t_(G(ejj2 z?NkGE!B`~Vn5xILtO_%8u(JGtqI46DY)PMlFictv+TMZxrViJe;-rgr2kDm{VaSx9Ka9U z@DA{!!KTM_&ytg->B6R&-i5J$;qkbUS?YkcJCY#9D|GE2pSQPLgqcYDrcQ7KFZ^1v zt5&KXeQ+>frW5sy6-p)fN&CxqN%{x+wMPS&zkaf`^a_M5X6$O#o7X^)R29kihx zg-RN=Im{hlN`tb0EL+>2?qjN*h++^bvq;5L$1G}IU5#So;N3Lg66eqq3a_5qg%xj? zPYaG{MCcH4e+;U_+hIJo?biWN*Wy(;)n3zj!Xz0`P`E(a)750qxW`MtuxUXN!tyRs zh90K*tq;r`Zckpjb@k*T4IpoMEINkkmhz7X0TsF35(OBN)pMxo{XAW!CS9@@uU$Fm z;3Y<_m)t>4EGqH}ZAw|szu-M9F4Su^^{y@tfx?_|*!kPItT=F_Up0LWxPZt&U|-wt zTyIXkbuBCfkloS4C1%aQTJ%Gr>E+rBJ>uV{x! zC(wy2?EPK5YW%Dz<-#mSo8(DF}QNDNwU*$G#909@roafytq1qugFB-5&p zgoDGwDY)0qwHJm*pRcB_@kltEhNu~UWl)GC&jqpKHZV{ku1(zEI@O_EgNdbx;KVm5 z)2=I%X_`H1AZ0}C#uvkvYsitG08PW>Qu)J)RJqg?&WnMxeRc5mRz@2NgOcNbz3ckdj(ozJ)0lHr zXAN6aaCPivA9}CJh%cGwbWePbDs(N)wDX`+4xl$HBYx^>=fqBf=*erogvEU! z3P|rV!=v07CM5dsr^gQ`^^|b$Bmrb|sbIw=Z#loUcngYd0Oj zMa}S~iP~>LtE{cOh|Ul?R)g4QPHx1+dHkXB@r-d_7}MWuMO52Ah=x|~ytPoSut7;j ziVgG_XM(e61)1SNVyG;L*3CU!edjsyH4?L;pQ0(6tgEZ+u&6q#N|H&i?0mfMR(wyq z%-1V~+`9hYgy=-5@o6IwBy%UX=d`?5%IdOMGW32X1DOQth=;Ky1}$Nn!im_XUW{8= zkGs5zQzAw|oX2R73MMx{4T3;@uU60@yma}>8FXOk=IKe!fwzmc>VdyTe?HqfD(P9S zX$3Lc(t z&R|h@f&cwak8Ul{blmF(%gj(5AdNn|CV^Ps#^TW@AU8hPxZ>0w5(k``I$n6dn0No> z+N}kaYZ5Rey~gBfK&ayGZzP}@yI>RmzW?wQRdL%^3Dl{XlAwJ^@AY9nI~Q-mW9Imu zVDyHh>z|XvJjtKa z1Kk~G4>{8ko7dnkgV@7`h15!h+%gBy@k@QCV#n8=hv8VK7^!`jm{pLKKeZ9SnZEgN7j#~B%% z_5C-wL-PRp5I9fJRdUl!9o|sOXMtD{rGLqu2xIG`Jm!yK#{A=BmO@GQR?!)a3aSu= zvJ#YK^8hYs_Lf1BkMaODlcxdJh_>OM!4`5~5C5-4Q@n9-W`ecR3O}$ZBL%&p^dwL4 z3M;+-`Of0LBGU4o4^^M-{MPnvyr=<=hSLtbV}HSFTjzBGb)&wOc0SjwF`XwG7BcM| zuiEDZdD-)r)Ig8ECk^9=VzJ%cN8w!Cj2wH@t+-!}S7Fu18JYZnYTCI^)O!kvjSm{ z=A(!k|MjJGs(XP^oXO57CypoR(N!Bmcnt@d@F*$k)3EpYE`q^TPh*M0r-|58&690D zY<=xf(wOAOUMr<2HS*|{A$fVQ)t)|o5@l#*%yIhrN68mm-H?m>QX##JIjw)Nl|^)Q zO>YPUJ}8;&VbHF~Tv22#tx&p2TU#*Qk0@RsG?Y)^mE~uf4T3PTNN~az=VyTZ{1-;l zuW*3d>%Y{vmCI#UA+FEcb$#%orqI}+@9!-Fq+8pDJp+h48W~Dl2E2K>^)!12TB_4?maeGPL#b)#AiFm!e1}{)kw%^L7y;)2P5u0!R4sSs*BTdUcN{ zFq?H&#RGiXl^3KTl$_k9!;1{>VrsY7=_B|psiE<9^VM^LljtXOMKI>a_j{72(=KJU z5WlbMKF=MvIOVTMeBhkFc}v~g`T6E+_O=#$w%1`;yb=dkUpTmQa9s}Bp!V`e|LGtp zB)_&6cz!4^WscceFCuLc&iwwB#nxDEw}=Oy5S>E2xS6>Y1ZPI%@}jKy{Jd@Mvbnj; z;4?OGeI0iarf+3w$)Q}BB7iY_TvPMJVl7AVVP~1kShIrl#j<&Vef(z*Ddn< z-=@mzAl-c3`tCFHGkg1cQX^fHD&k_N!!1(H4{>?8Tnk*z{3W)j-&os;V7z<#NW$wn z(dq=Lt$8l3)t?Fg=846n)fz6Z(@Wl?I9#N8@1NDy`(~-pRINO*$7fH>W&3@D@Z`e? z?yXEnU2_stVtOk@=)ur9FPw}(Mh*{@g31tlB@}8@B2G-*a(b^sLuKs(NSezJ@P_fR zDQFod+f)n$n|_J?1jmp3`9npVm6`dyaPH6R^h+AAl=JD|li>l^MdDVIqG$CkGY2w=wefH&o0T4_*S zXlS={qW60NtK)|^;t;^7@e*sPu!VLflN`fL3hwzrWu&1a$E2qIx*{AL8~xDH`&X@y zW`HNsqa?A*k;-1*EBJ+#a82r^@x|e=pA&)cSY-GwBkmca{#L8j!%Oraa{3eL28L@} zXBk2iB+t3obRNltE`;+qT-T@93$6XNa!e!6^DidYg8+PHw~qaz z=>DgJK8w1g+G2udj#zs*JHG4fQ**(4S+5!%k~0$gp<<99+xlb3Sqee)JSch2Dc`zG zVAM66Wy8?>oA|l>$w03Eu>Jf=)!e#fY5kgq;8{ER#q6UEl+rU$bv6bY{10-_HppobdiSvV6sPGjskot~n9i|L%2#B9y#fZt8wQ_$R*>*-J ziREPP@AAotytgrwmf&Zj=fm1JEyefl-P_sOEQ3kbhgu!IIkF$$F5S2IfH1?uh*yjs zHqaL}Eb9M89tp810k5(*tXm!_5u0^=;$KE?d2>!MtbgF$B3>pitgWgl7@NmaKgj``k&O3?AKq{|lI{sJc4o@7yeB?F zQNTG9QQ}yKHD4Odj|xKT$88ZMkTt-}@a4W<2)9HrN@lgj*&9DdX6Bv45r&SCm5?ct zLp7A))pgv~z+zNOHl$pO5?Ft<(YuIQ=NbI*c&t%F*1-3CLo6<#Yvsus3()=RZ9S!T zhh)-bSSv``9r@79eChv#>#!bQoMfQYvKZmmD|oNU@+nS?Vz!7GS1s{K+N&E`zCQ!M zzSUQwaT{^G_Th9}-H!T`<;;-$;Uz{#2wnd(tGFy>W;@g_p{m)_xuJPPp*!(zQUPW9RWk5l-B^kW)H)9m_66J`- zfmncmCska(vC^Ei>k{fh>7xhfat~#S{^+lA5&Im*5#kX3oW<$hK(A84M3sz7!?Mh3 z%Cl(3!=-&Jrygv@f%-W{!nId#rDyszZE2yn%GokuK%e4h3pw!m(0tguuBiFTv^tkd ziDDI^H+S?t27aGqB*ESj%HD@bge^Pg`em7JKC`-EFrm+K+jvTuEy?Y4x?AskQ6%A; ztOA>T%<&Wg*qX1S8YO1#TZJnP>L;5xi}u!A{YhWLX+7mYvXZAwaCZFO@jEO!{&8hd z7X9a3w{p46p1mf?p_9f!;U*3Tc5zi1Z&!L)S5gOKlkLGO1-$;cL7DTG z6hK=YV#{BHtA#s(m=2--zG0O$>fqF0H!EiMN40;LT9s)iIrIKnlL1j`B~!E62Fn%> z4xXfZLY`i_r@h~o0kc+Tj)f1SPUEJSEA95Bja9#ni(38G0+w99fDw>}=o?>TPamnx zf~Pr(PatM!hEOeWc35Y;xr_YhBNxL{QZ)9_AL1zr^4TjV#eHt6s684#gsrHtqM-5Z zMb_pDSz>D~8G+Ys#?3(o*48rg6uk8uuaebIqrCU779=izx7?yw`4WyFxUYgu(r3ZB z81U@qs#{nJO~N!K*kQJ>v9bpkxohLFVsK(V-u&SFxIta7USqk&qi{Pzy8c{00YDD* zm!XU9C~9yxyjdfULAmb91e2wbtpxM+<*ZR_%eu$gg44Am!)E)%dF&b%Qj_=BQ9>9E zpYEGtu)k8jCkZh#p$kF6nBfM!Ba3$oRvawJUwKB3L-k8aFXKMX>#u*}INe;iX?y0T z-f@k3&QgsHggw8{Zr8{uzLhpVA@IZgxtn9UQmCfcLA)YDLJ8d&!H9JLWI(X}(GlK1A1lxrEyiuIw1319xDx+Y4Xu*kO1 zrOmh$~2{x zSs=r`{tcE1CM`j{ACzQE85}dBM0x{#MfxVT7&Fs8A#vh+Y07z0coF5*p7^Z(F+RX2 zVAOi9&CwHaZ1C{-MLGl}@aOi6GNIAdq8c%m`{b41;uZVLbQ;eDD13c<;HT@#O(%Y@ zGx=ri^XSqXOrEdxQ{RVJVdWs`6ag;~lJs9rUt>+2n#ne}%BFm!I!X52g+;R2l#3s` zuv9#4mDJr`YcLCyXaWPP=+Wg3g{GleOGaMfBU9?YpQw+cg8P4@VHG|iKB9&IgTCTB zdmMk#b05v-wb2qq5r0FxARP;QH6G*5f(SoL8l2lN`V#xV!O)xDcXMQApauhwBt&^* zg3d9mG-6NRQHc!%e#L?n$Cr0vbXKUze#(CEYUI=8yx-Pb$lh1@$G82Fn(T`Q-_O~P zsz?TnT8_IXqCPEmbVse1-M#gk1OUnIWMRjC;nL43dR4~16Wybx28^0L7S7pOS($Id zkLG9JMb$NbElK_CHQ0ExSE{E^`LeX!)?_qwJM_GJ;*bQ6v7=v+7pvEFi<1-^HUWh% zI1KeZ`;1CnVKHw8v{+Eqtqp-AaxNa2-p~T&#z%=HQ}BkDzq5QC`y0FMeyvWHK8O~B zs4H;PqzR7h|EU^wICt>$?3nwGjni!6^EK63?b#KY5%fYs?o7 zFSoIiM)ria!e0(1-w%2#TVZvmtT#s#`1AgJ;@RVJUkS;d@t-PGP!|wgn1X;{>TgJciz5M)+2#`FhXvcL&|87*jw_;qFmNI zTvj8R0G=WN##2Qi7yWYrEsX(#M{5(nAa~Ki=Pi;w#p-|50u;=|i8j6*{Urdl;pNLu z&_0atu*eWQ%uci3$Xj?(CSNi+%DD~CiFRL!Ws7UI`ZhMg6OCi7M8oOlcZOwkIm7Vw zX3TGAAQ$iDgA_;d#-F`py#v^qbIrbo$^uu*tztxWQ_gl-I4`hOWR6y=r#>83ii^@9 z)crC%*#`4_=UM<@#7>yl>&c>cs`Y8+QD>c=-6UddO;2K;NMF0gYw+H5ja9%wjE-AN zKnegnn$$NMZ{Fx!7o2#`Zde5X8Qv%1P4N;>ADBGUEf@R=IU@~MQIlatk2ZI~m=&1e ze=+5{S85M;Ecq06b#h~Vc*C;$k^C}rWFB)uYqu$_5)F+izRGSmJZiGU2zhrxi)gTE zshRWjMugUG!O!!fJ;S9%eJ!M>rFp_P`m;4wH}sw^dqYbBOD!MYMp1*T79X6dI+-4p zE1P=>d%SJ|a9vp*77(o!atMpWQyz22Emor! z_gj-F-C=}>cv&EM@9*|&^s;GbzNirhH%iVXX8L3`C|r6zK|Nnbkx`h{SkodjE3Q@; z)8KkT3$?Pt4iXMYCTtfet+VUtE${dq^42Xj#pvwekPn`WEb3cD$XX!7TSd)kq7A>Z zU(0o0e2{$dFH1-=1p3~te~%3HJ^7JBlv%L8&X+~=_=)b4UUn=0wFe3%S31B@ZoIes5}6L=0h zyA@;`iEP_ISS=fKEi)#a$nBKpm~V}H*okI%!NWX9?+w-O!w(-Y@p#!{AEKu>B&1p7 zVsr+6{lPwDjjgctL9x*zb7y@tLdfP2#KGel|MKleM*R z(;H%X@-(S8OKt|N1z(5jOCZpD7PuS-#<(na}miGr8$7v+Pny5hz%^^|A zZje$u@>kv$`-f}=`}==1wl@Y-58FEJjOGuye(_tjk7FPIuUGp#ZD2Z>- z!m1_+#@TpWC)vK>W-=;-LM%vH#2vgHZUjg&PrqOGl2!K}QGxThxuRaIGP(Le5peab zhV}CkwQsz&Ai6b$7jRj2MV2h_g)7>+tZG)Kd@dx?+pY!lis)v^*|7ITZxc|Npq~^_ zAt2H@Blv8&S69I3YVRQ!mI8qX2|N ztxwh8PoEF;D93PKjau`OVu!KE1b8`)W0Bbrfi8jGb`ocR5>3ir;+Wra-iX$d*R0YY zaLrtELG{ab*-DT6X8(fy_=4JpK?(kxNG`+L9Bw=ob{N_smstB|^)XH>&3f_kicru6t;mvt?8h8?X~al3O(+P?)#^fJ zXru{?-JgRgH>g)^m4lg=U8x6iHwU)SoiPMW#nX@dbA_C5^-fxw61;k9jF4;ZTrg1 zljTyYjCNfQljZb##y&~r>q?d$($9Tqh!Z=XzM@CCC_eu~MOS}!N)vKg6rX6v(g#oU zMRENxl#ycup?+|p-;zRC+MSYWc1rhQCupUf5yp{czRdq{StzXViIXKbMSgp1fWRco^> z=2aF=a40>PVXQm2mSSr|mc#<~pVQXIkGMLw&2{TWX}^fW!pn5Kjrx~1A$ZtUW{AeM zAjOKmv#qC`moe_FmlTjWV`S@S!Q?t!c5GJN3(hrct0GF#z9A?6*!!oF*vq@zrKi2A zSedl#w~RhP@Ve~LOjqm^uTM~Sg|6l zYP}Lfl|vDc%o`{j=AY|`3ZkE#t)2He=}z18<*~ zT>L;F9Amu6d{s9@mAC`k7bA~rIZrqwAduCAW#oO>T`J&&Nrt`e@B!s%AEOi3>0Hc&xb>hPb=B*jj$X=F;p~yS&c*Y+c{U0RBS3Er0wixIEj2>EAApvpRy> zmF}nPOr%c}oliLE*+o!pe&S$H88;;{B1{u2`jzr@PAbha#1UhXWdc33tB2kpOA6v} zZTa`Key*dWgc~YD8G~)povr>CpKm&S`HKVmew3v*daMaBNBZ>CA_2e7SK@AJZaS;e z9iiZHivo;;b{}9Jxj)ATP#h`jXg$=b=R$R3Ep|XV`6`q)c!A?yCr?((kS$5OyJ;#%96p=aOA8gH9K-dq`+e(Nf$!RnSNi#GF9I)!HI2!x-+TzPcEv>hEXQOzA zXPv~6_hMuZKOH5BEKYxjaK*8BXDL$+)1P0^r1bpn+*Y>Hk@U(4B6K=ZKg zQM5)i=Yh3kT{nk{ANW^5&Ev%N3K0lc^6q>129#ti3{6#%U5P?a<=txp5n-Gfu0d%t z^#z5tF1rGsae>?0WIm6C$1Q91$dd+TvEx;Yd+_Jib(4qBJDhO%Kem((;R4G>&cd3j z(@a^J-!fZB@uTl@VKloiu+%(*rrC%#2zYj|kJREW(Y|M?Q*drsu zCEGMicIm!IQK;ByX`?6UO~C%+U2aiWo3#ctVcih9FROge8I?-^zg5U`!#B<=N4)B`-51564_!uI?G!e z7}@PXuRQI|E#4DTT~1v;)?W_eyX1LhW)FOIvIhW}vQs8s|96I0_#wYOAqCTA!wc~M z>>IvdX(V7lS!CnbR*`X#MLCQj6iUuo>AMqj&r>-IIBN4(6zPi|$k?Z#8( z=je-?r271^x0UA-%#mdmZ^bJ3R~($>{AcETUt?Jeqn>J4JQ?cU$Q4yFZx~+vRV1q6 zl$Y=ebQGWpA9rx@77NTo@(G_HIX!nn5S{llG+I@YHAY5%8RsvIsD$3#?I$Oe--NQs z>n6PlrGJbeMSm9+VI*rQJ;3dSUc=i&I2dx5Dff07mUDj3*Eg^K&OTd02hAlph7LIu zVfF)|aIGJcu_9dVZ2IW#xLj%c?nA<0F_d`;!?s`b3oKwU!hb(&6f- zj20LvH{P^{4L0e5fqnZ=&%I)#>3}8WkDiR5vP?%)i>5d}?N5~#m>lL!YErxj?8%K& zS6~4~o(`!=diNZ82ahR-@yayvzK^3yy>m%QR%jbPc{BcAVXYH=pdSB8Q4Pc@K)_^s zUF+l*{Mcx1ssy(yWZ~lrB>M(lXAREmRB zN<~zrQc0dG`R=C)B2;HrpL@NMrt*dTMr%3o89cNCHXL;CUC*)|1Yfph2H z?R8B?7wA+29=kKtnq#eYD!xaTGZWl+5hIPR6D{pCImhewRmB?UypljfE@+iVGe?yd zHY&}O#KoVX)PJ;gT5Y9}7_L*-c;2!#&g&vQjwS!Ez_(q>2$C*i=^%IqSWtp zg;e^I&+ozI&TJ9#F#cqydY}Jc%~{vA;6?Zg@vh&|Kv_ZAc1P{sNJg;9x5&yDDMwW5 zp-~Z8=Bw+bS!z@bDDk63inj-P|o-l?-DsnusItxgIetl=!2;N)oo=eN zMxO7y_4L9AkLrcD)hhUfgvyVAWC74s)jc+}Xos&Sb#@2t(*d6lPKh6w%2xsc_b)s>GNGajn4UYfaXRRI;HUYW+wYOhut! zR>SwZJu!riS)d9;w=;G5)$#K#a5|Q~nZwcFBWV94J8zangZ;%Btos1y;5E9a&Q2dg ze+Y)13e1!|Zz64pEhpEBH6yzR&@G*6$D@PU$Xaq#8}bvhUw_(P2Moc>5lip~!Qgc~ zG;kZ%8dWq8Jt7$D(K#2;6{L9TWw&rZ)W2OUYU043$l!hpCCBQ?|Y{aG{ z_ubHR5wb;JqW~bwU|DBU2`EPn8Uvv-Isfd8lh=>JOu#5PgFtOrrb8P7ygEK_T<&_! z94RUH{a47>IrK4&E+t8Oj94557coL@9MP!c4wgR z8m$n*LH~Q@?Vg~nLGjz-fMonv{+q(g3 z_A5{Lc$H|hP{H8Kp<;64V*J_Uz+<8qm%GRaG%bOz-0->YL%S14cqT2)GAmj+-U-Rk zaqdt0u&UVVVBLWWXb&N_l0#ONrx=WWb7&{g1CZ&N?xus zup+{+?u2OH2ZCGF%+L4i0UEY11f!04RP4(GSv89s_kJsC=g#PhKxtlqj?i`MD`p5? zClfC>ac2E~(4w6Gw~kP(rW9@gtCiq&5I@Uk%F9;x{@nKFRmF|_ehVDCj{P7JmD@Qo zf@@w38R_AvcvIVzXBIQTz2+L+@H{mo8xRmCe(9=49C`Tgg}J5+r)bW7T84K@P!m#D zYx$_JQJK8jW}0%4_JsQiG((<`Cq5`(!&-fa5x7nLEj3I>AH+P5C) z(ljn!K}VD*%zA({N@^Uk_8{dJHMj*5|5wz�BH;NNh)p1_6z{*NF@h0&dj-T=Bmm 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ð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,rP?O!LRzSSzCFOl+h%&D)V!wzR2hJ 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_WorkFileSaveAs.png b/website/docs/assets/nuke_tut/nuke_WorkFileSaveAs.png new file mode 100644 index 0000000000000000000000000000000000000000..661f44632a97348db0735343b22c6471630a3511 GIT binary patch literal 26227 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_WorkfileOnStartup.png b/website/docs/assets/nuke_tut/nuke_WorkfileOnStartup.png new file mode 100644 index 0000000000000000000000000000000000000000..450589ee3a0ef5ab20f117c2044ddb9e7be103d1 GIT binary patch literal 22326 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 VUFi#6R|5T8Ohj5Z=Y^){{{j!)=o0_{ literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_versionless.png b/website/docs/assets/nuke_tut/nuke_versionless.png new file mode 100644 index 0000000000000000000000000000000000000000..fbb98c55e211fce76af63564a8ff9cf709b10c8c GIT binary patch literal 5034 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 Date: Sun, 22 Aug 2021 04:36:51 +0800 Subject: [PATCH 064/716] 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 065/716] 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 066/716] 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 067/716] 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 6b6877e76ed934b22bfc3b4206de7ed16984a52e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 15:52:49 +0200 Subject: [PATCH 068/716] fixed get_general_environments function --- openpype/settings/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index b5810deef4..9797458fd5 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -2,6 +2,7 @@ from .exceptions import ( SaveWarningExc ) from .lib import ( + get_general_environments, get_system_settings, get_project_settings, get_current_project_settings, @@ -18,6 +19,7 @@ from .entities import ( __all__ = ( "SaveWarningExc", + "get_general_environments", "get_system_settings", "get_project_settings", "get_current_project_settings", From cf811c7e0f56353af0a3a18e01593db310d1edce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 15:53:03 +0200 Subject: [PATCH 069/716] added addons_path key to settings --- .../settings/defaults/system_settings/modules.json | 5 +++++ .../schemas/system_schema/schema_modules.json | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 3a70b90590..12cca7ccf1 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -1,4 +1,9 @@ { + "addon_paths": { + "windows": [], + "darwin": [], + "linux": [] + }, "avalon": { "AVALON_TIMEOUT": 1000, "AVALON_THUMBNAIL_ROOT": { diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 75c08b2cd9..0e52cea69e 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -5,6 +5,18 @@ "collapsible": true, "is_file": true, "children": [ + { + "type": "path", + "key": "addon_paths", + "label": "OpenPype AddOn Paths", + "use_label_wrap": true, + "multiplatform": true, + "multipath": true, + "require_restart": true + }, + { + "type": "separator" + }, { "type": "dict", "key": "avalon", From b92621a270704bb8b61842d977d883ea230b304f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 15:53:19 +0200 Subject: [PATCH 070/716] don't crash if path does not exists --- openpype/modules/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 3d3d7ae6cb..e407a34606 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -165,6 +165,9 @@ def _load_interfaces(): os.path.join(get_default_modules_dir(), "interfaces.py") ) for dirpath in dirpaths: + if not os.path.exists(dirpath): + continue + for filename in os.listdir(dirpath): if filename in ("__pycache__", ): continue From bf1a5c85ccf4db4d3be2c99a7ff3f1c9d5cdf519 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 15:54:08 +0200 Subject: [PATCH 071/716] added get_dynamic_modules_dirs to be able get paths to openpype addons --- openpype/modules/base.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index e407a34606..a3269e99e9 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -115,11 +115,24 @@ def get_default_modules_dir(): return os.path.join(current_dir, "default_modules") +def get_dynamic_modules_dirs(): + output = [] + return output + + def get_module_dirs(): """List of paths where OpenPype modules can be found.""" - dirpaths = [ - get_default_modules_dir() - ] + _dirpaths = [] + _dirpaths.append(get_default_modules_dir()) + _dirpaths.extend(get_dynamic_modules_dirs()) + + dirpaths = [] + for path in _dirpaths: + if not path: + continue + normalized = os.path.normpath(path) + if normalized not in dirpaths: + dirpaths.append(normalized) return dirpaths From e3754a85a662dbcbaf2d06cc8285638b7ea9d7d2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 15:54:23 +0200 Subject: [PATCH 072/716] implemented logic of dynamic addons paths --- openpype/modules/base.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index a3269e99e9..d3b83e85b1 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -5,6 +5,7 @@ import sys import time import inspect import logging +import platform import threading import collections from uuid import uuid4 @@ -13,6 +14,7 @@ import six import openpype from openpype.settings import get_system_settings +from openpype.settings.lib import get_studio_system_settings_overrides from openpype.lib import PypeLogger @@ -117,6 +119,21 @@ def get_default_modules_dir(): def get_dynamic_modules_dirs(): output = [] + value = get_studio_system_settings_overrides() + for key in ("modules", "addon_paths", platform.system().lower()): + if key not in value: + return output + value = value[key] + + for path in value: + if not path: + continue + + try: + path = path.format(**os.environ) + except Exception: + pass + output.append(path) return output From 2495cffd509a51838fc1c8c5c77d05a007794322 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 16:44:58 +0200 Subject: [PATCH 073/716] don't crash whole openpype on broken addon/module --- openpype/modules/base.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index d3b83e85b1..9df9b3a97b 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -305,12 +305,19 @@ def _load_modules(): # TODO add more logic how to define if folder is module or not # - check manifest and content of manifest - if os.path.isdir(fullpath): - import_module_from_dirpath(dirpath, filename, modules_key) + try: + if os.path.isdir(fullpath): + import_module_from_dirpath(dirpath, filename, modules_key) - elif ext in (".py", ): - module = import_filepath(fullpath) - setattr(openpype_modules, basename, module) + elif ext in (".py", ): + module = import_filepath(fullpath) + setattr(openpype_modules, basename, module) + + except Exception: + log.error( + "Failed to import '{}'.".format(fullpath), + exc_info=True + ) class _OpenPypeInterfaceMeta(ABCMeta): From bd791c971985bd2229c256d8946f808733307597 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:08:44 +0200 Subject: [PATCH 074/716] moved few settings constants to constants.py --- openpype/settings/__init__.py | 25 +++++++++++++++++++++++++ openpype/settings/constants.py | 9 ++++++++- openpype/settings/entities/lib.py | 7 ++++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index 9797458fd5..0adb5db0bd 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -1,3 +1,16 @@ +from .constants import ( + GLOBAL_SETTINGS_KEY, + SYSTEM_SETTINGS_KEY, + PROJECT_SETTINGS_KEY, + PROJECT_ANATOMY_KEY, + LOCAL_SETTING_KEY, + + SCHEMA_KEY_SYSTEM_SETTINGS, + SCHEMA_KEY_PROJECT_SETTINGS, + + KEY_ALLOWED_SYMBOLS, + KEY_REGEX +) from .exceptions import ( SaveWarningExc ) @@ -17,6 +30,18 @@ from .entities import ( __all__ = ( + "GLOBAL_SETTINGS_KEY", + "SYSTEM_SETTINGS_KEY", + "PROJECT_SETTINGS_KEY", + "PROJECT_ANATOMY_KEY", + "LOCAL_SETTING_KEY", + + "SCHEMA_KEY_SYSTEM_SETTINGS", + "SCHEMA_KEY_PROJECT_SETTINGS", + + "KEY_ALLOWED_SYMBOLS", + "KEY_REGEX", + "SaveWarningExc", "get_general_environments", diff --git a/openpype/settings/constants.py b/openpype/settings/constants.py index a53e88a91e..2ea19ead4b 100644 --- a/openpype/settings/constants.py +++ b/openpype/settings/constants.py @@ -14,13 +14,17 @@ METADATA_KEYS = ( M_DYNAMIC_KEY_LABEL ) -# File where studio's system overrides are stored +# Keys where studio's system overrides are stored GLOBAL_SETTINGS_KEY = "global_settings" SYSTEM_SETTINGS_KEY = "system_settings" PROJECT_SETTINGS_KEY = "project_settings" PROJECT_ANATOMY_KEY = "project_anatomy" LOCAL_SETTING_KEY = "local_settings" +# Schema hub names +SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema" +SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema" + DEFAULT_PROJECT_KEY = "__default_project__" KEY_ALLOWED_SYMBOLS = "a-zA-Z0-9-_ " @@ -39,6 +43,9 @@ __all__ = ( "PROJECT_ANATOMY_KEY", "LOCAL_SETTING_KEY", + "SCHEMA_KEY_SYSTEM_SETTINGS", + "SCHEMA_KEY_PROJECT_SETTINGS", + "DEFAULT_PROJECT_KEY", "KEY_ALLOWED_SYMBOLS", diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index f7036726d2..d4b0e10864 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -11,6 +11,10 @@ from .exceptions import ( SchemaDuplicatedEnvGroupKeys ) +from openpype.settings.constants import ( + SCHEMA_KEY_SYSTEM_SETTINGS, + SCHEMA_KEY_PROJECT_SETTINGS +) try: STRING_TYPE = basestring except Exception: @@ -25,9 +29,6 @@ TEMPLATE_METADATA_KEYS = ( DEFAULT_VALUES_KEY, ) -SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema" -SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema" - SCHEMA_EXTEND_TYPES = ( "schema", "template", "schema_template", "dynamic_schema" ) From 2706c7759f6ed01aecb7a205f4cfc806aa22b7d3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:15:06 +0200 Subject: [PATCH 075/716] a littlebit safer return value check --- openpype/settings/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index d7684082f3..60ed54bd4a 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -337,7 +337,9 @@ def _get_default_settings(): module_settings_defs = get_module_settings_defs() for module_settings_def_cls in module_settings_defs: module_settings_def = module_settings_def_cls() - system_defaults = module_settings_def.get_system_defaults() + system_defaults = module_settings_def.get_defaults( + SYSTEM_SETTINGS_KEY + ) or {} for path, value in system_defaults.items(): if not path: continue @@ -349,7 +351,9 @@ def _get_default_settings(): subdict = subdict[key] subdict[last_key] = value - project_defaults = module_settings_def.get_project_defaults() + project_defaults = module_settings_def.get_defaults( + PROJECT_SETTINGS_KEY + ) or {} for path, value in project_defaults.items(): if not path: continue From 735f4b847b7e990c8e6c0e22ed45a5d6d4c6c829 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:15:29 +0200 Subject: [PATCH 076/716] added mapping of schema hub key to top key value --- openpype/settings/entities/lib.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index d4b0e10864..f207322dee 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -12,6 +12,8 @@ from .exceptions import ( ) from openpype.settings.constants import ( + SYSTEM_SETTINGS_KEY, + PROJECT_SETTINGS_KEY, SCHEMA_KEY_SYSTEM_SETTINGS, SCHEMA_KEY_PROJECT_SETTINGS ) @@ -734,6 +736,12 @@ class SchemasHub: class DynamicSchemaValueCollector: + # Map schema hub type to store keys + schema_hub_type_map = { + SCHEMA_KEY_SYSTEM_SETTINGS: SYSTEM_SETTINGS_KEY, + SCHEMA_KEY_PROJECT_SETTINGS: PROJECT_SETTINGS_KEY + } + def __init__(self, schema_hub): self._schema_hub = schema_hub self._dynamic_entities = [] @@ -756,7 +764,7 @@ class DynamicSchemaValueCollector: schema_def = self._schema_hub.get_dynamic_modules_settings_defs( schema_def_id ) - if self._schema_hub.schema_type == SCHEMA_KEY_SYSTEM_SETTINGS: - schema_def.save_system_defaults(schema_def_value) - elif self._schema_hub.schema_type == SCHEMA_KEY_PROJECT_SETTINGS: - schema_def.save_project_defaults(schema_def_value) + top_key = self.schema_hub_type_map.get( + self._schema_hub.schema_type + ) + schema_def.save_defaults(top_key, schema_def_value) From f30253697127285c21650dc400f81de577f24074 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:15:46 +0200 Subject: [PATCH 077/716] eliminated methods in ModuleSettingsDef --- openpype/modules/base.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 9df9b3a97b..ce555c6bbf 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -1025,17 +1025,9 @@ class ModuleSettingsDef: pass @abstractmethod - def save_system_defaults(self, data): + def get_defaults(self, top_key): pass @abstractmethod - def save_project_defaults(self, data): - pass - - @abstractmethod - def get_system_defaults(self): - pass - - @abstractmethod - def get_project_defaults(self): + def save_defaults(self, top_key, data): pass From c16cee6f810ecd1cd0a45b99cf77079e83c6d51f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:47:37 +0200 Subject: [PATCH 078/716] added few docstrings --- openpype/modules/base.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index ce555c6bbf..9972126136 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -1008,26 +1008,63 @@ def get_module_settings_defs(): @six.add_metaclass(ABCMeta) class ModuleSettingsDef: + """Definition of settings for OpenPype module or AddOn.""" _id = None @property def id(self): + """ID created on initialization. + + ID should be per created object. Helps to store objects. + """ if self._id is None: self._id = uuid4() return self._id @abstractmethod def get_settings_schemas(self, schema_type): + """Setting schemas for passed schema type. + + These are main schemas by dynamic schema keys. If they're using + sub schemas or templates they should be loaded with + `get_dynamic_schemas`. + + Returns: + dict: Schema by `dynamic_schema` keys. + """ pass @abstractmethod def get_dynamic_schemas(self, schema_type): + """Settings schemas and templates that can be used anywhere. + + It is recommended to add prefix specific for addon/module to keys + (e.g. "my_addon/real_schema_name"). + + Returns: + dict: Schemas and templates by their keys. + """ pass @abstractmethod def get_defaults(self, top_key): + """Default values for passed top key. + + Top keys are (currently) "system_settings" or "project_settings". + + Should return exactly what was passed with `save_defaults`. + + Returns: + dict: Default values by path to first key in OpenPype defaults. + """ pass @abstractmethod def save_defaults(self, top_key, data): + """Save default values for passed top key. + + Top keys are (currently) "system_settings" or "project_settings". + + Passed data are by path to first key defined in main schemas. + """ pass From 60ff21534219da5eeada4c69873ae0baff4b71a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:54:04 +0200 Subject: [PATCH 079/716] added few infor to readme --- openpype/settings/entities/schemas/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 2034d4e463..a34732fbad 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -112,6 +112,22 @@ ``` - It is possible to define default values for unfilled fields to do so one of items in list must be dictionary with key `"__default_values__"` and value as dictionary with default key: values (as in example above). +### dynamic_schema +- dynamic templates that can be defined by class of `ModuleSettingsDef` +- example: +``` +{ + "type": "dynamic_schema", + "name": "project_settings/global" +} +``` +- all valid `ModuleSettingsDef` classes where calling of `get_settings_schemas` + will return dictionary where is key "project_settings/global" with schemas + will extend and replace this item +- works almost the same way as templates + - one item can be replaced by multiple items (or by 0 items) +- goal is to dynamically loaded settings of OpenPype addons without having + their schemas or default values in main repository ## Basic Dictionary inputs - these inputs wraps another inputs into {key: value} relation From b869f2ab82657d24af093554654c48538a177be6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 18:00:18 +0200 Subject: [PATCH 080/716] added few more docstrings --- openpype/modules/base.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 9972126136..c8cc911ca6 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -118,6 +118,18 @@ def get_default_modules_dir(): def get_dynamic_modules_dirs(): + """Possible paths to OpenPype Addons of Modules. + + Paths are loaded from studio settings under: + `modules -> addon_paths -> {platform name}` + + Path may contain environment variable as a formatting string. + + They are not validated or checked their existence. + + Returns: + list: Paths loaded from studio overrides. + """ output = [] value = get_studio_system_settings_overrides() for key in ("modules", "addon_paths", platform.system().lower()): @@ -963,6 +975,17 @@ class TrayModulesManager(ModulesManager): def get_module_settings_defs(): + """Check loaded addons/modules for existence of thei settings definition. + + Check if OpenPype addon/module as python module has class that inherit + from `ModuleSettingsDef` in python module variables (imported + in `__init__py`). + + Returns: + list: All valid and not abstract settings definitions from imported + openpype addons and modules. + """ + # Make sure modules are loaded load_modules() import openpype_modules From 900e3aac7ae1e9a1bee97cf12fa4a8544df3208b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 25 Aug 2021 16:40:13 +0200 Subject: [PATCH 081/716] 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 082/716] 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 083/716] 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 e5456fe55b1b22c5b8f97dd33201eaf5e28f705e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Aug 2021 19:40:05 +0200 Subject: [PATCH 084/716] initial commit of "NiceSlide" widget --- openpype/widgets/sliders.py | 138 ++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 openpype/widgets/sliders.py diff --git a/openpype/widgets/sliders.py b/openpype/widgets/sliders.py new file mode 100644 index 0000000000..2f26c3eb97 --- /dev/null +++ b/openpype/widgets/sliders.py @@ -0,0 +1,138 @@ +from Qt import QtWidgets, QtCore, QtGui + + +class NiceSlider(QtWidgets.QSlider): + def __init__(self, *args, **kwargs): + super(NiceSlider, self).__init__(*args, **kwargs) + self._mouse_clicked = False + self._handle_size = 0 + + self._bg_brush = QtGui.QBrush(QtGui.QColor("#21252B")) + self._fill_brush = QtGui.QBrush(QtGui.QColor("#5cadd6")) + + def mousePressEvent(self, event): + self._mouse_clicked = True + if event.button() == QtCore.Qt.LeftButton: + self._set_value_to_pos(event.pos()) + return event.accept() + return super(NiceSlider, self).mousePressEvent(event) + + def mouseMoveEvent(self, event): + if self._mouse_clicked: + self._set_value_to_pos(event.pos()) + + super(NiceSlider, self).mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + self._mouse_clicked = True + super(NiceSlider, self).mouseReleaseEvent(event) + + def _set_value_to_pos(self, pos): + if self.orientation() == QtCore.Qt.Horizontal: + self._set_value_to_pos_x(pos.x()) + else: + self._set_value_to_pos_y(pos.y()) + + def _set_value_to_pos_x(self, pos_x): + _range = self.maximum() - self.minimum() + handle_size = self._handle_size + half_handle = handle_size / 2 + pos_x -= half_handle + width = self.width() - handle_size + value = ((_range * pos_x) / width) + self.minimum() + self.setValue(value) + + def _set_value_to_pos_y(self, pos_y): + _range = self.maximum() - self.minimum() + handle_size = self._handle_size + half_handle = handle_size / 2 + pos_y = self.height() - pos_y - half_handle + height = self.height() - handle_size + value = (_range * pos_y / height) + self.minimum() + self.setValue(value) + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + opt = QtWidgets.QStyleOptionSlider() + self.initStyleOption(opt) + + painter.fillRect(event.rect(), QtCore.Qt.transparent) + + painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + + horizontal = self.orientation() == QtCore.Qt.Horizontal + + rect = self.style().subControlRect( + QtWidgets.QStyle.CC_Slider, + opt, + QtWidgets.QStyle.SC_SliderGroove, + self + ) + + _range = self.maximum() - self.minimum() + if horizontal: + _handle_half = rect.height() / 2 + _handle_size = _handle_half * 2 + width = rect.width() - _handle_size + pos_x = ((width / _range) * self.value()) + pos_y = rect.center().y() - _handle_half + 1 + else: + _handle_half = rect.width() / 2 + _handle_size = _handle_half * 2 + height = rect.height() - _handle_size + pos_x = rect.center().x() - _handle_half + 1 + pos_y = height - ((height / _range) * self.value()) + + handle_rect = QtCore.QRect( + pos_x, pos_y, _handle_size, _handle_size + ) + + self._handle_size = _handle_size + _offset = 2 + _size = _handle_size - _offset + if horizontal: + if rect.height() > _size: + new_rect = QtCore.QRect(0, 0, rect.width(), _size) + center_point = QtCore.QPoint( + rect.center().x(), handle_rect.center().y() + ) + new_rect.moveCenter(center_point) + rect = new_rect + + ratio = rect.height() / 2 + fill_rect = QtCore.QRect( + rect.x(), + rect.y(), + handle_rect.right() - rect.x(), + rect.height() + ) + + else: + if rect.width() > _size: + new_rect = QtCore.QRect(0, 0, _size, rect.height()) + center_point = QtCore.QPoint( + handle_rect.center().x(), rect.center().y() + ) + new_rect.moveCenter(center_point) + rect = new_rect + + ratio = rect.width() / 2 + fill_rect = QtCore.QRect( + rect.x(), + handle_rect.y(), + rect.width(), + rect.height() - handle_rect.y(), + ) + + painter.save() + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(self._bg_brush) + painter.drawRoundedRect(rect, ratio, ratio) + + painter.setBrush(self._fill_brush) + painter.drawRoundedRect(fill_rect, ratio, ratio) + + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(self._fill_brush) + painter.drawEllipse(handle_rect) + painter.restore() From c130b72f126a31142e71f6d749d5b5ee65e1a3b3 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 27 Aug 2021 11:02:05 +0100 Subject: [PATCH 085/716] 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 086/716] 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 4c55040a58c9b68ebf1a804b7b2df84fb45b9c16 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Aug 2021 16:55:10 +0200 Subject: [PATCH 087/716] enhanced `ModuleSettingsDef` to split each method into 2 separated abstract methods --- openpype/modules/__init__.py | 2 + openpype/modules/base.py | 124 ++++++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index 6169f99f77..6b3c0dc3a6 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -9,6 +9,7 @@ from .base import ( ModulesManager, TrayModulesManager, + BaseModuleSettingsDef, ModuleSettingsDef, get_module_settings_defs ) @@ -24,6 +25,7 @@ __all__ = ( "ModulesManager", "TrayModulesManager", + "BaseModuleSettingsDef", "ModuleSettingsDef", "get_module_settings_defs" ) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index c8cc911ca6..66f962526f 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -13,8 +13,17 @@ from abc import ABCMeta, abstractmethod import six import openpype -from openpype.settings import get_system_settings -from openpype.settings.lib import get_studio_system_settings_overrides +from openpype.settings import ( + get_system_settings, + SYSTEM_SETTINGS_KEY, + PROJECT_SETTINGS_KEY, + SCHEMA_KEY_SYSTEM_SETTINGS, + SCHEMA_KEY_PROJECT_SETTINGS +) + +from openpype.settings.lib import ( + get_studio_system_settings_overrides, +) from openpype.lib import PypeLogger @@ -1030,7 +1039,7 @@ def get_module_settings_defs(): @six.add_metaclass(ABCMeta) -class ModuleSettingsDef: +class BaseModuleSettingsDef: """Definition of settings for OpenPype module or AddOn.""" _id = None @@ -1091,3 +1100,112 @@ class ModuleSettingsDef: Passed data are by path to first key defined in main schemas. """ pass + + +class ModuleSettingsDef(BaseModuleSettingsDef): + def get_defaults(self, top_key): + """Split method into 2 methods by top key.""" + if top_key == SYSTEM_SETTINGS_KEY: + return self.get_default_system_settings() or {} + elif top_key == PROJECT_SETTINGS_KEY: + return self.get_default_project_settings() or {} + return {} + + def save_defaults(self, top_key, data): + """Split method into 2 methods by top key.""" + if top_key == SYSTEM_SETTINGS_KEY: + self.save_system_defaults(data) + elif top_key == PROJECT_SETTINGS_KEY: + self.save_project_defaults(data) + + def get_settings_schemas(self, schema_type): + """Split method into 2 methods by schema type.""" + if schema_type == SCHEMA_KEY_SYSTEM_SETTINGS: + return self.get_system_settings_schemas() or {} + elif schema_type == SCHEMA_KEY_PROJECT_SETTINGS: + return self.get_project_settings_schemas() or {} + return {} + + def get_dynamic_schemas(self, schema_type): + """Split method into 2 methods by schema type.""" + if schema_type == SCHEMA_KEY_SYSTEM_SETTINGS: + return self.get_system_dynamic_schemas() or {} + elif schema_type == SCHEMA_KEY_PROJECT_SETTINGS: + return self.get_project_dynamic_schemas() or {} + return {} + + @abstractmethod + def get_system_settings_schemas(self): + """Schemas and templates usable in system settings schemas. + + Returns: + dict: Schemas and templates by it's names. Names must be unique + across whole OpenPype. + """ + pass + + @abstractmethod + def get_project_settings_schemas(self): + """Schemas and templates usable in project settings schemas. + + Returns: + dict: Schemas and templates by it's names. Names must be unique + across whole OpenPype. + """ + pass + + @abstractmethod + def get_system_dynamic_schemas(self): + """System schemas by dynamic schema name. + + If dynamic schema name is not available in then schema will not used. + + Returns: + dict: Schemas or list of schemas by dynamic schema name. + """ + pass + + @abstractmethod + def get_project_dynamic_schemas(self): + """Project schemas by dynamic schema name. + + If dynamic schema name is not available in then schema will not used. + + Returns: + dict: Schemas or list of schemas by dynamic schema name. + """ + pass + + @abstractmethod + def get_default_system_settings(self): + """Default system settings values. + + Returns: + dict: Default values by path to first key. + """ + pass + + @abstractmethod + def get_default_project_settings(self): + """Default project settings values. + + Returns: + dict: Default values by path to first key. + """ + pass + + @abstractmethod + def save_system_defaults(self, data): + """Save default system settings values. + + Passed data are by path to first key defined in main schemas. + """ + pass + + @abstractmethod + def save_project_defaults(self, data): + """Save default project settings values. + + Passed data are by path to first key defined in main schemas. + """ + pass From ab7ea51bab5eacfb8ccd6d4c913acb89b115855e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Aug 2021 17:12:35 +0200 Subject: [PATCH 088/716] preimplemented json files settings definition which needs only one method to implement --- openpype/modules/__init__.py | 4 + openpype/modules/base.py | 195 +++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index 6b3c0dc3a6..68b5f6c247 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -11,6 +11,8 @@ from .base import ( BaseModuleSettingsDef, ModuleSettingsDef, + JsonFilesSettingsDef, + get_module_settings_defs ) @@ -27,5 +29,7 @@ __all__ = ( "BaseModuleSettingsDef", "ModuleSettingsDef", + "JsonFilesSettingsDef", + "get_module_settings_defs" ) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 66f962526f..a11867ea15 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -2,6 +2,7 @@ """Base class for Pype Modules.""" import os import sys +import json import time import inspect import logging @@ -23,6 +24,7 @@ from openpype.settings import ( from openpype.settings.lib import ( get_studio_system_settings_overrides, + load_json_file ) from openpype.lib import PypeLogger @@ -1103,6 +1105,11 @@ class BaseModuleSettingsDef: class ModuleSettingsDef(BaseModuleSettingsDef): + """Settings definiton with separated system and procect settings parts. + + Reduce conditions that must be checked and adds predefined methods for + each case. + """ def get_defaults(self, top_key): """Split method into 2 methods by top key.""" if top_key == SYSTEM_SETTINGS_KEY: @@ -1209,3 +1216,191 @@ class ModuleSettingsDef(BaseModuleSettingsDef): Passed data are by path to first key defined in main schemas. """ pass + + +class JsonFilesSettingsDef(ModuleSettingsDef): + """Preimplemented settings definition using json files and file structure. + + Expected file structure: + β”• root + β”‚ + β”‚ # Default values + ┝ defaults + β”‚ ┝ system_settings.json + β”‚ β”• project_settings.json + β”‚ + β”‚ # Schemas for `dynamic_template` type + ┝ dynamic_schemas + β”‚ ┝ system_dynamic_schemas.json + β”‚ β”• project_dynamic_schemas.json + β”‚ + β”‚ # Schemas that can be used anywhere (enhancement for `dynamic_schemas`) + β”• schemas + ┝ system_schemas + β”‚ ┝ # Any schema or template files + β”‚ β”• ... + β”• project_schemas + ┝ # Any schema or template files + β”• ... + + Schemas can be loaded with prefix to avoid duplicated schema/template names + across all OpenPype addons/modules. Prefix can be defined with class + attribute `schema_prefix`. + + Only think which must be implemented in `get_settings_root_dir` which + should return directory path to `root` (in structure graph above). + """ + # Possible way how to define `schemas` prefix + schema_prefix = "" + + @abstractmethod + def get_settings_root_dir(self): + """Directory path where settings and it's schemas are located.""" + pass + + def __init__(self): + settings_root_dir = self.get_settings_root_dir() + defaults_dir = os.path.join( + settings_root_dir, "defaults" + ) + dynamic_schemas_dir = os.path.join( + settings_root_dir, "dynamic_schemas" + ) + schemas_dir = os.path.join( + settings_root_dir, "schemas" + ) + + self.system_defaults_filepath = os.path.join( + defaults_dir, "system_settings.json" + ) + self.project_defaults_filepath = os.path.join( + defaults_dir, "project_settings.json" + ) + + self.system_dynamic_schemas_filepath = os.path.join( + dynamic_schemas_dir, "system_dynamic_schemas.json" + ) + self.project_dynamic_schemas_filepath = os.path.join( + dynamic_schemas_dir, "project_dynamic_schemas.json" + ) + + self.system_schemas_dir = os.path.join( + schemas_dir, "system_schemas" + ) + self.project_schemas_dir = os.path.join( + schemas_dir, "project_schemas" + ) + + def _load_json_file_data(self, path): + if os.path.exists(path): + return load_json_file(path) + return {} + + def get_default_system_settings(self): + """Default system settings values. + + Returns: + dict: Default values by path to first key. + """ + return self._load_json_file_data(self.system_defaults_filepath) + + def get_default_project_settings(self): + """Default project settings values. + + Returns: + dict: Default values by path to first key. + """ + return self._load_json_file_data(self.project_defaults_filepath) + + def _save_data_to_filepath(self, path, data): + dirpath = os.path.dirname(path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + with open(path, "w") as file_stream: + json.dump(data, file_stream, indent=4) + + def save_system_defaults(self, data): + """Save default system settings values. + + Passed data are by path to first key defined in main schemas. + """ + self._save_data_to_filepath(self.system_defaults_filepath, data) + + def save_project_defaults(self, data): + """Save default project settings values. + + Passed data are by path to first key defined in main schemas. + """ + self._save_data_to_filepath(self.project_defaults_filepath, data) + + def get_system_dynamic_schemas(self): + """System schemas by dynamic schema name. + + If dynamic schema name is not available in then schema will not used. + + Returns: + dict: Schemas or list of schemas by dynamic schema name. + """ + return self._load_json_file_data(self.system_dynamic_schemas_filepath) + + def get_project_dynamic_schemas(self): + """Project schemas by dynamic schema name. + + If dynamic schema name is not available in then schema will not used. + + Returns: + dict: Schemas or list of schemas by dynamic schema name. + """ + return self._load_json_file_data(self.project_dynamic_schemas_filepath) + + def _load_files_from_path(self, path): + output = {} + if not path or not os.path.exists(path): + return output + + if os.path.isfile(path): + filename = os.path.basename(path) + basename, ext = os.path.splitext(filename) + if ext == ".json": + if self.schema_prefix: + key = "{}/{}".format(self.schema_prefix, basename) + else: + key = basename + output[key] = self._load_json_file_data(path) + return output + + path = os.path.normpath(path) + for root, _, files in os.walk(path, topdown=False): + for filename in files: + basename, ext = os.path.splitext(filename) + if ext != ".json": + continue + + json_path = os.path.join(root, filename) + store_key = os.path.join( + root.replace(path, ""), basename + ).replace("\\", "/") + if self.schema_prefix: + store_key = "{}/{}".format(self.schema_prefix, store_key) + output[store_key] = self._load_json_file_data(json_path) + + return output + + def get_system_settings_schemas(self): + """Schemas and templates usable in system settings schemas. + + Returns: + dict: Schemas and templates by it's names. Names must be unique + across whole OpenPype. + """ + return self._load_files_from_path(self.system_schemas_dir) + + def get_project_settings_schemas(self): + """Schemas and templates usable in project settings schemas. + + Returns: + dict: Schemas and templates by it's names. Names must be unique + across whole OpenPype. + """ + return self._load_files_from_path(self.project_schemas_dir) From 82f361d48aa041e6b9da4b2c919dc567e2f8aae1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Aug 2021 17:14:20 +0200 Subject: [PATCH 089/716] enable addon by default --- openpype/modules/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index a11867ea15..1fc1d900a0 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -431,7 +431,8 @@ class OpenPypeModule: class OpenPypeAddOn(OpenPypeModule): - pass + # Enable Addon by default + enabled = True class ModulesManager: From 2188e62b694dc53a7a07959ee51ebbf36ef1f3d4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Aug 2021 17:14:33 +0200 Subject: [PATCH 090/716] implement abstract methods required for modules --- openpype/modules/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1fc1d900a0..23ec3b8c6f 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -434,6 +434,14 @@ class OpenPypeAddOn(OpenPypeModule): # Enable Addon by default enabled = True + def initialize(self, module_settings): + """Initialization is not be required for most of addons.""" + pass + + def connect_with_modules(self, enabled_modules): + """Do not require to implement connection with modules for addon.""" + pass + class ModulesManager: """Manager of Pype modules helps to load and prepare them to work. From 5dd6d010609f91647b416d81e38ac282f7de3269 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Aug 2021 17:23:34 +0200 Subject: [PATCH 091/716] changed name of `get_settings_root_dir` to `get_settings_root_path` --- openpype/modules/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 23ec3b8c6f..01c3cebe60 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -1256,19 +1256,19 @@ class JsonFilesSettingsDef(ModuleSettingsDef): across all OpenPype addons/modules. Prefix can be defined with class attribute `schema_prefix`. - Only think which must be implemented in `get_settings_root_dir` which + Only think which must be implemented in `get_settings_root_path` which should return directory path to `root` (in structure graph above). """ # Possible way how to define `schemas` prefix schema_prefix = "" @abstractmethod - def get_settings_root_dir(self): + def get_settings_root_path(self): """Directory path where settings and it's schemas are located.""" pass def __init__(self): - settings_root_dir = self.get_settings_root_dir() + settings_root_dir = self.get_settings_root_path() defaults_dir = os.path.join( settings_root_dir, "defaults" ) From dd4c4342e0a36dff7ef7366ebf13849a79074aee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Aug 2021 11:33:00 +0200 Subject: [PATCH 092/716] recalculate relativelly offset by value --- openpype/widgets/sliders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/widgets/sliders.py b/openpype/widgets/sliders.py index 2f26c3eb97..32ade58af5 100644 --- a/openpype/widgets/sliders.py +++ b/openpype/widgets/sliders.py @@ -70,18 +70,19 @@ class NiceSlider(QtWidgets.QSlider): ) _range = self.maximum() - self.minimum() + _offset = self.value() - self.minimum() if horizontal: _handle_half = rect.height() / 2 _handle_size = _handle_half * 2 width = rect.width() - _handle_size - pos_x = ((width / _range) * self.value()) + pos_x = ((width / _range) * _offset) pos_y = rect.center().y() - _handle_half + 1 else: _handle_half = rect.width() / 2 _handle_size = _handle_half * 2 height = rect.height() - _handle_size pos_x = rect.center().x() - _handle_half + 1 - pos_y = height - ((height / _range) * self.value()) + pos_y = height - ((height / _range) * _offset) handle_rect = QtCore.QRect( pos_x, pos_y, _handle_size, _handle_size From d6fc47d1c1ae6914b138f65878052dc4af874705 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Aug 2021 11:34:51 +0200 Subject: [PATCH 093/716] added option to have sliders in number widgets --- openpype/settings/entities/input_entities.py | 3 ++ .../tools/settings/settings/item_widgets.py | 41 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 336d1f5c1e..f7e85294a2 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -369,6 +369,9 @@ class NumberEntity(InputEntity): self.valid_value_types = valid_value_types self.value_on_not_set = value_on_not_set + # UI specific attributes + self.show_slider = self.schema_data.get("show_slider", False) + def _convert_to_valid_type(self, value): if isinstance(value, str): new_value = None diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index d29fa6f42b..6f304a1f88 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -21,6 +21,7 @@ from .base import ( BaseWidget, InputWidget ) +from openpype.widgets.sliders import NiceSlider from openpype.tools.settings import CHILD_OFFSET @@ -377,6 +378,8 @@ class TextWidget(InputWidget): class NumberWidget(InputWidget): + _slider_widget = None + def _add_inputs_to_layout(self): kwargs = { "minimum": self.entity.minimum, @@ -384,14 +387,33 @@ class NumberWidget(InputWidget): "decimal": self.entity.decimal } self.input_field = NumberSpinBox(self.content_widget, **kwargs) + input_field_stretch = 1 + + if self.entity.show_slider: + slider_widget = NiceSlider(QtCore.Qt.Horizontal, self) + slider_widget.setRange( + self.entity.minimum, + self.entity.maximum + ) + + self.content_layout.addWidget(slider_widget, 1) + + slider_widget.valueChanged.connect(self._on_slider_change) + + self._slider_widget = slider_widget + + input_field_stretch = 0 self.setFocusProxy(self.input_field) - self.content_layout.addWidget(self.input_field, 1) + self.content_layout.addWidget(self.input_field, input_field_stretch) self.input_field.valueChanged.connect(self._on_value_change) self.input_field.focused_in.connect(self._on_input_focus) + self._ignore_slider_change = False + self._ignore_input_change = False + def _on_input_focus(self): self.focused_in() @@ -402,10 +424,25 @@ class NumberWidget(InputWidget): def set_entity_value(self): self.input_field.setValue(self.entity.value) + def _on_slider_change(self, new_value): + if self._ignore_slider_change: + return + + self._ignore_input_change = True + self.input_field.setValue(new_value) + self._ignore_input_change = False + def _on_value_change(self): if self.ignore_input_changes: return - self.entity.set(self.input_field.value()) + + value = self.input_field.value() + if self._slider_widget is not None and not self._ignore_input_change: + self._ignore_slider_change = True + self._slider_widget.setValue(value) + self._ignore_slider_change = False + + self.entity.set(value) class RawJsonInput(SettingsPlainTextEdit): From 25478317d619909422a0f16a5b5e792b52ec24ab Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 31 Aug 2021 10:42:47 +0200 Subject: [PATCH 094/716] modified sizes of slider in style --- .../tools/settings/settings/style/style.css | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css index 250c15063f..d9d85a481e 100644 --- a/openpype/tools/settings/settings/style/style.css +++ b/openpype/tools/settings/settings/style/style.css @@ -114,6 +114,30 @@ QPushButton[btn-type="expand-toggle"] { background: #21252B; } +/* SLider */ +QSlider::groove { + border: 1px solid #464b54; + border-radius: 0.3em; +} +QSlider::groove:horizontal { + height: 8px; +} +QSlider::groove:vertical { + width: 8px; +} +QSlider::handle { + width: 10px; + height: 10px; + + border-radius: 5px; +} +QSlider::handle:horizontal { + margin: -2px 0; +} +QSlider::handle:vertical { + margin: 0 -2px; +} + #GroupWidget { border-bottom: 1px solid #21252B; } From ff4ed83519c89473925999b7d749b31405187365 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 31 Aug 2021 10:58:50 +0200 Subject: [PATCH 095/716] added slider multiplier as slider can't handle decimal places --- openpype/tools/settings/settings/item_widgets.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 6f304a1f88..1f74308211 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -389,11 +389,13 @@ class NumberWidget(InputWidget): self.input_field = NumberSpinBox(self.content_widget, **kwargs) input_field_stretch = 1 + self._slider_multiplier = 10 ** self.entity.decimal if self.entity.show_slider: + slider_widget = NiceSlider(QtCore.Qt.Horizontal, self) slider_widget.setRange( - self.entity.minimum, - self.entity.maximum + int(self.entity.minimum * self._slider_multiplier), + int(self.entity.maximum * self._slider_multiplier) ) self.content_layout.addWidget(slider_widget, 1) @@ -429,7 +431,7 @@ class NumberWidget(InputWidget): return self._ignore_input_change = True - self.input_field.setValue(new_value) + self.input_field.setValue(new_value / self._slider_multiplier) self._ignore_input_change = False def _on_value_change(self): From 00d1ae5d43500116f735522e5db79b8a92ca777b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 31 Aug 2021 10:58:59 +0200 Subject: [PATCH 096/716] added show_slider to examples --- .../entities/schemas/system_schema/example_schema.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/example_schema.json b/openpype/settings/entities/schemas/system_schema/example_schema.json index f633d5cb1a..af6a2d49f4 100644 --- a/openpype/settings/entities/schemas/system_schema/example_schema.json +++ b/openpype/settings/entities/schemas/system_schema/example_schema.json @@ -183,6 +183,15 @@ "minimum": -10, "maximum": -5 }, + { + "type": "number", + "key": "number_with_slider", + "label": "Number with slider", + "decimal": 2, + "minimum": 0.0, + "maximum": 1.0, + "show_slider": true + }, { "type": "text", "key": "singleline_text", From 8f5254ff234e7bd88298bcb230b1cd44252f1d7c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 31 Aug 2021 11:00:44 +0200 Subject: [PATCH 097/716] added show slider to readme --- openpype/settings/entities/schemas/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 2034d4e463..2709f5bed9 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -300,6 +300,7 @@ How output of the schema could look like on save: - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +- for UI it is possible to show slider to enable this option set `show_slider` to `true` ``` { "type": "number", @@ -311,6 +312,18 @@ How output of the schema could look like on save: } ``` +``` +{ + "type": "number", + "key": "ratio", + "label": "Ratio" + "decimal": 3, + "minimum": 0, + "maximum": 1, + "show_slider": true +} +``` + ### text - simple text input - key `"multiline"` allows to enter multiple lines of text (Default: `False`) From b8608bccaeaba9810473d6516124ea5d14b04cc6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 31 Aug 2021 11:10:49 +0200 Subject: [PATCH 098/716] added explaining comment to slider multiplier --- openpype/tools/settings/settings/item_widgets.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 1f74308211..a7b1208269 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -389,13 +389,15 @@ class NumberWidget(InputWidget): self.input_field = NumberSpinBox(self.content_widget, **kwargs) input_field_stretch = 1 - self._slider_multiplier = 10 ** self.entity.decimal + slider_multiplier = 1 if self.entity.show_slider: - + # Slider can't handle float numbers so all decimals are converted + # to integer range. + slider_multiplier = 10 ** self.entity.decimal slider_widget = NiceSlider(QtCore.Qt.Horizontal, self) slider_widget.setRange( - int(self.entity.minimum * self._slider_multiplier), - int(self.entity.maximum * self._slider_multiplier) + int(self.entity.minimum * slider_multiplier), + int(self.entity.maximum * slider_multiplier) ) self.content_layout.addWidget(slider_widget, 1) @@ -406,6 +408,8 @@ class NumberWidget(InputWidget): input_field_stretch = 0 + self._slider_multiplier = slider_multiplier + self.setFocusProxy(self.input_field) self.content_layout.addWidget(self.input_field, input_field_stretch) From d1e8032fcdfb5f1c27872e8c99e07084856e8e55 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 1 Sep 2021 09:06:04 +0200 Subject: [PATCH 099/716] 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 100/716] 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 101/716] 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 102/716] Reset submodule. --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 91867aeb4b..f48fce09c0 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 91867aeb4bfde115c0595c683282cc0b8265e694 +Subproject commit f48fce09c0986c1fd7f6731de33907be46b436c5 From 537b9e7bab559419c37460c1c67d709bb482169a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Sep 2021 08:31:18 +0100 Subject: [PATCH 103/716] Stop timer was within validator order range. --- openpype/plugins/publish/stop_timer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/stop_timer.py b/openpype/plugins/publish/stop_timer.py index 81afd16378..5c939b5f1b 100644 --- a/openpype/plugins/publish/stop_timer.py +++ b/openpype/plugins/publish/stop_timer.py @@ -8,7 +8,7 @@ from openpype.api import get_system_settings class StopTimer(pyblish.api.ContextPlugin): label = "Stop Timer" - order = pyblish.api.ExtractorOrder - 0.5 + order = pyblish.api.ExtractorOrder - 0.49 hosts = ["*"] def process(self, context): From 9887e00859850cf0044452b0413db8406624f051 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 1 Sep 2021 09:36:20 +0200 Subject: [PATCH 104/716] 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 105/716] 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 106/716] 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 107/716] 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 108/716] 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 109/716] 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 110/716] 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 4ec7e18aad0044164fd56688dc70183205401866 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:25:19 +0200 Subject: [PATCH 111/716] pass workfile template to `_prepare_last_workfile` --- openpype/lib/applications.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index fbf991a32e..45b8e6468d 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -29,7 +29,7 @@ from .local_settings import get_openpype_username from .avalon_context import ( get_workdir_data, get_workdir_with_workdir_data, - get_workfile_template_key_from_context + get_workfile_template_key ) from .python_module_tools import ( @@ -1226,8 +1226,12 @@ def prepare_context_environments(data): # Load project specific environments project_name = project_doc["name"] + project_settings = get_project_settings(project_name) + data["project_settings"] = project_settings # Apply project specific environments on current env value - apply_project_environments_value(project_name, data["env"]) + apply_project_environments_value( + project_name, data["env"], project_settings + ) app = data["app"] workdir_data = get_workdir_data( @@ -1237,17 +1241,19 @@ def prepare_context_environments(data): anatomy = data["anatomy"] - template_key = get_workfile_template_key_from_context( - asset_doc["name"], - task_name, + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + workfile_template_key = get_workfile_template_key( + task_type, app.host_name, project_name=project_name, - dbcon=data["dbcon"] + project_settings=project_settings ) try: workdir = get_workdir_with_workdir_data( - workdir_data, anatomy, template_key=template_key + workdir_data, anatomy, template_key=workfile_template_key ) except Exception as exc: @@ -1281,10 +1287,10 @@ def prepare_context_environments(data): ) data["env"].update(context_env) - _prepare_last_workfile(data, workdir) + _prepare_last_workfile(data, workdir, workfile_template_key) -def _prepare_last_workfile(data, workdir): +def _prepare_last_workfile(data, workdir, workfile_template_key): """last workfile workflow preparation. Function check if should care about last workfile workflow and tries @@ -1345,7 +1351,7 @@ def _prepare_last_workfile(data, workdir): if extensions: anatomy = data["anatomy"] # Find last workfile - file_template = anatomy.templates["work"]["file"] + file_template = anatomy.templates[workfile_template_key]["file"] workdir_data.update({ "version": 1, "user": get_openpype_username(), From 23f17609db0549f1b8ed56f10e530b1330adb109 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:25:33 +0200 Subject: [PATCH 112/716] removed todo which is already done --- openpype/tools/workfiles/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index b542e6e718..3d2633f8dc 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -430,7 +430,6 @@ class FilesWidget(QtWidgets.QWidget): # Pype's anatomy object for current project self.anatomy = Anatomy(io.Session["AVALON_PROJECT"]) # Template key used to get work template from anatomy templates - # TODO change template key based on task self.template_key = "work" # This is not root but workfile directory From e43f7bc007a74f033f39ddc4dee628e183518218 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:34:18 +0200 Subject: [PATCH 113/716] removed duplicated line --- .../standalonepublisher/plugins/publish/extract_harmony_zip.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py index f7f96c7d03..e3e5e94d30 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py @@ -244,7 +244,6 @@ class ExtractHarmonyZip(openpype.api.Extractor): os.path.dirname(work_path), file_template, data, [".zip"] )[1] - work_path = anatomy_filled["work"]["path"] base_name = os.path.splitext(os.path.basename(work_path))[0] staging_work_path = os.path.join(os.path.dirname(staging_scene), From 2f9f1ad00c72c52a17bbdd01d46672c03e334a64 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:34:42 +0200 Subject: [PATCH 114/716] use `HOST_WORKFILE_EXTENSIONS` to get workfile extensions --- .../plugins/publish/extract_harmony_zip.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py index e3e5e94d30..e422837441 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py @@ -233,6 +233,7 @@ class ExtractHarmonyZip(openpype.api.Extractor): "version": 1, "ext": "zip", } + host_name = "harmony" # Get a valid work filename first with version 1 file_template = anatomy.templates["work"]["file"] @@ -241,7 +242,10 @@ class ExtractHarmonyZip(openpype.api.Extractor): # Get the final work filename with the proper version data["version"] = api.last_workfile_with_version( - os.path.dirname(work_path), file_template, data, [".zip"] + os.path.dirname(work_path), + file_template, + data, + api.HOST_WORKFILE_EXTENSIONS[host_name] )[1] base_name = os.path.splitext(os.path.basename(work_path))[0] From 79819a21a01f17d907d116789932976b7d9f9321 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:34:54 +0200 Subject: [PATCH 115/716] use get_workfile_template_key_from_context to get right work template name --- .../plugins/publish/extract_harmony_zip.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py index e422837441..85da01c890 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py @@ -11,6 +11,7 @@ import zipfile import pyblish.api from avalon import api, io import openpype.api +from openpype.lib import get_workfile_template_key_from_context class ExtractHarmonyZip(openpype.api.Extractor): @@ -234,11 +235,18 @@ class ExtractHarmonyZip(openpype.api.Extractor): "ext": "zip", } host_name = "harmony" + template_name = get_workfile_template_key_from_context( + instance.data["asset"], + instance.data.get("task"), + host_name, + project_name=project_entity["name"], + dbcon=io + ) # Get a valid work filename first with version 1 - file_template = anatomy.templates["work"]["file"] + file_template = anatomy.templates[template_name]["file"] anatomy_filled = anatomy.format(data) - work_path = anatomy_filled["work"]["path"] + work_path = anatomy_filled[template_name]["path"] # Get the final work filename with the proper version data["version"] = api.last_workfile_with_version( From 691fcc4f31cd64647cdcd57394c249adad33c084 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 2 Sep 2021 13:49:59 +0200 Subject: [PATCH 116/716] More pictures, more tidy, hopefully more readable. --- .../assets/nuke_tut/nuke_AssetLoadOutOfDate.png | Bin 0 -> 38359 bytes .../docs/assets/nuke_tut/nuke_ManageVersion.png | Bin 0 -> 71506 bytes .../assets/nuke_tut/nuke_PyblishCheckBox.png | Bin 0 -> 7732 bytes .../nuke_PyblishDialogNukeNoteIntent.png | Bin 0 -> 8626 bytes .../nuke_tut/nuke_RunNukeFtrackAction_p3.png | Bin 0 -> 26720 bytes .../assets/nuke_tut/nuke_ValidateContainers.png | Bin 0 -> 76046 bytes .../assets/nuke_tut/nuke_WriteNodeCreated.png | Bin 0 -> 34442 bytes .../assets/nuke_tut/nuke_WriteNodeReview.png | Bin 0 -> 12896 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/assets/nuke_tut/nuke_AssetLoadOutOfDate.png create mode 100644 website/docs/assets/nuke_tut/nuke_ManageVersion.png create mode 100644 website/docs/assets/nuke_tut/nuke_PyblishCheckBox.png create mode 100644 website/docs/assets/nuke_tut/nuke_PyblishDialogNukeNoteIntent.png create mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p3.png create mode 100644 website/docs/assets/nuke_tut/nuke_ValidateContainers.png create mode 100644 website/docs/assets/nuke_tut/nuke_WriteNodeCreated.png create mode 100644 website/docs/assets/nuke_tut/nuke_WriteNodeReview.png diff --git a/website/docs/assets/nuke_tut/nuke_AssetLoadOutOfDate.png b/website/docs/assets/nuke_tut/nuke_AssetLoadOutOfDate.png new file mode 100644 index 0000000000000000000000000000000000000000..f7f807a94f7824fc2c08374089ca3f24890ffc17 GIT binary patch literal 38359 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Π*QVsYGnG?hdX6a@ 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

    9oN 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 GJ?>wbX@OS& literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_ManageVersion.png b/website/docs/assets/nuke_tut/nuke_ManageVersion.png new file mode 100644 index 0000000000000000000000000000000000000000..c9f2091347e2a76053736f0fead1151c6145a681 GIT binary patch literal 71506 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%0A{1<2_ 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_ValidateContainers.png b/website/docs/assets/nuke_tut/nuke_ValidateContainers.png new file mode 100644 index 0000000000000000000000000000000000000000..78e0f2edd7d33e1926f50340d61c99b30d8878c9 GIT binary patch literal 76046 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 H-J|~jBH&;k literal 0 HcmV?d00001 diff --git a/website/docs/assets/nuke_tut/nuke_WriteNodeCreated.png b/website/docs/assets/nuke_tut/nuke_WriteNodeCreated.png new file mode 100644 index 0000000000000000000000000000000000000000..b283593d6a461d7c6ae71a66eff19f2f844e211b GIT binary patch literal 34442 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 MN>TEgxKY6W0%TQr-v9sr literal 0 HcmV?d00001 From edf460d73909667c15c81eec5ac429f37740f6a7 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 2 Sep 2021 13:51:44 +0200 Subject: [PATCH 117/716] more images --- website/docs/artist_hosts_nuke_tut.md | 150 +++++++++++------- .../docs/assets/nuke_tut/nuke_AssetLoader.png | Bin 49946 -> 67174 bytes website/docs/assets/nuke_tut/nuke_Creator.png | Bin 15493 -> 30511 bytes .../nuke_tut/nuke_PyblishDialogNuke.png | Bin 52744 -> 66349 bytes .../nuke_tut/nuke_RunNukeFtrackAction_p2.png | Bin 87912 -> 0 bytes 5 files changed, 89 insertions(+), 61 deletions(-) delete mode 100644 website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png diff --git a/website/docs/artist_hosts_nuke_tut.md b/website/docs/artist_hosts_nuke_tut.md index 5743c8c756..d285d8a6ff 100644 --- a/website/docs/artist_hosts_nuke_tut.md +++ b/website/docs/artist_hosts_nuke_tut.md @@ -4,120 +4,134 @@ title: Nuke QuickStart sidebar_label: Nuke QuickStart --- -# Nuke QuickStart -- [Nuke QuickStart](#nuke-quickstart) - - [Run Nuke - Shot and Task Context](#run-nuke---shot-and-task-context) - - [Save Nuke script – the Work File](#save-nuke-script--the-work-file) - - [Load plate – Asset Loader](#load-plate--asset-loader) - - [Create Write Node – Instance Creator](#create-write-node--instance-creator) - - [What Nuke Publish Does](#what-nuke-publish-does) - - [Publish steps](#publish-steps) - - [Pyblish Note and Intent](#pyblish-note-and-intent) - - [Pyblish Checkbox](#pyblish-checkbox) - - [Pyblish Dialog](#pyblish-dialog) - - [Review](#review) - - [Render and Publish](#render-and-publish) - - [Version-less Render](#version-less-render) - - [Fixing Validate Containers](#fixing-validate-containers) - - [Fixing Validate Version](#fixing-validate-version) - This QuickStart is just a small introduction to what OpenPype can do for you. It attempts to make an overview for compositing artists, and simplifies processes that are better described in specific parts of the documentation. -## Run Nuke - Shot and Task Context +## Launch Nuke - Shot and Task Context OpenPype has to know what shot and task you are working on. You need to run Nuke in context of the task, using Ftrack Action or OpenPype Launcher to select the task and run Nuke. -![Run Nuke From Ftrack](assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png) +![Run Nuke From Ftrack](assets/nuke_tut/nuke_RunNukeFtrackAction_p3.png) ![Run Nuke From Launcher](assets/nuke_tut/nuke_RunNukeLauncher_p2.png) +:::tip Admin Tip - Nuke version +You can [configure](admin_settings_project_anatomy.md#Attributes) which DCC version(s) will be available for current project in **Studio Settings β†’ Project β†’ Anatomy β†’ Attributes β†’ Applications** +::: -OpenPype menu shows the current context - shot and task. - -If you use Ftrack, executing Nuke with context stops your timer, and starts the Ftrack clock on the shot and task you picked. +## Nuke OpenPype menu shows the current context ![Context](assets/nuke_tut/nuke_Context.png) -:::tip Admin Tip - Nuke version -You can [configure](admin_settings_project_anatomy.md#Attributes) which version(s) will be available for current project in **Studio Settings β†’ Project β†’ Anatomy β†’ Attributes β†’ Applications** -::: +Launching Nuke with context stops your timer, and starts the clock on the shot and task you picked. + +## Nuke Initial setup +Openpype makes initial setup for your Nuke script. It is the same as running [Apply All Settings](artist_hosts_nuke.md#apply-all-settings) from the OpenPype menu. + +Reads frame range and resolution from Avalon database, sets it in Nuke Project Settings, +Creates Viewer node, sets it’s range and indicates handles by In and Out points. + +Reads Color settings from the project configuration, and sets it in Nuke Project Settings and Viewer. + +Sets project directory in the Nuke Project Settings to the Nuke Script Directory + ## Save Nuke script – the Work File Use OpenPype - Work files menu to create a new Nuke script. Openpype offers you the preconfigured naming. -![Context](assets/nuke_tut/nuke_AssetLoader.png) +![Context](assets/nuke_tut/nuke_WorkFileSaveAs.png) -Openpype makes initial setup for your Nuke script. It is the same as running [Apply All Settings](artist_hosts_nuke.md#apply-all-settings) from the OpenPype menu. -Reads frame range and resolution from Avalon database, sets it in Nuke Project Settings -Creates Viewer node, sets it’s range and indicates handles by In and Out points -Reads Color settings from the project configuration, and sets it in Nuke Project Settings and Viewer -Sets project directory in the Nuke Project Settings to the Nuke Script Directory +The Next Available Version checks the work folder for already used versions and offers the lowest unused version number automatically. -:::tip Admin Tip - Workfile Naming -The [workfile naming](admin_settings_project_anatomy#templates) is configured in anatomy, see **Studio Settings β†’ Project β†’ Anatomy β†’ Templates β†’ Work** -::: +Subversion can be used to distinguish or name versions. For example used to add shortened artist name. -:::tip Admin Tip - Open Workfile -You can [configure](project_settings/settings_project_nuke#create-first-workfile) Nuke to automatically open the last version, or create a file on startup. See **Studio Settings β†’ Project β†’ Global β†’ Tools β†’ Workfiles** -::: +More about [workfiles](artist_tools#workfiles). -:::tip Admin Tip - Nuke Color Settings -[Color setting](project_settings/settings_project_nuke) for Nuke can be found in **Studio Settings β†’ Project β†’ Anatomy β†’ Color Management and Output Formats β†’ Nuke** + +:::tip Admin Tips +- **Workfile Naming** + + - The [workfile naming](admin_settings_project_anatomy#templates) is configured in anatomy, see **Studio Settings β†’ Project β†’ Anatomy β†’ Templates β†’ Work** + +- **Open Workfile** + + - You can [configure](project_settings/settings_project_nuke#create-first-workfile) Nuke to automatically open the last version, or create a file on startup. See **Studio Settings β†’ Project β†’ Global β†’ Tools β†’ Workfiles** + +- **Nuke Color Settings** + + - [Color setting](project_settings/settings_project_nuke) for Nuke can be found in **Studio Settings β†’ Project β†’ Anatomy β†’ Color Management and Output Formats β†’ Nuke** ::: ## Load plate – Asset Loader -If your IO or editorial prepared plates and references, or your CG team has a render to be composited, we need to load it. +Use Load from OpenPype menu to load any plates or renders available. -![OpenPype Load](assets/nuke_tut/nuke_Load.png) ![Asset Load](assets/nuke_tut/nuke_AssetLoader.png) -Pick the plate asset, right click and choose Load Image Sequence to create a Read node in Nuke. Note that the Read node created by OpenPype is green and has an OpenPype Tab. Green color indicates the highest version available. +Pick the plate asset, right click and choose Load Image Sequence to create a Read node in Nuke. + +Note that the Read node created by OpenPype is green. Green color indicates the highest version of asset is loaded. Asset versions could be easily changed by [Manage](#managing-versions). Lower versions will be highlighted by orange color on the read node. + +![Asset Load](assets/nuke_tut/nuke_AssetLoadOutOfDate.png) + +More about [Asset loader](artist_tools#loader). ## Create Write Node – Instance Creator To create OpenPype managed Write node, select the Read node you just created, from OpenPype menu, pick Create. In the Instance Creator, pick Create Write Render, and Create. -![OpenPype Create](assets/nuke_tut/nuke_Create.png) ![OpenPype Create](assets/nuke_tut/nuke_Creator.png) This will create a Group with a Write node inside. +![OpenPype Create](assets/nuke_tut/nuke_WriteNodeCreated.png) + :::tip Admin Tip - Configuring write node You can configure write node parameters in **Studio Settings β†’ Project β†’ Anatomy β†’ Color Management and Output Formats β†’ Nuke β†’ Nodes** ::: ## What Nuke Publish Does From Artist perspective, Nuke publish gathers all the stuff found in the Nuke script with Publish checkbox set to on, exports stuff and raises the Nuke script (workfile) version. + The Pyblish dialog shows the progress of the process. -![OpenPype Publish](assets/nuke_tut/nuke_Publish.png) +The left column of the dialog shows what will be published. Typically it is one or more renders or prerenders, plus work file. ![OpenPype Publish](assets/nuke_tut/nuke_PyblishDialogNuke.png) +The right column shows the publish steps + #### Publish steps -- gathers all the stuff found in the Nuke script with Publish checkbox set to on (1) -- collects all the info (from the script, database…) (2) -- validates components to be published (checks render range and resolution...) (3) -- extracts data from the script (generates thumbnail, creates reviews like h264 ...) (4) -- copies and renames components (render, reviews, thumbnail, Nuke script...) to publish folder -- integrates components (writes to database, sends preview of the render to Ftrack ...) (5) -- increments Nuke script version, cleans up the render directory (6) +1. Gathers all the stuff found in the Nuke script with Publish checkbox set to on +2. Collects all the info (from the script, database…) +3. Validates components to be published (checks render range and resolution...) +4. Extracts data from the script + - generates thumbnail + - creates review(s) like h264 + - adds burnins to review(s) + - Copies and renames components like render(s), review(s), Nuke script... to publish folder +5. Integrates components (writes to database, sends preview of the render to Ftrack ... +6. Increments Nuke script version, cleans up the render directory + +Gathering all the info and validating usually takes just a few seconds. Creating reviews for long, high resolution shots can however take significant amount of time when publishing locally. #### Pyblish Note and Intent -Artist can add Note and Intent before firing the publish (7) button. The Note and Intent is ment for easy communication between artist and supervisor. After publish, Note and Intent can be seen in Ftrack notes. +![Note and Intent](assets/nuke_tut/nuke_PyblishDialogNukeNoteIntent.png) + +Artist can add Note and Intent before firing the publish button. The Note and Intent is ment for easy communication between artist and supervisor. After publish, Note and Intent can be seen in Ftrack notes. #### Pyblish Checkbox -Some may say Pyblish Dialog looks unnecessarily complex; it just tries to put a lot of info in a small area. One of the more tricky parts is that it uses non-standard checkboxes. Some squares can be turned on and off by the artist, some are mandatory. -#### Pyblish Dialog -The left column of the dialog shows what will be published. If you run the publish and decide to not publish the Nuke script, you can turn it off right in the Pyblish dialog by clicking on the checkbox. If you decide to render and publish the shot in lower resolution to speed up the turnaround, you have to turn off the Write Resolution validator. If you want to use an older version of the asset (older version of the plate...), you have to turn off the Validate containers, and so on. +![Note and Intent](assets/nuke_tut/nuke_PyblishCheckBox.png) -Time wise, gathering all the info and validating usually takes just a few seconds, but creating reviews for long, high resolution shots can take too much time on the artist machine. +Pyblish Dialog tries to pack a lot of info in a small area. One of the more tricky parts is that it uses non-standard checkboxes. Some squares can be turned on and off by the artist, some are mandatory. -More info about [Using Pyblish](artist_tools#using-pyblish) +If you run the publish and decide to not publish the Nuke script, you can turn it off right in the Pyblish dialog by clicking on the checkbox. If you decide to render and publish the shot in lower resolution to speed up the turnaround, you have to turn off the Write Resolution validator. If you want to use an older version of the asset (older version of the plate...), you have to turn off the Validate containers, and so on. + +More info about [Using Pyblish](artist_tools#publisher) :::tip Admin Tip - Configuring validators You can configure Nuke validators like Output Resolution in **Studio Settings β†’ Project β†’ Nuke β†’ Publish plugins** ::: ## Review +![Write Node Review](assets/nuke_tut/nuke_WriteNodeReview.png) + When you turn the review checkbox on in your OpenPype write node, here is what happens: - OpenPype uses the current Nuke script to - Load the render @@ -139,26 +153,40 @@ Reviews can be configured separately for each host, task, or family. For example ::: ## Render and Publish + +![OpenPype Create](assets/nuke_tut/nuke_WriteNode.png) + Let’s say you want to render and publish the shot right now, with only a Read and Write node. You need to decide if you want to render, check the render and then publish it, or you want to execute the render and publish in one go. If you wish to check your render before publishing, you can use your local machine or your farm to render the write node as you would do without OpenPype, load and check your render (OpenPype Write has a convenience button for that), and if happy, use publish with Use existing frames option selected in the write node to generate the review on your local machine. If you want to render and publish on the farm in one go, run publish with On farm option selected in the write node to render and make the review on farm. -![OpenPype Create](assets/nuke_tut/nuke_WriteNode.png) - ## Version-less Render + +![Versionless](assets/nuke_tut/nuke_versionless.png) + OpenPype is configured so your render file names have no version number. The main advantage is that you can keep the render from the previous version and re-render only part of the shot. With care, this is handy. Main disadvantage of this approach is that you can render only one version of your shot at one time. Otherwise you risk to partially overwrite your shot render before publishing copies and renames the rendered files to the properly versioned publish folder. When making quick farm publishes, like making two versions with different color correction, care must be taken to let the first job (first version) completely finish before the second version starts rendering. -![Versionless](assets/nuke_tut/nuke_versionless.png) +## Managing Versions + +![Versionless](assets/nuke_tut/nuke_ManageVersion.png) + +OpenPype checks all the assets loaded to Nuke on script open. All out of date assets are colored orange, up to date assets are colored green. + +Use Manage to switch versions for loaded assets. + ## Fixing Validate Containers -If your Pyblish dialog fails on Validate Containers, you might have an old asset loaded. Use OpenPype - Manage... to switch the asset(s) to the latest greatest version. + +![Versionless](assets/nuke_tut/nuke_ValidateContainers.png) + +If your Pyblish dialog fails on Validate Containers, you might have an old asset loaded. Use OpenPype - Manage... to switch the asset(s) to the latest version. ## Fixing Validate Version If your Pyblish dialog fails on Validate Version, you might be trying to publish already published version. Rise your version in the OpenPype WorkFiles SaveAs. diff --git a/website/docs/assets/nuke_tut/nuke_AssetLoader.png b/website/docs/assets/nuke_tut/nuke_AssetLoader.png index cf7f40d5db39f7cd6e1b5d4943056463ad77cf49..e52abdc42835b3d680e6915f85fd8536d01195e7 100644 GIT binary patch literal 67174 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 z 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

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

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

  • 8jh8c%*RhB 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 Q6b&HpuQI}Ag1UbH3#et^U;qFB diff --git a/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png b/website/docs/assets/nuke_tut/nuke_RunNukeFtrackAction_p2.png deleted file mode 100644 index 47c8b61824e0428424c6b6fb4f8ab97fd74899db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87912 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�`ul7l497JN6CRceF>W#qXx`GjE-u zVdDJ%5qn@0`4hZaqpR&jkJR+d!9kbbreVOz>z6V;m+DkV`t;4)x&qbo&^&9Ykl|ce z5p8ZZc9d@+34g(MI7W~fq0z=CNui&AB!2VoJk4fyA?GnaPm^|!xgQMV7mBZ(*uFX! z=9O5eLm^U-rvoL_@b?lPg879dg4DG;yNnbq{~=ru`tP*Fo?TD}`WU@bHAzp5?u@fg z@DE>nXMLtb>QSCQXhl!V{l&JcBw^CT!3F{?e9%Tt$LEHmU>)oytG(P_u#Ak@&Tyci z{(x@4QJ6U{e&Y=ms|W6N67`TKEDW4 z22Jcy;=(37#P#~@?xJ#x?Xyx~oC#ElB*c|K)*Klu2Q@1JxiT^DnmJqFJcr;d)FTLI zGsF`n>Y7wfh}Yyio~=De`>|D>+GMce_9R{&R~%2x-*|ZIg!;8b@ZptU#@zGK$r33Z zVELIJ*$>q{j^%lCy+tsJ_O{Yv~% z7n`2}L1huqsZBxz0ReM^8FHX!$wnX!4lgS&f4{qIwUtFGp$z_20#P9!N;I#Ys$@mIt z=RuS88Q-4@Uz*%tonz<;+!14SWJLu=@8%h(V0#=wXRHAB~t^0hY5_Xmn6vh)*_;!kjiuu(Uy+pMB~e7omMs$$ubVzS{z0QLH$E+avV5G`CD zXD|-td0FOL$glolBzlS9Wy%AlG@cKXs1vR=B@i%_@k5l!Uhf9A6jLrT6GIp-)N07& zt)NvykMYUA__5!9X)#Z_h(1mx3Vz>=R1lAU24lvcg4H;`apB>{v@cpFCY>f?uBRg>?*TQzDIb;YUxf!d>lhS-MRYq?bQ)If}C$qZ;Tj6m@fv|yK&q~;H z@c@NxYc|>hJm5TnC)sdaVSgzo(;Cz8Z4m4i8!iasq=Y3jao%|08w^e9qGKOo*(8>M zOmSDg(z_D$^0cK)plt2K85cqbra^Z?jFEW>e;uQ36yhu+h=tdRw;i!Qlp1(B_3hoK z%7|-2LL#>}{i>`fyO=1cAsG-xZ7b6cOg58Cf}q^#94v0sE^^Ci1+90M-U*e_*BrsP z?|BHKWF{5YkBvC3q=V1sg7nLFLm6>VB!lJ0k`SO`Aa=oKGIYgoVx4){4TkGYYH^ad zVvRbStt$y|S!#XOZvsL}z3-Y??xRp{(eqX0AjGi45{;gB3t27_Ke1is$G=pmno~k# zNd`}|AWRE?!gYm9=P+VJ$0VA6wJnwtVw548NoQX6bu0={IcaDJ<_PWG?>1-E9u9nZ zkWL4Qxhzk<0->)LIST0Nc}rJT;7_mFf7OLDHnC% z2E< z_mM{Zidz&44o{aeHeYpRi(_nI`28bV282wEBUWiS?#$EX<9rnv&;Gld$E8dnv)t*1 z+}fU}6ekf%efBuLhRj&D)_jRiT!Y=|2Zl zA9}~|KpJ6X^XE`*JSho~VeaCW@1*Foyq~Aupsyl!5xX8kca&rQ^AJ=kjP( zG+)HavHwM4GFe2CN+kSSoyd$Fj?A zNVP?w!F_V0ui{gVfx;YYFQlj6lwB7)_)x_hCLu+coJdTVwbbYBP~gxT>4Lx=++<_x zSq#rzsBqF=Zpyb7PB8Y?pI4A2Tp`!ExBAV21ic`;yG(n$4|-3PEH>*A2pP4Uy0a@% z08t~PNQ0z|xK73k{UeTzV%Y;rztW(aXXO7qD^~w#ocuTn?5Tw3+1T4x9%ZjQFQSr{ zG!QBZ66P{x;aj{Jo&3XH;I|40P2P}Ewl9l|3w%a5Yb2U&lf^z z1dN0k@Ag4VVnVyW46Y;gbBKijpvJ}--KGj#>+)A&k{85L6(iA#rQtL!ifM}Q_0eCA zcai>k8}sR|B=rjvl0Le9M2P4rSzkuS7kwT>+5PSLSqW#_*l-b%gWkqO(AbvO=}LJ> z$9rch01^>n9$r>il7A3A^7LY9*K>u$H2#ev0w;TUU9}hz)+Cz{Ql`x>d58= zG*ORn>m1OJW`7;n_~X^C#i%TWzHxO?VV0`>T;(g*l=#-anjl5JcjLpF2Yt)w#N@)N zN4|iYE!_NU^@j9s7DC9{F3#tlS66)O&1KegWy7&ty)|wtP=<2?HMKLZcIDp)L6ay) z6G8(^EtX8l_|l}l$boEU%RW5o6AlMkw=@$a|~{sU|N$_p=Sw#fCm za}Vi36Rj4;F-N>*bRI!g&S=hZm8xkf%D}}>&u(OG+bY-dNsS}6$ zQ#=f2p7nvwHR~cgqK+X^0;DxmVhb&Wl!7?N-k9FuN}%@$?q>ORP&H5SiHmo+nad?2 z1Ow5dQ{>(O$+|H500M1BnJ04;g~=v@LH|diI6AO#V&1tc6~+=8dj{dYt5OTcr5j1K z=UHl!kCkFdW*BOyNX#ir3Mi2ShXJZVJi8H#lS~QSR*K!M3tMtx1O_>}B&Tu3DP-gj zS*FS|OiS(sJ@$|2rI?=q(;WUbL}CxA!7FfUT_pnp@V~N+n8d0<8RY6%ZPN{g3I`;a zw)JH3Yb$J@oREY5#$?NpSgP-RNoK^C610pg8LS(dpEOMdN8#4_EjYY$hyV>M{RMK9 zV6f1x;ujp-2cLJvMMwcJn{gtnD*JAN_)!I8f`x_8`^uOq7|4&pO%8y>IAh1k#%{U8 zAC2yKp^+zbWCCJ=Cts=otkQtQElth;j#nsha?Wna+8q~KYqvI)_V)+ksEp~%D^Fce z4gJsJq7*t4VN7B9<<<4^a&2XxQ%L~}gWSHA#hzonFX;x|xZQ*p&k-l)# zoH`5#CoU=YvE%>F0u0u<*&@1(3smVYP0wrTs#TWR`0ML?YR(n1Jk^1Fe!eOt9I8Ln z1|6<>I{u*J6cM45Q?=GYmKb_R8}{#b-_G8ls=QJT{4K(mIl9jHX|bqZrfVWop>Vcp zud=#2Gb5u@4i*y$=zRbaj&&z=5U~GxFE5hes277aDKYWq(Z1E3`$|Iv{#0tfiF4ca z;p*Ovx4#&${ew2L@Z;m%0M!r)W_`UYU?cGOJG5!6$kHO^7@Zg<#IUvVU2M0#t$b?F z>DNZoan)C&4Fb!0Kq3QcZ zf52sks@YbIykmPTtjIc3pz3>OrY52HRKykBkDUUukSBez)n;!`EgD@a1tWw89bHN- zdF;&`3>HpT?+O@O*t4-+;kXZ}w`#Hah~~K40!pX@5%wFl5swFF9;Lr7bn}#-vzLIq zVy)&zP9?x#ioM~<>A!#UuPHed%C)AYlO=n3Wn}tWTi>&e03JxMmjl;61wJ<{?Jd{a zelD_EcPnYD-wdbcxbg5T&B%}i@8)4oLMp@D19@6V&aS?8x>*c$}jD5dcEkncRfR)AIp4_}D z!_G;=NjzPCM^(bRh0uwcYc~R^ot=E8;$A=*v~gDEqykutfw{dvZc>8Aean<)Y zBWjd;oWH_QfQw68SBUc)djFza}DzojLO z?9kB_0N*0FRxtTZZm<8}{9@~B36gliq;erH0*_Vm5PO8z&9ak|t@%SO3tqfxs`g?N zgen88uPgh5?enHdu(-#+b~At{pegMkkonEpnkGQuo1Nvb%XEN2aDf6NpA6(L9GAL z&W{a7)XjX;05h1%$8=XQi=g_-hhGl4AAvgxSba$n zcbKm|vTIVDDlRHtt+tS6&a&nE3-DPSU7WJ`U0SO+h_KW()WGEPK5N4Y+mngUv6b&X z2WhD$<-uCpiGb#Yj|J7$zgkLKSdwI>XVu1}jhbAJN-Cf89Q7C z{vLsCd$vsbV0;=qnN{TZUzZijNcF`9c^xU}VYf6y{sBUbB@f~018_3v+bbrsqR_2Jmek z^<>vzJg(oGfYs;T)WoIk)Sy#bTw7Q3+Q|zrqd^9MS#E4ha~0y}6`7k|6c7-oHG@ap z{lnfmbFD0lj#m21#Y?pr8ec%8wOHfDlm~|54QyWo>o3=us7gyO1rzLH_4RSKU*Vzk z-lb((tE)>(NzdI}<$c;q?LQpgG&6;pR$yT{IXOz0noaFRtij_=b`mwCM zY9#ISFE9U_zn$^R+3Sy37a~IgkZ+*&Bt_DK{DN$c=hL0%64fcrf{)p6-jI&%T+luY zvsn00fe?TjR?uazR9ar!%Y`vjB}6{v$>sjs*^{K1p_p0-Y)?ucfwj3C+Zwj4{8*(cs{qsAPK;F(Q3fv$d5z zM`Buf^u3pGaDL0#?Acjp=x9{b{(dp=TYvMkpFFs_zI0UlsAT5Qpj;UD-g!Gu6L;V@ zRFuBOVyt|isHh{yAi57PyhpmPm$y{^z^*C2XnOt@yg4$fj|(c!O-t&rZ2C(aXS_Ev zr&m#4OdW!U7f;|P>gDCdjC14O0t=BbzFrOtw<=>ISGxv*&`T0TR zqfx&qT7)?S8m+A;K+|fP{9H#3;=%A;I4MwA_`SG~j;?+^u_H`})#WgfH#kUo^Nf!= zhgXw2*BI;Ku9=%#9T;?>l2B~Rhoe-#T)w#fT{bSN<;;{PofBhgWa}M->fOgo9MDp& zwPD7~`2u{|+C|mqL-i41RI}xhGuLZ}1|1aPYLVAUzbC)-x=H>qyeWTE(dO2yOb;0i zgdzseu+&C2(<;lN{RgF?%}ao8-rUWW9pQnD@bG{_!h=tF9*Hu*) z8MLS1?(E&V?JW*FUab}c5{X_a z*Zq7YFX(4kO;4>=M_?{}4LA41=VADT-W#HftW-di1Q}4qS<(q|@ML{7ld~28K@mTt zDzqxc(C%#yOuhYOMBo<&?4DSP4o$jOPBl+R0DGqAx-7kdcf$KKrN0S4#ET6bC>s3Q zv#UsY4-5(lGbGCYmIfp^?>vRoh!2nV9nHT8XS3}~@g)DJXqT%>TUWELyu7f2P1APQ zPa#ysH&I6cG1YL^`jv73IjDWjyBGni{w*WCjxaPi+9`H3Q?nJ;xy#F8V1UFoq9A~C zm^%8KbHP$tx@l-{7>-WsUhld$c-w8vlU!|;4yf^loAMSw4;)eu{lr~qaS#-<-yhbh zm=k=@Pn?>WNHfs7w_2MJQ+Z}5IuAUgn$psgP?@~Re3Q|UJ{nm@{HqH>HtV&AvR>-~00T6~;emAB|@EuNd(djL-+s&-bn&|X+zj7YwKGomu< z_@lZC-heS(`V`P?J}?z(Tx_gHoDHyc!hx3POZOID&SnTDxXt!#GN()Wz5=VTxXSEi z^!{F8H1I!UMc_Sm4_~z87O*U*IB~M)mVBJr56<}B zMCq_*E96N@^#Kf&>1nlAWl@QOsZ=>S`0g(dG_n_<=NQXw`i~7uP&+vM=z4o5fs12G zU0p|)HMix=N0yckI1>s=N&p=M&=$qH+vq(D4g6Hy{r~8xXmC@*`|vhw``IJHlUbu9 zBjs^N2+x5pU#bO621rz8`^w@0@QKPv8_Vlzx%zNrlcyM%&Ql%WfGSp#M23oYz2$w& zNWF`WjG#<_)wEo0n3FQH6C^EVbQVxtD5)FWo4a7inmi?Re zV~2)`%CqZP4Xq?V50+nIC-|qW>VKtbKe8W@{^f9?620_Bi3ZrYgp;$QdxE5-fLR^m zrx4m{u934)S;<)W)dyhl19hvNIYmH`XmYu-97=Fd=12H#_wT+f>u=W2@N5yQw0vNe z)80N=c1i-R+xPEb%3p{}|4|S%d*)GC?~O=+V1->dm>3v#R9xzBh)IniviY3CK7FCz zA26;`rjJrmP*6}*R|7wKzW`#gCp@_U3M%|oY(NCu?t}aL$y5c_Y!oQa_8#za=?K__ z)YNE60BZ165>ol{UqSgx`h zKfKl|`L$z+xmFVMP+I=`UPoO`e90u56?Xc(yw7hWrkz^wlN^S4Wf)kTlmMa&3=-U> z5+)}hp{|bE6=Rb6>AO}ww4I1)K#K$u27#!g~7kqOX2|cH836z zX6=oeNM0^J74y^Ni|6}6RTX@#B*m%ijFRH-OU|-lC1JUwLAm`2pSswSuW98q9nDF$ zmf106Kh-`>8w>kS|v6d*xr@ zfta0Wa|75%lMPeVBquO49sY}>QF*tW_&uC(Dqa+bcr;$t$^!uqNzs*Y`I8HdH3f;^ zc^wU`#e=m+lc!&jdI+h8s+fo7^vm%>dWhmpQgLDQH7wz5t(6I%z~h)kCP)UF*gHZ% zIH@iP<&AyRBrEZSw|qz+QueNEiEI>o{y{}3}ohMZmbr&Q#+dk zEK~^&*;U?ko79q&1Euf~9f<$@iFwRqXOZf+}Tcr+2W z6twz5#e$eMjMU~X$gVz>ub(u|clE^=n|2@bUGqk0TU(4g3L8KS)_WhWQg-6%uzD51 z%XWIj;B@k+-Q9x~J%@Vw@x7edgtGqJ#H#aiXWnO_)?4f5@V`SooD~##q}zTj7`K?% z_fKn)(LIZ80qIn}_W0wOfqBwSPn_EB`d-5NW*$v*2L=(k$8C*(KcYZ4H8IOWkqJ2b zA3o@g3Ewp57b|)1p^H2YC~bvyPd1vWOR|XBu1-%rj{BkX_4WlqlT6eD#bKwOtpM%S z$~0O@LE{w@z)~t#E-WeAJ2-ON)kGoCx_0v_QgAn=t8%YuIY{OeVWx214t}$dj&t%f z=DFg5DE|z15k3YmN*afZC)XcapFp2W4{f*FDqg6ljK8~^dKy;!{ow}Z*W8zE5nI1W zLq53}>5~~^!vi8>^B|n5y~_)>=6Z|1+DOt_3oI*V>*MXBskVzUo9C;S3tpfcVJoOc zIGZyn)sk;o-f1}*|4pgyhB`IeC;~FRpd6EI4WK~vH z(enI?mL_?~q)H1WLl>_*S4S7Cpxa=7wcq|#Ail$7F8bjQ6b4w0saTnE<$l8R;$i`= ztL_0+z+{}?2_ZVx)+OS~X@5z>4ef3pw`EM!t8*SvpuxooUOwJ=^)^98ikRZ{e^)LF zQPCoP?1Ym`c5iINnltNZYER8f#Eu&C*eu(=c-`nVE>|2#F= zC#SGnz!R`!1vr{N2nf2~8to}uKR*D$m^l?WmZ9@{0Cqf7RqDIu187pu*3-QOmt>XR zH~#iS{}?xC1^inG`QR)gBM0}hfM<;Z>+O4IVPVlsK9*R5tbLwJEc*v~tme&_qo69-t`(2hNSEOgXODC_jDIgGh) z6Sbo6EfWXutFswbYeg^V$~iWU+^a!=+96LpY(nbHQJDl|2RGR`!1WHJNtG)qF6RFW zY+r2nkHXm_Dc?xEv&U+WT&cC_d^rX`izRf4ipZ7nA+#IzB~HRWMO<7_$Ym4SFW`va znz(!Yet}u_OQu!~gvylTRbA3Pod)U;(*KIM5#z_Wj1T&BJUscr>eVXd;T=%PHK*^+ z@OZ4d`w`(KmU&ptMNUSDE+ScAz*2hIX^-PMGiAt zp5W)|HIVaOo<$6VwGR>&O5b*?m$2Zqzd-??Qobvh)%Qbqjoe_hLb_T~cK0T*XLNsV zg#rSBoR%=)!P(*x8#^R6) zTpBIr0xeWTL^M^xB6=ElN3E~{84#@(L?>&PzkM5(5d3Fmd;$X9nXE?6rs^+l?Wb*t z4ymFUs8WDPf~o3FIG3vCVNd(fDd12t13;Hlg4>F8?XUz4jUly))WDMJk+u+l=+&8Sb{33fL$Tf0uqxp)-zvE{gT>T9RZ8t|5 z3=ng4MgTl0tAE#?LLDz9PD9;z!yzOlIJl*`b40h85}z+PBPvuV)KrE`d{UCjDj>Sw znhvatNdB(k5_p@YdwpMr2^yj(FS{0HC3EW7S863kyukXhVp)h^NRDlNOG~wpVzwYr zRbd*>QHnuT?$H9S$;Xjp?OIL@O z0>-(@TFYxoN`BQ;R~HwFT|A^PBIN-UfT~NoJ%-<%z3pv{f4=OW_M20uGIIxOap$Zu z(DcU@UY})ssm))tF8|w{hV1`ioi^{4jBkvB3VFDEa{e~clp~K~srtC)2^)rBP1E1us*}bUuU9Lc*5v#h%IW1aU0iM!F*kZ^ z%{y8JV(GtNKxtDy$%RN1Nt`zZ;=hm2PAC#jwD8}sLJ?k?obuYZ4MjH_1FUmnRSOfi zZ}rWelcAo8p;Y~+7JhfF6anVyw|IsV8?$qk7ay(#_x)_b7VL;xAh)RB41p(A5GU_y zuQ)9*qfe<;6A_gR%I!M`a-R@}9lbN~6g`me*U^r<&)Y+;2lw60~6< z{y>c;B@_%1L%_k|BYI65o=?qKR#EhD7+(f-H~T!hD7!_yb3X05-l zGsI{>LweGv9q6f>18+t1)@`1&8;=l#o!w}=Ti*Vcm_uI`cUU9Vo`}Q#QITfmW zw1vV$gk3s=Dekis=lxjhWIvt}WjBTEX%~R%pT8dD+|VSpZ^h;8SfYEg5X@?hGR9EN z;7+QOVy$?1#6z)Gijvt=E#8~{G#iTGV7tgl;?EKkLgg<}*^&f|e_67K%=|Do=dAUi zW^wGJx#nA3c*)gw+tD{za6c4^&rK$Cp&bk1Mx0g5s&c6;d}cCyRQ>d{3G7nzlB4t{ z0S=6)KXEH9-{Z;?Ag$_Ve}G~k``pK8kB;@x>xRNviSEVcD|1c{D2T-2I0J(iVgt|b z>b-Xh-`n=Kcw_tQ+O&_K#1z#j0QMy+7)l9(_%kCV`VIPm`H)Nzs@r+QDA%P2zUT3;Ex)lQwM=8o`_$aI`z~O7?ZzBkRJ4N8YyG zD>v-tdu1S2@c}Ogs;t@3+9|8U0Jhbf4x`&e znBDhrch~f%JS*mH6Qwr}3hy*xkpe(Mf|o0PdmEN^iD^sY9TzchTQsR zncnUwJ(ClXt9g@)whzf`GZr8ICaOQ&x#-#Jq$J!i*1H8j<7ifV)>?IMgipuVGBY!j zzNhWm@u^9|V9;2ogue_bl?((2A`|oDD^FcL)L#y+!l`ZlrrfwU4D>73d3*wyov>fCUYh;Qq7$xrR|Cf+6*D`2=VSbd?k z?j3|dVeD$tzVAR-*if`uggcDiM$l}335+V0KbO&rAGILn7FPGYyshO|&W}Tv|I~PX zfY1eSV>vbYO%3DY11)*f*4EJj@fDG1@B=_0`1Wli6sx+bFAHowv7FE0KgNiAi&HB3 zgJY6?tfE`n>P{~!A06IZ9qA4d-Uuvy|Lynffz2nu?Kwb&tDGM9JdPaHZ(SKQVGBMpTo*SmD> z{YcBRDrVi|g;x=~#cz+4cuxVeu??MNXjn zCjz4VrEG0kn=FO-an8(MyJ>pOXMP462PV`byt_6cpTb@9$VnMywe@B8la~~=#E@-s zXkXEK_cAkP|8g_c@N$HDKGL#Xk>xOwH-IMfU_big*Zu=r_;K*Aa-+yiU0qaEQ`N~q zF$Nu>{%^V7XPviNpH1=nU&BLn41|&*cF&WbFy8C=Cie=b6DA2+Ogy?m54Zcs_)r ziy*Oh{cHe%_>e|@7btH#N842qu5|0|D1PtuAv=oShRC(vze0Zx7@X;KZ>$ zxZg|`77-E<;Qmlug>H%Pr**;I(BmwUsVBQ7|K<8edBu?fF1QyA0&Ynilh3nyg|*2? z9d0L8TP$*v34DAQJC+{GGd7A&$mvb+`4!b(=+X8jzL$fH)&kWo;{M~|fyY&@$|GA_ zzm28l^R_&BTY5qOnC?5`rO%x|uT6*#e|~|z4rCnvk}m|>UjGv;TrKNBe`@v;^YqYl zx8{=~%*XO{>NKL)z^9Gy$80PM(fnP7jxxR^B!PC{&O8Er^v>|B2sP^@NQ)BjW6Y=0 zYmb(XM~~gZo^-*!A)+Q6@xuxpA5h5e54&$)%u;vJ`tPIg{v+`2XL+9M1IhAVg-a1Q zFi`jO_AQ&wT}eUqvm15nXDj&Bn{eQM7_5_#F)lyRx}M4&t|G)?4EsCcbD+wMi1Q%y zkg=1dzf$^NE`TvXhAo|+H>x7f1q2uvJBUJQx1AbYwUH;n`t?5X_d88*zy<^V*ud;i!N!EP+cY=@ z7D0aQ@ZX+My_o}=JR>5M&~}?;I7#UI8h?uZ8nu@}iRU-D(f#^Q2NnpTsE67{WksCN zTS{o;D0jcc8lK7}kFaUqOX$Wt@8PZbrf0mVRX{IB{QI6cF>YUu>rb81Q%mr?&$FAg zcENcA%jF1FgO5}$6VU!Aq(;2h>;$iV_X>vr7atKMZ(xGHdqtYum)AxGBIEOg4j0j{ z)NCjKxL!*M5EwSFliJ{MfA8+zIGQ2MBwLaj3!$niRSq-zn4OW4ap~IpCQ}!9wW*v% z{(D@mM#VztcKS6ksNt4 zkY!VhTu*+1skC&QgTr#IZ}SREWjolMSjyOyotu~R`}bmN>$5;u*lr5Two?9<%TW`& ze@OaWeu3uj6ysN`A2q*L9#^R{ac_IQlf}i-S6tb2+KxT4HTcRb|k_e^u(8_F>y8ad!Ls~5elkj;Y}o=xDuk^facNO>cGq<=gbcd)(eKM z$qIR3o3n5;@8|QOiu&c_NK&5Xj=Gn^k|wLQ)~55^zs+u`5D8;_Hh_N8Pf5gKsx zcRO+AVN*I*eK{%W8J4oh4=u$@UaZ1xzsg*;8lCW_rkJRIgJNjv3;HH84^F>5Sv=?{ z&ZvdB;3ZiTo+k97 zkk4!8+?rq3yvpb2Xikn*k((cG?Xavd4;cT2n_Vi15hcyf78MTGpjiT&%%bod#f#Pc zWsIFt^!Y{nqGcGf$Z*`JqLQYY&3~2ye^le{b)zM^jp?!n09-8Ay-r#&NQcR`pTFYV z?UiY6ZI7Hc;%{T-q~+O(Kk0SbSn_}| zu%pykEv+su=N8&mDhv4>FAuS`8J9^lo@WO9x0CdD=e(4A#qs6~v!~mE$q3~G&b#{=E5|UX-Hv;T5KPbr7 zVX7k)2kIN*VD!tOOI(zJvVoyR1_FtV> z2#kQUEM-Y+1HTpsWH9kSxPewy&KwpkGd5mVVh8M+E^9ZLzAyLKV}g#Arkjs*rWL)OfkMvw#OeedH_z z5+IZCC7&R=FA$`zVk}#Xrs;K{-Bx4s@)TA&KRn4Lt-W+$)9ejQu?M0>cBFogTH2^* z{MB131s!Cqz+Jt0Tei|RlyvuwHz3(pqaF03_8JO0e&K!Vnu z&~!~#@-sLe#o!GLbt_e! z`lgIUFZx}qLOBFs`rfT3`pLCFO|goCj6uzvkTHjv`L(+^2;nO6k~6&~ON#%nK#WZa z_c2?KVYTJslYQRBI_lGAN+3w)E8VE!H}CJ`jX*2O^~K%rh{_5I>Ym-{D2az{FL`z~ zAjd{&?{H;V8Z{8Wu9>cLUz{{6_GkZ4DZKl2n0V2v6#cr^iJ>64OuXO#{TmZA zdXU-C%;CIm)o|LUj0>QWDmStrH|N{pK1&eN$laQR@6x41$n>K8o~TJ|0O-reF#P7{kkTqlKSS%1Osw z?i(aHR5Z1dWzB>!ou-C>%>e;*JMQ#w4d(rdjQn#oI{B^Qay_l0REWU^RsoHp{m zUESgYg#>PMSN`AuM-IP`Vc&r8s)HZTBn%}b%1XY1@7Dijj)qe-D}N>lwLqlu z1!beNMJ)Q92YAQ_pKGJ&h`1G{%(w~kRU@Cdw2ij8cl-omW~Tn=L{amQW~%;CrP6i` zTtvi=IM?TMV@nJEe2#J=p7)DudQ&xs_{91GX3uw;1~wJaM zVk79Q^>s-U($5GImE=Bt5dd5CxIE*RXZa`lU}#eWU_QM5SPMtr?2q2)oKT+!L;Qc| zQpfN_N;?t?*)x4YRI5k!PmyP3E=nG~ z=ZQp)84mRk`UItXeq>s;5!tt{h^6~=czjjdBf-%}jN+_YeyS6OPF38@G}Ryt;#1$4 zm7wWZFq9cv%G17$whq%AC5;Ggd(1zHwfYj6{vT>?GOrMq&Y|<~8js;#v&@b}WVXCS z%5_!o7Y9~GCPca|nLoxk4BR?ekPBwoTsE{3{tDE4h*-3E2|5bj7FAnQ=K&0@(O`^@ z6M2RCH49e8QU(W!#HB{bgf@00?4u%oBW06vCRkK5Py0<^~dxE@xyJ`p97Wp$9qOcX+((xg1r7z=p4aF zn?WaY=~;#LNpjK)I!ad!c72RkjJ%oQ*)5vsa=#kL*V_9ORZ&~$%1|u!bS(SQ^GW#K zmMD}%QiY&?g0UwI1Ss$1iW$aYlU6_ptUtG3j z2>R$j?9R7Chmuj68W(*|ZeG4V-A4vvi}*a2)dBT^K=i(T1nLX=u(ZMCu@E!4^)THR}`p!!?=`^6y_K z#PJS8{%8hTNeZv8U)SFrMb=gSen=ihKFLr;e}x8?fXVP=&R%WGjeH2F0u`PsW{#Mi zJ`th`69W30ye<4ce@eJ@$j-YvO;HE<^+z?hD&%cHlM_j&`#DcFm{er&KA%f3yj*Ix z9=nG+P=ar4roGSdy}i4?oRjzC4&v@Ab|$x|P=PN3{h1UXJ)f*A=@TXB-8^5TN3bbJ zLMg%3{J!JH4C{8_W^2h9QdP%Xam=*?5?uaDI87|_=Oun|QxQv-2cH8muVE8=(y(#IBX zwGlgStpo^K9Bhk30{}UUnE2oW5>=8KsJZnWx&9!SvTPjf0()egga1!8x`59~ZbwbD z*#4pg%nn1W_?L zM$wpR3*xx~QhDMOqGV)y=!$ z^0q7bM7n`7!2A$T(i*CgA|#sO1hbefK+{bpD$b>@+Y;64Y2u zA#__(c{cHI-1PE1CN#b}aXsU6b;Vhrq`!BfpSS#15S+d3@$eZ2IR*dmc<6lDcRTC6 z*~orolj-xN=3Dtn+AiM#Cu41Wg{S!%0>b%G;}0T|Ov!F`h7}MtCo)+hz$BJx3-a@8 z>BHo{#a6nIZ#la8C*~~l|JCbfV}3-2!WjJ%)#7%kr~J{|B~P|tF(HM@nHsGz116fq z2`7$m^Zt&r@^h(0BA@{>ip&~$e>()dcXwTV%R#6ww)7N=4q)?&JhFZ@LlYsUkQl~w z&mhNXS=LD7aNZijI%15E3;8T+s0|>A+eg-5gD^k`9*#$_fTz{tekMmqg^n1cx`6^> z?#vy-=~Pfd)rv2${gHC&m7g$s{Apt@?qeznYJ^iq2dmWZieu;p2qRq?%@!2YX|k8x z#`3dv)5g0kOKXHZO@z*)rK5lbYXgu7m^B+_SQ`1hm8u$|3J^8F5|Lg^3bOzi@V2apP-H{<}Vw zA^JNMAz>d~an3Dfoba?DEZ@8yV+LbXZl1V|B+f)84 zf6D&Jnvx|Q@Q2H25%?_2^IA3&1R;GX@s2A~wp@W00`AARvrV$LN+RjpKEEAFvouCc znuG>`!cKRZ$buN2Z+wPFyBG3#QgH>im{9$O91(H4Q&U5v>X-7qC{X4CDFk{k7-+yB zC)(lIk-qFoU97F36aES`W%>iJn()=Tccw-Q{y3Knyw2QZN9L9`h2P82@%;6Yx4LI0 zRK@YU75N2(TL-z_%GHZKmR4{8RiF_pPWpzl8m-4Or@vu@$Y+V}3J=JeF1ta)Tl52< zX8J*?29yv9|0n&vz8B@kTak`!^>su@V0jt3$_uDHHRTNw0 zxb(YOHT5WyDc)toM1qPjHrY@0z|5VIhJO5NtAZufx%tg&SC%3B#^wB0qUBwC21_AS>>#%=_Koj8Ebcm;B^5hhKCfOh1TV7DWPkE1}m<0tj&6u?h>%xwcyjZlLyj8^-KiynvGocQ}z%tfHN zDXRdSJoFOsUpZ?vr{WBOIHwBZfIs@EZ%m%(&p-=4ZpkEiql{0~0YpUUcN-P@A|iYv z$WV#&N>xp%wtAl>ucZerN!G+YY_}D?V7bW0+wScnwn2@3(@Kl6)rXTc5gs1Xe^T=D zOo{yqoMuu|Qo?7Fo5CA)H)pS^DLD`oe4)_Nj;A&RKgM}`k!cs$&)4oQ?eJ$vXCyjG zff2ha8pv1K_Msw^CfiTJkvof%nohN7%Z5yU>l#@-G8qwxP#_sKcxulPsC%iRnVaPX zGs4n#OD)W=QT#!aC1Gsj02Eml=sdjE004{W;orPxCpIk=xvl~bXi3HR2C&rleL+tYc!artMkua3&gRi%TC z9+92E8`7rq+Upvg*??Z^0sUrsQ%^cH5YHA{cB6f+&)+-&K5l^wxdKh`4Qj(2Wg=o? za&i*SxlzuHi=rag75duRn$o?4Jlxj1du^qs^#@K)CY{7N)Jl#SBe{PVk zmq+*93kwbPc=em6x~@;lK&+MB83)?F5mcOS@hGH z<&oG_R7oekjN>?UYAA%U3JnpArarZwmWH;V!>$t;g6Kzi&%}spCsJo6N~s2uMH2wP zb2j=bjX3uFkYU~dwkP9_gMP| zRCbo@oBe-;L8=+`HZjGXPS6U_@D27vCH`2s?mBsy_ddJm-x?{2a^K1@I0i()YfXn?z(26NTs5yHtF;AE;RUtWn(QfrRspuJ`5I z=9!7g(b6)a^#c(0ZWA0@IO3@c-L|! zG~+&W;Pu>f)m-kb?p?D}R>m@R>~RfNiuo{*MI4umVo zhZKaqd~v3wRG_f9drit>H@k=HIbP2-{bp!nwDid0(o1?AVMT4}2uqJ$$7)t`mK3#K2C_q~VC1uMq^E@ZRA$Bw)_ zb)T5RiQd+AHB4w41W*&Q=~E@J8eKRIps-rgHshw^jad1TWbG;N#-;-qTC zscM*r2&nK)6g(xv51rG2ciMlE9LoJn7)XP$T2FPX_15by=9f!%Gv6%*5hM6(Jo6fQ zx(K45DsPQVOOjx{thagQ`H!i|Q?fc84YarT;XSMw{8KG2qKEDxNT7)KlmR%sDIACx z2quuXQE+D3bS8WU6N!|!@m-3Nt?>EZlK@cyO5Z)I`pdz` zeOB0sxsM#BK#%tow(XIkD1;9|;KtvKDo;2)AiNDxPNT3G6&3)j9alJo#yjbtqZ-&Lmsa z0L&?S+%r|xNBeRz#{b3QZmkUa{VPd{KkMED1Ph_UM=y`bmg2rI_bPog@Aq~r;wn8? zzJh$amQy*++_>w?ThWYq(IM>HPLU&UyWu6KE-vmB5Gb8`fuOQ5eL!KA!?ZZx?7X<$ z?xW-@5bWFdfi)^$blV*r71L*zseSocqRYEy!LJHL+=}rCpCh|8WKvMV;t*U(_srG5|AiDK;x@cWw0*l~jZA|ON~S!>;WjH+ z{PM(fchVLs(y0DCNd+H3xO)fR*&L*20^7PPyPo}RE}D7P&vtR%@I`-_6LFNZKd<{pzhQEOf(jz~<;ZxoFW?V9rP?Yf^=rsDC^EbF8Jd@(XW7R>#1e3`a))GD#|C&!-xKteLAsP&?6qd2y@r z#^JwtqB_eNx$zHzLWG2WeGbSenrQvv(ElMI8PFSlEye!+9pvznZzA@k+f-_OmpFyo zbnS)db2TP+DM|qfy}=n2LoaATkKL)As9D{1yJWoC?pe9|?cq*aFpT^*vBiQvij;)> z?%JjGA&<>#!&Z01O@(ALP{Mq4oO?)QcwBHKaU4bp4T|xt)Q;2EF|lf%^tfNHR4qu( z17AL%hf=2Xta-{K_>PH${SbcaszXDHH)WI_x#0^?Kr}LlWB6my)w`$tgdG6N1)aj=sa;D_q?2)*oAU``!zASz13xfCE@scmgwg@C@Ow7BmrGgUM< z3a$7e_B{2Uqo{>Q_6%L-MFJ8ZF5|V zb^f+cB&_%Jg}VMI%*C3-={KRw4k}t=w@cgvht@;_Wua{)yFX*Q3VA7S%z^?}0)y7nEQDu{v?jFFh+Gw;Rr_ zot$ioFcV?4Z^fU<~j)j;nCKVOwoQOJIdF{KQ|%DDTQ9rjW&dm$`>}!vioxirXhUziM;V`SaSGIVkL1e^oA~^w@dDBaKX~+; z>5z+F8C4lXkjbF5Yi2%mR&floZ^)fq$h6`yL)w~pi`Td2d9ZjTJjJD{_MrV0%#XQ} zvD@`+nP}am43DP{GNP`N7+rF@?u(*%SKmunm!E`vUmvC3xwUjCxOjBte-v!Cqpz13 zADBq;uojv4+VeV79R+)oCg=Rn_hqEuW1_dIe_Z5x#5jxu0-py+Nu`{tQg-fqmUCwEJMa`ZeMz z-q@Hwc;CO!@yPD@1*6eEVujCQrtC}Yj|vQmZV;u78?sxQ&@(nxFAKPB_l9i5iJLYX97FteSdEUmuNzVcIfw| zp9XJkOAtVXs1po)*Gh+ZZNbya9v+Bqzdkjd2T$~J>EftMG3>at=9D`-+~m*JSji+& zFDUro;vrCWzokL^L_~e(Y02aHaGkj^F`*-j4Y~o^o4;g~GrmnvA6AT%`mu)Zmiw)n zo!A89EzPM6E`O#ip560cq3yQ-?7aFO+M@?L8YKGa%25HMrSHFJ%>@W@4T_^(m+At)5Zy)T@d=ozgkCo6>#jUiAc8q55hWLba4 zCpktGfe*~eRn_VG|G5C9CHC%h_#07a=0=^nc?aP1v~m5x5eat17_x0E!CDeB#FC zZ~4rH^TXeiQnY3#>$vv*!-@rgtPBnA7T#3f#|+Pd?SaqCy%iEQUcI6Tr>$e1`wD{K zk{p8%&AV62YZfJjio^ZX9&|#DH@_>L^!Ec7 zD+U@O1iG1tkQbsFuVFXB+pGooA4k>-evdUR2~sl4q-=e5BpGcRw9Z1R`lMK!ocZ7N z-Fad9)}$v2ne0hhFtMXSTB14o7UL^j%2|yfa6g+b8b8gX8LG+|7$Do6DSg##@_2H# zq|(|w#=d`}Z}rSQtj!#8wt~{2-RUCRBylWUdAjtD?4(f+f{=GvH&7jc%j!Mo(WAY5 z8x`{vd5qQb?N{>+mIMx_=Ia(h7vj7pE0-vfSM0q!=_3aW4gzE!3-F9G2243qErnA5 zet$9w&``c&lQCp@G1P}9Yx)&7-<&-}7VPp?ZW0^|AB4Qm-~DX)YBJP?aMAtG9p$T^ z_^<9o+llbNV$fTd7y(!ezXupG%>JoYBc1kOK?6LC=J`}_o8+;ee`x1*&FZ$wXg=zG(4 z%PY9`i9VMP=}r? zidw^ZlgsesFG_Lo$6fn`PlH5=zv-)1OyebR4xQE2SxT^7w04^M-pjYmLUK6jNR2F0 zUa3yaY4Nf(FKjJ$UfMmLsI-$zB9P2aI4A3~BZE=O;^rx+ z;1^7tC5k*h4qMKjT+n@DNT+71vP?5I5^w2KeI41u-g|T~d$?HXWo)^;XM5=7>LL!D zIY`aOK=cLr)JwIecK-+(R^EPaY|T%2#e_H}d}Yi<64`^7-o$(4Y5tHIi<5l%wi4PNe^&9hNV(%Et=^jA!A1Jk?5<-7U$_H7lP zr=3q0Cz^{VXlQUFS?ywDJ~iewl^BUXoc=Qhm?sDOyS6lqDaQ_%{ZthNmX3!0a5ojG zB@d}l0(6jw>E;3Z&Ac>&ES;)gt&Q%=vYI;a|6}W{gQEW4x4)z-QX;W*iXhz~U6Rt> z-Q8_0Ah^`bq(vn%U5DHeriW(urxa?N+RKh+8}JgQVtP<@N4SviLWJC1zn zpJ$I07anNKFnok2Vuoz2-aq#UDz|IZk0n(}uxs?%e~8lDqh(|x_$Z7M!t_2JG;Otb zFGgH_>yN#-*qv{&Xj^KH+NDWc1;7k(#NE=y#v;JheKG$Ea8{r%W>$U0|2TSyVeT1b z$U{U$1x(k2eCfn|!gWaDiKi#vY=mF`Fef@Wp?xBDOhO1ugrnz2R6O~xcqPyvzrUMO zk`4$hC2@>e&Vmy{jdw`?xVw3TTo11X28u)#$`)cYQbSI!Vp^t+dPec&3GoN?w}z1S zs00+~;=t==FSnev?7aSLhO9NuBr!;&_d5XCfSC>nW0N;}ZWU zQ($^}4&``M*5A(=*Ha(0NYlfVvT5-+-__NP4DajCfyeF=dBL0MX@l9_L>J+(nlqGb zZnMZ#TabtGo8BiqC3wx*7^;HEi})(w2Qo{4=*mmx>g5&@aJ#f4<~1H}XpRub8*Xv$ z3+lp9B|Q=i46IacF0D8pIzIv|j~E4@kgb=o!^+4N5fQak zdsWudb_&j1{jTeNPeVz z(QA3GW2=qt=IE#}YGgMk1G#8P`-I4{Yc9OQF|cuB<@?6H1EpWH=60j& z3zaSq@#F#A3-B-?pD(a{Vk9DJlB+P$q+gDCsLzS;mCWyk%}h@ZDYL}d3Zn7x;{tkq zfH#heeHT@=Q=>mW^fbck$sB}+@bmsRWFFOfA7x_wMNV8W8}vAVilNYHQ1O#91z znqz4Egg%4`@L{yP7sq#x!U+HO=@XTPR!PqLJ#F9$UL0SnFOoL7A97XS6IFPYQ_j#j z>j5k3q!kOl^kRbRZ5_{-V@MTB>OY(5+(+9#NHeZGu^AeURX7VcrF_PMjmuG>%7h5= zSOz8C%&K&V4PW01{d^0!mNin3kJFwyno555_TJf^G8YZ>TyDq_5ET@Sa&|cW`69Ah zyKvB!Fmd#>1y9dRRrcCRrb&eTd(Wmem637hRJy@_zNK^l5RrrlY~ysEM?iBY0GYIhC{vm1xVK4QjdXgc2-N>(SG@CyzOwww(N zU*K+^Z^qZ3-@g5xTC!>#bTlT11DZ3U!RCF0pWpWQ8rN*Z$iO=m`0?WBz>d0oUhlhh zhzfufrYjj*J@_D$laphbHDP+6!nm2V`ZVj`$qIvlnp*GcJsEhtS#y07Ijgh}l z%#l}C+D7OE+hQInNTvx4sgI7%Uux;Tdv=G}!IVMB>gOZ&N6tMfHRdR4oIzHPM$U8r zL_KC>cHI}@VGoUd)&I}yiDqQu7MX$#BbOXr;)gOO~)4lB_ns9(Ti(9m9 z9UdB*oHQ~Yp1v3J(hHVZ-UhyVea{{5g+GNkIh<#gj;2fJqb=yzSo=Qhh44}$G=!-T z^Msx_PwOB(gF058BR`+Wx?}o{0N_;J$7RlcMn&+rw5hdjA3bbNhhc_?Cgv67L>|P+ zO8B2%r6?xNd9;{c<5U=2xM_O>Ylh|GpHK6FmPwDuG^`m_prqZoWk6-+J0V#Z*#MRT zOKJxCnz||;a)J#$s{sc=Y)rY{mJ9cKfTY*LXAxm3JyTuZt!+bHvZ|`b%JQE>tClbH zny02p?CrC7t(#}bl~a0F{x-XJl(}BCFO?=~Fp#R_|9k=L)=+xJLUI4S2ybia#~BQt zrC}dGZfTK(6Hlt-#F?mE~9)i zE%c0fc-;?9THcc~xYmbe=SQ2j2DH|hx3$!OcB@|DVE`csAB94(iiBz2EN(OUh|`IA zoopP%%;nO6LnuV6UfyUN7FPN4E11-jbpcyfp|Qw8P>pj<+zZ@Wkuv+E`L+tGym|BO zp$|$yx2yE^x3QerStS+;2@yk6Y7gsHFNXk4!>l3!hk6t4-}eA?X=hZ7k2otosqu(_~A-^jG+1Xhed5r=$!yPXYCx2va?lu^e^eT?e?`5&1 zdNO5sj}r0J9p}QfmOH(SbulVWgpbzAV+P8{HLI-9i3NHZ1r#DXo}7U zA2|u;9EVAy863=U|(Y4$J$D> zDHB-{q3|^`%$mD2C>ItSb3B4X$%!$_)sWG@3l`iA+HCqSnQVxI1B2*feG@kR1-j2G z6#cC+zoz1~=B!liTK3LmBPZrtb6|l#ua976Bml!4mWgN{80bU@?E?Y=M9C`&@V$u$ zk53mz11ybxbIpXcGiZzENk^_*Fp>I5F?Gq*qfwR2@z($)}yAMLXZuz{Ej5JF9 zAW~DKFVxu45ecqQ#=AIwQrfT*xDTCkF1!5~Ul>?|8VTSz zc(yKe`@UOas~^bXlbN43$Q5uR>-MLE3>ixl?k~N4E>N}BEA4DAi2nXESqfu*UxuCe zOZITtpUB@U7jwB((f=NBeHkODYLXe`Cmj6kL-&-kB;5`no;X3&qfXQVd!PWrDcHzd z6A;pRNy=RyhNyQXlY72VrN@f?m|^YNJ9Bt-^?r#B7poga9gAhe6AG{1R8fdOS? zf7@1sPm%my}ol&p=7{HTu_?>CY4QnTtrPv{ZNLjw%{joV6PdUNKQokO#*d zug5{hKuy21Fq$ZUifG;tYoMZTv-yu4FWNq8|BdD0wM=4!UX%P!!Ll;Zc1q_4^|&VhfJ z$hmZdR3CR#4lot{9__q_C&vZvqe820i#*LS*6l<0+Lw2B_qMObv~N!~R{%`U3OOC# zDn!7X&PNWQl70J`yfs+WOlLa87xvHB(C1L1boB+=^l{W&?qXXIIRT#53}8qENjELB zePAI~Ef>N_Ujr~!_(go5P|i?Kcn~ujWT_4gUO#E|2KWJz{iVQz!zsu9;~OTELP24v zcf%V`=4QYWWNv$VKVrZ4G|-)OY;&-#-OSQ*?I?6v#{ZeFL*Y#DUUzSB&08Lg;={=G zSJhY%3xsCzWDoa95cP>fv`SV0;jx$}8sz2DCE{~Ayz0G$8u7K@kUBff@!!&1KSmz8 z$)AKuOJRJo+k^xU2kMqdfp!MJ+@dc{M_SZAJ=+B^J3thAMM|cO#SFi1*5$Q}9!Y%g zNV{CGB7nWEfj*jnUq;@#St*`u@HLy7`<$6o==EFctII)b432`8wyC{64l^7K%ar#O z%<5m?9}isQCsY(8oB*&N7^|V-ej^6L9ALB0I$T9lXVH;4=p>?L-W62CrUqNpmc1MT z9v}~K?ta12Nq)p7=Z`pze!Qla5rUR-4`uG^sMn+xZtz+pC>2ZfX=6qbUYZygiOR?b zh)Mtz?Dr%&#+TK86twB@^rjAXPss>}xm+%G5cFlB1ysxuP-x_36drB+yd+QIa%mCvu&5ZSpcTapGl5 zMo;FSWaeA0aGk`S^pQJrX1U)JjxD!Pqx-D83Ko3a1}kOhwo-hb*4E0?&3voQOj|PE zlj9i_E5%d5H#R8N7HN+5bg13q&bB8=lzR??BR@WT{%~s%JNoKZM3h)zwYNF=Q?iGY zoYc?B9;&DRgX;-Gz%$3e-BJe#c-H;(K)oLVo{PUdN>9ZC&r0#y91n4H{(D)WjynGP z|6VRrlujrIp4B51{y%SIaPQ-{fx{)U5E7@9-r)Xo8v@s_FsU!&8UHwR!KWF)l_T9w zVw>Ff1y!hD^`d|MwcVL*i}@{1%nawZOBXS^l{Tm@~crQkQJx`;2;#{G~gxky!zTb7zVDK1o zl*%^iAzD7++BoYes+`KEH3KSiGY$tqYD2>}y~VD%j0wqIoaHW!vmOa{t@97nKBQAj z+3^+{8;aCX5-z?^=b1L=)%}G7juWFJDL!Ij5etD`Dz_E4N&l%SgIn!|Tqw}yH2n1q z4JqR4nO=h_Z%9{ND@{kG5&7r)k00N^Z%!T?JkCLRzm=z4`yINpafJ6O!*ELEYb8yB z#4G+-ejEfT(>Eo=a~l7~#+BKMoUJV-7T;x>AG`$dq^SPO{VFLc!!I7<91`LdCYsjIJz@qVS`tQ}QknpiJhn*xBQE$Zh9w$xxs z|8Pk+bTbhUAz)wOSRthp=ca6o@XPFFH(^(q;%>8El~=D|)5w#QWSHxmvehpuD|-;n z)@?$Hix5E5q3OE8KQzpim=s?wmF*7KfM>#w;FA3s{c^PmsVi|Z_|knWh~Cxg)}Jtxaz+K&jp-X zXC+0OWRHG7NcuC|dt||q0s}%0Ep1+YzE=4}ZW%LNNtc_8tE#5P)6LDz!y}+=aYCpl zE9zu`tFB8xT3T^$|3l?B996K%md#yVzEXTP2naKR-)!{q z4I1L#e)1CQ7hX|Cy-a?>^|5tykh4Qbj7l9sV8|C}6Nxf6APaIPmH* zMX8j-%Ym~$3!);@^zllzNMj2gi59Qf!{7Rt_S4f0eX1~+JspO1a7&o)p`wSpC>jRA z#)m!!qJrAi-`^f)A_Y=S7#;uB2l`y~zXo7{c7=)Fo~63^T?JG!Z|{BWsKd0?z9ele zEgs%%^K##VR@Yo0Nd^Qom6tyP^ZBc!NdAVg}&I&8Y%hgYh4{Q$QbGx$$zYsJ? z;X_zU%f?$A)0_y-2d@L{Xk2rr%xFm!vU}*_`0qfg$GGg$DGXUDCXPb$TG1<~xo&e7 zz}sz)*!cX2QMTc-$kV&?K!rE!f%4b|4y4gwt2=smtnnz?@rY)6dm>-6?4fC>111ru zQJ|e_mPpRrd>$z2XKB{?n_bfDesd4iNbx{OM-iP$0|zLUsY}xjq{>yXF}CWQ_DqNG z*#44XG+v57c&n|PilL7T;1zyCm17>n>}Yy1DWrY7Q5UKF^Bny8+rM$LbTl-AT1R`I z4AeQ9zg>8Cd@2L9C0ie#PhDNIFhZ}n$*HEwgOR`AE!Y|mWSRe5z$C#aIA{m)5TO{U z$wwk1PRAjG>jEm7GuX`Vht$TnMV*O9VG;=WnHRb?HZk#ez&(-w33;_A*j1nbL+_(V z_K4KCPCd;FLa#RK?Y$`pDme%sN81G*{!h6PRW(EiGxI%4Jgi^hycr{(EgbViy*9Tx z3NRa48fFTrOgaZo3om)i`%i8Xc|i~mU<;*oyW|2;g~(EcGL{U1#Kzr&;<4BUsaJIA}5-$&LajGyMzvLaH}%JIJ>uz2mU zW2Ez?1D-F>?bcSV2uh@?@~dMF_q`<3*Kc%($L%u3@70RIHa5 zER@J&+V+I--|#XD%bq@cjte%i7+8m3V6AZW){%geDY2(Mq%zd-75BZ4nM<1-I(cYO z$qF-S;!mI)$>c;1>nnQjs_2(I%+{&y7nI9W5Rf1f@*HFeW_d)ig& z3Vs?;LlEer)kr~`mUmh!lQr*=f5^Tr}-?QfUzj|1rqd(3_7!T zn)F59G;SpZnzrTzqZULA8L}{R)I#HGaGRwVB00?xlkXYfTzEr%$lr&N67SPxf;Ptz z{FYC4W|VcCVsA6^e(lkE>CPDA;<(RLTQ*k;=vY02_OJU*6GJIIAPUc!^80}pJ{8`< zm!irY8KK4D>E*T56guD4F-yAC$C-I#l^-*9Svo5T{$8S#JbLhJ*E>(@{O+{I#yPl z!TU>f5o=sLJUn$>H(`P~2qhM9_x2^y4_6skBIkr?33ygDOTRshA+dv~xAx)?lnm)^ zWN-}@8>7|a6{lkqhAzG~a0j)P_%RM_a@fi##5M8aRnk>W+a%@{VsU>HrfiPj5Zv|0 z^K6Jj+Ueb8UR%4NpKm=rJvCD%r}^K5O>I3?Ue`mWu&!O|XHXkI8c~nBKq=lK@mfS7 zYg+((>*&i(M^Ij|I}I~CGroY9x~973HxtQx&W{&kJ#m@J&)}o1;S8NY?n+aHTx4ou zLps6?iR|5D4pvj7*#F(ulNQ3dlfo%IJLWWnlK=jWMLv67fZ0x!k?5eJ*xjt>}k`@FED;9YB0h-pk^V|vSeyt|a zr?UCZrs=9jNO24Uhgs^Ya8d6) zfbc9&|Gc<2H;0;=n;RIgiHww1_*l&HDSBB3P_Y?hqt=02$o1*dzT@Kn_Pn%*cOe|C zMnk11Ppoysf~ubct|KZbvwpR`Pse}!IA}NOcSh?7t3ymNi`LAevTpqq4rY&{7m1W<7}>%rt{Em29&tBOfI zV{BMy61~jvf{=D10@42O*PD2yZUS>RSJ%*$7%H$6=CzxN+3;ROWvH?RfI7wYY;Q3? zKi~T57gg}yf*SN3P>c@7HTam})fB59qUPL`XS?&p`j&yeCbt0FFc&vs92{MlO8xI_=F9+?SwU&(d`7ajqZP7of>N>-A zjJg)R`1nXvQztuICptU8mczo*Z2i}*PY>)!?ey%hM-3hi-?JOsp3)f70pHuxJumz1 z`V`ChCyKz_L8{BRzBkJBE9c44_`sSH;$^-uR~LDMObq0io|hork93(Rl)6>YP&AaW zhQV@lrdY7?5zNnvy$~ZRU<=>c?gS5=&8F5CEQ5;x08YoSusNDl(0$+WxBDKb#>^J{ zcjub<+W)>BTpJIR&Kfi{GP*f0ybXQ&^bSN*ORM8zyDHX}R6$sai}FowS-ylnh!7}m zDRZ*#iybF?|Lqbikx1Bw40k6RV=RQlWV^6;T2F9<>j={HMYy{X#AqbUNSX?$v3NYc zv2zN*8YnGmR%8{wKulN)e^GyCyBs&fvLEtl6%(C|>#>#pSz}gcRHi4zzzF+1>>FWe z$+x&`2tBpeR~xd7jEvsi-nT=}G98hZq6Hi7Zk2%E$)G*kQj;eZ?A;B$Kk;F* zDPsO0#INTfP@^!frKRb3nR)~F>-b4}yjD80IE2bI^j0_4oy*fXF zj5gP&ZG0|;n*Lfv84q#|ccZS7L4 zw{>V}s1!q2z}9%FM%L;9%$}6c$Rxv)F6eT>079FLYWb#yMLlCMp+?H)_=Y3-W|T%-KTqrTJ%SEsOfS-PHOz=_`FOe z$lpE;RJ?+D=vUbR#&MfDp$0-Sbw3BM0@b^G3+eW=c= z!_3_L^4kM#6MP8dyCpXRT+b~uin2<{WFB;^3eIZ2ZwIEQZ@&(J?(@H6Cy(oIlAf*0 zPpR)BfRBdQvlC>mhQb9N@<;5BP~0A1WNOH}&(r#r-`yP^z)pIBRRqtLG#&bqTgOn6 z7t7rbV@3(j5ZDhnHJT#L-bmuvDp3=~bf>5qkmJdd23Yb@ia>sbc6M}hbau+5sM5(s zT^+7z^cJIi>3}3phoeH>3pKUfh1$w#P9!u~)C=Q48o1x2R_Br1x*xm)4jT#D^AYW6 zinosh)*)Za*S$X0G%V@u9kshNB|-SRVXO5Oqp~-Hb;bRJ3NH0Va4%Rp6K6t0`b0lO z%Fzn~HXxA}&r(0IK1~FXwAaJSSHqxkvz1@v+w&$mCF&UsysqUN46tmypj>$SU$C=P z{VW}ExG-f~FsDX@CeXd^+(*3FfU*WK7ZT{IKg_9eO*RriMl#5jjaVeAu*{CVI4Gou zD!{nJJJ|WV+am|JAO((xpcdeuD17|;B!(rD5N2=iAzRu_zN7g zN&Ux)-r|fjP0r?dKv3o>b1eW;1h}A3wf^^bUDZ;#iQHpE_0$DQ31I?6#H*!mO>(B< zhX&r_Bc20oR9=-OO@vb!hV!Y-li8kdPtX+tfnwRivc0n79niv>aBh-h!-O~%xOn2L z(wZtv-?Ekv<-zc5YHG?v3Ztj*h&XR3Go33GKs%V8W?g!Ug8g9+-g`$B-cbA3s}jv0c^_gZ znLliyTw%o=ZYbNSlMisC%g8p-R>mjcB4pakgu!=u&9)x6=?*#^Wuhj9>47y*B;y5r zo>+Te=cC*q8~Af($WI9m8dPPaMnH{;gl|IeUnxc??tit=pC1D5^)tV`7Cn(+P-dgt z?Bnv#7}UGx=mM7K3jKkj_ykR$8P_xfl*1BBzKsFg^dSLqMI5KB9mgcofbh&TJ|3+l!v%h{>;jS@R17)Il^FYCZ zH>+2Fe#z>CS&x?V zJ37BQ{xhB2!Dhl2@k8-J?61=W2ocS-G1HkWBL2#wp_#TeWa=D)NlCF!*`3nV1%=10 zH%=x(2<#?E3vDZe`{15XGu)_=QE@b>y&+weh#`STJkkcXY9Ps@2C&KL^)X*%%{h)m zt?!VV-a?QIdv^8W0s=_!;?^N}8HLtF^22injG7_O-Oof2amvk{d@gRvlq}eu9MEtpo?y_UTOD1{P>ke1~7tn(6Vur-nXfQV;aiS|h_eMN_GB-kzOUO_h037s<0q zI(=mB9#_)-LEy^BEJs~B`9cS0y*SKw>uoqcI$nu|Ch5*Ac{GHt>7rpSikXWK!ZTg2 z@~k`!Sa+LM$}*{aBrSJ$>A?~(QtSdBA0I-QRmEA_ghKh*_$2cb4U#IT&Ybv&Pa2r3 zQ8!jP+XJu4o7>IORq58JD(uc~TwE{LI(u6U(l56wUj?%}q04*N?b-9KnL*>{$k6o> zCYoOD!`~|w{)c6=I+pj}4psC%Ead`97E8)Xif&xb*wT_$?z$a!G`PU@P?SW^nV`gR zim4wO8XBgjr+XF1iQ`HDn768G=M5f-!YSBL5jz z{9SvG)_I`$GDaQ)(yz3zlvKp_B!G&(PBs)xG;8K@R6|Y4ep0H#Lw^PqpB6L?7s-F} ztP@m74sSSQ>)*Ne&A+VG31)XqEsp$m#L+uXyCSv#oFj(C>LZfqoNNufx`;`yq0-Y`4#WwSpMX~@m_24m>&6e_Eb z6~3Ead~9kBEyke4Ke4_&0V}1q!+B!XpMAcX<$Afh?`&>1D&cytjKpaP@!%zH<#iv>sEI7$n?`Eb*Ci=L1Ol|eMCl3&a@I04y9Rt0 z{{8s;S~bv7qKJ^cIDN|SyzB4K*95=1XWw_6Ya=$w`QAPoaYM<-P+UwW-Rx#O*m#4# zBJS0x3WA!m*(>U5|8me={^HI zWFlIP7INTX!^qyYFYmVofr?c7_E@{?tYiOr%KAD(WW2TIP4VZz9V1SDJc&N`pFoL)q8(})-hiEI8vPlANxfE6dht6_1Ov)`(f$f$r_X? zoWK1SGOP|K#QH7BOnVs)CF9qd2=4fWkw4a;j;;OBw6Ee@FlYs z_$$Y^G%qg#fs~jrDJider`Nb z8G2wW<89krwvs*ANuwf4Bf#d1mx`;OipbiKc%fLMb)pTfwW}%G6eSvFB~FPZ=fRa_nym}l#1D-L(IA|CYRm~ zUvt0f(*FSYsQ0VT?*~57F1|pfJ35Y3kdPJBrjnmzX|w^_3$bz1MR6fSq6l5P7o)?W z1XU}a0N+JCc+VpG7Kps^M%q8maj^NB*iAdan$^COL|^cI8695)jSgEmv%rq3Bow15 z?@0Lg+A&2LTli?pXIHV?O` zD4GkG*8Yplmme^`diy874>Qya%PUiB_hf`lnw@2i&n|TQ*N?Z3E5Ku`A~)c1#)l^v z;=ZL%eB1X$@n4Az5u=}vzWi`ja6BEp9}$u*o?jpw|CO!u^2s|o(=opjw#8e}&VJeT zH52t#c}32S9c)nsu9eQW7#P-a@o^FqqDE6x6X3}dy)ouh1ItmDo;@bdF9=8#v?kb0 zz1$YeOijTS`Bas0%_x}fpFWMVg@J^DN|YPDuv)5B^!3S%RroM4?e(mHl;~Tqe&7G| zrzz~#NBnA{@CM8z7r(a}Z_nO}i5NP=B(akHT_7fQNe>`QxUd%v=^mBrUQ|y+)ClQ6 zD2oeY?-M^S^5t-@2{=YJ$YztKPO4fDwC9XoC<(oV;xZLF7alyJelDYHNYDN4@2xWr ze9WZ%Bmb3wqoXiG;XMBOSd3v~E3OWC@7J$I;IXy!I%D;Fgl&G7Yw=aYZ6=R-)FpZ! zcuA>#578)mtRxUbta}pStlr0A&Qj#yA)x$sh>vD3bJhd!kIp);)!4O%D=+HfU09G| z%KdwpVRe0x-Y(oc10sey!CqA-VG9pZeyr8b7pEV5&R-5it#uHL&a@`JrL}__G}(k zYAkRlv2#J!Pv2i$Xq&ai?e0_);aSMA;dBS7wjbM#!N)0p= zKY2^(bbPA|W$s!&6T4lj(^9&)^Aw84WkqPhH4*n!*wNXf4r`~$vkd_@qW6x4;5IM$ z8|Gt9PfztXc-t>;?%bQIACaEbSPqX5!7jAea$*|tmxMt}HH{{SF z?aQ~pPw?Q$9FAH&K9aauHmvY$XlW@S`|sXYZKjG>C*D!_s9Yx0?0JIh|q-eHVL9(Jmsi{23hYwzCP-t+Twpm0v#vH+V%|55wN<@@*Vf5w1m55zD2 z)?D|tzU+On-+8vFVE4FO4l1N4$r?FZHhN6;g(d7tYlRuNpPHjo&60@l9O*#dGUu#Z zz<7fN{abSD0Qpp}S&_GFy-CpUx5pu9{M#Rv0Bph@2SIi3=;(0sszr9y2E=`QlB#Lx zKReDECa-QWeT4Qe1t@W!+S@C7-~OcSik_dd^C+Tm9d0;XYMYrhV1&k^%k1=whCfQO zH|{W#qN$aKIcU0Z6-{uXzx$e5M?o#<~@uSp` zutzmjN=i`A3>Bdj8tU)gIPCPMZ#C8y7~6oML6jCPo}sSH0%EE#N7KdDz%uFku4jdV zkMDMcB>LZVmQ|`y&T{)rTkFl1rRB`lMcq5)xaXNaR_{O{5V#HdUDo>zwjv)nHeg>i zCHe1vo1Rx;=y+%gQaafG{1&}K*QQ9^E7$jH`}o)K4P_N+z<$RK$kQpZ;m^^O)7U4mlB`|R}k^sQKzxbX46>Vf>=@bI%1VQzCH!TVY;sHr0t6BY!* zPnvLqjG0 z>#&0-i+BO$0CPmwXWAj#=L`0kKYaL5Q?ol|Y`xeS8O|+mlouVbI^4RHB1A2DmN&Rb zI;v-u%=}&ghSQp6VEko3(8w@*<&KR<_q&u&UZf9fp^!d)>^nHmyqafP28OY40@2NR zEPD4tu#ck4T=B7A+#XZO-o+(|je9!LpY8>l-s6S8pC#zU#ZjofH9O&9f=gkjC@C{* z%mNSpoF^p}n*8v;jmBaA?IRuysC0PI{|B~C${dOn8UC|n;AMogkE`sNg5vKiv%h6jo*O!OaGRnFjvlI*!qQb(!58`0j zLY%+o1@bdjd_ET!7Xva(aWS^7l(_KK#eURnfB7RrLwnP(y~2rgAzUS#FLtIsAdu>9 z0tk6Xe=LQMV{)Sqx8U;%90v%#JTZiUpBu{*kAw};#`#VZ+Q{^#bG^?j66Nmf?r+a3 zIQH+=UDHfL%f677p-1S(a97hnNz=@GxY&cn>1aEl=&OHeMJp-FyVpE0d&tWA@D$j# z)2jH~80_;ncQRm+AMi@#emp+1F?_bh+;m{^@kRdI~D{F%)0-;egGj`krn zSyq4CItKZaPALU)vmWSw#gE|q8H}Bj-|SDy^xjQr{JSRQBN00$mnqq(?rK4quX688 zUCpQTGw6EC#?{nPlU=jRUXa>RQwkDd{6TW!+Tn-sUu*94mW$Ht3fdkR1@~!T%+6w4 zTcmzqph};;etpWF`&wcF!~B|Q_V4rULlZ4^$*fH20_Knf1qBbvF{;JieHR|&>GT5s z=T(&T?)vaN@VJImHt^Fm%!Xt-Qhz6v<42dp3sRroT;DO4lpL}CydcJJLv%4=YE3W- z`P4+o2}z#)Li0Lj5YqguBx`HhV<}PpAc9J@;eSwu(od&Udnjyk@=SFfiqbCQ<=r!SDH-MX^E zM?tx{=tYZ>HLHNhEcg$go(t42nHZYDauYN`K5P&&q+nHIiC;_M^;8uL9L(K*Px7aO zny2|_-NWlK(#uljr+9RIYipqv&$4V<+bnBQJR$+J%qHy;+n-|W;Eqpa(}=#BS)GO@R&qe?TL+b) zI_cc*jRK^iVj`85)1f8K=M9dx=XFs4XZ7ps@`-mqp+xx7R=_91Cu!;;lQZmT6$kyC zFG(GkAfv)sZHrE@Sf)4U$YuzHgh@?F-as4%)q*$7{+SVPf!TXoEhX*_gLGT{ldcSqg5B6M+wEA})lkC;4=HWvib>|0iMzCj!*g!u)4f?d8N5_B<_GoMgGW*h?2KZO4zfB z@`9Bq0!Vh5>s$yen4ApdKlyV%a&8#R;&ERd3p*I6NL;-+oML>FDx9WS(B{LLPl@+6 z>gQHRF&lExOK~ROYh%`*Dzu<+_D`(sCW;v<8f#0g967{zJy}Rd1IdK-@Z+7|tUh{1 zK^Y6O?m=|zE9@DWW-^6r{xpf{G$KP_r^Qr zC1Vt&rMRoIbk{xse{SYP*5Zi&Xb-WO;){CDvnLc6f25$;VTL$ktOF$9#gXjwL6>)T z2AwEC1$ew9O)UR8yl($reA* zZ}4|gkwJl>p=!P>xl?O1PrM-txL-$(u0ZwGTHJKp$9Ky_^LF>g67XG)SbqNC8m6Tc z$PfFo&&ZVg!}D|}U#vhn;C-~sr^<$6CLMlZ;luU$TL;Il(Nj3I+H6aax33b2b|hLerp~M(Wt5SjTn6@s>`}HReEiG% zvSEKpzJ6W#^{cq+qMr*@yAdiSg{d>Ov`Q{yB)}VYb-kXOvZ^!ge>~$IAlp$cU7$*L znUz#tUJ?)w!E=$=eCT{VwA>bMtQfhtRy9Msw=5<>vEtdX==tR33(%|bJ%AhKeOCLR z{26Folj#&K|NkC=iLsEB`nU{xs=6WV>b3CD*8}v@9T0&D(kzG%nVJ_TGMK<{nH$BwC`v zPI{?2CnkT4j<@s3%0@>-v`0Gc9AW66v8ADEWy3df);-OWB99L&B(xQvDo|{3VJW-Z z^_5q5+C%p)&-%_f-YhzSIs4hpq)whb8Ky&=qSj8fnNYCNT>w~XFD|5MSS%YeS|IoO{+Me#uH%2cP zME>WvPh_|Rw|DEjp@oI8xNyM1c!q~)ThtpNt(A~ON9+! zGXKT|XBVss3P>paP5KA)MD`S2uC8v3SVx>)m(WhvEY5chjuNS5I+{9}#5&I2yuj)a zHaE}pT0&{@b3P^@AtpLIBl)r0QcEjcjgiy zoSM`W?WyN}u0x7j4hWQzn0H+^Bm3m@sNpdZ*|7ar_Tj<%W&kI&;sb$VEI;Ei6W)21 z^8I*mDIB({2^bm<>w9;YeuqSzH;`e!X6v`A%Odr=`Q-?aW6v|Bn9BVOCnU#xf(~&B z`A}Lhw&g=elq3_Re)*@27ldg(h>!1AL)6D7_b@|=u>gg`0U$Cs)DONPIMm<}M9RDV z#|hZXa%B?qZ_y9#P6&eoIx#o?=Oj$vYnkh>zHF(+gh)v`;?};J9ef22nj~%IGG$#m zn3JE|W1a-=K}sr$lZE!)PeZ{;QpPQQYb9~rW=j!=$Fr-Cv@0svodqibB&#^U0!9-d z(03$-aRe(NqZA?2#Ep`%9~MQf3D^Npm;U<-4iZ8)KVJZou&7aBKoBJbWtlRy;1C$7 z`V9tFNP!cXWG{LyZKE^fd^@&zN00O1*?vc4+dQvTXE(nlzYdeSpIIuD8JypXYZ={i z!C68Fo7L$sXc0&MScrwL0rY1Pjy4qqW#Do<()Jn!awU8?K32F1@{IWFz?cmzEGA8? zNvOpoBtj1cnmx^Tz_*kt5cfZgn9`>(Ri>tN@Qc2B!30f0{xM2@ zxHu}nT+DGFtX5yE*pqg5?6wUURRe%$h66R^J%{OpaNr^ke2Gn6Jm;M3CZ{5WNICck zUy(97H*oh1vE}>RR0a>vPBKqLr~6hr{nH&I(_3jDuU5pq0gP&^1k! znd9nWm)xMO0gpDu%zIhbX;>Ac$4_dKhFBGB7nYfq}D+G zws&J9<-P*0JjY`~AwpX1Z)yDowzfZZ`ZeCzNu_}Qyhq#@;6P(*VPWvG(#m$aFh>ohZ|Lg7g zznNa+_=kk9Vja8P`PMcmZY|_XT@*#j;pEFin=h+rGh9r>2vP2|6Pc?*2t~eZ)DQ_b zinP;^e9vYiCf^mUVd6gB`w!fo?r+a|&htL!dCqg5^ZxR@Ua$9kR6;*LV{<-qb0O?$ zjYJlg%$aFekjmWHcRHy|s1l>4tmuw)D8`tI5QkDjbu33C6rG)&Er^D_ z#%4r7DQtMav_qHEZMwc(5B#iM;8@yxYCDPndTzQJmrV;^y?WR#o*)<=Ot$9Ydm|c2 zx!DeYOZ;OuzWYjat+|7=I)~~CPLAkxm);pz%W>2P$+K-}^)oxUN4n<2-&AA#O%jQJ zQtFMhGbtKqxT>Q6Ez?51*yoqo?lIQnD}4n}gDUCQ&IFyS884Ke%7G=qsLL!o4I+g& z1rgs=VotqxRCfz)-n>F|7g|@2vz%K zGt(domY=7hyfW&Y*_Fr9Lq2EAGS&k9PJd*svIn+!88LjR$W-4C>8;H5cqH!$fGaj$ zoaT?~8F?`W`IdeN>SIQJULO?1HM1Ofe>!n5kX3jKJ@V=nprn=@WNA_{^8Sd#@dF>sFjG4SDqLp(h z2*f7-ba<_qIZZ$)p7lBrZ_NW~9Vtrqwx>r7u!%y}ym@YUib|`!DzZkHl-`Z=$RpO6 z;QLX$STlKuSFMykB7E-Fy%Hx&lrdP?vX<4xm z5y7N@_w(U`;G?a^@y*V&sgKow}HfgF1#4A(8qx9PDlzE~>Fe>}bTmuzI^k2n3|@+rB8O`LnXiN(8Ma;Uw@ zf7rEx+K1z#=k1}QdJs){WttX|rH@3rLPbqhI;p8?Ht!7QLd90 zk(jl{nbLlt%g?g_fPLE3__$id{zwjoC2B^xblha6YFtm>)dqw>&U&1sRP&Ln_^Gf) zi%dXO)7q`E{7lqMnENI=#~S~5gW-!XgF32AvOu3|ik`{g!y|(>_XBf8v?J*jM@?G7 zd@K*r>5;;%vd-K(@XqYjbR-51dnerZYz*piWleQP6*&X~HAW9Vb#zUPJ{Qng+8)Tzay~VtgtXrXLu!EV+T8(jAA5oi6m(kvxNr8B-9nFVZtbO~e4r zDK~yC^jSPdx9VE1#u!D~8SKY*=tJ);pcw~O4-XtXXTlCQYG4Mrx+K<)w!76O7M68A z@TUNIA&X>1D7mmq8@mO<;@nwIgU>)WxSu7viP+2;z+NI@~6b*1lWJD?P0as~S*G=;5Yi*tW_4W>(pTMYsKAQ+gf z-JsjH$baTpiLm%plmb|tqAG`jOH#4=*8rLIzZ_u$sV~Ln2RdtUKy?>#z&f9NY)eY~ E8}T?SegFUf From b63737a0e3f2a9b6ef13a9450fdcf33b2a6bd8aa Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 2 Sep 2021 14:21:36 +0200 Subject: [PATCH 118/716] render on farm / local flowchart --- website/docs/artist_hosts_nuke_tut.md | 1 + .../assets/nuke_tut/nuke_RenderLocalFarm.png | Bin 0 -> 66946 bytes 2 files changed, 1 insertion(+) create mode 100644 website/docs/assets/nuke_tut/nuke_RenderLocalFarm.png diff --git a/website/docs/artist_hosts_nuke_tut.md b/website/docs/artist_hosts_nuke_tut.md index d285d8a6ff..0ec1e104b9 100644 --- a/website/docs/artist_hosts_nuke_tut.md +++ b/website/docs/artist_hosts_nuke_tut.md @@ -162,6 +162,7 @@ If you wish to check your render before publishing, you can use your local machi If you want to render and publish on the farm in one go, run publish with On farm option selected in the write node to render and make the review on farm. +![Versionless](assets/nuke_tut/nuke_RenderLocalFarm.png) ## Version-less Render diff --git a/website/docs/assets/nuke_tut/nuke_RenderLocalFarm.png b/website/docs/assets/nuke_tut/nuke_RenderLocalFarm.png new file mode 100644 index 0000000000000000000000000000000000000000..4c4c8977a0bdfbcfed3c509c5aaf2b626c2b4341 GIT binary patch literal 66946 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 Fe*pnbU4H-o literal 0 HcmV?d00001 From 8be9838e011f187641ff4cc78c087c2fc1e513a6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Sep 2021 18:47:03 +0200 Subject: [PATCH 119/716] #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 120/716] #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 121/716] #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 122/716] Hound --- openpype/plugins/publish/collect_host_name.py | 1 - tests/lib/testing_wrapper.py | 4 ++-- .../sync_server/test_publish_in_maya.py | 20 +++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/collect_host_name.py b/openpype/plugins/publish/collect_host_name.py index 17cb59b212..b731e3ed26 100644 --- a/openpype/plugins/publish/collect_host_name.py +++ b/openpype/plugins/publish/collect_host_name.py @@ -35,4 +35,3 @@ class CollectHostName(pyblish.api.ContextPlugin): host_name = app.host_name context.data["hostName"] = host_name - diff --git a/tests/lib/testing_wrapper.py b/tests/lib/testing_wrapper.py index b2a89edec7..0a7c9a382f 100644 --- a/tests/lib/testing_wrapper.py +++ b/tests/lib/testing_wrapper.py @@ -137,8 +137,8 @@ class TestCase: @pytest.fixture(scope="module") def last_workfile_path(self, download_test_data): - raise NotImplemented + raise NotImplementedError @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): - raise NotImplemented + raise NotImplementedError diff --git a/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py b/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py index 2aca0314dc..612a657c12 100644 --- a/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py +++ b/tests/unit/openpype/modules/sync_server/test_publish_in_maya.py @@ -122,18 +122,17 @@ class TestPublishInMaya(TestCase): assert 5 == dbcon.find({"type": "version"}).count(), \ "Not expected no of versions" - assert 0 == \ - dbcon.find({"type": "version", "name": {"$ne": 1}}).count(), \ - "Only versions with 1 expected" + assert 0 == dbcon.find({"type": "version", + "name": {"$ne": 1}}).count(), \ + "Only versions with 1 expected" - assert 1 == \ - dbcon.find({"type": "subset", "name": "modelMain"}).count(), \ - "modelMain subset must be present" + assert 1 == dbcon.find({"type": "subset", + "name": "modelMain"}).count(), \ + "modelMain subset must be present" - assert 1 == \ - dbcon.find( - {"type": "subset", "name": "workfileTest_task"}).count(), \ - "workfileTest_task subset must be present" + assert 1 == dbcon.find({"type": "subset", + "name": "workfileTest_task"}).count(), \ + "workfileTest_task subset must be present" assert 11 == dbcon.find({"type": "representation"}).count(), \ "Not expected no of representations" @@ -202,5 +201,6 @@ class TestPublishInMaya(TestCase): "test_project_test_asset_workfileTest_task_v001.mb") ), "test_project_test_asset_workfileTest_task_v001.mb doesn't exist" + if __name__ == "__main__": test_case = TestPublishInMaya() From b6754d8827a1761d2a70792713bf4317a5c6a25d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Sep 2021 14:46:15 +0200 Subject: [PATCH 123/716] added single selection option to task type enum --- openpype/settings/entities/enum_entity.py | 57 +++++++++++++++++++---- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index cb532c5ae0..6c0e63fa1f 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -376,11 +376,16 @@ class TaskTypeEnumEntity(BaseEnumEntity): schema_types = ["task-types-enum"] def _item_initalization(self): - self.multiselection = True - self.value_on_not_set = [] + self.multiselection = self.schema_data.get("multiselection", True) + if self.multiselection: + self.valid_value_types = (list, ) + self.value_on_not_set = [] + else: + self.valid_value_types = (STRING_TYPE, ) + self.value_on_not_set = "" + self.enum_items = [] self.valid_keys = set() - self.valid_value_types = (list, ) self.placeholder = None def _get_enum_values(self): @@ -396,15 +401,51 @@ class TaskTypeEnumEntity(BaseEnumEntity): return enum_items, valid_keys + def _convert_value_for_current_state(self, source_value): + if self.multiselection: + output = [] + for key in source_value: + if key in self.valid_keys: + output.append(key) + return output + + if source_value not in self.valid_keys: + # Take first item from enum items + for item in self.enum_items: + for key in item.keys(): + source_value = key + break + return source_value + def set_override_state(self, *args, **kwargs): super(TaskTypeEnumEntity, self).set_override_state(*args, **kwargs) self.enum_items, self.valid_keys = self._get_enum_values() - new_value = [] - for key in self._current_value: - if key in self.valid_keys: - new_value.append(key) - self._current_value = new_value + + if self.multiselection: + new_value = [] + for key in self._current_value: + if key in self.valid_keys: + new_value.append(key) + + if self._current_value != new_value: + self.set(new_value) + else: + if not self.enum_items: + self.valid_keys.add("") + self.enum_items.append({"": "< Empty >"}) + + for item in self.enum_items: + for key in item.keys(): + value_on_not_set = key + break + + self.value_on_not_set = value_on_not_set + if ( + self._current_value is NOT_SET + or self._current_value not in self.valid_keys + ): + self.set(value_on_not_set) class ProvidersEnum(BaseEnumEntity): From 64e7dbb3475e326d999b094c88d2c9579b36eb4b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Sep 2021 14:46:29 +0200 Subject: [PATCH 124/716] fixed valid_value_types for providers --- openpype/settings/entities/enum_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 6c0e63fa1f..ee54bc6e02 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -456,7 +456,7 @@ class ProvidersEnum(BaseEnumEntity): self.value_on_not_set = "" self.enum_items = [] self.valid_keys = set() - self.valid_value_types = (str, ) + self.valid_value_types = (STRING_TYPE, ) self.placeholder = None def _get_enum_values(self): From 8399de95cb9cb71e3d8b185267724aebb7256ce4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Sep 2021 16:51:11 +0200 Subject: [PATCH 125/716] nuke, resolve, hiero: precollector order lest then 0.5 --- openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_workfile.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_workfile.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_writes.py | 2 +- openpype/hosts/resolve/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/resolve/plugins/publish/precollect_workfile.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py index b0b171fb61..80c6abbaef 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py @@ -5,7 +5,7 @@ import pyblish.api class PreCollectClipEffects(pyblish.api.InstancePlugin): """Collect soft effects instances.""" - order = pyblish.api.CollectorOrder - 0.579 + order = pyblish.api.CollectorOrder - 0.479 label = "Precollect Clip Effects Instances" families = ["clip"] diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 9b529edf88..936ea2be58 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -13,7 +13,7 @@ from pprint import pformat class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Precollect Instances" hosts = ["hiero"] diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 530a433423..ff5d516065 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -12,7 +12,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.6 + order = pyblish.api.CollectorOrder - 0.5 def process(self, context): diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index c2c25d0627..75d0b4f9a9 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -8,7 +8,7 @@ from avalon.nuke import lib as anlib class PreCollectNukeInstances(pyblish.api.ContextPlugin): """Collect all nodes with Avalon knob.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Pre-collect Instances" hosts = ["nuke", "nukeassist"] diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 5d3eb5f609..8b1ccb8cef 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -9,7 +9,7 @@ reload(anlib) class CollectWorkfile(pyblish.api.ContextPlugin): """Collect current script for publish.""" - order = pyblish.api.CollectorOrder - 0.60 + order = pyblish.api.CollectorOrder - 0.50 label = "Pre-collect Workfile" hosts = ['nuke'] diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index 0b5fbc0479..47189c31fc 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -11,7 +11,7 @@ from avalon import io, api class CollectNukeWrites(pyblish.api.InstancePlugin): """Collect all write nodes.""" - order = pyblish.api.CollectorOrder - 0.58 + order = pyblish.api.CollectorOrder - 0.48 label = "Pre-collect Writes" hosts = ["nuke", "nukeassist"] families = ["write"] diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index 95b891d95a..8f1a13a4e5 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -8,7 +8,7 @@ from pprint import pformat class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Precollect Instances" hosts = ["resolve"] diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index ee05fb6f13..1333516177 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -13,7 +13,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Precollect the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.6 + order = pyblish.api.CollectorOrder - 0.5 def process(self, context): From 7e088d6b977f02560001cfef82650b8a47d16684 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Sep 2021 17:45:32 +0200 Subject: [PATCH 126/716] #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 127/716] #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 6d270bfeb5d951fe09ef580e5b7d625c3f2ec753 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 3 Sep 2021 19:44:10 +0200 Subject: [PATCH 128/716] script fixes --- Dockerfile | 9 ++++++--- pyproject.toml | 2 +- tools/build.sh | 15 +++++++++------ tools/create_env.sh | 22 ++++++++++++++-------- tools/docker_build.sh | 4 +++- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2d8ed27b15..78611860ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n readline-devel \ sqlite sqlite-devel \ openssl-devel \ + openssl-libs \ tk-devel libffi-devel \ qt5-qtbase-devel \ patchelf \ @@ -73,10 +74,12 @@ RUN source $HOME/.bashrc \ && ./tools/fetch_thirdparty_libs.sh RUN source $HOME/.bashrc \ - && bash ./tools/build.sh \ - && cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.7/lib \ + && bash ./tools/build.sh + +RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.7/lib \ && cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.7/lib \ - && cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.7/lib + && cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.7/lib \ + && cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib RUN cd /opt/openpype \ rm -rf ./vendor/bin diff --git a/pyproject.toml b/pyproject.toml index e376986606..a57ae19224 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ slack-sdk = "^3.6.0" flake8 = "^3.7" autopep8 = "^1.4" coverage = "*" -cx_freeze = "^6.6" +cx_freeze = "*" GitPython = "^3.1.17" jedi = "^0.13" Jinja2 = "^2.11" diff --git a/tools/build.sh b/tools/build.sh index c44e7157af..bc79f03db7 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -58,7 +58,7 @@ BICyan='\033[1;96m' # Cyan BIWhite='\033[1;97m' # White args=$@ -disable_submodule_update = 0 +disable_submodule_update=0 while :; do case $1 in --no-submodule-update) @@ -90,6 +90,7 @@ done ############################################################################### detect_python () { echo -e "${BIGreen}>>>${RST} Using python \c" + command -v python >/dev/null 2>&1 || { echo -e "${BIRed}- NOT FOUND${RST} ${BIYellow}You need Python 3.7 installed to continue.${RST}"; return 1; } local version_command version_command="import sys;print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))" local python_version @@ -122,7 +123,7 @@ clean_pyc () { local path path=$openpype_root echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c" - find "$path" -path ./build -prune -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete + find "$path" -path ./build -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete echo -e "${BIGreen}DONE${RST}" } @@ -173,7 +174,7 @@ main () { else echo -e "${BIYellow}NOT FOUND${RST}" echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..." - . "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; } + . "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return 1; } fi if [ "$disable_submodule_update" == 1 ]; then @@ -184,9 +185,9 @@ if [ "$disable_submodule_update" == 1 ]; then fi echo -e "${BIGreen}>>>${RST} Building ..." if [[ "$OSTYPE" == "linux-gnu"* ]]; then - "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" build > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } + "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" build &> "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; } elif [[ "$OSTYPE" == "darwin"* ]]; then - "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" bdist_mac > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } + "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" bdist_mac &> "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; } fi "$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/build_dependencies.py" @@ -210,4 +211,6 @@ if [ "$disable_submodule_update" == 1 ]; then echo -e "${BIWhite}$openpype_root/build${RST} directory." } -main +return_code=0 +main || return_code=$? +exit $return_code diff --git a/tools/create_env.sh b/tools/create_env.sh index cc9eddc317..4ed6412c43 100755 --- a/tools/create_env.sh +++ b/tools/create_env.sh @@ -88,6 +88,7 @@ done ############################################################################### detect_python () { echo -e "${BIGreen}>>>${RST} Using python \c" + command -v python >/dev/null 2>&1 || { echo -e "${BIRed}- NOT FOUND${RST} ${BIYellow}You need Python 3.7 installed to continue.${RST}"; return 1; } local version_command="import sys;print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))" local python_version="$(python <<< ${version_command})" oIFS="$IFS" @@ -125,7 +126,7 @@ clean_pyc () { local path path=$openpype_root echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c" - find "$path" -path ./build -prune -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete + find "$path" -path ./build -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete echo -e "${BIGreen}DONE${RST}" } @@ -166,7 +167,7 @@ main () { echo -e "${BIGreen}OK${RST}" else echo -e "${BIYellow}NOT FOUND${RST}" - install_poetry || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; } + install_poetry || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return 1; } fi if [ -f "$openpype_root/poetry.lock" ]; then @@ -175,7 +176,11 @@ main () { echo -e "${BIGreen}>>>${RST} Installing dependencies ..." fi - "$POETRY_HOME/bin/poetry" install --no-root $poetry_verbosity || { echo -e "${BIRed}!!!${RST} Poetry environment installation failed"; return; } + "$POETRY_HOME/bin/poetry" install --no-root $poetry_verbosity || { echo -e "${BIRed}!!!${RST} Poetry environment installation failed"; return 1; } + if [ $? -ne 0 ] ; then + echo -e "${BIRed}!!!${RST} Virtual environment creation failed." + return 1 + fi echo -e "${BIGreen}>>>${RST} Cleaning cache files ..." clean_pyc @@ -184,10 +189,11 @@ main () { # cx_freeze will crash on missing __pychache__ on these but # reinstalling them solves the problem. echo -e "${BIGreen}>>>${RST} Fixing pycache bug ..." - "$POETRY_HOME/bin/poetry" run python -m pip install --force-reinstall pip - "$POETRY_HOME/bin/poetry" run pip install --force-reinstall setuptools - "$POETRY_HOME/bin/poetry" run pip install --force-reinstall wheel - "$POETRY_HOME/bin/poetry" run python -m pip install --force-reinstall pip + "$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall setuptools + "$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall wheel + "$POETRY_HOME/bin/poetry" run python -m pip install --disable-pip-version-check --force-reinstall pip } -main -3 +return_code=0 +main || return_code=$? +exit $return_code diff --git a/tools/docker_build.sh b/tools/docker_build.sh index 7600fe044b..dca217d534 100755 --- a/tools/docker_build.sh +++ b/tools/docker_build.sh @@ -32,7 +32,8 @@ main () { openpype_version="$(python3 <<< ${version_command})" echo -e "${BIGreen}>>>${RST} Running docker build ..." - docker build --pull --no-cache -t pypeclub/openpype:$openpype_version . + # docker build --pull --no-cache -t pypeclub/openpype:$openpype_version . + docker build --pull -t pypeclub/openpype:$openpype_version . if [ $? -ne 0 ] ; then echo -e "${BIRed}!!!${RST} Docker build failed." return 1 @@ -47,6 +48,7 @@ main () { fi echo -e "${BIYellow}---${RST} Copying ..." docker cp "$id:/opt/openpype/build/exe.linux-x86_64-3.7" "$openpype_root/build" + docker cp "$id:/opt/openpype/build/build.log" "$openpype_root/build" if [ $? -ne 0 ] ; then echo -e "${BIRed}!!!${RST} Copying failed." return 1 From d26095883921ac187dfccde229f38ceee5eea745 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 6 Sep 2021 10:43:50 +0200 Subject: [PATCH 129/716] try to get error log from failed build --- tools/docker_build.sh | 44 +++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/tools/docker_build.sh b/tools/docker_build.sh index dca217d534..c27041a1af 100755 --- a/tools/docker_build.sh +++ b/tools/docker_build.sh @@ -20,6 +20,28 @@ realpath () { echo $(cd $(dirname "$1"); pwd)/$(basename "$1") } +create_container () { + if [ ! -f "$openpype_root/build/docker-image.id" ]; then + echo -e "${BIRed}!!!${RST} Docker command failed, cannot find image id." + exit 1 + fi + local id=$(<"$openpype_root/build/docker-image.id") + echo -e "${BIYellow}---${RST} Creating container from $id ..." + local cid="$(docker create $id bash)" + if [ $? -ne 0 ] ; then + echo -e "${BIRed}!!!${RST} Cannot create container." + exit 1 + fi + return $cid +} + +retrieve_build_log () { + create_container + local cid=$? + echo -e "${BIYellow}***${RST} Copying build log to ${BIWhite}$openpype_root/build/build.log${RST}" + docker cp "$cid:/opt/openpype/build/build.log" "$openpype_root/build" +} + # Main main () { openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) @@ -28,34 +50,32 @@ main () { echo -e "${BIYellow}---${RST} Cleaning build directory ..." rm -rf "$openpype_root/build" && mkdir "$openpype_root/build" > /dev/null - version_command="import os;exec(open(os.path.join('$openpype_root', 'openpype', 'version.py')).read());print(__version__);" - openpype_version="$(python3 <<< ${version_command})" + local version_command="import os;exec(open(os.path.join('$openpype_root', 'openpype', 'version.py')).read());print(__version__);" + local openpype_version="$(python3 <<< ${version_command})" echo -e "${BIGreen}>>>${RST} Running docker build ..." # docker build --pull --no-cache -t pypeclub/openpype:$openpype_version . - docker build --pull -t pypeclub/openpype:$openpype_version . + docker build --pull --iidfile $openpype_root/build/docker-image.id -t pypeclub/openpype:$openpype_version . if [ $? -ne 0 ] ; then + echo $? echo -e "${BIRed}!!!${RST} Docker build failed." + retrieve_build_log return 1 fi echo -e "${BIGreen}>>>${RST} Copying build from container ..." - echo -e "${BIYellow}---${RST} Creating container from pypeclub/openpype:$openpype_version ..." - id="$(docker create -ti pypeclub/openpype:$openpype_version bash)" - if [ $? -ne 0 ] ; then - echo -e "${BIRed}!!!${RST} Cannot create just built container." - return 1 - fi + create_container + local cid=$? echo -e "${BIYellow}---${RST} Copying ..." - docker cp "$id:/opt/openpype/build/exe.linux-x86_64-3.7" "$openpype_root/build" - docker cp "$id:/opt/openpype/build/build.log" "$openpype_root/build" + docker cp "$cid:/opt/openpype/build/exe.linux-x86_64-3.7" "$openpype_root/build" + docker cp "$cid:/opt/openpype/build/build.log" "$openpype_root/build" if [ $? -ne 0 ] ; then echo -e "${BIRed}!!!${RST} Copying failed." return 1 fi echo -e "${BIGreen}>>>${RST} Fixing user ownership ..." - username="$(logname)" + local username="$(logname)" chown -R $username ./build echo -e "${BIGreen}>>>${RST} All done, you can delete container:" From d4a6db63f467c740d13af5fc644b6ee6ab669d0c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 6 Sep 2021 11:41:26 +0200 Subject: [PATCH 130/716] Fix added underscore to internal methods --- .../plugins/publish/extract_harmony_zip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py index 85da01c890..adbac6ef09 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py @@ -66,10 +66,10 @@ class ExtractHarmonyZip(openpype.api.Extractor): # Get Task types and Statuses for creation if needed self.task_types = self._get_all_task_types(project_entity) - self.task_statuses = self.get_all_task_statuses(project_entity) + self.task_statuses = self._get_all_task_statuses(project_entity) # Get Statuses of AssetVersions - self.assetversion_statuses = self.get_all_assetversion_statuses( + self.assetversion_statuses = self._get_all_assetversion_statuses( project_entity ) From d0a4293b9d12e59b59bac0c9845f45fc84839ff3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 6 Sep 2021 18:42:34 +0200 Subject: [PATCH 131/716] #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 zcmb7I1z1$u8XdZE=nm;_LAtv}N|27ByE`OAU}#VpL68msX(XiuK^hd0ZV(Uz^$nNj zMY-H}@7sKbGc(`6*4g{t|K98D1yY2A#|6Lw008_XI(6vn&xQ*9 z8Ao(24(GQxu%{!~)D>*@3xMhx0Qeig%HGk<^_OVuYti^OM4P$UIlBB048`AK92~*+ zj$V%7Kf%ZV~5Z-Q;;l_Z{v#(ni-;f;D+uA>_3_4;u= zI>y9!s)`ru+DgUns)GsnWtapLep49acxd0{2Wk0TWj4*{imJI2Uk69VWvnWFZM?j| ze%qMx8u;{#E>lYWvOov!cI4*YZd;S;@PlY*>`z9zUMyVrD>_u zk;FT^8hqleVs%_zQL(~>Hsp7;rFNAStI;$Q-g4g}F+&Z$Wt|dOA(V#f@d()?^JueI z2U)cFn;&KHmxB*NZdS^iDfzlzBFgn{(~=Rb+ClY_syq@ zw+DCa2k9DL|6G@sbzJI8MOF;0-xKJ?`7i6(4(w`tvBcS|p#=?&>BoG?0hIm$UQ{tb zO$2FgwnIdX(AP~lAbGT67z!sIx^qt}npX#!QxNn-tt?~<2x6Od?}-azkmC>@k(f4A zwcy+(%^;wIk0qB&dBOuG65f_auz@AZ`Y}+I@lXpWVT1wF4do6C+Jqf~BzK)5wNP1B zPwySh7<&=C2p(?rR%|^hkkGAEQqx5JfUgm!$MJ@rogpqsstbr6`<5_qzpKBzqwWlO z6Y2Mn3wYKS?n6iH0A0l_|2%RRu&b+;{eM?+Mx3%jrx;L%8K)~S4NK1(*@JDCX{|kU zzNO?jbBg;{dRI+EXPhq|I;#p+E8K?ToZ#i{Qgb$iMNTqN1L3L>cMP)}>?vltdgr?O zmAW&HF#%i=FJ+B2Nd$sMVzE!qlSES6Dgpj1rF7QGj>&NmaAVOJt>h+pP733Wt+X)@ zk-JY#+)*|CTXwTGHu+J!QQteImc()L@Y_7F6O8yFh@RlYyLSjwXp4;uI(z1>gV8{#P(K>s0e!7z(GEu z7O8qsi2wP_XUyMI=k**{#TS~gH0X8b|47~Mg)<-yEgamCg+?X4mq3NQV?_I=Ku-9t z(~yR;Gd4N@39nxds4$ z>*7KFx5K$qHj`g;K*MlVt$nVv_X$)3cDVrn_J73w!QtraU=3BgKjdKhfxa#2eY_UI zMjkuV_tg*uMbsVDhzxIG?#-hzzCKl);>U0*CfM}iyrUz zfzt#u3p-niNlru03{b6<2 ze|47I|I9RG%Z5L+ai3iuct==wY(TroCgcHI{)4@l!$y z?7%j};EwBS$(9Xc3 zugDm^S_!1PFaDxQBC~61!1HXOu(;I9S)P$mOkJIaSv&vb1E%CaEvg~u>TU$PJ2WbJ z;bg%L?s6;%sxqDQ^N$N_CBQ=y$x5i`qf17efjb+MLH62VC2w(G1nJ4g8OP_T-)FCvpK0!;0$R3atfiSKInhJW>?c1L4GigStKEX#py?w z3EQ8S%J?KQ54C9-QZ}9+ru+d`bU9^XsnK#n_f_L8msnT8C)-A=$*KX`RY~RHFzJj} z)vY0|(uh0UvT)_90Lg`9kGL=$g#}wLS8B9no91|Dm9W}4z|bIVOe!$*A}FuZ+bl3iO(Z4 zH+j0GKb>D}!Aid;>`aEhck0z8K-76bK&8WPj!!qY=4JBTVd zxu8?YW@7igM%t@QPHCUU)|Zg5W(oa?;leTPd>eyY@i&fkp+bm1q+4P&R@&<{D3QD$ zp2LjyyNvaGzMCY?yh#YdcW@nChc~P$mkzc_)r(R=?N4rA zit{0De1m-w!e&{~Nm*)X4jAB&yRVN3cJ7S$-dzG*?IZ*-=PHJYPQM$#hpV$*jOE)d zg99xSvCL1=$SEzQE<{Ba<{GG14P$;n|Mmvk$12EyPk#pG#WOI$Tp$;V2DZ_1E+UJ+ zggL3EmM$xTe<{9Xw<^7sgM@Wu!FrdQ`-~zw13h+Rw@SpkvS+!10@j&&&)cSv;Wmge zGHI}P8lF+|H`CctcE?(Bxdyj31l+k)i;uHonPe1eIvOk0=H4B3TB9|znjax$vX!xSMi@b_C_J~>WmEWUV6y%-V`pG{#)xe=lmcPcUy~>u5LKm z`0GU!bvy|jNHMYJO?K>?uAxulI5AzzPhzk5O==WuwYRp76(YPv~iCu zjVLUI>7d=%^ql7^ZDTaTK-*wo@#d}_2}D(TA*w2pnX`h$_OwiUJZF@GU z%*u1sPNHdxS9k?&_RxXK-!O=DmffOOF|?6yP2ba_OMlE)OhSUuHa8yykxPK&LfklT z@g+&R=_hV07~{EzahwIeSK^x7zN6?ur1!olgz6*&kX|y#h{{- zL3klV!X+3NyDFYZJQgnBmQ+-L5&gCFatLUtYxcuv+5O{$Pd&h9UQj)H&Adqr3{Iu- z#s?rt+cpZjs%WHiTYEwVUm2%iNxJ2f27r|B$Hy_pvu#RHq=6EYef9px)-zrx$^$x#(auVSGjhrUzcAC4bWD9}#R zP524BC4d5j_T$=t;;mP8iA8Gq8e`6O4ZaXLP?cnFMQXAHTn6sEY$V~%TdfH7DpeEm<2N@|@f(b$&Cp%iLu$%A}KGw9GD~eJ)_-Zse z!qyc@IRFl|sKg~o|HAmGlckX&G3V<@8&}NKcOi4HWc-~ z>E2nB#knaP-=d~0;wxBt=t2>gPqq3WM;eQT-Po%7OVM(j9Q*G(%+z9@yi~93`bwKc z6CeZoaNn671Bt zUO0D=u1}u_OUc(Nwu~Bj{CMIy$G>JCpCPV=Tg>J!Wt5*2m6VBB`g)0(6IV~dv$)Ha zCgwAbTI(>X7O3Yj)(E)BvZ^NaSu4{b)XfEXRfxl`@RLd-D8l<^lkj>)eNj9N+ex7K zzIflmVHJK{Z%{1x9hppW+S?sz{KRcIv(qdw{GB4!+i0d_fh=gGIxAeU%U$|mECr~t z$vz)@QOxS=^v#007JIn#S(Eyf1&dH=HNe{1rxv7IEgAP(TB-*Rl0KoiE57R zyutJbBhN~jTv_@QYr_Twu9*lmG#iT0gjED=_ z=g=eL{pN_b^qekuROR(uC5yBn^d4R!8=3?qa8A)UZl2bZd9v51CzUMJ7J;27m=EFo zySLpQi3UcHg+{>r@Bqc3mx~8-4#MO?%Yd@(+1rODaBAHsnF2*b zAoUfP(>!B!*@BStadXG7YZP%pLFo*_Y}h^Lf$;J5eefkCinlkUN}p@>k8*nysjH8z zPgn3Sb(Wj*FNt0qcP`%XjWOTP`9t6Bv!K`A|L-0D_Y+T)IA%K~F2e4?Vmv}Mgtj?| zgx!dEq@7+|mxGlMV_|2c&h^xsj=jT6(qWo&mdk*UOGTuV+m3*OGh5u)b9fclmmkGMsN!zmT!^fS*ro`a?QBb>S*Lx&RB>`r508eC3%T1E@oY)t%+l!XH7yC1C&n+W!UnFa8gUpm=3j z4FF5~191O!U=Luz(=b5PBY>6}A#VdXF(2!xvHyUUtiZdQLzW^y)I_tROP^GdG=nr) z|I;^8KmE!zI22QD?8WuX4GLmdHG5klqd>Op#So63E|f+r81e50Wp8gIhk{^u#(;bc z0-+7Ttde|PQy+&0aafWJV^i;gbYhD-`!&%3f1d!n8%A>BZTxxGgL-_|Oh&>RATF-P z&aQ5bzo00up>E2whl8_?xs~m&1|v@-mHLcuU2G0C%l%M$;&;PTO!a+H)5y9}-;i(2QZ6$( zyjpBe)!FHi4r3}lF{=b%j2}WqthNI0*0pYfRS_mb==kASfeM7p&jgXyZZ`(p2KVLT z3sR<8Xx0}WXT4%y#*FA1UD-VL;cwZgP3t0K(c0XY*#Jw3lpFMiG~0^^mAel`f-{$= zek4{N!n8@3-fMq1gDJr*6cH>l+?P(j)jN#?M^f$$zc4Eq|3G%vi%>X_3IwzFq$5l_ z?16Z#_`I@JM#Q<6Jz_zqmI}F+ z=D@|lub&GXyKS9JXrG&V4aQ`kk;Z5#9+C?A&iZi@_@>tv2->Ld-abltl>hNdJJQoY zSM#+6*$O)O;;_U!?YBm=&-J}*)V{QOG3k)Z4-q};HdqS}a8Pqe+EX{oOh7Ud*S$^O zT*&I)>0R{9(zIJ}3DQgBYsVA2hpXRk$ABMoR7=7pH+eqS_{WGLLMgbv&Z&!$KrQnd z97quc78mgMZaK7wpzXrmzG%OC=U2F!-1E!$3*7JC^Dkcd6~Rqz`W1oBZx8Jx@f5-aUc7L7zy5V?15BP(=0sbH9f3qL2Ghg@PE_g3HeuVJv zng6Eqf7Oc%kL;W@z+c4lDib%!=>^SYXLtV>&9CBmHU7U&)P=5Hwu_$C z Date: Tue, 7 Sep 2021 11:47:23 +0200 Subject: [PATCH 132/716] added tine addon --- openpype/modules/example_addons/tiny_addon.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 openpype/modules/example_addons/tiny_addon.py diff --git a/openpype/modules/example_addons/tiny_addon.py b/openpype/modules/example_addons/tiny_addon.py new file mode 100644 index 0000000000..62962954f5 --- /dev/null +++ b/openpype/modules/example_addons/tiny_addon.py @@ -0,0 +1,9 @@ +from openpype.modules import OpenPypeAddOn + + +class TinyAddon(OpenPypeAddOn): + """This is tiniest possible addon. + + This addon won't do much but will exist in OpenPype modules environment. + """ + name = "tiniest_addon_ever" From 3dd69032510e8087a889c3bf38fbde96ce1204d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 11:53:59 +0200 Subject: [PATCH 133/716] added base of example addon --- .../example_addons/example_addon/__init__.py | 13 ++ .../example_addons/example_addon/addon.py | 124 ++++++++++++++++++ .../example_addon/interfaces.py | 28 ++++ .../plugins/publish/example_plugin.py | 10 ++ .../settings/defaults/project_settings.json | 1 + .../project_dynamic_schemas.json | 6 + .../system_dynamic_schemas.json | 6 + .../schemas/project_schemas/main.json | 29 ++++ .../schemas/project_schemas/the_template.json | 30 +++++ .../settings/schemas/system_schemas/main.json | 14 ++ .../example_addons/example_addon/widgets.py | 30 +++++ 11 files changed, 291 insertions(+) create mode 100644 openpype/modules/example_addons/example_addon/__init__.py create mode 100644 openpype/modules/example_addons/example_addon/addon.py create mode 100644 openpype/modules/example_addons/example_addon/interfaces.py create mode 100644 openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py create mode 100644 openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json create mode 100644 openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json create mode 100644 openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json create mode 100644 openpype/modules/example_addons/example_addon/widgets.py diff --git a/openpype/modules/example_addons/example_addon/__init__.py b/openpype/modules/example_addons/example_addon/__init__.py new file mode 100644 index 0000000000..df4d61650b --- /dev/null +++ b/openpype/modules/example_addons/example_addon/__init__.py @@ -0,0 +1,13 @@ +""" Addon class definition and Settings definition must be imported here. + +If addon class or settings definition won't be here their definition won't +be found by OpenPype discovery. +""" + +from .addon import ( + AddonSettingsDef, +) + +__all__ = ( + "AddonSettingsDef", +) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py new file mode 100644 index 0000000000..64504be756 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -0,0 +1,124 @@ +"""Addon definition is located here. + +Import of python packages that may not be available should not be imported +in global space here until are required or used. +- Qt related imports +- imports of Python 3 packages + - we still support Python 2 hosts where addon definition should available +""" + +import os + +from openpype.modules import ( + JsonFilesSettingsDef, + OpenPypeAddOn +) +# Import interface defined by this addon to be able find other addons using it +from openpype_interfaces import ( + IExampleInterface, + IPluginPaths, + ITrayAction +) + + +# Settings definiton of this addon using `JsonFilesSettingsDef` +# - JsonFilesSettingsDef is prepared settings definiton using json files +# to define settings and store defaul values +class AddonSettingsDef(JsonFilesSettingsDef): + # This will add prefix to every schema and template from `schemas` + # subfolder. + # - it is not required to fill the prefix but it is highly + # recommended as schemas and templates may have name clashes across + # multiple addons + # - it is also recommended that prefix has addon name in it + schema_prefix = "addon_with_settings" + + def get_settings_root_path(self): + """Implemented abstract class of JsonFilesSettingsDef. + + Return directory path where json files defying addon settings are + located. + """ + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "settings" + ) + + +class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): + """This Addon has defined it's settings and interface. + + This example has system settings with enabled option. And use + few other interfaces: + - `IPluginPaths` to define custom plugin paths + - `ITrayAction` to be shown in tray tool + """ + label = "Example Addon" + name = "example_addon" + + def initialize(self, settings): + """Initialization of addon.""" + module_settings = settings[self.name] + # Enabled by settings + self.enabled = module_settings.get("enabled", False) + + # Prepare variables that can be used or set afterwards + self._connected_modules = None + # UI which must not be created at this time + self._dialog = None + + def connect_with_modules(self, enabled_modules): + """Method where you should find connected modules. + + It is triggered by OpenPype modules manager at the best possible time. + Some addons and modules may required to connect with other modules + before their main logic is executed so changes would require to restart + whole process. + """ + self._connected_modules = [] + for module in enabled_modules: + if isinstance(module, IExampleInterface): + self._connected_modules.append(module) + + def _create_dialog(self): + # Don't recreate dialog if already exists + if self._dialog is not None: + return + + from .widgets import MyExampleDialog + + self._dialog = MyExampleDialog() + + def show_dialog(self): + """Show dialog with connected modules. + + This can be called from anywhere but can also crash in headless mode. + There is not way how to prevent addon to do invalid operations if he's + not handling them. + """ + # Make sure dialog is created + self._create_dialog() + # Change value of dialog by current state + self._dialog.set_connected_modules(self.get_connected_modules()) + # Show dialog + self._dialog.open() + + def get_connected_modules(self): + """Custom implementation of addon.""" + names = set() + if self._connected_modules is not None: + for module in self._connected_modules: + names.add(module.name) + return names + + def on_action_trigger(self): + """Implementation of abstract method for `ITrayAction`.""" + self.show_dialog() + + def get_plugin_paths(self): + """Implementation of abstract method for `IPluginPaths`.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + + return { + "publish": [os.path.join(current_dir, "plugins", "publish")] + } diff --git a/openpype/modules/example_addons/example_addon/interfaces.py b/openpype/modules/example_addons/example_addon/interfaces.py new file mode 100644 index 0000000000..371536efc7 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/interfaces.py @@ -0,0 +1,28 @@ +""" Using interfaces is one way of connecting multiple OpenPype Addons/Modules. + +Interfaces must be in `interfaces.py` file (or folder). Interfaces should not +import module logic or other module in global namespace. That is because +all of them must be imported before all OpenPype AddOns and Modules. + +Ideally they should just define abstract and helper methods. If interface +require any logic or connection it should be defined in module. + +Keep in mind that attributes and methods will be added to other addon +attributes and methods so they should be unique and ideally contain +addon name in it's name. +""" + +from abc import abstractmethod +from openpype.modules import OpenPypeInterface + + +class IExampleInterface(OpenPypeInterface): + """Example interface of addon.""" + _example_module = None + + def get_example_module(self): + return self._example_module + + @abstractmethod + def example_method_of_example_interface(self): + pass diff --git a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py new file mode 100644 index 0000000000..8e7fb410bd --- /dev/null +++ b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py @@ -0,0 +1,10 @@ +import os +import pyblish.api + + +class CollectExampleAddon(pyblish.api.ContextPlugin): + order = pyblish.api.CollectorOrder + 0.4 + label = "Collect Example Addon" + + def process(self, context): + self.log.info("I'm in example addon's plugin!") diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json @@ -0,0 +1 @@ +{} diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json new file mode 100644 index 0000000000..f6b7d5d146 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json @@ -0,0 +1,6 @@ +{ + "project_settings/global": { + "type": "schema", + "name": "addon_with_settings/main" + } +} diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json new file mode 100644 index 0000000000..6895fb8f6d --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json @@ -0,0 +1,6 @@ +{ + "system_settings/modules": { + "type": "schema", + "name": "addon_with_settings/main" + } +} diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json new file mode 100644 index 0000000000..80e53ace7f --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "key": "exmaple_addon", + "collapsible": true, + "children": [ + { + "type": "number", + "key": "number", + "label": "This is your lucky number:", + "minimum": 7, + "maximum": 7, + "decimals": 0 + }, + { + "type": "template", + "name": "example_addon/the_template", + "template_data": [ + { + "name": "color_1", + "lable": "Color 1" + }, + { + "name": "color_2", + "lable": "Color 2" + } + ] + } + ] +} diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json new file mode 100644 index 0000000000..af8fd9dae4 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json @@ -0,0 +1,30 @@ +[ + { + "type": "list-strict", + "key": "{name}", + "label": "{label}", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + } + ] + } +] diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json new file mode 100644 index 0000000000..0fb0a7c1be --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json @@ -0,0 +1,14 @@ +{ + "type": "dict", + "key": "example_addon", + "label": "Example addon", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] +} diff --git a/openpype/modules/example_addons/example_addon/widgets.py b/openpype/modules/example_addons/example_addon/widgets.py new file mode 100644 index 0000000000..8a74ad859f --- /dev/null +++ b/openpype/modules/example_addons/example_addon/widgets.py @@ -0,0 +1,30 @@ +from Qt import QtWidgets + + +class MyExampleDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super(MyExampleDialog, self).__init__(parent) + + self.setWindowTitle("Connected modules") + + label_widget = QtWidgets.QLabel(self) + + ok_btn = QtWidgets.QPushButton("OK", self) + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(label_widget) + layout.addLayout(btns_layout) + + self._label_widget = label_widget + + def set_connected_modules(self, connected_modules): + if connected_modules: + message = "\n".join(connected_modules) + else: + message = ( + "Other enabled modules/addons are not using my interface." + ) + self._label_widget.setText(message) From 53c6c9818454cb850751a58b8a6eec3907e075c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 12:08:49 +0200 Subject: [PATCH 134/716] #1938 - changed warning to info method Modified logging a bit --- openpype/lib/profiles_filtering.py | 7 ++++--- .../ftrack/plugins/publish/collect_ftrack_family.py | 3 --- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/lib/profiles_filtering.py b/openpype/lib/profiles_filtering.py index c4410204dd..992d757059 100644 --- a/openpype/lib/profiles_filtering.py +++ b/openpype/lib/profiles_filtering.py @@ -165,7 +165,8 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None): if match == -1: profile_value = profile.get(key) or [] logger.debug( - "\"{}\" not found in {}".format(key, profile_value) + "\"{}\" not found in \"{}\": {}".format(value, key, + profile_value) ) profile_points = -1 break @@ -192,13 +193,13 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None): ]) if not matching_profiles: - logger.warning( + logger.info( "None of profiles match your setup. {}".format(log_parts) ) return None if len(matching_profiles) > 1: - logger.warning( + logger.info( "More than one profile match your setup. {}".format(log_parts) ) diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_family.py index cc2a5b7d37..70030acad9 100644 --- a/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -68,9 +68,6 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): instance.data["families"].append("ftrack") else: instance.data["families"] = ["ftrack"] - else: - self.log.debug("Instance '{}' doesn't match any profile".format( - instance.data.get("family"))) def _get_add_ftrack_f_from_addit_filters(self, additional_filters, From 8c37b2b1419d1b5e3065a4ebfe42e305165c1273 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:11:18 +0200 Subject: [PATCH 135/716] fixed settings and widget of example addon --- .../example_addons/example_addon/__init__.py | 2 ++ .../example_addons/example_addon/addon.py | 10 +++++++++- .../settings/defaults/project_settings.json | 16 +++++++++++++++- .../settings/defaults/system_settings.json | 5 +++++ .../dynamic_schemas/project_dynamic_schemas.json | 2 +- .../dynamic_schemas/system_dynamic_schemas.json | 2 +- .../settings/schemas/project_schemas/main.json | 7 ++++--- .../example_addons/example_addon/widgets.py | 9 +++++++++ 8 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json diff --git a/openpype/modules/example_addons/example_addon/__init__.py b/openpype/modules/example_addons/example_addon/__init__.py index df4d61650b..721d924436 100644 --- a/openpype/modules/example_addons/example_addon/__init__.py +++ b/openpype/modules/example_addons/example_addon/__init__.py @@ -6,8 +6,10 @@ be found by OpenPype discovery. from .addon import ( AddonSettingsDef, + ExampleAddon ) __all__ = ( "AddonSettingsDef", + "ExampleAddon" ) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 64504be756..5a25b80616 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -31,7 +31,7 @@ class AddonSettingsDef(JsonFilesSettingsDef): # recommended as schemas and templates may have name clashes across # multiple addons # - it is also recommended that prefix has addon name in it - schema_prefix = "addon_with_settings" + schema_prefix = "example_addon" def get_settings_root_path(self): """Implemented abstract class of JsonFilesSettingsDef. @@ -67,6 +67,14 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): # UI which must not be created at this time self._dialog = None + def tray_init(self): + """Implementation of abstract method for `ITrayAction`. + + We're definetely in trat tool so we can precreate dialog. + """ + + self._create_dialog() + def connect_with_modules(self, enabled_modules): """Method where you should find connected modules. diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json index 0967ef424b..0a01fa8977 100644 --- a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json +++ b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json @@ -1 +1,15 @@ -{} +{ + "project_settings/example_addon": { + "number": 0, + "color_1": [ + 0.0, + 0.0, + 0.0 + ], + "color_2": [ + 0.0, + 0.0, + 0.0 + ] + } +} \ No newline at end of file diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json new file mode 100644 index 0000000000..1e77356373 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json @@ -0,0 +1,5 @@ +{ + "modules/example_addon": { + "enabled": true + } +} \ No newline at end of file diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json index f6b7d5d146..1f3da7b37f 100644 --- a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json @@ -1,6 +1,6 @@ { "project_settings/global": { "type": "schema", - "name": "addon_with_settings/main" + "name": "example_addon/main" } } diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json index 6895fb8f6d..6faa48ba74 100644 --- a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json @@ -1,6 +1,6 @@ { "system_settings/modules": { "type": "schema", - "name": "addon_with_settings/main" + "name": "example_addon/main" } } diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json index 80e53ace7f..ba692d860e 100644 --- a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json @@ -1,6 +1,7 @@ { "type": "dict", - "key": "exmaple_addon", + "key": "example_addon", + "label": "Example addon", "collapsible": true, "children": [ { @@ -17,11 +18,11 @@ "template_data": [ { "name": "color_1", - "lable": "Color 1" + "label": "Color 1" }, { "name": "color_2", - "lable": "Color 2" + "label": "Color 2" } ] } diff --git a/openpype/modules/example_addons/example_addon/widgets.py b/openpype/modules/example_addons/example_addon/widgets.py index 8a74ad859f..0acf238409 100644 --- a/openpype/modules/example_addons/example_addon/widgets.py +++ b/openpype/modules/example_addons/example_addon/widgets.py @@ -1,5 +1,7 @@ from Qt import QtWidgets +from openpype.style import load_stylesheet + class MyExampleDialog(QtWidgets.QDialog): def __init__(self, parent=None): @@ -18,8 +20,15 @@ class MyExampleDialog(QtWidgets.QDialog): layout.addWidget(label_widget) layout.addLayout(btns_layout) + ok_btn.clicked.connect(self._on_ok_clicked) + self._label_widget = label_widget + self.setStyleSheet(load_stylesheet()) + + def _on_ok_clicked(self): + self.done(1) + def set_connected_modules(self, connected_modules): if connected_modules: message = "\n".join(connected_modules) From 4b0c8abcd52cd0254e4df73c314b98a2ec49f2fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:11:36 +0200 Subject: [PATCH 136/716] added new dynamic schema with name `system_settings/modules` --- .../entities/schemas/system_schema/schema_modules.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..4287dd7905 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -242,6 +242,10 @@ "label": "Enabled" } ] + }, + { + "type": "dynamic_schema", + "name": "system_settings/modules" } ] } From fa8383859cc0c3d01520a0e6213ff3caf3d65da5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:12:10 +0200 Subject: [PATCH 137/716] added ability to use schema as first children of dynamic schema definition --- openpype/settings/entities/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index f207322dee..bf3868c08d 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -168,9 +168,13 @@ class SchemasHub: if isinstance(def_schema, dict): def_schema = [def_schema] + all_def_schema = [] for item in def_schema: - item["_dynamic_schema_id"] = def_id - output.extend(def_schema) + items = self.resolve_schema_data(item) + for _item in items: + _item["_dynamic_schema_id"] = def_id + all_def_schema.extend(items) + output.extend(all_def_schema) return output def get_template_name(self, item_def, default=None): From 5bd1fa8a56b41913932df4aeb61a101109892c88 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:12:22 +0200 Subject: [PATCH 138/716] skip OpenPypeAddOn items too --- openpype/modules/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 01c3cebe60..2cd11e5b94 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -495,6 +495,7 @@ class ModulesManager: if ( not inspect.isclass(modules_item) or modules_item is OpenPypeModule + or modules_item is OpenPypeAddOn or not issubclass(modules_item, OpenPypeModule) ): continue From 7f7c7e00620e4a20143b6d1a2d14a51958a5fc9e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:19:42 +0200 Subject: [PATCH 139/716] added few more information about addons settings to readme --- openpype/modules/README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index a3733518ac..a6857b2c51 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -10,8 +10,6 @@ OpenPype modules should contain separated logic of specific kind of implementati - add module/addon manifest - definition of module (not 100% defined content e.g. minimum require OpenPype version etc.) - defying that folder is content of a module or an addon -- module/addon have it's settings schemas and default values outside OpenPype -- add general setting of paths to modules ## Base class `OpenPypeModule` - abstract class as base for each module @@ -25,6 +23,26 @@ OpenPype modules should contain separated logic of specific kind of implementati - also keep in mind that they may be initialized in headless mode - connection with other modules is made with help of interfaces +## Addon class `OpenPypeAddOn` +- inherit from `OpenPypeModule` but is enabled by default and don't have to implement `initialize` and `connect_with_modules` methods + - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) + +## How to add addons/modules +- in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder +- for openpype example addons use `{OPENPYPE_REPOS_ROOT}/openpype/modules/example_addons` + +## Addon/module settings +- addons/modules may have defined custom settings definitions with default values +- it is based on settings type `dynamic_schema` which has `name` + - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults + - they can't be added to any schema hierarchy + - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) + - addons may define it's dynamic schema items +- they can be defined with class which inherit from `BaseModuleSettingsDef` + - it is recommended to use preimplemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values + - check it's docstring and check for `example_addon` in example addons +- settings definition returns schemas by dynamic schemas names + # Interfaces - interface is class that has defined abstract methods to implement and may contain preimplemented helper methods - module that inherit from an interface must implement those abstract methods otherwise won't be initialized From 0932ea8aba1ec9af50fcbafa87b7022a7ac6dca0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 13:25:13 +0200 Subject: [PATCH 140/716] removed unsused import --- .../example_addon/plugins/publish/example_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py index 8e7fb410bd..695120e93b 100644 --- a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py +++ b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py @@ -1,4 +1,3 @@ -import os import pyblish.api From c6d781d7df7ab6e847328fc8188d77e29fedc941 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 13:34:56 +0200 Subject: [PATCH 141/716] #1976 - added methods to get configurable items for providers without use of Settings --- .../sync_server/sync_server_module.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index e65a410551..d2c70ec75a 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -403,6 +403,59 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """Wrapper for Local settings - all projects incl. Default""" return self.get_configurable_items(EditableScopes.LOCAL) + def get_system_configurable_items_for_provider(self, provider_name): + """ Gets system level configurable items without use of Setting + + Used for Setting UI to provide forms. + """ + scope = EditableScopes.SYSTEM + return self._get_configurable_items_for_provider(provider_name, scope) + + def get_project_configurable_items_for_provider(self, provider_name): + """ Gets project level configurable items without use of Setting + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.PROJECT + return self._get_configurable_items_for_provider(provider_name, scope) + + def get_system_configurable_items_for_providers(self): + """ Gets system level configurable items for all providers. + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.SYSTEM + ret_dict = {} + for provider_name in lib.factory.providers: + ret_dict[provider_name] = \ + self._get_configurable_items_for_provider(provider_name, scope) + + return ret_dict + + def get_project_configurable_items_for_providers(self): + """ Gets project level configurable items for all providers. + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.PROJECT + ret_dict = {} + for provider_name in lib.factory.providers: + ret_dict[provider_name] = \ + self._get_configurable_items_for_provider(provider_name, scope) + + return ret_dict + + def _get_configurable_items_for_provider(self, provider_name, scope): + items = lib.factory.get_provider_configurable_items(provider_name) + ret_dict = {} + + for item_key, item in items.items(): + if scope in item["scope"]: + item.pop("scope") + ret_dict[item_key] = item + + return ret_dict + def get_configurable_items(self, scope=None): """ Returns list of sites that could be configurable for all projects. From 3628ab8904b397fb71484e294c0d706bb22c8eda Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 13:51:53 +0200 Subject: [PATCH 142/716] fix changing of slider value from input field --- openpype/tools/settings/settings/item_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index a7b1208269..3b1fc061ec 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -445,7 +445,7 @@ class NumberWidget(InputWidget): value = self.input_field.value() if self._slider_widget is not None and not self._ignore_input_change: self._ignore_slider_change = True - self._slider_widget.setValue(value) + self._slider_widget.setValue(value * self._slider_multiplier) self._ignore_slider_change = False self.entity.set(value) From a4706062d3d345757a4248c2d650b19647553fe4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 14:50:55 +0200 Subject: [PATCH 143/716] #1976 - made new methods class methods --- .../sync_server/sync_server_module.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index d2c70ec75a..3e10ddac1d 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -403,23 +403,28 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """Wrapper for Local settings - all projects incl. Default""" return self.get_configurable_items(EditableScopes.LOCAL) - def get_system_configurable_items_for_provider(self, provider_name): + @classmethod + def get_system_configurable_items_for_provider(cls, provider_name): """ Gets system level configurable items without use of Setting Used for Setting UI to provide forms. """ scope = EditableScopes.SYSTEM - return self._get_configurable_items_for_provider(provider_name, scope) + return SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) - def get_project_configurable_items_for_provider(self, provider_name): + @classmethod + def get_project_configurable_items_for_provider(cls, provider_name): """ Gets project level configurable items without use of Setting It is not using Setting! Used for Setting UI to provide forms. """ scope = EditableScopes.PROJECT - return self._get_configurable_items_for_provider(provider_name, scope) + return SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) - def get_system_configurable_items_for_providers(self): + @classmethod + def get_system_configurable_items_for_providers(cls): """ Gets system level configurable items for all providers. It is not using Setting! Used for Setting UI to provide forms. @@ -428,11 +433,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - self._get_configurable_items_for_provider(provider_name, scope) + SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) return ret_dict - def get_project_configurable_items_for_providers(self): + @classmethod + def get_project_configurable_items_for_providers(cls): """ Gets project level configurable items for all providers. It is not using Setting! Used for Setting UI to provide forms. @@ -441,11 +448,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - self._get_configurable_items_for_provider(provider_name, scope) + SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) return ret_dict - def _get_configurable_items_for_provider(self, provider_name, scope): + @classmethod + def _get_configurable_items_for_provider(cls, provider_name, scope): items = lib.factory.get_provider_configurable_items(provider_name) ret_dict = {} From eef59e6fffb67671f752c6aef890c0c5d0ae5255 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 15:25:54 +0200 Subject: [PATCH 144/716] #1976 - standardize return to list --- .../sync_server/sync_server_module.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 3e10ddac1d..eeff5c499d 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -455,15 +455,24 @@ class SyncServerModule(OpenPypeModule, ITrayModule): @classmethod def _get_configurable_items_for_provider(cls, provider_name, scope): + """ + Args: + provider_name (str) + scope (EditableScopes) + Returns + (list) of (dict) + """ items = lib.factory.get_provider_configurable_items(provider_name) - ret_dict = {} - for item_key, item in items.items(): + ret = [] + for key in sorted(items.keys()): + item = items[key] if scope in item["scope"]: - item.pop("scope") - ret_dict[item_key] = item + item.pop("scope") # unneeded by UI + item.pop("namespace", None) # unneeded by UI + ret.append(item) - return ret_dict + return ret def get_configurable_items(self, scope=None): """ From 39341b1c2bb40caf038b5cb5595efcb1561ee047 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 15:32:14 +0200 Subject: [PATCH 145/716] #1976 - explicitly remove namespace in some cases Used cls instead of class name --- .../sync_server/sync_server_module.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index eeff5c499d..f0ae64d3fd 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -410,8 +410,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Used for Setting UI to provide forms. """ scope = EditableScopes.SYSTEM - return SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + return cls._get_configurable_items_for_provider(provider_name, scope) @classmethod def get_project_configurable_items_for_provider(cls, provider_name): @@ -420,8 +419,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): It is not using Setting! Used for Setting UI to provide forms. """ scope = EditableScopes.PROJECT - return SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + return cls._get_configurable_items_for_provider(provider_name, scope) @classmethod def get_system_configurable_items_for_providers(cls): @@ -433,8 +431,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + cls._get_configurable_items_for_provider(provider_name, scope) return ret_dict @@ -448,8 +445,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + cls._get_configurable_items_for_provider(provider_name, scope) return ret_dict @@ -469,7 +465,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): item = items[key] if scope in item["scope"]: item.pop("scope") # unneeded by UI - item.pop("namespace", None) # unneeded by UI + if scope in [EditableScopes.SYSTEM, EditableScopes.PROJECT]: + item.pop("namespace", None) # unneeded by UI ret.append(item) return ret From 7eed3da7c3c2d525a5dc5e5784749c05b8e7a4bd Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 8 Sep 2021 05:57:12 +0800 Subject: [PATCH 146/716] refactor, publishing render sets as model metadata Instead of loading whole render(augmented) model from Loader, *import* those render sets from scene inventory as model's metadata which allow lookDev artist to modify it and optionally publish a look from there. --- openpype/hosts/maya/api/__init__.py | 2 + .../plugins/inventory/import_modelrender.py | 85 +++++++++++ .../hosts/maya/plugins/load/load_reference.py | 12 -- .../maya/plugins/publish/collect_look.py | 24 +--- .../maya/plugins/publish/extract_look.py | 133 +++--------------- 5 files changed, 106 insertions(+), 150 deletions(-) create mode 100644 openpype/hosts/maya/plugins/inventory/import_modelrender.py diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 9219da407f..b48027ddba 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -35,6 +35,7 @@ def install(): pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) log.info(PUBLISH_PATH) menu.install() @@ -97,6 +98,7 @@ def uninstall(): pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) menu.uninstall() diff --git a/openpype/hosts/maya/plugins/inventory/import_modelrender.py b/openpype/hosts/maya/plugins/inventory/import_modelrender.py new file mode 100644 index 0000000000..2737901b51 --- /dev/null +++ b/openpype/hosts/maya/plugins/inventory/import_modelrender.py @@ -0,0 +1,85 @@ +from avalon import api, io + + +class ImportModelRender(api.InventoryAction): + + label = "Import Model Render Sets" + icon = "industry" + color = "#55DDAA" + + scene_type = "meta.render.ma" + look_data_type = "meta.render.json" + + @staticmethod + def is_compatible(container): + return container.get("loader") == "ReferenceLoader" \ + and container.get("name", "").startswith("model") + + def process(self, containers): + from maya import cmds + + for container in containers: + container_name = container["objectName"] + nodes = [] + for n in cmds.sets(container_name, query=True, nodesOnly=True) or []: + if cmds.nodeType(n) == "reference": + nodes += cmds.referenceQuery(n, nodes=True) + else: + nodes.append(n) + + repr_doc = io.find_one({"_id": io.ObjectId(container["representation"])}) + version_id = repr_doc["parent"] + + print("Importing render sets for model %r" % container_name) + self.assign_model_render_by_version(nodes, version_id) + + def assign_model_render_by_version(self, nodes, version_id): + """Assign nodes a specific published model render data version by id. + + This assumes the nodes correspond with the asset. + + Args: + nodes(list): nodes to assign render data to + version_id (bson.ObjectId): database id of the version of model + + Returns: + None + """ + import json + from maya import cmds + from avalon import maya, io, pipeline + from openpype.hosts.maya.api import lib + + # Get representations of shader file and relationships + look_representation = io.find_one({"type": "representation", + "parent": version_id, + "name": self.scene_type}) + + if not look_representation: + print("No model render sets for this model version..") + return + + json_representation = io.find_one({"type": "representation", + "parent": version_id, + "name": self.look_data_type}) + + context = pipeline.get_representation_context(look_representation['_id']) + maya_file = pipeline.get_representation_path_from_context(context) + + context = pipeline.get_representation_context(json_representation['_id']) + json_file = pipeline.get_representation_path_from_context(context) + + # Import the look file + with maya.maintained_selection(): + shader_nodes = cmds.file(maya_file, + i=True, # import + returnNewNodes=True) + # imprint context data + + # Load relationships + shader_relation = json_file + with open(shader_relation, "r") as f: + relationships = json.load(f) + + # Assign relationships + lib.apply_shaders(relationships, shader_nodes, nodes) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 77c9f28d10..96269f2771 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -152,15 +152,3 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): options={"useSelection": True}, data={"dependencies": dependency} ) - - -class AugmentedModelLoader(ReferenceLoader): - """Load augmented model via Maya referencing""" - - families = ["model"] - representations = ["fried.ma", "fried.mb"] - - label = "Fried Model" - order = -9 - icon = "code-fork" - color = "yellow" diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index ecc89e9032..712c7f19ff 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -223,8 +223,8 @@ class CollectLook(pyblish.api.InstancePlugin): def process(self, instance): """Collect the Look in the instance with the correct layer settings""" - - with lib.renderlayer(instance.data["renderlayer"]): + renderlayer = instance.data.get("renderlayer", "defaultRenderLayer") + with lib.renderlayer(renderlayer): self.collect(instance) def collect(self, instance): @@ -579,26 +579,6 @@ class CollectModelRenderSets(CollectLook): hosts = ["maya"] maketx = True - def process(self, instance): - """Collect the Look in the instance with the correct layer settings""" - model_nodes = instance[:] - renderlayer = instance.data.get("renderlayer", "defaultRenderLayer") - - with lib.renderlayer(renderlayer): - self.collect(instance) - - set_nodes = [m for m in instance if m not in model_nodes] - instance[:] = model_nodes - - if set_nodes: - instance.data["modelRenderSets"] = set_nodes - instance.data["modelRenderSetsHistory"] = \ - cmds.listHistory(set_nodes, future=False, pruneDagObjects=True) - - self.log.info("Model render sets collected.") - else: - self.log.info("No model render sets.") - def collect_sets(self, instance): """Collect all related objectSets except shadingEngines diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 121c99fc47..62b58623e7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -135,6 +135,7 @@ class ExtractLook(openpype.api.Extractor): families = ["look"] order = pyblish.api.ExtractorOrder + 0.2 scene_type = "ma" + look_data_type = "json" @staticmethod def get_renderer_name(): @@ -186,7 +187,7 @@ class ExtractLook(openpype.api.Extractor): # Define extract output file path dir_path = self.staging_dir(instance) maya_fname = "{0}.{1}".format(instance.name, self.scene_type) - json_fname = "{0}.json".format(instance.name) + json_fname = "{0}.{1}".format(instance.name, self.look_data_type) # Make texture dump folder maya_path = os.path.join(dir_path, maya_fname) @@ -252,19 +253,21 @@ class ExtractLook(openpype.api.Extractor): instance.data["files"].append(maya_fname) instance.data["files"].append(json_fname) - instance.data["representations"] = [] + if instance.data.get("representations") is None: + instance.data["representations"] = [] + instance.data["representations"].append( { - "name": "ma", - "ext": "ma", + "name": self.scene_type, + "ext": self.scene_type, "files": os.path.basename(maya_fname), "stagingDir": os.path.dirname(maya_fname), } ) instance.data["representations"].append( { - "name": "json", - "ext": "json", + "name": self.look_data_type, + "ext": self.look_data_type, "files": os.path.basename(json_fname), "stagingDir": os.path.dirname(json_fname), } @@ -483,119 +486,17 @@ class ExtractLook(openpype.api.Extractor): return filepath, COPY, texture_hash -class ExtractAugmentedModel(ExtractLook): - """Extract as Augmented Model (Maya Scene). +class ExtractModelRenderSets(ExtractLook): + """Extract model render attribute sets as model metadata - Rendering attrs augmented model. - - Only extracts contents based on the original "setMembers" data to ensure - publishing the least amount of required shapes. From that it only takes - the shapes that are not intermediateObjects - - During export it sets a temporary context to perform a clean extraction. - The context ensures: - - Smooth preview is turned off for the geometry - - Default shader is assigned (no materials are exported) - - Remove display layers + Only extracts the render attrib sets (NO shadingEngines) alongside a .json file + that stores it relationships for the sets and "attribute" data for the + instance members. """ - label = "Augmented Model (Maya Scene)" + label = "Model Render Sets" hosts = ["maya"] families = ["model"] - scene_type = "ma" - augmented = "fried" - - def process(self, instance): - """Plugin entry point. - - Args: - instance: Instance to process. - - """ - render_sets = instance.data.get("modelRenderSetsHistory") - if not render_sets: - self.log.info("Model is not render augmented, skip extraction.") - return - - self.get_maya_scene_type(instance) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - # Define extract output file path - stagingdir = self.staging_dir(instance) - ext = "{0}.{1}".format(self.augmented, self.scene_type) - filename = "{0}.{1}".format(instance.name, ext) - path = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction ...") - - results = self.process_resources(instance, staging_dir=stagingdir) - transfers = results["fileTransfers"] - hardlinks = results["fileHardlinks"] - hashes = results["fileHashes"] - remap = results["attrRemap"] - - self.log.info(remap) - - # Get only the shape contents we need in such a way that we avoid - # taking along intermediateObjects - members = instance.data("setMembers") - members = cmds.ls(members, - dag=True, - shapes=True, - type=("mesh", "nurbsCurve"), - noIntermediate=True, - long=True) - members += instance.data.get("modelRenderSetsHistory") - - with lib.no_display_layers(instance): - with lib.displaySmoothness(members, - divisionsU=0, - divisionsV=0, - pointsWire=4, - pointsShaded=1, - polygonObject=1): - with lib.shader(members, - shadingEngine="initialShadingGroup"): - # To avoid Maya trying to automatically remap the file - # textures relative to the `workspace -directory` we force - # it to a fake temporary workspace. This fixes textures - # getting incorrectly remapped. (LKD-17, PLN-101) - with no_workspace_dir(): - with lib.attribute_values(remap): - with avalon.maya.maintained_selection(): - - cmds.select(members, noExpand=True) - cmds.file(path, - force=True, - typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 - exportSelected=True, - preserveReferences=False, - channels=False, - constraints=False, - expressions=False, - constructionHistory=False) - - if "hardlinks" not in instance.data: - instance.data["hardlinks"] = [] - if "transfers" not in instance.data: - instance.data["transfers"] = [] - - # Set up the resources transfers/links for the integrator - instance.data["transfers"].extend(transfers) - instance.data["hardlinks"].extend(hardlinks) - - # Source hash for the textures - instance.data["sourceHashes"] = hashes - - instance.data["representations"].append({ - 'name': ext, - 'ext': ext, - 'files': filename, - "stagingDir": stagingdir, - }) - - self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) + scene_type = "meta.render.ma" + look_data_type = "meta.render.json" From 94b3a182ef37d8d58f21026463fa0a00f50442e8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 11:50:20 +0200 Subject: [PATCH 147/716] #1976 - refactored, created simplified methods Commented out currently unneeded methods, not used anywhere, but logic could be salvaged after Settings will be modified --- .../providers/abstract_provider.py | 28 +- .../sync_server/providers/gdrive.py | 62 ++- .../sync_server/providers/lib.py | 8 + .../sync_server/providers/local_drive.py | 55 ++- .../sync_server/sync_server_module.py | 461 ++++++++---------- 5 files changed, 342 insertions(+), 272 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py index 2e9632134c..7fd25b9852 100644 --- a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py +++ b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py @@ -29,13 +29,35 @@ class AbstractProvider: @classmethod @abc.abstractmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties + Returns dict for editable properties on system settings level Returns: - (dict) + (list) of dict + """ + + @classmethod + @abc.abstractmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + + @classmethod + @abc.abstractmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level + + + Returns: + (list) of dict """ @abc.abstractmethod diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 18d679b833..5db728f2de 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -96,30 +96,62 @@ class GDriveHandler(AbstractProvider): return self.service is not None @classmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties. + Returns dict for editable properties on system settings level + + + Returns: + (list) of dict + """ + return [] + + @classmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + # {platform} tells that value is multiplatform and only specific OS + # should be returned + editable = [ + # credentials could be override on Project or User level + { + 'label': "Credentials url", + 'type': 'text', + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + }, + # roots could be override only on Project leve, User cannot + # + { + 'label': "Roots", + 'type': 'dict' + } + ] + return editable + + @classmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level Returns: (dict) """ - # {platform} tells that value is multiplatform and only specific OS - # should be returned - editable = { + editable = [ # credentials could be override on Project or User level - 'credentials_url': { - 'scope': [EditableScopes.PROJECT, - EditableScopes.LOCAL], + { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 - }, - # roots could be override only on Project leve, User cannot - 'root': {'scope': [EditableScopes.PROJECT], - 'label': "Roots", - 'type': 'dict'} - } + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + } + ] return editable def get_roots_config(self, anatomy=None): diff --git a/openpype/modules/default_modules/sync_server/providers/lib.py b/openpype/modules/default_modules/sync_server/providers/lib.py index 816ccca981..463e49dd4d 100644 --- a/openpype/modules/default_modules/sync_server/providers/lib.py +++ b/openpype/modules/default_modules/sync_server/providers/lib.py @@ -76,6 +76,14 @@ class ProviderFactory: return provider_info[0].get_configurable_items() + def get_provider_cls(self, provider_code): + """ + Returns class object for 'provider_code' to run class methods on. + """ + provider_info = self._get_creator_info(provider_code) + + return provider_info[0] + def _get_creator_info(self, provider): """ Collect all necessary info for provider. Currently only creator diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 4b80ed44f2..b3482ac1d8 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -30,18 +30,59 @@ class LocalDriveHandler(AbstractProvider): return True @classmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties + Returns dict for editable properties on system settings level + + + Returns: + (list) of dict + """ + return [] + + @classmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + # {platform} tells that value is multiplatform and only specific OS + # should be returned + editable = [ + # credentials could be override on Project or User level + { + 'label': "Credentials url", + 'type': 'text', + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + }, + # roots could be override only on Project leve, User cannot + # + { + 'label': "Roots", + 'type': 'dict' + } + ] + return editable + + @classmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level + Returns: (dict) """ - editable = { - 'root': {'scope': [EditableScopes.LOCAL], - 'label': "Roots", - 'type': 'dict'} - } + editable = [ + { + 'label': "Roots", + 'type': 'dict' + } + ] return editable def upload_file(self, source_path, target_path, diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index f0ae64d3fd..2eb749801e 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -8,7 +8,7 @@ import copy from avalon.api import AvalonMongoDB from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayModule +from openpype.modules.default_modules.interfaces import ITrayModule from openpype.api import ( Anatomy, get_project_settings, @@ -399,272 +399,239 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return remote_site - def get_local_settings_schema(self): - """Wrapper for Local settings - all projects incl. Default""" - return self.get_configurable_items(EditableScopes.LOCAL) - + # Methods for Settings UI to draw appropriate forms @classmethod - def get_system_configurable_items_for_provider(cls, provider_name): - """ Gets system level configurable items without use of Setting + def get_system_settings_schema(cls): + """ Gets system level schema of configurable items Used for Setting UI to provide forms. """ - scope = EditableScopes.SYSTEM - return cls._get_configurable_items_for_provider(provider_name, scope) - - @classmethod - def get_project_configurable_items_for_provider(cls, provider_name): - """ Gets project level configurable items without use of Setting - - It is not using Setting! Used for Setting UI to provide forms. - """ - scope = EditableScopes.PROJECT - return cls._get_configurable_items_for_provider(provider_name, scope) - - @classmethod - def get_system_configurable_items_for_providers(cls): - """ Gets system level configurable items for all providers. - - It is not using Setting! Used for Setting UI to provide forms. - """ - scope = EditableScopes.SYSTEM ret_dict = {} - for provider_name in lib.factory.providers: - ret_dict[provider_name] = \ - cls._get_configurable_items_for_provider(provider_name, scope) + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_system_settings_schema() return ret_dict @classmethod - def get_project_configurable_items_for_providers(cls): - """ Gets project level configurable items for all providers. + def get_project_settings_schema(cls): + """ Gets project level schema of configurable items. It is not using Setting! Used for Setting UI to provide forms. """ - scope = EditableScopes.PROJECT ret_dict = {} - for provider_name in lib.factory.providers: - ret_dict[provider_name] = \ - cls._get_configurable_items_for_provider(provider_name, scope) + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_project_settings_schema() return ret_dict @classmethod - def _get_configurable_items_for_provider(cls, provider_name, scope): + def get_local_settings_schema(cls): + """ Gets local level schema of configurable items. + + It is not using Setting! Used for Setting UI to provide forms. """ - Args: - provider_name (str) - scope (EditableScopes) - Returns - (list) of (dict) - """ - items = lib.factory.get_provider_configurable_items(provider_name) + ret_dict = {} + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_local_settings_schema() - ret = [] - for key in sorted(items.keys()): - item = items[key] - if scope in item["scope"]: - item.pop("scope") # unneeded by UI - if scope in [EditableScopes.SYSTEM, EditableScopes.PROJECT]: - item.pop("namespace", None) # unneeded by UI - ret.append(item) + return ret_dict - return ret - - def get_configurable_items(self, scope=None): - """ - Returns list of sites that could be configurable for all projects. - - Could be filtered by 'scope' argument (list) - - Args: - scope (list of utils.EditableScope) - - Returns: - (dict of list of dict) - { - siteA : [ - { - key:"root", label:"root", - "value":"{'work': 'c:/projects'}", - "type": "dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, - { - key:"credentials_url", label:"Credentials url", - "value":"'c:/projects/cred.json'", "type": "text", - "namespace": "{project_setting}/global/sync_server/ - sites" - } - ] - } - """ - editable = {} - applicable_projects = list(self.connection.projects()) - applicable_projects.append(None) - for project in applicable_projects: - project_name = None - if project: - project_name = project["name"] - - items = self.get_configurable_items_for_project(project_name, - scope) - editable.update(items) - - return editable - - def get_local_settings_schema_for_project(self, project_name): - """Wrapper for Local settings - for specific 'project_name'""" - return self.get_configurable_items_for_project(project_name, - EditableScopes.LOCAL) - - def get_configurable_items_for_project(self, project_name=None, - scope=None): - """ - Returns list of items that could be configurable for specific - 'project_name' - - Args: - project_name (str) - None > default project, - scope (list of utils.EditableScope) - (optional, None is all scopes, default is LOCAL) - - Returns: - (dict of list of dict) - { - siteA : [ - { - key:"root", label:"root", - "type": "dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, - { - key:"credentials_url", label:"Credentials url", - "value":"'c:/projects/cred.json'", "type": "text", - "namespace": "{project_setting}/global/sync_server/ - sites" - } - ] - } - """ - allowed_sites = set() - sites = self.get_all_site_configs(project_name) - if project_name: - # Local Settings can select only from allowed sites for project - allowed_sites.update(set(self.get_active_sites(project_name))) - allowed_sites.update(set(self.get_remote_sites(project_name))) - - editable = {} - for site_name in sites.keys(): - if allowed_sites and site_name not in allowed_sites: - continue - - items = self.get_configurable_items_for_site(project_name, - site_name, - scope) - # Local Settings need 'local' instead of real value - site_name = site_name.replace(get_local_site_id(), 'local') - editable[site_name] = items - - return editable - - def get_local_settings_schema_for_site(self, project_name, site_name): - """Wrapper for Local settings - for particular 'site_name and proj.""" - return self.get_configurable_items_for_site(project_name, - site_name, - EditableScopes.LOCAL) - - def get_configurable_items_for_site(self, project_name=None, - site_name=None, - scope=None): - """ - Returns list of items that could be configurable. - - Args: - project_name (str) - None > default project - site_name (str) - scope (list of utils.EditableScope) - (optional, None is all scopes) - - Returns: - (list) - [ - { - key:"root", label:"root", type:"dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, ... - ] - """ - provider_name = self.get_provider_for_site(site=site_name) - items = lib.factory.get_provider_configurable_items(provider_name) - - if project_name: - sync_s = self.get_sync_project_setting(project_name, - exclude_locals=True, - cached=False) - else: - sync_s = get_default_project_settings(exclude_locals=True) - sync_s = sync_s["global"]["sync_server"] - sync_s["sites"].update( - self._get_default_site_configs(self.enabled)) - - editable = [] - if type(scope) is not list: - scope = [scope] - scope = set(scope) - for key, properties in items.items(): - if scope is None or scope.intersection(set(properties["scope"])): - val = sync_s.get("sites", {}).get(site_name, {}).get(key) - - item = { - "key": key, - "label": properties["label"], - "type": properties["type"] - } - - if properties.get("namespace"): - item["namespace"] = properties.get("namespace") - if "platform" in item["namespace"]: - try: - if val: - val = val[platform.system().lower()] - except KeyError: - st = "{}'s field value {} should be".format(key, val) # noqa: E501 - log.error(st + " multiplatform dict") - - item["namespace"] = item["namespace"].replace('{site}', - site_name) - children = [] - if properties["type"] == "dict": - if val: - for val_key, val_val in val.items(): - child = { - "type": "text", - "key": val_key, - "value": val_val - } - children.append(child) - - if properties["type"] == "dict": - item["children"] = children - else: - item["value"] = val - - editable.append(item) - - return editable + # Needs to be refactored after Settings are updated + # # Methods for Settings to get appriate values to fill forms + # def get_configurable_items(self, scope=None): + # """ + # Returns list of sites that could be configurable for all projects. + # + # Could be filtered by 'scope' argument (list) + # + # Args: + # scope (list of utils.EditableScope) + # + # Returns: + # (dict of list of dict) + # { + # siteA : [ + # { + # key:"root", label:"root", + # "value":"{'work': 'c:/projects'}", + # "type": "dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, + # { + # key:"credentials_url", label:"Credentials url", + # "value":"'c:/projects/cred.json'", "type": "text", + # "namespace": "{project_setting}/global/sync_server/ + # sites" + # } + # ] + # } + # """ + # editable = {} + # applicable_projects = list(self.connection.projects()) + # applicable_projects.append(None) + # for project in applicable_projects: + # project_name = None + # if project: + # project_name = project["name"] + # + # items = self.get_configurable_items_for_project(project_name, + # scope) + # editable.update(items) + # + # return editable + # + # def get_local_settings_schema_for_project(self, project_name): + # """Wrapper for Local settings - for specific 'project_name'""" + # return self.get_configurable_items_for_project(project_name, + # EditableScopes.LOCAL) + # + # def get_configurable_items_for_project(self, project_name=None, + # scope=None): + # """ + # Returns list of items that could be configurable for specific + # 'project_name' + # + # Args: + # project_name (str) - None > default project, + # scope (list of utils.EditableScope) + # (optional, None is all scopes, default is LOCAL) + # + # Returns: + # (dict of list of dict) + # { + # siteA : [ + # { + # key:"root", label:"root", + # "type": "dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, + # { + # key:"credentials_url", label:"Credentials url", + # "value":"'c:/projects/cred.json'", "type": "text", + # "namespace": "{project_setting}/global/sync_server/ + # sites" + # } + # ] + # } + # """ + # allowed_sites = set() + # sites = self.get_all_site_configs(project_name) + # if project_name: + # # Local Settings can select only from allowed sites for project + # allowed_sites.update(set(self.get_active_sites(project_name))) + # allowed_sites.update(set(self.get_remote_sites(project_name))) + # + # editable = {} + # for site_name in sites.keys(): + # if allowed_sites and site_name not in allowed_sites: + # continue + # + # items = self.get_configurable_items_for_site(project_name, + # site_name, + # scope) + # # Local Settings need 'local' instead of real value + # site_name = site_name.replace(get_local_site_id(), 'local') + # editable[site_name] = items + # + # return editable + # + # def get_configurable_items_for_site(self, project_name=None, + # site_name=None, + # scope=None): + # """ + # Returns list of items that could be configurable. + # + # Args: + # project_name (str) - None > default project + # site_name (str) + # scope (list of utils.EditableScope) + # (optional, None is all scopes) + # + # Returns: + # (list) + # [ + # { + # key:"root", label:"root", type:"dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, ... + # ] + # """ + # provider_name = self.get_provider_for_site(site=site_name) + # items = lib.factory.get_provider_configurable_items(provider_name) + # + # if project_name: + # sync_s = self.get_sync_project_setting(project_name, + # exclude_locals=True, + # cached=False) + # else: + # sync_s = get_default_project_settings(exclude_locals=True) + # sync_s = sync_s["global"]["sync_server"] + # sync_s["sites"].update( + # self._get_default_site_configs(self.enabled)) + # + # editable = [] + # if type(scope) is not list: + # scope = [scope] + # scope = set(scope) + # for key, properties in items.items(): + # if scope is None or scope.intersection(set(properties["scope"])): + # val = sync_s.get("sites", {}).get(site_name, {}).get(key) + # + # item = { + # "key": key, + # "label": properties["label"], + # "type": properties["type"] + # } + # + # if properties.get("namespace"): + # item["namespace"] = properties.get("namespace") + # if "platform" in item["namespace"]: + # try: + # if val: + # val = val[platform.system().lower()] + # except KeyError: + # st = "{}'s field value {} should be".format(key, val) # noqa: E501 + # log.error(st + " multiplatform dict") + # + # item["namespace"] = item["namespace"].replace('{site}', + # site_name) + # children = [] + # if properties["type"] == "dict": + # if val: + # for val_key, val_val in val.items(): + # child = { + # "type": "text", + # "key": val_key, + # "value": val_val + # } + # children.append(child) + # + # if properties["type"] == "dict": + # item["children"] = children + # else: + # item["value"] = val + # + # editable.append(item) + # + # return editable def reset_timer(self): """ From 3f2a4d5a5aa5f8d037d55551a127e86990b60c70 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 11:59:40 +0200 Subject: [PATCH 148/716] Hound --- .../default_modules/sync_server/sync_server_module.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 2eb749801e..39b5c9314e 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -8,7 +8,7 @@ import copy from avalon.api import AvalonMongoDB from openpype.modules import OpenPypeModule -from openpype.modules.default_modules.interfaces import ITrayModule +from openpype_interfaces import ITrayModule from openpype.api import ( Anatomy, get_project_settings, @@ -16,14 +16,13 @@ from openpype.api import ( get_local_site_id) from openpype.lib import PypeLogger from openpype.settings.lib import ( - get_default_project_settings, get_default_anatomy_settings, get_anatomy_settings) from .providers.local_drive import LocalDriveHandler from .providers import lib -from .utils import time_function, SyncStatus, EditableScopes +from .utils import time_function, SyncStatus log = PypeLogger().get_logger("SyncServer") @@ -646,7 +645,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): enabled_projects = [] if self.enabled: - for project in self.connection.projects(): + for project in self.connection.projects(projection={"name": 1}): project_name = project["name"] project_settings = self.get_sync_project_setting(project_name) if project_settings and project_settings.get("enabled"): From 7d37971d64f57408b22fc582e1a388a99cc28c3a Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 15:47:38 +0200 Subject: [PATCH 149/716] get last version string from path This changes get_version_from_path() to produce the same result as version_up --- openpype/lib/path_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index e1dd1e7f10..88bb1a216a 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -77,7 +77,7 @@ def get_version_from_path(file): """ pattern = re.compile(r"[\._]v([0-9]+)", re.IGNORECASE) try: - return pattern.findall(file)[0] + return pattern.findall(file)[-1] except IndexError: log.error( "templates:get_version_from_workfile:" From 7a88a4ac1326796b6224e5547a3656b2a5b9032c Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 16:11:30 +0200 Subject: [PATCH 150/716] Makes thumbnail from the middle of the clip If read node frame range is 1001-1010, thumbnail is now made from frame 1005, not 505. --- .../nuke/plugins/publish/extract_thumbnail.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index da30dcc632..6921d6e9b3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -112,12 +112,12 @@ class ExtractThumbnail(openpype.api.Extractor): # create write node write_node = nuke.createNode("Write") - file = fhead + "jpeg" + file = fhead + "jpg" name = "thumbnail" path = os.path.join(staging_dir, file).replace("\\", "/") instance.data["thumbnail"] = path write_node["file"].setValue(path) - write_node["file_type"].setValue("jpeg") + write_node["file_type"].setValue("jpg") write_node["raw"].setValue(1) write_node.setInput(0, previous_node) temporary_nodes.append(write_node) @@ -126,10 +126,11 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 + mid_frame = int((int(last_frame) - int(first_frame) ) / 2) + int(first_frame) repre = { 'name': name, - 'ext': "jpeg", + 'ext': "jpg", "outputName": "thumb", 'files': file, "stagingDir": staging_dir, @@ -140,7 +141,7 @@ class ExtractThumbnail(openpype.api.Extractor): instance.data["representations"].append(repre) # Render frames - nuke.execute(write_node.name(), int(first_frame), int(last_frame)) + nuke.execute(write_node.name(), int(mid_frame), int(mid_frame)) self.log.debug( "representations: {}".format(instance.data["representations"])) @@ -157,12 +158,12 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: - ip = v['input_process'].getValue() - ipn = v['input_process_node'].getValue() - if "VIEWER_INPUT" not in ipn and ip: - ipn_orig = nuke.toNode(ipn) - ipn_orig.setSelected(True) + if "Viewer" == n.Class()]: + ip = v['input_process'].getValue() + ipn = v['input_process_node'].getValue() + if "VIEWER_INPUT" not in ipn and ip: + ipn_orig = nuke.toNode(ipn) + ipn_orig.setSelected(True) if ipn_orig: nuke.nodeCopy('%clipboard%') From 84ca6a591c5aa1d15cb16cde3e05318eb5b71915 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 8 Sep 2021 23:19:23 +0800 Subject: [PATCH 151/716] 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 152/716] fix linter --- openpype/hosts/maya/plugins/inventory/import_modelrender.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/inventory/import_modelrender.py b/openpype/hosts/maya/plugins/inventory/import_modelrender.py index c13b4d6a1c..3675b757ea 100644 --- a/openpype/hosts/maya/plugins/inventory/import_modelrender.py +++ b/openpype/hosts/maya/plugins/inventory/import_modelrender.py @@ -12,8 +12,10 @@ class ImportModelRender(api.InventoryAction): @staticmethod def is_compatible(container): - return container.get("loader") == "ReferenceLoader" \ - and container.get("name", "").startswith("model") + return ( + container.get("loader") == "ReferenceLoader" + and container.get("name", "").startswith("model") + ) def process(self, containers): from maya import cmds From 0518064e96ef6b3a714988dfe88c6061173795f2 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:30:17 +0200 Subject: [PATCH 153/716] indent change --- .../hosts/nuke/plugins/publish/extract_thumbnail.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 6921d6e9b3..93a5c9b51d 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,11 +159,11 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() if "Viewer" == n.Class()]: - ip = v['input_process'].getValue() - ipn = v['input_process_node'].getValue() - if "VIEWER_INPUT" not in ipn and ip: - ipn_orig = nuke.toNode(ipn) - ipn_orig.setSelected(True) + ip = v['input_process'].getValue() + ipn = v['input_process_node'].getValue() + if "VIEWER_INPUT" not in ipn and ip: + ipn_orig = nuke.toNode(ipn) + ipn_orig.setSelected(True) if ipn_orig: nuke.nodeCopy('%clipboard%') From 81432101164a3731c65e6d60b9880029a28e8202 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:33:32 +0200 Subject: [PATCH 154/716] long line fix --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 93a5c9b51d..dfb26aab80 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -126,7 +126,7 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 - mid_frame = int((int(last_frame) - int(first_frame) ) / 2) + int(first_frame) + mid_frame = int((int(last_frame)-int(first_frame))/2)+int(first_frame) repre = { 'name': name, From 25a7c5d0f0d15525d1fe58ffdc8f120558eac462 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:38:29 +0200 Subject: [PATCH 155/716] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index dfb26aab80..4c21891f48 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -126,7 +126,8 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 - mid_frame = int((int(last_frame)-int(first_frame))/2)+int(first_frame) + mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ + + int(first_frame) repre = { 'name': name, From 17fd666a49a374a895a4c6913cb8f326c411de56 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:40:58 +0200 Subject: [PATCH 156/716] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 4c21891f48..c0dc1417c3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -127,7 +127,7 @@ class ExtractThumbnail(openpype.api.Extractor): first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ - + int(first_frame) + + int(first_frame) repre = { 'name': name, From 1ab40ddd244b27db31fe639cc63e15096301e1e9 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:43:31 +0200 Subject: [PATCH 157/716] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index c0dc1417c3..7ffeec2db9 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -124,10 +124,10 @@ class ExtractThumbnail(openpype.api.Extractor): tags = ["thumbnail", "publish_on_farm"] # retime for - first_frame = int(last_frame) / 2 - last_frame = int(last_frame) / 2 mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ + int(first_frame) + first_frame = int(last_frame) / 2 + last_frame = int(last_frame) / 2 repre = { 'name': name, From ca3034f2725fb1be97b37850f8a7d1ab0e017be7 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:46:09 +0200 Subject: [PATCH 158/716] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 7ffeec2db9..b9d6762880 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,7 +159,7 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From e3b319ffc66fc24668480fc3405bb05aa51089c2 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:58:31 +0200 Subject: [PATCH 159/716] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index b9d6762880..55f7b746fc 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,7 +159,7 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From 777facc69c792356ace880d5c19a7f8f9cf1da51 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 18:46:56 +0200 Subject: [PATCH 160/716] #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 46697d8d816c3a603a4ec8f0ed8cd8f1c2ef17e8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 8 Sep 2021 18:53:37 +0200 Subject: [PATCH 161/716] fix docker build, switch to cx_freeze 6.7 --- poetry.lock | 684 +++++++++++++++++++++++++----------------- pyproject.toml | 2 +- tools/docker_build.sh | 7 +- 3 files changed, 416 insertions(+), 277 deletions(-) diff --git a/poetry.lock b/poetry.lock index e011b781c9..6dae442c9d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -80,7 +80,7 @@ python-dateutil = ">=2.7.0" [[package]] name = "astroid" -version = "2.5.6" +version = "2.7.3" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -89,6 +89,7 @@ python-versions = "~=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} wrapt = ">=1.11,<1.13" [[package]] @@ -146,11 +147,11 @@ pytz = ">=2015.7" [[package]] name = "blessed" -version = "1.18.0" +version = "1.18.1" description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7" [package.dependencies] jinxed = {version = ">=0.5.4", markers = "platform_system == \"Windows\""} @@ -175,7 +176,7 @@ python-versions = "*" [[package]] name = "cffi" -version = "1.14.5" +version = "1.14.6" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -192,6 +193,17 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "charset-normalizer" +version = "2.0.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + [[package]] name = "click" version = "7.1.2" @@ -253,7 +265,7 @@ toml = ["toml"] [[package]] name = "cryptography" -version = "3.4.7" +version = "3.4.8" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -272,15 +284,20 @@ test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pret [[package]] name = "cx-freeze" -version = "6.6" +version = "6.7" description = "Create standalone executables from Python scripts" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -cx-Logging = {version = ">=3.0", markers = "sys_platform == \"win32\""} -importlib-metadata = ">=3.1.1" +cx-logging = {version = ">=3.0", markers = "sys_platform == \"win32\""} +importlib-metadata = ">=4.3.1" + +[package.source] +type = "legacy" +url = "https://distribute.openpype.io/wheels" +reference = "openpype" [[package]] name = "cx-logging" @@ -386,19 +403,19 @@ smmap = ">=3.0.1,<5" [[package]] name = "gitpython" -version = "3.1.17" +version = "3.1.20" description = "Python Git Library" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.0", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} [[package]] name = "google-api-core" -version = "1.30.0" +version = "1.31.2" description = "Google API client core library" category = "main" optional = false @@ -436,7 +453,7 @@ uritemplate = ">=3.0.0,<4dev" [[package]] name = "google-auth" -version = "1.31.0" +version = "1.35.0" description = "Google Authentication Library" category = "main" optional = false @@ -493,11 +510,11 @@ pyparsing = ">=2.4.2,<3" [[package]] name = "idna" -version = "2.10" +version = "3.2" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" [[package]] name = "imagesize" @@ -509,7 +526,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.5.0" +version = "4.8.1" description = "Read metadata from Python packages" category = "main" optional = false @@ -521,7 +538,8 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -533,16 +551,17 @@ python-versions = "*" [[package]] name = "isort" -version = "5.8.0" +version = "5.9.3" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6.1,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] [[package]] name = "jedi" @@ -560,14 +579,15 @@ testing = ["colorama", "docopt", "pytest (>=3.1.0)"] [[package]] name = "jeepney" -version = "0.6.0" +version = "0.7.1" description = "Low-level, pure Python DBus protocol wrapper." category = "main" optional = false python-versions = ">=3.6" [package.extras] -test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio"] +test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] +trio = ["trio", "async-generator"] [[package]] name = "jinja2" @@ -695,11 +715,11 @@ reference = "openpype" [[package]] name = "packaging" -version = "20.9" +version = "21.0" description = "Core utilities for Python packages" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" @@ -718,7 +738,7 @@ testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "pathlib2" -version = "2.3.5" +version = "2.3.6" description = "Object-oriented filesystem paths" category = "main" optional = false @@ -729,25 +749,38 @@ six = "*" [[package]] name = "pillow" -version = "8.2.0" +version = "8.3.2" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "platformdirs" +version = "2.3.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + [[package]] name = "pluggy" -version = "0.13.1" +version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "prefixed" @@ -849,7 +882,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.9.0" +version = "2.10.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -857,22 +890,23 @@ python-versions = ">=3.5" [[package]] name = "pylint" -version = "2.8.3" +version = "2.10.2" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] -astroid = "2.5.6" +astroid = ">=2.7.2,<2.8" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" +platformdirs = ">=2.2.0" toml = ">=0.7.1" [[package]] name = "pymongo" -version = "3.11.4" +version = "3.12.0" description = "Python driver for MongoDB " category = "main" optional = false @@ -880,9 +914,9 @@ python-versions = "*" [package.extras] aws = ["pymongo-auth-aws (<2.0.0)"] -encryption = ["pymongocrypt (<2.0.0)"] +encryption = ["pymongocrypt (>=1.1.0,<2.0.0)"] gssapi = ["pykerberos"] -ocsp = ["pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +ocsp = ["pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)", "certifi"] snappy = ["python-snappy"] srv = ["dnspython (>=1.16.0,<1.17.0)"] tls = ["ipaddress"] @@ -971,15 +1005,15 @@ python-versions = ">=3.5" [[package]] name = "pyrsistent" -version = "0.17.3" +version = "0.18.0" description = "Persistent/Functional/Immutable data structures" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [[package]] name = "pytest" -version = "6.2.4" +version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -992,7 +1026,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0.0a1" +pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" @@ -1017,21 +1051,21 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale [[package]] name = "pytest-print" -version = "0.2.1" +version = "0.3.0" description = "pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout)" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] -pytest = ">=3.0.0" +pytest = ">=6" [package.extras] -test = ["coverage (>=5)", "pytest (>=4)"] +test = ["coverage (>=5)"] [[package]] name = "python-dateutil" -version = "2.8.1" +version = "2.8.2" description = "Extensions to the standard Python datetime module" category = "main" optional = false @@ -1042,7 +1076,7 @@ six = ">=1.5" [[package]] name = "python-xlib" -version = "0.30" +version = "0.31" description = "Python X Library" category = "main" optional = false @@ -1085,7 +1119,7 @@ python-versions = "*" [[package]] name = "qt.py" -version = "1.3.3" +version = "1.3.6" description = "Python 2 & 3 compatibility wrapper around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5." category = "main" optional = false @@ -1106,21 +1140,21 @@ sphinx = ">=1.3.1" [[package]] name = "requests" -version = "2.25.1" +version = "2.26.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "rsa" @@ -1163,15 +1197,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "slack-sdk" -version = "3.6.0" +version = "3.10.1" description = "The Slack API Platform SDK for Python" category = "main" optional = false python-versions = ">=3.6.0" [package.extras] -optional = ["aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "SQLAlchemy (>=1,<2)", "websockets (>=9.1,<10)", "websocket-client (>=0.57,<1)"] -testing = ["pytest (>=5.4,<6)", "pytest-asyncio (<1)", "Flask-Sockets (>=0.2,<1)", "pytest-cov (>=2,<3)", "codecov (>=2,<3)", "flake8 (>=3,<4)", "black (==21.5b1)", "psutil (>=5,<6)", "databases (>=0.3)"] +optional = ["aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "SQLAlchemy (>=1,<2)", "websockets (>=9.1,<10)", "websocket-client (>=1,<2)"] +testing = ["pytest (>=5.4,<6)", "pytest-asyncio (<1)", "Flask-Sockets (>=0.2,<1)", "Flask (>=1,<2)", "Werkzeug (<2)", "pytest-cov (>=2,<3)", "codecov (>=2,<3)", "flake8 (>=3,<4)", "black (==21.7b0)", "psutil (>=5,<6)", "databases (>=0.3)", "boto3 (<=2)", "moto (<2)"] [[package]] name = "smmap" @@ -1199,7 +1233,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "4.0.2" +version = "4.1.2" description = "Python documentation generator" category = "dev" optional = false @@ -1218,14 +1252,14 @@ requests = ">=2.5.0" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.900)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] @@ -1367,7 +1401,7 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "3.10.0.0" +version = "3.10.0.2" description = "Backported and Experimental Type Hints for Python 3.5+" category = "main" optional = false @@ -1383,7 +1417,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "urllib3" -version = "1.26.5" +version = "1.26.6" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1453,7 +1487,7 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.4.1" +version = "3.5.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1461,12 +1495,12 @@ python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "8875d530ae66f9763b5b0cb84d9d35edc184ef5c141b63d38bf1ff5a1226e556" +content-hash = "ca2a0258a784674ff489a07d0dc8dd2a22373ee39add02cb4676898b8a6993a1" [metadata.files] acre = [] @@ -1530,8 +1564,8 @@ arrow = [ {file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"}, ] astroid = [ - {file = "astroid-2.5.6-py3-none-any.whl", hash = "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e"}, - {file = "astroid-2.5.6.tar.gz", hash = "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"}, + {file = "astroid-2.7.3-py3-none-any.whl", hash = "sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e"}, + {file = "astroid-2.7.3.tar.gz", hash = "sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c"}, ] async-timeout = [ {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, @@ -1554,8 +1588,8 @@ babel = [ {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] blessed = [ - {file = "blessed-1.18.0-py2.py3-none-any.whl", hash = "sha256:5b5e2f0563d5a668c282f3f5946f7b1abb70c85829461900e607e74d7725106e"}, - {file = "blessed-1.18.0.tar.gz", hash = "sha256:1312879f971330a1b7f2c6341f2ae7e2cbac244bfc9d0ecfbbecd4b0293bc755"}, + {file = "blessed-1.18.1-py2.py3-none-any.whl", hash = "sha256:dd7c0d33db9a2e7f597b446996484d0ed46e1586239db064fb5025008937dcae"}, + {file = "blessed-1.18.1.tar.gz", hash = "sha256:8b09936def6bc06583db99b65636b980075733e13550cb6af262ce724a55da23"}, ] cachetools = [ {file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"}, @@ -1566,48 +1600,60 @@ certifi = [ {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] cffi = [ - {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, - {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, - {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, - {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, - {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, - {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, - {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, - {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, - {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, - {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, - {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, - {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, - {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, - {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-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-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-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-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"}, + {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, + {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, + {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, + {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, + {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, + {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, + {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, + {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, + {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, + {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, + {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, + {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, + {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, + {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, + {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, + {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, + {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, + {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, + {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, ] chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] +charset-normalizer = [ + {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, + {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, +] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, @@ -1683,30 +1729,25 @@ coverage = [ {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] cryptography = [ - {file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"}, - {file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"}, - {file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"}, - {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"}, - {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"}, - {file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"}, - {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-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.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, -] -cx-freeze = [ - {file = "cx_Freeze-6.6-cp36-cp36m-win32.whl", hash = "sha256:b3d3a6bcd1a07c50b4e1c907f14842642156110e63a99cd5c73b8a24751e9b97"}, - {file = "cx_Freeze-6.6-cp36-cp36m-win_amd64.whl", hash = "sha256:1935266ec644ea4f7e584985f44cefc0622a449a09980d990833a1a2afcadac8"}, - {file = "cx_Freeze-6.6-cp37-cp37m-win32.whl", hash = "sha256:1eac2b0f254319cc641ce25bd83337effd7936092562fde701f3ffb40e0274ec"}, - {file = "cx_Freeze-6.6-cp37-cp37m-win_amd64.whl", hash = "sha256:2bc46ef6d510811b6002f34a3ae4cbfdea44e18644febd2a404d3ee8e48a9fc4"}, - {file = "cx_Freeze-6.6-cp38-cp38-win32.whl", hash = "sha256:46eb50ebc46f7ae236d16c6a52671ab0f7bb479bea668da19f4b6de3cc413e9e"}, - {file = "cx_Freeze-6.6-cp38-cp38-win_amd64.whl", hash = "sha256:8c3b00476ce385bb58595bffce55aed031e5a6e16ab6e14d8bee9d1d569e46c3"}, - {file = "cx_Freeze-6.6-cp39-cp39-win32.whl", hash = "sha256:6e9340cbcf52d4836980ecc83ddba4f7704ff6654dd41168c146b74f512977ce"}, - {file = "cx_Freeze-6.6-cp39-cp39-win_amd64.whl", hash = "sha256:2fcf1c8b77ae5c06f45be3a9aff79e1dd808c0d624e97561f840dec5ea9b214a"}, - {file = "cx_Freeze-6.6.tar.gz", hash = "sha256:c4af8ad3f7e7d71e291c1dec5d0fb26bbe92df834b098ed35434c901fbd6762f"}, + {file = "cryptography-3.4.8-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14"}, + {file = "cryptography-3.4.8-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb"}, + {file = "cryptography-3.4.8-cp36-abi3-win32.whl", hash = "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7"}, + {file = "cryptography-3.4.8-cp36-abi3-win_amd64.whl", hash = "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e"}, + {file = "cryptography-3.4.8.tar.gz", hash = "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c"}, ] +cx-freeze = [] cx-logging = [ {file = "cx_Logging-3.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9fcd297e5c51470521c47eff0f86ba844aeca6be97e13c3e2114ebdf03fa3c96"}, {file = "cx_Logging-3.0-cp36-cp36m-win32.whl", hash = "sha256:0df4be47c5022cc54316949e283403214568ef599817ced0c0972183d6d4fabb"}, @@ -1753,20 +1794,20 @@ gitdb = [ {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, ] gitpython = [ - {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"}, - {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"}, + {file = "GitPython-3.1.20-py3-none-any.whl", hash = "sha256:b1e1c269deab1b08ce65403cf14e10d2ef1f6c89e33ea7c5e5bb0222ea593b8a"}, + {file = "GitPython-3.1.20.tar.gz", hash = "sha256:df0e072a200703a65387b0cfdf0466e3bab729c0458cf6b7349d0e9877636519"}, ] google-api-core = [ - {file = "google-api-core-1.30.0.tar.gz", hash = "sha256:0724d354d394b3d763bc10dfee05807813c5210f0bd9b8e2ddf6b6925603411c"}, - {file = "google_api_core-1.30.0-py2.py3-none-any.whl", hash = "sha256:92cd9e9f366e84bfcf2524e34d2dc244906c645e731962617ba620da1620a1e0"}, + {file = "google-api-core-1.31.2.tar.gz", hash = "sha256:8500aded318fdb235130bf183c726a05a9cb7c4b09c266bd5119b86cdb8a4d10"}, + {file = "google_api_core-1.31.2-py2.py3-none-any.whl", hash = "sha256:384459a0dc98c1c8cd90b28dc5800b8705e0275a673a7144a513ae80fc77950b"}, ] google-api-python-client = [ {file = "google-api-python-client-1.12.8.tar.gz", hash = "sha256:f3b9684442eec2cfe9f9bb48e796ef919456b82142c7528c5fd527e5224f08bb"}, {file = "google_api_python_client-1.12.8-py2.py3-none-any.whl", hash = "sha256:3c4c4ca46b5c21196bec7ee93453443e477d82cbfa79234d1ce0645f81170eaf"}, ] google-auth = [ - {file = "google-auth-1.31.0.tar.gz", hash = "sha256:154f7889c5d679a6f626f36adb12afbd4dbb0a9a04ec575d989d6ba79c4fd65e"}, - {file = "google_auth-1.31.0-py2.py3-none-any.whl", hash = "sha256:6d47c79b5d09fbc7e8355fd9594cc4cf65fdde5d401c63951eaac4baa1ba2ae1"}, + {file = "google-auth-1.35.0.tar.gz", hash = "sha256:b7033be9028c188ee30200b204ea00ed82ea1162e8ac1df4aa6ded19a191d88e"}, + {file = "google_auth-1.35.0-py2.py3-none-any.whl", hash = "sha256:997516b42ecb5b63e8d80f5632c1a61dddf41d2a4c2748057837e06e00014258"}, ] google-auth-httplib2 = [ {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, @@ -1781,32 +1822,32 @@ httplib2 = [ {file = "httplib2-0.19.1.tar.gz", hash = "sha256:0b12617eeca7433d4c396a100eaecfa4b08ee99aa881e6df6e257a7aad5d533d"}, ] idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, ] imagesize = [ {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, - {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, + {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, + {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, - {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, + {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, + {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, ] jedi = [ {file = "jedi-0.13.3-py2.py3-none-any.whl", hash = "sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c"}, {file = "jedi-0.13.3.tar.gz", hash = "sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b"}, ] jeepney = [ - {file = "jeepney-0.6.0-py3-none-any.whl", hash = "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae"}, - {file = "jeepney-0.6.0.tar.gz", hash = "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657"}, + {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"}, + {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"}, ] jinja2 = [ {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, @@ -1852,12 +1893,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"}, @@ -1866,14 +1917,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"}, @@ -1883,6 +1941,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"}, @@ -1932,56 +1993,79 @@ multidict = [ ] opentimelineio = [] packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] parso = [ {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, ] pathlib2 = [ - {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, - {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, + {file = "pathlib2-2.3.6-py2.py3-none-any.whl", hash = "sha256:3a130b266b3a36134dcc79c17b3c7ac9634f083825ca6ea9d8f557ee6195c9c8"}, + {file = "pathlib2-2.3.6.tar.gz", hash = "sha256:7d8bcb5555003cdf4a8d2872c538faa3a0f5d20630cb360e518ca3b981795e5f"}, ] pillow = [ - {file = "Pillow-8.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9"}, - {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b"}, - {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b"}, - {file = "Pillow-8.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9"}, - {file = "Pillow-8.2.0-cp36-cp36m-win32.whl", hash = "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727"}, - {file = "Pillow-8.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f"}, - {file = "Pillow-8.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d"}, - {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a"}, - {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9"}, - {file = "Pillow-8.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388"}, - {file = "Pillow-8.2.0-cp37-cp37m-win32.whl", hash = "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5"}, - {file = "Pillow-8.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2"}, - {file = "Pillow-8.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4"}, - {file = "Pillow-8.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812"}, - {file = "Pillow-8.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178"}, - {file = "Pillow-8.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb"}, - {file = "Pillow-8.2.0-cp38-cp38-win32.whl", hash = "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232"}, - {file = "Pillow-8.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797"}, - {file = "Pillow-8.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5"}, - {file = "Pillow-8.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484"}, - {file = "Pillow-8.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602"}, - {file = "Pillow-8.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2"}, - {file = "Pillow-8.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef"}, - {file = "Pillow-8.2.0-cp39-cp39-win32.whl", hash = "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713"}, - {file = "Pillow-8.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c"}, - {file = "Pillow-8.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9"}, - {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9"}, - {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8b56553c0345ad6dcb2e9b433ae47d67f95fc23fe28a0bde15a120f25257e291"}, - {file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, + {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, + {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, + {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, + {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, + {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, + {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, + {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, + {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, + {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, + {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, + {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, + {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, + {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, +] +platformdirs = [ + {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, + {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, ] pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] prefixed = [ {file = "prefixed-0.3.2-py2.py3-none-any.whl", hash = "sha256:5e107306462d63f2f03c529dbf11b0026fdfec621a9a008ca639d71de22995c3"}, @@ -2006,9 +2090,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"}, ] @@ -2071,78 +2159,112 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pygments = [ - {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, - {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] pylint = [ - {file = "pylint-2.8.3-py3-none-any.whl", hash = "sha256:792b38ff30903884e4a9eab814ee3523731abd3c463f3ba48d7b627e87013484"}, - {file = "pylint-2.8.3.tar.gz", hash = "sha256:0a049c5d47b629d9070c3932d13bff482b12119b6a241a93bc460b0be16953c8"}, + {file = "pylint-2.10.2-py3-none-any.whl", hash = "sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852"}, + {file = "pylint-2.10.2.tar.gz", hash = "sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1"}, ] pymongo = [ - {file = "pymongo-3.11.4-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:b7efc7e7049ef366777cfd35437c18a4166bb50a5606a1c840ee3b9624b54fc9"}, - {file = "pymongo-3.11.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:517ba47ca04a55b1f50ee8df9fd97f6c37df5537d118fb2718952b8623860466"}, - {file = "pymongo-3.11.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:225c61e08fe517aede7912937939e09adf086c8e6f7e40d4c85ad678c2c2aea3"}, - {file = "pymongo-3.11.4-cp27-cp27m-win32.whl", hash = "sha256:e4e9db78b71db2b1684ee4ecc3e32c4600f18cdf76e6b9ae03e338e52ee4b168"}, - {file = "pymongo-3.11.4-cp27-cp27m-win_amd64.whl", hash = "sha256:8e0004b0393d72d76de94b4792a006cb960c1c65c7659930fbf9a81ce4341982"}, - {file = "pymongo-3.11.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:fedf0dee7a412ca6d1d6d92c158fe9cbaa8ea0cae90d268f9ccc0744de7a97d0"}, - {file = "pymongo-3.11.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f947b359cc4769af8b49be7e37af01f05fcf15b401da2528021148e4a54426d1"}, - {file = "pymongo-3.11.4-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:3a3498a8326111221560e930f198b495ea6926937e249f475052ffc6893a6680"}, - {file = "pymongo-3.11.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:9a4f6e0b01df820ba9ed0b4e618ca83a1c089e48d4f268d0e00dcd49893d4549"}, - {file = "pymongo-3.11.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d65bac5f6724d9ea6f0b5a0f0e4952fbbf209adcf6b5583b54c54bd2fcd74dc0"}, - {file = "pymongo-3.11.4-cp34-cp34m-win32.whl", hash = "sha256:15b083d1b789b230e5ac284442d9ecb113c93f3785a6824f748befaab803b812"}, - {file = "pymongo-3.11.4-cp34-cp34m-win_amd64.whl", hash = "sha256:f08665d3cc5abc2f770f472a9b5f720a9b3ab0b8b3bb97c7c1487515e5653d39"}, - {file = "pymongo-3.11.4-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:977b1d4f868986b4ba5d03c317fde4d3b66e687d74473130cd598e3103db34fa"}, - {file = "pymongo-3.11.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:510cd3bfabb63a07405b7b79fae63127e34c118b7531a2cbbafc7a24fd878594"}, - {file = "pymongo-3.11.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:071552b065e809d24c5653fcc14968cfd6fde4e279408640d5ac58e3353a3c5f"}, - {file = "pymongo-3.11.4-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:f4ba58157e8ae33ee86fadf9062c506e535afd904f07f9be32731f4410a23b7f"}, - {file = "pymongo-3.11.4-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:b413117210fa6d92664c3d860571e8e8727c3e8f2ff197276c5d0cb365abd3ad"}, - {file = "pymongo-3.11.4-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:08b8723248730599c9803ae4c97b8f3f76c55219104303c88cb962a31e3bb5ee"}, - {file = "pymongo-3.11.4-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:8a41fdc751dc4707a4fafb111c442411816a7c225ebb5cadb57599534b5d5372"}, - {file = "pymongo-3.11.4-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:f664ed7613b8b18f0ce5696b146776266a038c19c5cd6efffa08ecc189b01b73"}, - {file = "pymongo-3.11.4-cp35-cp35m-win32.whl", hash = "sha256:5c36428cc4f7fae56354db7f46677fd21222fc3cb1e8829549b851172033e043"}, - {file = "pymongo-3.11.4-cp35-cp35m-win_amd64.whl", hash = "sha256:d0a70151d7de8a3194cdc906bcc1a42e14594787c64b0c1c9c975e5a2af3e251"}, - {file = "pymongo-3.11.4-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:9b9298964389c180a063a9e8bac8a80ed42de11d04166b20249bfa0a489e0e0f"}, - {file = "pymongo-3.11.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b2f41261b648cf5dee425f37ff14f4ad151c2f24b827052b402637158fd056ef"}, - {file = "pymongo-3.11.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e02beaab433fd1104b2804f909e694cfbdb6578020740a9051597adc1cd4e19f"}, - {file = "pymongo-3.11.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:8898f6699f740ca93a0879ed07d8e6db02d68af889d0ebb3d13ab017e6b1af1e"}, - {file = "pymongo-3.11.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:62c29bc36a6d9be68fe7b5aaf1e120b4aa66a958d1e146601fcd583eb12cae7b"}, - {file = "pymongo-3.11.4-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:424799c71ff435094e5fb823c40eebb4500f0e048133311e9c026467e8ccebac"}, - {file = "pymongo-3.11.4-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:3551912f5c34d8dd7c32c6bb00ae04192af47f7b9f653608f107d19c1a21a194"}, - {file = "pymongo-3.11.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:5db59223ed1e634d842a053325f85f908359c6dac9c8ddce8ef145061fae7df8"}, - {file = "pymongo-3.11.4-cp36-cp36m-win32.whl", hash = "sha256:fea5cb1c63efe1399f0812532c7cf65458d38fd011be350bc5021dfcac39fba8"}, - {file = "pymongo-3.11.4-cp36-cp36m-win_amd64.whl", hash = "sha256:d4e62417e89b717a7bcd8576ac3108cd063225942cc91c5b37ff5465fdccd386"}, - {file = "pymongo-3.11.4-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:4c7e8c8e1e1918dcf6a652ac4b9d87164587c26fd2ce5dd81e73a5ab3b3d492f"}, - {file = "pymongo-3.11.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38a7b5140a48fc91681cdb5cb95b7cd64640b43d19259fdd707fa9d5a715f2b2"}, - {file = "pymongo-3.11.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:aff3656af2add93f290731a6b8930b23b35c0c09569150130a58192b3ec6fc61"}, - {file = "pymongo-3.11.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:03be7ad107d252bb7325d4af6309fdd2c025d08854d35f0e7abc8bf048f4245e"}, - {file = "pymongo-3.11.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:6060794aac9f7b0644b299f46a9c6cbc0bc470bd01572f4134df140afd41ded6"}, - {file = "pymongo-3.11.4-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:73326b211e7410c8bd6a74500b1e3f392f39cf10862e243d00937e924f112c01"}, - {file = "pymongo-3.11.4-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:20d75ea11527331a2980ab04762a9d960bcfea9475c54bbeab777af880de61cd"}, - {file = "pymongo-3.11.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:3135dd574ef1286189f3f04a36c8b7a256376914f8cbbce66b94f13125ded858"}, - {file = "pymongo-3.11.4-cp37-cp37m-win32.whl", hash = "sha256:7c97554ea521f898753d9773891d0347ebfaddcc1dee2ad94850b163171bf1f1"}, - {file = "pymongo-3.11.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a08c8b322b671857c81f4c30cd3c8df2895fd3c0e9358714f39e0ef8fb327702"}, - {file = "pymongo-3.11.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f3d851af3852f16ad4adc7ee054fd9c90a7a5063de94d815b7f6a88477b9f4c6"}, - {file = "pymongo-3.11.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3bfc7689a1bacb9bcd2f2d5185d99507aa29f667a58dd8adaa43b5a348139e46"}, - {file = "pymongo-3.11.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b8f94acd52e530a38f25e4d5bf7ddfdd4bea9193e718f58419def0d4406b58d3"}, - {file = "pymongo-3.11.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e4b631688dfbdd61b5610e20b64b99d25771c6d52d9da73349342d2a0f11c46a"}, - {file = "pymongo-3.11.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:474e21d0e07cd09679e357d1dac76e570dab86665e79a9d3354b10a279ac6fb3"}, - {file = "pymongo-3.11.4-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:421d13523d11c57f57f257152bc4a6bb463aadf7a3918e9c96fefdd6be8dbfb8"}, - {file = "pymongo-3.11.4-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:0cabfc297f4cf921f15bc789a8fbfd7115eb9f813d3f47a74b609894bc66ab0d"}, - {file = "pymongo-3.11.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:fe4189846448df013cd9df11bba38ddf78043f8c290a9f06430732a7a8601cce"}, - {file = "pymongo-3.11.4-cp38-cp38-win32.whl", hash = "sha256:eb4d176394c37a76e8b0afe54b12d58614a67a60a7f8c0dd3a5afbb013c01092"}, - {file = "pymongo-3.11.4-cp38-cp38-win_amd64.whl", hash = "sha256:fffff7bfb6799a763d3742c59c6ee7ffadda21abed557637bc44ed1080876484"}, - {file = "pymongo-3.11.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:13acf6164ead81c9fc2afa0e1ea6d6134352973ce2bb35496834fee057063c04"}, - {file = "pymongo-3.11.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d360e5d5dd3d55bf5d1776964625018d85b937d1032bae1926dd52253decd0db"}, - {file = "pymongo-3.11.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:0aaf4d44f1f819360f9432df538d54bbf850f18152f34e20337c01b828479171"}, - {file = "pymongo-3.11.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:08bda7b2c522ff9f1e554570da16298271ebb0c56ab9699446aacba249008988"}, - {file = "pymongo-3.11.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:1a994a42f49dab5b6287e499be7d3d2751776486229980d8857ad53b8333d469"}, - {file = "pymongo-3.11.4-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:161fcd3281c42f644aa8dec7753cca2af03ce654e17d76da4f0dab34a12480ca"}, - {file = "pymongo-3.11.4-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:78f07961f4f214ea8e80be63cffd5cc158eb06cd922ffbf6c7155b11728f28f9"}, - {file = "pymongo-3.11.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ad31f184dcd3271de26ab1f9c51574afb99e1b0e484ab1da3641256b723e4994"}, - {file = "pymongo-3.11.4-cp39-cp39-win32.whl", hash = "sha256:5e606846c049ed40940524057bfdf1105af6066688c0e6a1a3ce2038589bae70"}, - {file = "pymongo-3.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:3491c7de09e44eded16824cb58cf9b5cc1dc6f066a0bb7aa69929d02aa53b828"}, - {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"}, + {file = "pymongo-3.12.0-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:072ba7cb65c8aa4d5c5659bf6722ee85781c9d7816dc00679b8b6f3dff1ddafc"}, + {file = "pymongo-3.12.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d6e11ffd43184d529d6752d6dcb62b994f903038a17ea2168ef1910c96324d26"}, + {file = "pymongo-3.12.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7412a36798966624dc4c57d64aa43c2d1100b348abd98daaac8e99e57d87e1d7"}, + {file = "pymongo-3.12.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e8a82e35d52ad6f867e88096a1a2b9bdc7ec4d5e65c7b4976a248bf2d1a32a93"}, + {file = "pymongo-3.12.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dcd3d0009fbb6e454d729f8b22d0063bd9171c31a55e0f0271119bd4f2700023"}, + {file = "pymongo-3.12.0-cp27-cp27m-win32.whl", hash = "sha256:1bc6fe7279ff40c6818db002bf5284aa03ec181ea1b1ceaeee33c289d412afa7"}, + {file = "pymongo-3.12.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e2b7670c0c8c6b501464150dd49dd0d6be6cb7f049e064124911cec5514fa19e"}, + {file = "pymongo-3.12.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:316c1b8723afa9870567cd6dff35d440b2afeda53aa13da6c5ab85f98ed6f5ca"}, + {file = "pymongo-3.12.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:255a35bf29185f44b412e31a927d9dcedda7c2c380127ecc4fbf2f61b72fa978"}, + {file = "pymongo-3.12.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ffbae429ba9e42d0582d3ac63fdb410338892468a2107d8ff68228ec9a39a0ed"}, + {file = "pymongo-3.12.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c188db6cf9e14dbbb42f5254292be96f05374a35e7dfa087cc2140f0ff4f10f6"}, + {file = "pymongo-3.12.0-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:6fb3f85870ae26896bb44e67db94045f2ebf00c5d41e6b66cdcbb5afd644fc18"}, + {file = "pymongo-3.12.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:aaa038eafb7186a4abbb311fcf20724be9363645882bbce540bef4797e812a7a"}, + {file = "pymongo-3.12.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:7d98ce3c42921bb91566121b658e0d9d59a9082a9bd6f473190607ff25ab637f"}, + {file = "pymongo-3.12.0-cp34-cp34m-win32.whl", hash = "sha256:b0a0cf39f589e52d801fdef418305562bc030cdf8929217463c8433c65fd5c2f"}, + {file = "pymongo-3.12.0-cp34-cp34m-win_amd64.whl", hash = "sha256:ceae3ab9e11a27aaab42878f1d203600dfd24f0e43678b47298219a0f10c0d30"}, + {file = "pymongo-3.12.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:5e574664f1468872cd40f74e4811e22b1aa4de9399d6bcfdf1ee6ea94c017fcf"}, + {file = "pymongo-3.12.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73b400fdc22de84bae0dbf1a22613928a41612ec0a3d6ed47caf7ad4d3d0f2ff"}, + {file = "pymongo-3.12.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:cbf8672edeb7b7128c4a939274801f0e32bbf5159987815e3d1eace625264a46"}, + {file = "pymongo-3.12.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:a634a4730ce0b0934ed75e45beba730968e12b4dafbb22f69b3b2f616d9e644e"}, + {file = "pymongo-3.12.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:c55782a55f4a013a78ac5b6ee4b8731a192dea7ab09f1b6b3044c96d5128edd4"}, + {file = "pymongo-3.12.0-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:11f9e0cfc84ade088a38df2708d0b958bb76360181df1b2e1e1a41beaa57952b"}, + {file = "pymongo-3.12.0-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:186104a94d39b8412f8e3de385acd990a628346a4402d4f3a288a82b8660bd22"}, + {file = "pymongo-3.12.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:70761fd3c576b027eec882b43ee0a8e5b22ff9c20cdf4d0400e104bc29e53e34"}, + {file = "pymongo-3.12.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:333bfad77aa9cd11711febfb75eed0bb537a1d022e1c252714dad38993590240"}, + {file = "pymongo-3.12.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fa8957e9a1b202cb45e6b839c241cd986c897be1e722b81d2f32e9c6aeee80b0"}, + {file = "pymongo-3.12.0-cp35-cp35m-win32.whl", hash = "sha256:4ba0def4abef058c0e5101e05e3d5266e6fffb9795bbf8be0fe912a7361a0209"}, + {file = "pymongo-3.12.0-cp35-cp35m-win_amd64.whl", hash = "sha256:a0e5dff6701fa615f165306e642709e1c1550d5b237c5a7a6ea299886828bd50"}, + {file = "pymongo-3.12.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:b542d56ed1b8d5cf3bb36326f814bd2fbe8812dfd2582b80a15689ea433c0e35"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a325600c83e61e3c9cebc0c2b1c8c4140fa887f789085075e8f44c8ff2547eb9"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:48d5bc80ab0af6b60c4163c5617f5cd23f2f880d7600940870ea5055816af024"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5cab230e7cabdae9ff23c12271231283efefb944c1b79bed79a91beb65ba547"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d73e10772152605f6648ba4410318594f1043bbfe36d2fadee7c4b8912eff7c5"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:b1c4874331ab960429caca81acb9d2932170d66d6d6f87e65dc4507a85aca152"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:a3566acfbcde46911c52810374ecc0354fdb841284a3efef6ff7105bc007e9a8"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:b3b5b3cbc3fdf4fcfa292529df2a85b5d9c7053913a739d3069af1e12e12219f"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd3854148005c808c485c754a184c71116372263709958b42aefbef2e5dd373a"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f55c1ddcc1f6050b07d468ce594f55dbf6107b459e16f735d26818d7be1e9538"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ced944dcdd561476deef7cb7bfd4987c69fffbfeff6d02ca4d5d4fd592d559b7"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78ecb8d42f50d393af912bfb1fb1dcc9aabe9967973efb49ee577e8f1cea494c"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1970cfe2aec1bf74b40cf30c130ad10cd968941694630386db33e1d044c22a2e"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8bf42d3b32f586f4c9e37541769993783a534ad35531ce8a4379f6fa664fba9"}, + {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bc9ac81e73573516070d24ce15da91281922811f385645df32bd3c8a45ab4684"}, + {file = "pymongo-3.12.0-cp36-cp36m-win32.whl", hash = "sha256:d04ca462cb99077e6c059e97c072957caf2918e6e4191e3161c01c439e0193de"}, + {file = "pymongo-3.12.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f2acf9bbcd514e901f82c4ca6926bbd2ae61716728f110b4343eb0a69612d018"}, + {file = "pymongo-3.12.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:b754240daafecd9d5fce426b0fbaaed03f4ebb130745c8a4ae9231fffb8d75e5"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:af586e85144023686fb0af09c8cdf672484ea182f352e7ceead3d832de381e1b"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fe5872ce6f9627deac8314bdffd3862624227c3de4c17ef0cc78bbf0402999eb"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f6977a520bd96e097c8a37a8cbb9faa1ea99d21bf84190195056e25f688af73d"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:2dbfbbded947a83a3dffc2bd1ec4750c17e40904692186e2c55a3ad314ca0222"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:a752ecd1a26000a6d67be7c9a2e93801994a8b3f866ac95b672fbc00225ca91a"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:1bab889ae7640eba739f67fcbf8eff252dddc60d4495e6ddd3a87cd9a95fdb52"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:f94c7d22fb36b184734dded7345a04ec5f95130421c775b8b0c65044ef073f34"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec5ca7c0007ce268048bbe0ffc6846ed1616cf3d8628b136e81d5e64ff3f52a2"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c72d08acdf573455b2b9d2b75b8237654841d63a48bc2327dc102c6ee89b75a"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6ea08758b6673610b3c5bdf47189286cf9c58b1077558706a2f6f8744922527"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d5ec90276f71af3a29917b30f2aec2315a2759b5f8d45b3b63a07ca8a070a3"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:625befa3bc9b40746a749115cc6a15bf20b9bd7597ca55d646205b479a2c99c7"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d1131562ddc2ea8a446f66c2648d7dabec2b3816fc818528eb978a75a6d23b2e"}, + {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eee42a1cc06565f6b21caa1f504ec15e07de7ebfd520ab57f8cb3308bc118e22"}, + {file = "pymongo-3.12.0-cp37-cp37m-win32.whl", hash = "sha256:94d38eba4d1b5eb3e6bfece0651b855a35c44f32fd91f512ab4ba41b8c0d3e66"}, + {file = "pymongo-3.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e018a4921657c2d3f89c720b7b90b9182e277178a04a7e9542cc79d7d787ca51"}, + {file = "pymongo-3.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c6a9948916a7bbcc6d3a9f6fb75db1acb5546078023bfb3db6efabcd5a67527"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e9faf8d4712d5ea301d74abfcf6dafe4b7f4af7936e91f283b0ad7bf69ed3e3a"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cc2894fe91f31a513860238ede69fe47fada21f9e7ddfe73f7f9fef93a971e41"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:053b4ebf91c7395d1fcd2ce6a9edff0024575b7b2de6781554a4114448a8adc9"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:39dafa2eaf577d1969f289dc9a44501859a1897eb45bd589e93ce843fc610800"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:246ec420e4c8744fceb4e259f906211b9c198e1f345e6158dcd7cbad3737e11e"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:208debdcf76ed39ebf24f38509f50dc1c100e31e8653817fedb8e1f867850a13"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:18290649759f9db660972442aa606f845c368db9b08c4c73770f6da14113569b"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657ad80de8ec9ed656f28844efc801a0802961e8c6a85038d97ff6f555ef4919"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b772bab31cbd9cb911e41e1a611ebc9497f9a32a7348e2747c38210f75c00f41"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2399a85b54f68008e483b2871f4a458b4c980469c7fe921595ede073e4844f1e"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e66780f14c2efaf989cd3ac613b03ee6a8e3a0ba7b96c0bb14adca71a427e55"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02dc0b0f48ed3cd06c13b7e31b066bf91e00dac5f8147b0a0a45f9009bfab857"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:070a4ef689c9438a999ec3830e69b208ff0d12251846e064d947f97d819d1d05"}, + {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:db93608a246da44d728842b8fa9e45aa9782db76955f634a707739a8d53ff544"}, + {file = "pymongo-3.12.0-cp38-cp38-win32.whl", hash = "sha256:5af390fa9faf56c93252dab09ea57cd020c9123aa921b63a0ed51832fdb492e7"}, + {file = "pymongo-3.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:a2239556ff7241584ce57be1facf25081669bb457a9e5cbe68cce4aae6567aa1"}, + {file = "pymongo-3.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cda9e628b1315beec8341e8c04aac9a0b910650b05e0751e42e399d5694aeacb"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:845a8b83798b2fb11b09928413cb32692866bfbc28830a433d9fa4c8c3720dd0"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:da8288bc4a7807c6715416deed1c57d94d5e03e93537889e002bf985be503f1a"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a9ba2a63777027b06b116e1ea8248e66fd1bedc2c644f93124b81a91ddbf6d88"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:9a13661681d17e43009bb3e85e837aa1ec5feeea1e3654682a01b8821940f8b3"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:6b89dc51206e4971c5568c797991eaaef5dc2a6118d67165858ad11752dba055"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:701e08457183da70ed96b35a6b43e6ba1df0b47c837b063cde39a1fbe1aeda81"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:e7a33322e08021c37e89cae8ff06327503e8a1719e97c69f32c31cbf6c30d72c"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd1f49f949a658c4e8f81ed73f9aad25fcc7d4f62f767f591e749e30038c4e1d"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6d055f01b83b1a4df8bb0c61983d3bdffa913764488910af3620e5c2450bf83"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd6ff2192f34bd622883c745a56f492b1c9ccd44e14953e8051c33024a2947d5"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19d4bd0fc29aa405bb1781456c9cfff9fceabb68543741eb17234952dbc2bbb0"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24f8aeec4d6b894a6128844e50ff423dd02462ee83addf503c598ee3a80ddf3d"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b6055e0ef451ff73c93d0348d122a0750dddf323b9361de5835dac2f6cf7fc1"}, + {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6261bee7c5abadeac7497f8f1c43e521da78dd13b0a2439f526a7b0fc3788824"}, + {file = "pymongo-3.12.0-cp39-cp39-win32.whl", hash = "sha256:2e92aa32300a0b5e4175caec7769f482b292769807024a86d674b3f19b8e3755"}, + {file = "pymongo-3.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ce83f17f641a62a4dfb0ba1b8a3c1ced7c842f511b5450d90c030c7828e3693"}, + {file = "pymongo-3.12.0-py2.7-macosx-10.14-intel.egg", hash = "sha256:d1740776b70367277323fafb76bcf09753a5cc9824f5d705bac22a34ff3668ea"}, + {file = "pymongo-3.12.0.tar.gz", hash = "sha256:b88d1742159bc93a078733f9789f563cef26f5e370eba810476a71aa98e5fbc2"}, ] pynput = [ {file = "pynput-1.7.3-py2.py3-none-any.whl", hash = "sha256:fea5777454f896bd79d35393088cd29a089f3b2da166f0848a922b1d5a807d4f"}, @@ -2210,27 +2332,47 @@ pyqt5-sip = [ {file = "PyQt5_sip-12.9.0.tar.gz", hash = "sha256:d3e4489d7c2b0ece9d203ae66e573939f7f60d4d29e089c9f11daa17cfeaae32"}, ] pyrsistent = [ - {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2"}, + {file = "pyrsistent-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427"}, + {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef"}, + {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c"}, + {file = "pyrsistent-0.18.0-cp38-cp38-win32.whl", hash = "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78"}, + {file = "pyrsistent-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b"}, + {file = "pyrsistent-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4"}, + {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680"}, + {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426"}, + {file = "pyrsistent-0.18.0-cp39-cp39-win32.whl", hash = "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b"}, + {file = "pyrsistent-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea"}, + {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, ] pytest = [ - {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, - {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] pytest-print = [ - {file = "pytest_print-0.2.1-py2.py3-none-any.whl", hash = "sha256:2cfcdeee8b398457d3e3488f1fde5f8303b404c30187be5fcb4c7818df5f4529"}, - {file = "pytest_print-0.2.1.tar.gz", hash = "sha256:8f61e5bb2d031ee88d19a5a7695a0c863caee7b1478f1a82d080c2128b76ad83"}, + {file = "pytest_print-0.3.0-py2.py3-none-any.whl", hash = "sha256:53fb0f71d371f137ac2e7171d92f204eb45055580e8c7920df619d9b2ee45359"}, + {file = "pytest_print-0.3.0.tar.gz", hash = "sha256:769f1b1b0943b2941dbeeaac6985766e76b341130ed538f88c23ebcd7087b90d"}, ] python-dateutil = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-xlib = [ - {file = "python-xlib-0.30.tar.gz", hash = "sha256:74131418faf9e7b83178c71d9d80297fbbd678abe99ae9258f5a20cd027acb5f"}, - {file = "python_xlib-0.30-py2.py3-none-any.whl", hash = "sha256:c4c92cd47e07588b2cbc7d52de18407b2902c3812d7cdec39cd2177b060828e2"}, + {file = "python-xlib-0.31.tar.gz", hash = "sha256:74d83a081f532bc07f6d7afcd6416ec38403d68f68b9b9dc9e1f28fbf2d799e9"}, + {file = "python_xlib-0.31-py2.py3-none-any.whl", hash = "sha256:1ec6ce0de73d9e6592ead666779a5732b384e5b8fb1f1886bd0a81cafa477759"}, ] python3-xlib = [ {file = "python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8"}, @@ -2256,16 +2398,16 @@ pywin32-ctypes = [ {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] "qt.py" = [ - {file = "Qt.py-1.3.3-py2.py3-none-any.whl", hash = "sha256:9e3f5417187c98d246918a9b27a9e1f8055e089bdb2b063a2739986bc19a3d2e"}, - {file = "Qt.py-1.3.3.tar.gz", hash = "sha256:601606127f70be9adc82c248d209d696cccbd1df242c24d3fb1a9e399f3ecaf1"}, + {file = "Qt.py-1.3.6-py2.py3-none-any.whl", hash = "sha256:7edf6048d07a6924707506b5ba34a6e05d66dde9a3f4e3a62f9996ccab0b91c7"}, + {file = "Qt.py-1.3.6.tar.gz", hash = "sha256:0d78656a2f814602eee304521c7bf5da0cec414818b3833712c77524294c404a"}, ] recommonmark = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, ] requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] rsa = [ {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"}, @@ -2284,8 +2426,8 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] slack-sdk = [ - {file = "slack_sdk-3.6.0-py2.py3-none-any.whl", hash = "sha256:e1b257923a1ef88b8620dd3abff94dc5b3eee16ef37975d101ba9e60123ac3af"}, - {file = "slack_sdk-3.6.0.tar.gz", hash = "sha256:195f044e02a2844579a7a26818ce323e85dde8de224730c859644918d793399e"}, + {file = "slack_sdk-3.10.1-py2.py3-none-any.whl", hash = "sha256:f17b71a578e94204d9033bffded634475f4ca0a6274c6c7a4fd8a9cb0ac7cd8b"}, + {file = "slack_sdk-3.10.1.tar.gz", hash = "sha256:2b4dde7728eb4ff5a581025d204578ccff25a5d8f0fe11ae175e3ce6e074434f"}, ] smmap = [ {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, @@ -2300,8 +2442,8 @@ speedcopy = [ {file = "speedcopy-2.1.0.tar.gz", hash = "sha256:8bb1a6c735900b83901a7be84ba2175ed3887c13c6786f97dea48f2ea7d504c2"}, ] sphinx = [ - {file = "Sphinx-4.0.2-py3-none-any.whl", hash = "sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4"}, - {file = "Sphinx-4.0.2.tar.gz", hash = "sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c"}, + {file = "Sphinx-4.1.2-py3-none-any.whl", hash = "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544"}, + {file = "Sphinx-4.1.2.tar.gz", hash = "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13"}, ] sphinx-qt-documentation = [ {file = "sphinx_qt_documentation-0.3-py3-none-any.whl", hash = "sha256:bee247cb9e4fc03fc496d07adfdb943100e1103320c3e5e820e0cfa7c790d9b6"}, @@ -2379,17 +2521,17 @@ typed-ast = [ {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] uritemplate = [ {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"}, ] urllib3 = [ - {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, - {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, @@ -2446,6 +2588,6 @@ yarl = [ {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, ] zipp = [ - {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, - {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, + {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, + {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, ] diff --git a/pyproject.toml b/pyproject.toml index a57ae19224..24e51a17bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ slack-sdk = "^3.6.0" flake8 = "^3.7" autopep8 = "^1.4" coverage = "*" -cx_freeze = "*" +cx_freeze = { version = "6.7", source = "openpype" } GitPython = "^3.1.17" jedi = "^0.13" Jinja2 = "^2.11" diff --git a/tools/docker_build.sh b/tools/docker_build.sh index c27041a1af..d2dbef2e48 100755 --- a/tools/docker_build.sh +++ b/tools/docker_build.sh @@ -27,17 +27,15 @@ create_container () { fi local id=$(<"$openpype_root/build/docker-image.id") echo -e "${BIYellow}---${RST} Creating container from $id ..." - local cid="$(docker create $id bash)" + cid="$(docker create $id bash)" if [ $? -ne 0 ] ; then echo -e "${BIRed}!!!${RST} Cannot create container." exit 1 fi - return $cid } retrieve_build_log () { create_container - local cid=$? echo -e "${BIYellow}***${RST} Copying build log to ${BIWhite}$openpype_root/build/build.log${RST}" docker cp "$cid:/opt/openpype/build/build.log" "$openpype_root/build" } @@ -65,7 +63,6 @@ main () { echo -e "${BIGreen}>>>${RST} Copying build from container ..." create_container - local cid=$? echo -e "${BIYellow}---${RST} Copying ..." docker cp "$cid:/opt/openpype/build/exe.linux-x86_64-3.7" "$openpype_root/build" docker cp "$cid:/opt/openpype/build/build.log" "$openpype_root/build" @@ -79,7 +76,7 @@ main () { chown -R $username ./build echo -e "${BIGreen}>>>${RST} All done, you can delete container:" - echo -e "${BIYellow}$id${RST}" + echo -e "${BIYellow}$cid${RST}" } return_code=0 From 5a425a5269c57496dfbd02adbaedbc969a812e22 Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Wed, 8 Sep 2021 16:09:40 -0700 Subject: [PATCH 162/716] Moving project folder structure creation out of ftrack module #1989 --- openpype/api.py | 11 ++- openpype/lib/__init__.py | 6 +- openpype/lib/path_tools.py | 43 ++++++++++ .../action_create_project_structure.py | 79 +++---------------- openpype/settings/__init__.py | 5 +- openpype/settings/lib.py | 29 +++++++ 6 files changed, 98 insertions(+), 75 deletions(-) diff --git a/openpype/api.py b/openpype/api.py index ce18097eca..dcff127e9f 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -4,6 +4,7 @@ from .settings import ( get_current_project_settings, get_anatomy_settings, get_environments, + get_project_basic_paths, SystemSettings, ProjectSettings @@ -24,7 +25,8 @@ from .lib import ( get_latest_version, get_global_environments, get_local_site_id, - change_openpype_mongo_url + change_openpype_mongo_url, + create_project_folders ) from .lib.mongo import ( @@ -72,6 +74,7 @@ __all__ = [ "get_current_project_settings", "get_anatomy_settings", "get_environments", + "get_project_basic_paths", "SystemSettings", @@ -120,5 +123,9 @@ __all__ = [ "get_global_environments", "get_local_site_id", - "change_openpype_mongo_url" + "change_openpype_mongo_url", + + "get_project_basic_paths", + "create_project_folders" + ] diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 12c04a4236..886d61fb39 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -139,7 +139,8 @@ from .plugin_tools import ( from .path_tools import ( version_up, get_version_from_path, - get_last_version_from_path + get_last_version_from_path, + create_project_folders ) from .editorial import ( @@ -268,5 +269,6 @@ __all__ = [ "range_from_frames", "frames_to_secons", "frames_to_timecode", - "make_sequence_collection" + "make_sequence_collection", + "create_project_folders" ] diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index e1dd1e7f10..fab0879759 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -2,8 +2,12 @@ import os import re import logging +from openpype.api import Anatomy + log = logging.getLogger(__name__) +pattern_array = re.compile(r"\[.*\]") +project_root_key = "__project_root__" def _rreplace(s, a, b, n=1): """Replace a with b in string s from right side n times.""" @@ -119,3 +123,42 @@ def get_last_version_from_path(path_dir, filter): return filtred_files[-1] return None + + +def compute_paths(basic_paths_items, project_root): + output = [] + for path_items in basic_paths_items: + clean_items = [] + for path_item in path_items: + matches = re.findall(pattern_array, path_item) + if len(matches) > 0: + path_item = path_item.replace(matches[0], "") + if path_item == project_root_key: + path_item = project_root + clean_items.append(path_item) + output.append(os.path.normpath(os.path.sep.join(clean_items))) + return output + + +def create_project_folders(basic_paths, project_name): + anatomy = Anatomy(project_name) + roots_paths = [] + if isinstance(anatomy.roots, dict): + for root in anatomy.roots.values(): + roots_paths.append(root.value) + else: + roots_paths.append(anatomy.roots.value) + + for root_path in roots_paths: + project_root = os.path.join(root_path, project_name) + full_paths = compute_paths(basic_paths, project_root) + # Create folders + for path in full_paths: + full_path = path.format(project_root=project_root) + if os.path.exists(full_path): + log.debug( + "Folder already exists: {}".format(full_path) + ) + else: + log.debug("Creating folder: {}".format(full_path)) + os.makedirs(full_path) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py index 035a1c60de..b0de792473 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py @@ -3,7 +3,7 @@ import re import json from openpype.modules.ftrack.lib import BaseAction, statics_icon -from openpype.api import Anatomy, get_project_settings +from openpype.api import get_project_basic_paths, create_project_folders class CreateProjectFolders(BaseAction): @@ -72,25 +72,18 @@ class CreateProjectFolders(BaseAction): def launch(self, session, entities, event): # Get project entity project_entity = self.get_project_from_entity(entities[0]) - # Load settings for project project_name = project_entity["full_name"] - project_settings = get_project_settings(project_name) - project_folder_structure = ( - project_settings["global"]["project_folder_structure"] - ) - if not project_folder_structure: - return { - "success": False, - "message": "Project structure is not set." - } - try: - if isinstance(project_folder_structure, str): - project_folder_structure = json.loads(project_folder_structure) - # Get paths based on presets - basic_paths = self.get_path_items(project_folder_structure) - self.create_folders(basic_paths, project_entity) + basic_paths = get_project_basic_paths(project_name) + if not basic_paths: + return { + "success": False, + "message": "Project structure is not set." + } + + # Invoking OpenPype API to create the project folders + create_project_folders(basic_paths, project_name) self.create_ftrack_entities(basic_paths, project_entity) except Exception as exc: @@ -195,58 +188,6 @@ class CreateProjectFolders(BaseAction): self.session.commit() return new_ent - def get_path_items(self, in_dict): - output = [] - for key, value in in_dict.items(): - if not value: - output.append(key) - else: - paths = self.get_path_items(value) - for path in paths: - if not isinstance(path, (list, tuple)): - path = [path] - - output.append([key, *path]) - - return output - - def compute_paths(self, basic_paths_items, project_root): - output = [] - for path_items in basic_paths_items: - clean_items = [] - for path_item in path_items: - matches = re.findall(self.pattern_array, path_item) - if len(matches) > 0: - path_item = path_item.replace(matches[0], "") - if path_item == self.project_root_key: - path_item = project_root - clean_items.append(path_item) - output.append(os.path.normpath(os.path.sep.join(clean_items))) - return output - - def create_folders(self, basic_paths, project): - anatomy = Anatomy(project["full_name"]) - roots_paths = [] - if isinstance(anatomy.roots, dict): - for root in anatomy.roots.values(): - roots_paths.append(root.value) - else: - roots_paths.append(anatomy.roots.value) - - for root_path in roots_paths: - project_root = os.path.join(root_path, project["full_name"]) - full_paths = self.compute_paths(basic_paths, project_root) - # Create folders - for path in full_paths: - full_path = path.format(project_root=project_root) - if os.path.exists(full_path): - self.log.debug( - "Folder already exists: {}".format(full_path) - ) - else: - self.log.debug("Creating folder: {}".format(full_path)) - os.makedirs(full_path) - def register(session): CreateProjectFolders(session).register() diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index b5810deef4..1905b6dc73 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -7,7 +7,8 @@ from .lib import ( get_current_project_settings, get_anatomy_settings, get_environments, - get_local_settings + get_local_settings, + get_project_basic_paths ) from .entities import ( SystemSettings, @@ -24,7 +25,7 @@ __all__ = ( "get_anatomy_settings", "get_environments", "get_local_settings", - + "get_project_basic_paths", "SystemSettings", "ProjectSettings" ) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 5c2c0dcd94..6d8ece1b53 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -901,6 +901,35 @@ def get_general_environments(): return environments +def _list_path_items(folder_structure): + output = [] + for key, value in folder_structure.items(): + if not value: + output.append(key) + else: + paths = _list_path_items(value) + for path in paths: + if not isinstance(path, (list, tuple)): + path = [path] + + output.append([key, *path]) + + return output + + +def get_project_basic_paths(project_name): + project_settings = get_project_settings(project_name) + folder_structure = ( + project_settings["global"]["project_folder_structure"] + ) + if not folder_structure: + return [] + + if isinstance(folder_structure, str): + folder_structure = json.loads(folder_structure) + return _list_path_items(folder_structure) + + def clear_metadata_from_settings(values): """Remove all metadata keys from loaded settings.""" if isinstance(values, dict): From 46f38b1e02888769f93727ae77baedc6182c5c26 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:18:02 +0200 Subject: [PATCH 163/716] Fix typos --- openpype/modules/README.md | 168 ++++++++++++++++++------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index a6857b2c51..abc7ed3961 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -1,31 +1,31 @@ # OpenPype modules/addons -OpenPype modules should contain separated logic of specific kind of implementation, like Ftrack connection and usage code or Deadline farm rendering or may contain only special plugins. Addons work the same way currently there is no difference in module and addon. +OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its usage code, Deadline farm rendering or may contain only special plugins. Addons work the same way currently, there is no difference between module and addon functionality. ## Modules concept -- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the modulo located -- modules or addons should never be imported directly even if you know possible full import path - - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts +- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the module located +- modules or addons should never be imported directly, even if you know possible full import path + - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts ### TODOs - add module/addon manifest - - definition of module (not 100% defined content e.g. minimum require OpenPype version etc.) - - defying that folder is content of a module or an addon + - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.) + - defying that folder is content of a module or an addon ## Base class `OpenPypeModule` - abstract class as base for each module -- implementation should be module's api withou GUI parts -- may implement `get_global_environments` method which should return dictionary of environments that are globally appliable and value is the same for whole studio if launched at any workstation (except os specific paths) +- implementation should contain module's api without GUI parts +- may implement `get_global_environments` method which should return dictionary of environments that are globally applicable and value is the same for whole studio if launched at any workstation (except os specific paths) - abstract parts: - - `name` attribute - name of a module - - `initialize` method - method for own initialization of a module (should not override `__init__`) - - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules -- `__init__` should not be overriden and `initialize` should not do time consuming part but only prepare base data about module - - also keep in mind that they may be initialized in headless mode + - `name` attribute - name of a module + - `initialize` method - method for own initialization of a module (should not override `__init__`) + - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules +- `__init__` should not be overridden and `initialize` should not do time consuming part but only prepare base data about module + - also keep in mind that they may be initialized in headless mode - connection with other modules is made with help of interfaces ## Addon class `OpenPypeAddOn` -- inherit from `OpenPypeModule` but is enabled by default and don't have to implement `initialize` and `connect_with_modules` methods - - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) +- inherits from `OpenPypeModule` but is enabled by default and doesn't have to implement `initialize` and `connect_with_modules` methods + - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) ## How to add addons/modules - in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder @@ -34,110 +34,110 @@ OpenPype modules should contain separated logic of specific kind of implementati ## Addon/module settings - addons/modules may have defined custom settings definitions with default values - it is based on settings type `dynamic_schema` which has `name` - - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults - - they can't be added to any schema hierarchy - - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) - - addons may define it's dynamic schema items -- they can be defined with class which inherit from `BaseModuleSettingsDef` - - it is recommended to use preimplemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values - - check it's docstring and check for `example_addon` in example addons + - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults + - they can't be added to any schema hierarchy + - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) + - addons may define it's dynamic schema items +- they can be defined with class which inherits from `BaseModuleSettingsDef` + - it is recommended to use pre implemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values + - check it's docstring and check for `example_addon` in example addons - settings definition returns schemas by dynamic schemas names # Interfaces -- interface is class that has defined abstract methods to implement and may contain preimplemented helper methods +- interface is class that has defined abstract methods to implement and may contain pre implemented helper methods - module that inherit from an interface must implement those abstract methods otherwise won't be initialized -- it is easy to find which module object inherited from which interfaces withh 100% chance they have implemented required methods +- it is easy to find which module object inherited from which interfaces with 100% chance they have implemented required methods - interfaces can be defined in `interfaces.py` inside module directory - - the file can't use relative imports or import anything from other parts - of module itself at the header of file - - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation + - the file can't use relative imports or import anything from other parts + of module itself at the header of file + - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation ## Base class `OpenPypeInterface` - has nothing implemented - has ABCMeta as metaclass - is defined to be able find out classes which inherit from this base to be - able tell this is an Interface + able tell this is an Interface ## Global interfaces - few interfaces are implemented for global usage ### IPluginPaths -- module want to add directory path/s to avalon or publish plugins +- module wants to add directory path/s to avalon or publish plugins - module must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"` - - each key may contain list or string with path to directory with plugins + - each key may contain list or string with a path to directory with plugins ### ITrayModule -- module has more logic when used in tray - - it is possible that module can be used only in tray +- module has more logic when used in a tray + - it is possible that module can be used only in the tray - abstract methods - - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules` - - `tray_menu` - add actions to tray widget's menu that represent the module - - `tray_start` - start of module's login in tray - - module is initialized and connected with other modules - - `tray_exit` - module's cleanup like stop and join threads etc. - - order of calling is based on implementation this order is how it works with `TrayModulesManager` - - it is recommended to import and use GUI implementaion only in these methods + - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules` + - `tray_menu` - add actions to tray widget's menu that represent the module + - `tray_start` - start of module's login in tray + - module is initialized and connected with other modules + - `tray_exit` - module's cleanup like stop and join threads etc. + - order of calling is based on implementation this order is how it works with `TrayModulesManager` + - it is recommended to import and use GUI implementation only in these methods - has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init` - - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations + - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations ### ITrayService -- inherit from `ITrayModule` and implement `tray_menu` method for you - - add action to submenu "Services" in tray widget menu with icon and label -- abstract atttribute `label` - - label shown in menu -- interface has preimplemented methods to change icon color - - `set_service_running` - green icon - - `set_service_failed` - red icon - - `set_service_idle` - orange icon - - these states must be set by module itself `set_service_running` is default state on initialization +- inherits from `ITrayModule` and implements `tray_menu` method for you + - adds action to submenu "Services" in tray widget menu with icon and label +- abstract attribute `label` + - label shown in menu +- interface has pre implemented methods to change icon color + - `set_service_running` - green icon + - `set_service_failed` - red icon + - `set_service_idle` - orange icon + - these states must be set by module itself `set_service_running` is default state on initialization ### ITrayAction -- inherit from `ITrayModule` and implement `tray_menu` method for you - - add action to tray widget menu with label -- abstract atttribute `label` - - label shown in menu +- inherits from `ITrayModule` and implements `tray_menu` method for you + - adds action to tray widget menu with label +- abstract attribute `label` + - label shown in menu - abstract method `on_action_trigger` - - what should happen when action is triggered -- NOTE: It is good idea to implement logic in `on_action_trigger` to api method and trigger that methods on callbacks this gives ability to trigger that method outside tray + - what should happen when an action is triggered +- NOTE: It is a good idea to implement logic in `on_action_trigger` to the api method and trigger that method on callbacks. This gives ability to trigger that method outside tray ## Modules interfaces -- modules may have defined their interfaces to be able recognize other modules that would want to use their features -- -### Example: -- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which of other modules want to add paths to server/user event handlers - - Clockify module use `IFtrackEventHandlerPaths` and return paths to clockify ftrack synchronizers +- modules may have defined their own interfaces to be able to recognize other modules that would want to use their features -- Clockify has more inharitance it's class definition looks like +### Example: +- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which other modules want to add paths to server/user event handlers + - Clockify module use `IFtrackEventHandlerPaths` and returns paths to clockify ftrack synchronizers + +- Clockify inherits from more interfaces. It's class definition looks like: ``` class ClockifyModule( - OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize. - ITrayModule, # Says has special implementation when used in tray. - IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher). - IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server. - ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module. + OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize. + ITrayModule, # Says has special implementation when used in tray. + IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher). + IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server. + ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module. ): ``` ### ModulesManager -- collect module classes and tries to initialize them +- collects module classes and tries to initialize them - important attributes - - `modules` - list of available attributes - - `modules_by_id` - dictionary of modules mapped by their ids - - `modules_by_name` - dictionary of modules mapped by their names - - all these attributes contain all found modules even if are not enabled + - `modules` - list of available attributes + - `modules_by_id` - dictionary of modules mapped by their ids + - `modules_by_name` - dictionary of modules mapped by their names + - all these attributes contain all found modules even if are not enabled - helper methods - - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them - - `collect_plugin_paths` collect plugin paths from all enabled modules - - output is always dictionary with all keys and values as list - ``` - { - "publish": [], - "create": [], - "load": [], - "actions": [] - } - ``` + - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them + - `collect_plugin_paths` collects plugin paths from all enabled modules + - output is always dictionary with all keys and values as an list + ``` + { + "publish": [], + "create": [], + "load": [], + "actions": [] + } + ``` ### TrayModulesManager -- inherit from `ModulesManager` -- has specific implementations for Pype Tray tool and handle `ITrayModule` methods +- inherits from `ModulesManager` +- has specific implementation for Pype Tray tool and handle `ITrayModule` methods \ No newline at end of file From a7932ff6d536fb00415158b617d59a348afaa455 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:38:15 +0200 Subject: [PATCH 164/716] Fix typos --- .../modules/example_addons/example_addon/addon.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 5a25b80616..5573e33cc1 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -21,11 +21,11 @@ from openpype_interfaces import ( ) -# Settings definiton of this addon using `JsonFilesSettingsDef` -# - JsonFilesSettingsDef is prepared settings definiton using json files -# to define settings and store defaul values +# Settings definition of this addon using `JsonFilesSettingsDef` +# - JsonFilesSettingsDef is prepared settings definition using json files +# to define settings and store default values class AddonSettingsDef(JsonFilesSettingsDef): - # This will add prefix to every schema and template from `schemas` + # This will add prefixes to every schema and template from `schemas` # subfolder. # - it is not required to fill the prefix but it is highly # recommended as schemas and templates may have name clashes across @@ -48,7 +48,7 @@ class AddonSettingsDef(JsonFilesSettingsDef): class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): """This Addon has defined it's settings and interface. - This example has system settings with enabled option. And use + This example has system settings with an enabled option. And use few other interfaces: - `IPluginPaths` to define custom plugin paths - `ITrayAction` to be shown in tray tool @@ -70,7 +70,7 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): def tray_init(self): """Implementation of abstract method for `ITrayAction`. - We're definetely in trat tool so we can precreate dialog. + We're definitely in tray tool so we can pre create dialog. """ self._create_dialog() @@ -101,7 +101,7 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): """Show dialog with connected modules. This can be called from anywhere but can also crash in headless mode. - There is not way how to prevent addon to do invalid operations if he's + There is no way to prevent addon to do invalid operations if he's not handling them. """ # Make sure dialog is created From 3643b8e1bd7936dd840b343f9b70946600e249d7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:43:43 +0200 Subject: [PATCH 165/716] Hound --- .../modules/default_modules/sync_server/providers/gdrive.py | 3 +-- .../default_modules/sync_server/providers/local_drive.py | 3 +-- .../default_modules/sync_server/sync_server_module.py | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 5db728f2de..da54eecb8e 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -122,8 +122,7 @@ class GDriveHandler(AbstractProvider): { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index b3482ac1d8..9678d38ed8 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -56,8 +56,7 @@ class LocalDriveHandler(AbstractProvider): { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 39b5c9314e..976a349bfa 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -445,7 +445,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # # Methods for Settings to get appriate values to fill forms # def get_configurable_items(self, scope=None): # """ - # Returns list of sites that could be configurable for all projects. + # Returns list of sites that could be configurable for all projects # # Could be filtered by 'scope' argument (list) # @@ -468,8 +468,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # }, # { # key:"credentials_url", label:"Credentials url", - # "value":"'c:/projects/cred.json'", "type": "text", - # "namespace": "{project_setting}/global/sync_server/ + # "value":"'c:/projects/cred.json'", "type": "text", # noqa: E501 + # "namespace": "{project_setting}/global/sync_server/ # noqa: E501 # sites" # } # ] From 2230d60ff407f033ab7127d5f03c3b62e0309e80 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:48:28 +0200 Subject: [PATCH 166/716] #1976 - added 'key' --- .../default_modules/sync_server/providers/gdrive.py | 3 +++ .../sync_server/providers/local_drive.py | 13 +++---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index da54eecb8e..3bfd6f4854 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -120,6 +120,7 @@ class GDriveHandler(AbstractProvider): editable = [ # credentials could be override on Project or User level { + 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 @@ -127,6 +128,7 @@ class GDriveHandler(AbstractProvider): # roots could be override only on Project leve, User cannot # { + 'key': "roots", 'label': "Roots", 'type': 'dict' } @@ -145,6 +147,7 @@ class GDriveHandler(AbstractProvider): editable = [ # credentials could be override on Project or User level { + 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 9678d38ed8..4b703267d5 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -49,18 +49,10 @@ class LocalDriveHandler(AbstractProvider): Returns: (list) of dict """ - # {platform} tells that value is multiplatform and only specific OS - # should be returned + # for non 'studio' sites, 'studio' is configured in Anatomy editable = [ - # credentials could be override on Project or User level - { - 'label': "Credentials url", - 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 - }, - # roots could be override only on Project leve, User cannot - # { + 'key': "roots", 'label': "Roots", 'type': 'dict' } @@ -78,6 +70,7 @@ class LocalDriveHandler(AbstractProvider): """ editable = [ { + 'key': "roots", 'label': "Roots", 'type': 'dict' } From fc0872a99750e0c648f7b5c77bb1de65c753a924 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:52:25 +0200 Subject: [PATCH 167/716] #1976 - small fixes --- .../modules/default_modules/sync_server/providers/gdrive.py | 5 ++--- .../default_modules/sync_server/providers/local_drive.py | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 3bfd6f4854..8c93f41d67 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -8,7 +8,7 @@ import platform from openpype.api import Logger from openpype.api import get_system_settings from .abstract_provider import AbstractProvider -from ..utils import time_function, ResumableError, EditableScopes +from ..utils import time_function, ResumableError log = Logger().get_logger("SyncServer") @@ -122,8 +122,7 @@ class GDriveHandler(AbstractProvider): { 'key': "credentials_url", 'label': "Credentials url", - 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 + 'type': 'text' }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 4b703267d5..8e5f170bc9 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -7,8 +7,6 @@ import time from openpype.api import Logger, Anatomy from .abstract_provider import AbstractProvider -from ..utils import EditableScopes - log = Logger().get_logger("SyncServer") From a5bbe779fbec6fb061234072017aeb212630a594 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:56:43 +0200 Subject: [PATCH 168/716] Hound --- .../default_modules/sync_server/providers/gdrive.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 8c93f41d67..f1ec0b6a0d 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -118,14 +118,13 @@ class GDriveHandler(AbstractProvider): # {platform} tells that value is multiplatform and only specific OS # should be returned editable = [ - # credentials could be override on Project or User level - { + # credentials could be overriden on Project or User level + { 'key': "credentials_url", 'label': "Credentials url", 'type': 'text' }, - # roots could be override only on Project leve, User cannot - # + # roots could be overriden only on Project leve, User cannot { 'key': "roots", 'label': "Roots", @@ -149,8 +148,7 @@ class GDriveHandler(AbstractProvider): 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 } ] return editable From 2167671d8e8adf8f2ea4eeaf2745266899494f2b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 14:18:13 +0200 Subject: [PATCH 169/716] #1784 - fixing Hound, typos, run_tests command Added documentation --- openpype/pype_commands.py | 14 ++++++++++---- tests/README.md | 15 ++++++++++++++- .../hosts/maya/test_publish_in_maya.py | 2 -- tests/lib/file_handler.py | 15 +++++++-------- tests/lib/testing_classes.py | 2 +- .../modules/sync_server/test_site_operations.py | 5 ++++- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index c309ee8c09..5288749e8b 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -268,13 +268,19 @@ class PypeCommands: """ print("run_tests") import subprocess - folder = folder or "../tests" + + if folder: + folder = " ".join(list(folder)) + else: + folder = "../tests" + mark_str = pyargs_str = '' if mark: - mark_str = "- m {}".format(mark) + mark_str = "-m {}".format(mark) if pyargs: pyargs_str = "--pyargs {}".format(pyargs) - subprocess.run("pytest {} {} {}".format(folder, mark_str, pyargs_str)) - + cmd = "pytest {} {} {}".format(folder, mark_str, pyargs_str) + print("Running {}".format(cmd)) + subprocess.run(cmd) diff --git a/tests/README.md b/tests/README.md index 727b89a86e..6317b2ab3c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,7 @@ Automatic tests for OpenPype ============================ Structure: -- integration - end to end tests, slow +- integration - end to end tests, slow (see README.md in the integration folder for more info) - openpype/modules/MODULE_NAME - structure follow directory structure in code base - fixture - sample data `(MongoDB dumps, test files etc.)` - `tests.py` - single or more pytest files for MODULE_NAME @@ -10,3 +10,16 @@ Structure: - fixture - `tests.py` +How to run: +---------- +- single test class could be run by PyCharm and its pytest runner directly +- OR +- use Openpype command 'runtests' from command line +-- `${OPENPYPE_ROOT}/start.py runtests` + +By default, this command will run all tests in ${OPENPYPE_ROOT}/tests. + +Specific location could be provided to this command as an argument, either as absolute path, or relative path to ${OPENPYPE_ROOT}. +(eg. `${OPENPYPE_ROOT}/start.py runtests ../tests/integration`) will trigger only tests in `integration` folder. + +See `${OPENPYPE_ROOT}/cli.py:runtests` for other arguments. diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index b9c63651f1..c178a6687e 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -1,8 +1,6 @@ import pytest -import sys import os import shutil -import glob from tests.lib.testing_classes import PublishTest diff --git a/tests/lib/file_handler.py b/tests/lib/file_handler.py index 98e14b0541..ee3abc6ecb 100644 --- a/tests/lib/file_handler.py +++ b/tests/lib/file_handler.py @@ -193,11 +193,10 @@ class RemoteFileHandler: urllib.request.Request(url, headers={"User-Agent": USER_AGENT})) \ as response: - for chunk in iter(lambda: response.read(chunk_size), - ""): - if not chunk: - break - fh.write(chunk) + for chunk in iter(lambda: response.read(chunk_size), ""): + if not chunk: + break + fh.write(chunk) @staticmethod def _get_redirect_url(url, max_hops): @@ -218,7 +217,7 @@ class RemoteFileHandler: ) @staticmethod - def _get_confirm_token(response): # type: ignore[name-defined] + def _get_confirm_token(response): for key, value in response.cookies.items(): if key.startswith('download_warning'): return value @@ -227,7 +226,7 @@ class RemoteFileHandler: @staticmethod def _save_response_content( - response_gen, destination, # type: ignore[name-defined] + response_gen, destination, ): with open(destination, "wb") as f: pbar = enlighten.Counter( @@ -241,7 +240,7 @@ class RemoteFileHandler: pbar.close() @staticmethod - def _quota_exceeded(first_chunk): # type: ignore[name-defined] + def _quota_exceeded(first_chunk): try: return "Google Drive - Quota exceeded" in first_chunk.decode() except UnicodeDecodeError: diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 6cd3c10d3e..1832efb7ed 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -260,4 +260,4 @@ class PublishTest(ModuleUnitTest): f != expected_dir_base and os.path.exists(f)) not_matched = expected.difference(published) - assert not not_matched, "Missing {} files".format(not_matched) \ No newline at end of file + assert not not_matched, "Missing {} files".format(not_matched) diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index ab15025399..6a861100a4 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -21,6 +21,9 @@ class TestSiteOperation(ModuleUnitTest): REPRESENTATION_ID = "60e578d0c987036c6a7b741d" + TEST_FILES = [("1eCwPljuJeOI8A3aisfOIBKKjcmIycTEt", + "test_site_operations.zip", '')] + @pytest.fixture(scope="module") def setup_sync_server_module(self, dbcon): """Get sync_server_module from ModulesManager""" @@ -109,7 +112,7 @@ class TestSiteOperation(ModuleUnitTest): site_names = [site["name"] for site in ret["files"][0]["sites"]] assert 'test_site' not in site_names, "Site name wasn't removed" - + @pytest.mark.usefixtures("setup_sync_server_module") def test_remove_site_again(self, dbcon, setup_sync_server_module): """Depends on test_add_site, must trow exception""" From 72b2f44fa9e829925858e70d621d8622f60d1b5b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:25:58 +0200 Subject: [PATCH 170/716] added loading of steps from schema --- openpype/settings/entities/input_entities.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index ebc70b840d..128625619a 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,6 +379,10 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) + steps = self.schema_data.get("steps", None) + if steps is None: + steps = 1 / (10 ** self.decimal) + self.steps = steps def _convert_to_valid_type(self, value): if isinstance(value, str): From 7e973a5de1fba402a04d1e780a4a6a35e78c8afc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:27:32 +0200 Subject: [PATCH 171/716] added steps to Number widget --- openpype/tools/settings/settings/widgets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index b821c3bb2c..2caf8c33ba 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -92,11 +92,15 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox): min_value = kwargs.pop("minimum", -99999) max_value = kwargs.pop("maximum", 99999) decimals = kwargs.pop("decimal", 0) + steps = kwargs.pop("steps", None) + super(NumberSpinBox, self).__init__(*args, **kwargs) self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setDecimals(decimals) self.setMinimum(min_value) self.setMaximum(max_value) + if steps is not None: + self.setSingleStep(steps) def focusInEvent(self, event): super(NumberSpinBox, self).focusInEvent(event) From 7af864a6e0eeb256177525785707d66cc385495c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:28:17 +0200 Subject: [PATCH 172/716] steps can not be set --- openpype/settings/entities/input_entities.py | 5 +---- openpype/tools/settings/settings/item_widgets.py | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 128625619a..4afa0d9484 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,10 +379,7 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) - steps = self.schema_data.get("steps", None) - if steps is None: - steps = 1 / (10 ** self.decimal) - self.steps = steps + self.steps = self.schema_data.get("steps", None) def _convert_to_valid_type(self, value): if isinstance(value, str): diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 736ba77652..da74c2adc5 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -411,7 +411,8 @@ class NumberWidget(InputWidget): kwargs = { "minimum": self.entity.minimum, "maximum": self.entity.maximum, - "decimal": self.entity.decimal + "decimal": self.entity.decimal, + "steps": self.entity.steps } self.input_field = NumberSpinBox(self.content_widget, **kwargs) input_field_stretch = 1 @@ -426,6 +427,10 @@ class NumberWidget(InputWidget): int(self.entity.minimum * slider_multiplier), int(self.entity.maximum * slider_multiplier) ) + if self.entity.steps is not None: + slider_widget.setSingleStep( + self.entity.steps * slider_multiplier + ) self.content_layout.addWidget(slider_widget, 1) From e3b8e25a1a15cbdc24519e49f0067126bb071610 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:33:09 +0200 Subject: [PATCH 173/716] added steps to readme --- openpype/settings/entities/schemas/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 9b53e89dd7..c8432f0f2e 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -316,6 +316,7 @@ How output of the schema could look like on save: - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +- key `"steps"` will change single step value of UI inputs (using arrows and wheel scroll) - for UI it is possible to show slider to enable this option set `show_slider` to `true` ``` { From e75e9a6465c59751ffb62ac143532255eef9a837 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:33:19 +0200 Subject: [PATCH 174/716] make sure that steps are not `0` --- openpype/settings/entities/input_entities.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 4afa0d9484..0ded3ab7e5 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,7 +379,11 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) - self.steps = self.schema_data.get("steps", None) + steps = self.schema_data.get("steps", None) + # Make sure that steps are not set to `0` + if steps == 0: + steps = None + self.steps = steps def _convert_to_valid_type(self, value): if isinstance(value, str): From 40a6712384e5fec86bd0ab1ccdae2ec8d8317fad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:35:19 +0200 Subject: [PATCH 175/716] added steps to avalon mongo timeout --- .../entities/schemas/system_schema/schema_modules.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..b52a646954 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -28,7 +28,8 @@ "type": "number", "key": "AVALON_TIMEOUT", "minimum": 0, - "label": "Avalon Mongo Timeout (ms)" + "label": "Avalon Mongo Timeout (ms)", + "steps": 100 }, { "type": "path", From d961e0a26209fa13da5c184d68be765bfca7c956 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 15:17:23 +0200 Subject: [PATCH 176/716] replaced `providers-enum` with `sync-server-providers` to be able set system settings for provider --- openpype/settings/entities/__init__.py | 8 ++-- .../settings/entities/dict_conditional.py | 46 +++++++++++++++++++ openpype/settings/entities/enum_entity.py | 38 --------------- .../schemas/system_schema/schema_modules.json | 9 +--- 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 8c30d5044c..aae2d1fa89 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -105,7 +105,6 @@ from .enum_entity import ( AppsEnumEntity, ToolsEnumEntity, TaskTypeEnumEntity, - ProvidersEnum, DeadlineUrlEnumEntity, AnatomyTemplatesEnumEntity ) @@ -113,7 +112,10 @@ from .enum_entity import ( from .list_entity import ListEntity from .dict_immutable_keys_entity import DictImmutableKeysEntity from .dict_mutable_keys_entity import DictMutableKeysEntity -from .dict_conditional import DictConditionalEntity +from .dict_conditional import ( + DictConditionalEntity, + SyncServerProviders +) from .anatomy_entities import AnatomyEntity @@ -161,7 +163,6 @@ __all__ = ( "AppsEnumEntity", "ToolsEnumEntity", "TaskTypeEnumEntity", - "ProvidersEnum", "DeadlineUrlEnumEntity", "AnatomyTemplatesEnumEntity", @@ -172,6 +173,7 @@ __all__ = ( "DictMutableKeysEntity", "DictConditionalEntity", + "SyncServerProviders", "AnatomyEntity" ) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d7b416921c..6f27760570 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -724,3 +724,49 @@ class DictConditionalEntity(ItemEntity): for children in self.children.values(): for child_entity in children: child_entity.reset_callbacks() + + +class SyncServerProviders(DictConditionalEntity): + schema_types = ["sync-server-providers"] + + def _add_children(self): + self.enum_key = "provider" + self.enum_label = "Provider" + + enum_children = self._get_enum_children() + if not enum_children: + enum_children.append({ + "key": None, + "label": "< Nothing >" + }) + self.enum_children = enum_children + + super(SyncServerProviders, self)._add_children() + + def _get_enum_children(self): + from openpype_modules import sync_server + + from openpype_modules.sync_server.providers import lib as lib_providers + + provider_code_to_label = {} + providers = lib_providers.factory.providers + for provider_code, provider_info in providers.items(): + provider, _ = provider_info + provider_code_to_label[provider_code] = provider.LABEL + + system_settings_schema = ( + sync_server + .SyncServerModule + .get_system_settings_schema() + ) + + enum_children = [] + for provider_code, configurables in system_settings_schema.items(): + label = provider_code_to_label.get(provider_code) or provider_code + + enum_children.append({ + "key": provider_code, + "label": label, + "children": configurables + }) + return enum_children diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index cb532c5ae0..66279f529d 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -407,44 +407,6 @@ class TaskTypeEnumEntity(BaseEnumEntity): self._current_value = new_value -class ProvidersEnum(BaseEnumEntity): - schema_types = ["providers-enum"] - - def _item_initalization(self): - self.multiselection = False - self.value_on_not_set = "" - self.enum_items = [] - self.valid_keys = set() - self.valid_value_types = (str, ) - self.placeholder = None - - def _get_enum_values(self): - from openpype_modules.sync_server.providers import lib as lib_providers - - providers = lib_providers.factory.providers - - valid_keys = set() - valid_keys.add('') - enum_items = [{'': 'Choose Provider'}] - for provider_code, provider_info in providers.items(): - provider, _ = provider_info - enum_items.append({provider_code: provider.LABEL}) - valid_keys.add(provider_code) - - return enum_items, valid_keys - - def set_override_state(self, *args, **kwargs): - super(ProvidersEnum, self).set_override_state(*args, **kwargs) - - self.enum_items, self.valid_keys = self._get_enum_values() - - value_on_not_set = list(self.valid_keys)[0] - if self._current_value is NOT_SET: - self._current_value = value_on_not_set - - self.value_on_not_set = value_on_not_set - - class DeadlineUrlEnumEntity(BaseEnumEntity): schema_types = ["deadline_url-enum"] diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..9961341ba5 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -121,14 +121,7 @@ "collapsible_key": false, "object_type": { - "type": "dict", - "children": [ - { - "type": "providers-enum", - "key": "provider", - "label": "Provider" - } - ] + "type": "sync-server-providers" } } ] From 971844ed867e71a87f146a78f8be411bf6ba2a17 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Sep 2021 16:25:20 +0200 Subject: [PATCH 177/716] nuke: python3 compatibility wip --- openpype/hosts/nuke/api/lib.py | 4 +++- openpype/hosts/nuke/plugins/publish/extract_ouput_node.py | 5 +++-- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 5 +++++ openpype/hosts/nuke/plugins/publish/precollect_workfile.py | 1 - openpype/hosts/nuke/startup/write_to_read.py | 3 ++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 7e7cd27f90..257bf8d64e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -727,7 +727,7 @@ class WorkfileSettings(object): log.error(msg) nuke.message(msg) - log.warning(">> root_dict: {}".format(root_dict)) + log.debug(">> root_dict: {}".format(root_dict)) # first set OCIO if self._root_node["colorManagement"].value() \ @@ -1277,6 +1277,7 @@ class ExporterReview: def clean_nodes(self): for node in self._temp_nodes: nuke.delete(node) + self._temp_nodes = [] self.log.info("Deleted nodes...") @@ -1301,6 +1302,7 @@ class ExporterReviewLut(ExporterReview): lut_style=None): # initialize parent class ExporterReview.__init__(self, klass, instance) + self._temp_nodes = [] # deal with now lut defined in viewer lut if hasattr(klass, "viewer_lut_raw"): diff --git a/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py b/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py index a144761e5f..c3a6a3b167 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py +++ b/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py @@ -2,6 +2,7 @@ import nuke import pyblish.api from avalon.nuke import maintained_selection + class CreateOutputNode(pyblish.api.ContextPlugin): """Adding output node for each ouput write node So when latly user will want to Load .nk as LifeGroup or Precomp @@ -15,8 +16,8 @@ class CreateOutputNode(pyblish.api.ContextPlugin): def process(self, context): # capture selection state with maintained_selection(): - active_node = [node for inst in context[:] - for node in inst[:] + active_node = [node for inst in context + for node in inst if "ak:family" in node.knobs()] if active_node: diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 55f7b746fc..0c9af66435 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -1,3 +1,4 @@ +import sys import os import nuke from avalon.nuke import lib as anlib @@ -5,6 +6,10 @@ import pyblish.api import openpype +if sys.version_info[0] >= 3: + unicode = str + + class ExtractThumbnail(openpype.api.Extractor): """Extracts movie and thumbnail with baked in luts diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 5d3eb5f609..e10cfe7b47 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -3,7 +3,6 @@ import pyblish.api import os import openpype.api as pype from avalon.nuke import lib as anlib -reload(anlib) class CollectWorkfile(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/nuke/startup/write_to_read.py b/openpype/hosts/nuke/startup/write_to_read.py index deb5ce1b82..295a6e3c85 100644 --- a/openpype/hosts/nuke/startup/write_to_read.py +++ b/openpype/hosts/nuke/startup/write_to_read.py @@ -69,7 +69,8 @@ def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame): frames = sorted(frames) firstframe = frames[0] lastframe = frames[len(frames) - 1] - if lastframe < 0: + + if int(lastframe) < 0: lastframe = firstframe return filepath, firstframe, lastframe From 6cced73ac9e07b36112311b2aa03653089571354 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 9 Sep 2021 16:36:50 +0200 Subject: [PATCH 178/716] change to debian, add platform selection --- Dockerfile | 84 +++++++++++++++++------------------------ Dockerfile.centos7 | 87 +++++++++++++++++++++++++++++++++++++++++++ README.md | 11 ++++++ tools/docker_build.sh | 17 ++++++++- 4 files changed, 148 insertions(+), 51 deletions(-) create mode 100644 Dockerfile.centos7 diff --git a/Dockerfile b/Dockerfile index 78611860ea..cef83b5811 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ # Build Pype docker image -FROM centos:7 AS builder -ARG OPENPYPE_PYTHON_VERSION=3.7.10 +FROM debian:bookworm-slim AS builder +ARG OPENPYPE_PYTHON_VERSION=3.7.12 +LABEL maintainer="info@openpype.io" +LABEL description="Docker Image to build and run OpenPype" LABEL org.opencontainers.image.name="pypeclub/openpype" LABEL org.opencontainers.image.title="OpenPype Docker Image" LABEL org.opencontainers.image.url="https://openpype.io/" @@ -9,57 +11,49 @@ LABEL org.opencontainers.image.source="https://github.com/pypeclub/pype" USER root -# update base -RUN yum -y install deltarpm \ - && yum -y update \ - && yum clean all +ARG DEBIAN_FRONTEND=noninteractive -# add tools we need -RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \ - && yum -y install centos-release-scl \ - && yum -y install \ +# update base +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ bash \ - which \ git \ - devtoolset-7-gcc* \ - make \ cmake \ + make \ curl \ wget \ - gcc \ - zlib-devel \ - bzip2 \ - bzip2-devel \ - readline-devel \ - sqlite sqlite-devel \ - openssl-devel \ - openssl-libs \ - tk-devel libffi-devel \ - qt5-qtbase-devel \ - patchelf \ - && yum clean all + build-essential \ + checkinstall \ + libssl-dev \ + zlib1g-dev \ + libbz2-dev \ + libreadline-dev \ + libsqlite3-dev \ + llvm \ + libncursesw5-dev \ + xz-utils \ + tk-dev \ + libxml2-dev \ + libxmlsec1-dev \ + libffi-dev \ + liblzma-dev \ + patchelf + +SHELL ["/bin/bash", "-c"] RUN mkdir /opt/openpype -# RUN useradd -m pype -# RUN chown pype /opt/openpype -# USER pype -RUN curl https://pyenv.run | bash -ENV PYTHON_CONFIGURE_OPTS --enable-shared - -RUN echo 'export PATH="$HOME/.pyenv/bin:$PATH"'>> $HOME/.bashrc \ +RUN curl https://pyenv.run | bash \ + && echo 'export PATH="$HOME/.pyenv/bin:$PATH"'>> $HOME/.bashrc \ && echo 'eval "$(pyenv init -)"' >> $HOME/.bashrc \ && echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/.bashrc \ - && echo 'eval "$(pyenv init --path)"' >> $HOME/.bashrc -RUN source $HOME/.bashrc && pyenv install ${OPENPYPE_PYTHON_VERSION} + && echo 'eval "$(pyenv init --path)"' >> $HOME/.bashrc \ + && source $HOME/.bashrc && pyenv install ${OPENPYPE_PYTHON_VERSION} COPY . /opt/openpype/ -RUN rm -rf /openpype/.poetry || echo "No Poetry installed yet." -# USER root -# RUN chown -R pype /opt/openpype -RUN chmod +x /opt/openpype/tools/create_env.sh && chmod +x /opt/openpype/tools/build.sh -# USER pype +RUN chmod +x /opt/openpype/tools/create_env.sh && chmod +x /opt/openpype/tools/build.sh WORKDIR /opt/openpype @@ -68,18 +62,8 @@ RUN cd /opt/openpype \ && pyenv local ${OPENPYPE_PYTHON_VERSION} RUN source $HOME/.bashrc \ - && ./tools/create_env.sh - -RUN source $HOME/.bashrc \ + && ./tools/create_env.sh \ && ./tools/fetch_thirdparty_libs.sh RUN source $HOME/.bashrc \ && bash ./tools/build.sh - -RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.7/lib \ - && cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.7/lib \ - && cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.7/lib \ - && cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib - -RUN cd /opt/openpype \ - rm -rf ./vendor/bin diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 new file mode 100644 index 0000000000..0e2fdd4ba0 --- /dev/null +++ b/Dockerfile.centos7 @@ -0,0 +1,87 @@ +# Build Pype docker image +FROM centos:7 AS builder +ARG OPENPYPE_PYTHON_VERSION=3.7.10 + +LABEL org.opencontainers.image.name="pypeclub/openpype" +LABEL org.opencontainers.image.title="OpenPype Docker Image" +LABEL org.opencontainers.image.url="https://openpype.io/" +LABEL org.opencontainers.image.source="https://github.com/pypeclub/pype" + +USER root + +# update base +RUN yum -y install deltarpm \ + && yum -y update \ + && yum clean all + +# add tools we need +RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \ + && yum -y install centos-release-scl \ + && yum -y install \ + bash \ + which \ + git \ + devtoolset-7-gcc* \ + make \ + cmake \ + curl \ + wget \ + gcc \ + zlib-devel \ + bzip2 \ + bzip2-devel \ + readline-devel \ + sqlite sqlite-devel \ + openssl-devel \ + openssl-libs \ + tk-devel libffi-devel \ + qt5-qtbase-devel \ + patchelf \ + ncurses \ + ncurses-devel \ + && yum clean all + +RUN mkdir /opt/openpype +# RUN useradd -m pype +# RUN chown pype /opt/openpype +# USER pype + +RUN curl https://pyenv.run | bash +# ENV PYTHON_CONFIGURE_OPTS --enable-shared + +RUN echo 'export PATH="$HOME/.pyenv/bin:$PATH"'>> $HOME/.bashrc \ + && echo 'eval "$(pyenv init -)"' >> $HOME/.bashrc \ + && echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/.bashrc \ + && echo 'eval "$(pyenv init --path)"' >> $HOME/.bashrc +RUN source $HOME/.bashrc && pyenv install ${OPENPYPE_PYTHON_VERSION} + +COPY . /opt/openpype/ +RUN rm -rf /openpype/.poetry || echo "No Poetry installed yet." +# USER root +# RUN chown -R pype /opt/openpype +RUN chmod +x /opt/openpype/tools/create_env.sh && chmod +x /opt/openpype/tools/build.sh + +# USER pype + +WORKDIR /opt/openpype + +RUN cd /opt/openpype \ + && source $HOME/.bashrc \ + && pyenv local ${OPENPYPE_PYTHON_VERSION} + +RUN source $HOME/.bashrc \ + && ./tools/create_env.sh + +RUN source $HOME/.bashrc \ + && ./tools/fetch_thirdparty_libs.sh + +RUN source $HOME/.bashrc \ + && bash ./tools/build.sh + +RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.7/lib \ + && cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.7/lib \ + && cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.7/lib \ + && cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib + +RUN cd /opt/openpype \ + rm -rf ./vendor/bin diff --git a/README.md b/README.md index 209af24c75..0e450fc48d 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,12 @@ Easiest way to build OpenPype on Linux is using [Docker](https://www.docker.com/ sudo ./tools/docker_build.sh ``` +This will by default use Debian as base image. If you need to make Centos 7 compatible build, please run: + +```sh +sudo ./tools/docker_build.sh centos7 +``` + If all is successful, you'll find built OpenPype in `./build/` folder. #### Manual build @@ -158,6 +164,11 @@ you'll need also additional libraries for Qt5: ```sh sudo apt install qt5-default ``` +or if you are on Ubuntu > 20.04, there is no `qt5-default` packages so you need to install its content individually: + +```sh +sudo apt-get install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools +```
    diff --git a/tools/docker_build.sh b/tools/docker_build.sh index d2dbef2e48..04c26424eb 100755 --- a/tools/docker_build.sh +++ b/tools/docker_build.sh @@ -40,6 +40,21 @@ retrieve_build_log () { docker cp "$cid:/opt/openpype/build/build.log" "$openpype_root/build" } +openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) + + +if [ -z $1 ]; then + dockerfile="Dockerfile" +else + dockerfile="Dockerfile.$1" + if [ ! -f "$openpype_root/$dockerfile" ]; then + echo -e "${BIRed}!!!${RST} Dockerfile for specifed platform ${BIWhite}$1${RST} doesn't exist." + exit 1 + else + echo -e "${BIGreen}>>>${RST} Using Dockerfile for ${BIWhite}$1${RST} ..." + fi +fi + # Main main () { openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) @@ -53,7 +68,7 @@ main () { echo -e "${BIGreen}>>>${RST} Running docker build ..." # docker build --pull --no-cache -t pypeclub/openpype:$openpype_version . - docker build --pull --iidfile $openpype_root/build/docker-image.id -t pypeclub/openpype:$openpype_version . + docker build --pull --iidfile $openpype_root/build/docker-image.id -t pypeclub/openpype:$openpype_version -f $dockerfile . if [ $? -ne 0 ] ; then echo $? echo -e "${BIRed}!!!${RST} Docker build failed." From d90a866b5bbcc55878068f20777c9db2ad6e68b1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 9 Sep 2021 17:11:40 +0200 Subject: [PATCH 179/716] =?UTF-8?q?add=20changes=20to=20docs=20?= =?UTF-8?q?=F0=9F=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/docs/dev_build.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/website/docs/dev_build.md b/website/docs/dev_build.md index b3e0c24fc2..f71118eba6 100644 --- a/website/docs/dev_build.md +++ b/website/docs/dev_build.md @@ -84,6 +84,13 @@ You can use Docker to build OpenPype. Just run: ```shell $ sudo ./tools/docker_build.sh ``` + +This will by default use Debian as base image. If you need to make Centos 7 compatible build, please run: + +```sh +sudo ./tools/docker_build.sh centos7 +``` + and you should have built OpenPype in `build` directory. It is using **Centos 7** as a base image. @@ -323,14 +330,18 @@ Same as: poetry run python ./tools/create_zip.py ``` -### docker_build.sh +### docker_build.sh *[variant]* Script to build OpenPype on [Docker](https://www.docker.com/) enabled systems - usually Linux and Windows with [Docker Desktop](https://docs.docker.com/docker-for-windows/install/) and [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL) installed. It must be run with administrative privileges - `sudo ./docker_build.sh`. -It will use **Centos 7** base image to build OpenPype. You'll see your build in `./build` folder. +It will use latest **Debian** base image to build OpenPype. If you need to build OpenPype for +older systems like Centos 7, use `centos7` as argument. This will use another Dockerfile to build +OpenPype with **Centos 7** as base image. + +You'll see your build in `./build` folder. ### fetch_thirdparty_libs This script will download necessary tools for OpenPype defined in `pyproject.toml` like FFMpeg, From 42bb2a866c1a56131119eff6562e3c9590ef5f94 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Sep 2021 10:37:04 +0200 Subject: [PATCH 180/716] nuke: fixing issue with shared python object --- .../hosts/nuke/plugins/publish/extract_review_data_lut.py | 6 ++++++ .../hosts/nuke/plugins/publish/extract_review_data_mov.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py index b0d3ec6241..a0f1c9a087 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py @@ -3,6 +3,12 @@ import pyblish.api from avalon.nuke import lib as anlib from openpype.hosts.nuke.api import lib as pnlib import openpype + +try: + from __builtin__ import reload +except ImportError: + from importlib import reload + reload(pnlib) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index cea7d86c26..f4fbc2d0e4 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -4,6 +4,13 @@ from avalon.nuke import lib as anlib from openpype.hosts.nuke.api import lib as pnlib import openpype +try: + from __builtin__ import reload +except ImportError: + from importlib import reload + +reload(pnlib) + class ExtractReviewDataMov(openpype.api.Extractor): """Extracts movie and thumbnail with baked in luts From a16924d0c4a3845722eba9146c4379a58b4f58f7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Sep 2021 10:46:28 +0200 Subject: [PATCH 181/716] nuke: removing (Testing only) from nuke and nukex --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 842c294599..cfdeca4b87 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -195,7 +195,7 @@ "environment": {} }, "__dynamic_keys_labels__": { - "13-0": "13.0 (Testing only)", + "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", @@ -331,7 +331,7 @@ "environment": {} }, "__dynamic_keys_labels__": { - "13-0": "13.0 (Testing only)", + "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", From e6144f42b1a134e316f397952e1085b500a98034 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Sep 2021 10:58:50 +0200 Subject: [PATCH 182/716] Fix - explicitly capitalize task type Settings expect task type capitalized --- .../webpublisher/plugins/publish/collect_published_files.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 6584120d97..6a4f83b0fa 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -174,6 +174,8 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): (family, [families], subset_template_name, tags) tuple AssertionError if not matching family found """ + if task_type: + task_type = task_type.capitalize() task_obj = settings.get(task_type) assert task_obj, "No family configuration for '{}'".format(task_type) From fd6bd1dbc15122145a29ea89946e6e3e45c658a5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Sep 2021 15:28:59 +0200 Subject: [PATCH 183/716] Fix - explicitly add frameStart, frameEnd for single frame Temporary files produced to temp folder --- .../webpublisher/plugins/publish/collect_published_files.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 6a4f83b0fa..434f82d3ea 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -10,6 +10,7 @@ Provides: import os import json import clique +import tempfile import pyblish.api from avalon import io @@ -94,7 +95,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): instance.data["families"] = families instance.data["version"] = \ self._get_last_version(asset, subset) + 1 - instance.data["stagingDir"] = task_dir + instance.data["stagingDir"] = tempfile.mkdtemp() instance.data["source"] = "webpublisher" # to store logging info into DB openpype.webpublishes @@ -113,6 +114,8 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): instance.data["frameEnd"] = \ instance.data["representations"][0]["frameEnd"] else: + instance.data["frameStart"] = 0 + instance.data["frameEnd"] = 1 instance.data["representations"] = self._get_single_repre( task_dir, task_data["files"], tags ) From b2b248f818f5e1d2cd411fd14cf3e43d23875cda Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Fri, 10 Sep 2021 14:43:09 -0700 Subject: [PATCH 184/716] addressing comments PR#1996 --- openpype/api.py | 4 +- openpype/lib/__init__.py | 6 ++- openpype/lib/path_tools.py | 40 +++++++++++++++++-- .../action_create_project_structure.py | 2 +- openpype/settings/__init__.py | 4 +- openpype/settings/lib.py | 29 -------------- 6 files changed, 44 insertions(+), 41 deletions(-) diff --git a/openpype/api.py b/openpype/api.py index dcff127e9f..e4bbb104a3 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -4,7 +4,6 @@ from .settings import ( get_current_project_settings, get_anatomy_settings, get_environments, - get_project_basic_paths, SystemSettings, ProjectSettings @@ -26,7 +25,8 @@ from .lib import ( get_global_environments, get_local_site_id, change_openpype_mongo_url, - create_project_folders + create_project_folders, + get_project_basic_paths ) from .lib.mongo import ( diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 4abfd69175..9bc68c9558 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -144,7 +144,8 @@ from .path_tools import ( version_up, get_version_from_path, get_last_version_from_path, - create_project_folders + create_project_folders, + get_project_basic_paths ) from .editorial import ( @@ -278,5 +279,6 @@ __all__ = [ "frames_to_secons", "frames_to_timecode", "make_sequence_collection", - "create_project_folders" + "create_project_folders", + "get_project_basic_paths" ] diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index fab0879759..42b5db9e25 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -1,13 +1,14 @@ +import json +import logging import os import re -import logging -from openpype.api import Anatomy + +from .anatomy import Anatomy +from openpype.settings import get_project_settings log = logging.getLogger(__name__) -pattern_array = re.compile(r"\[.*\]") -project_root_key = "__project_root__" def _rreplace(s, a, b, n=1): """Replace a with b in string s from right side n times.""" @@ -126,6 +127,8 @@ def get_last_version_from_path(path_dir, filter): def compute_paths(basic_paths_items, project_root): + pattern_array = re.compile(r"\[.*\]") + project_root_key = "__project_root__" output = [] for path_items in basic_paths_items: clean_items = [] @@ -162,3 +165,32 @@ def create_project_folders(basic_paths, project_name): else: log.debug("Creating folder: {}".format(full_path)) os.makedirs(full_path) + + +def _list_path_items(folder_structure): + output = [] + for key, value in folder_structure.items(): + if not value: + output.append(key) + else: + paths = _list_path_items(value) + for path in paths: + if not isinstance(path, (list, tuple)): + path = [path] + + output.append([key, *path]) + + return output + + +def get_project_basic_paths(project_name): + project_settings = get_project_settings(project_name) + folder_structure = ( + project_settings["global"]["project_folder_structure"] + ) + if not folder_structure: + return [] + + if isinstance(folder_structure, str): + folder_structure = json.loads(folder_structure) + return _list_path_items(folder_structure) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py index b0de792473..94f359c317 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py @@ -2,7 +2,7 @@ import os import re import json -from openpype.modules.ftrack.lib import BaseAction, statics_icon +from openpype_modules.ftrack.lib import BaseAction, statics_icon from openpype.api import get_project_basic_paths, create_project_folders diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index 0d6be51253..74f2684b2a 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -21,8 +21,7 @@ from .lib import ( get_current_project_settings, get_anatomy_settings, get_environments, - get_local_settings, - get_project_basic_paths + get_local_settings ) from .entities import ( SystemSettings, @@ -52,7 +51,6 @@ __all__ = ( "get_anatomy_settings", "get_environments", "get_local_settings", - "get_project_basic_paths", "SystemSettings", "ProjectSettings" ) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 749b337df7..60ed54bd4a 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -941,35 +941,6 @@ def get_general_environments(): return environments -def _list_path_items(folder_structure): - output = [] - for key, value in folder_structure.items(): - if not value: - output.append(key) - else: - paths = _list_path_items(value) - for path in paths: - if not isinstance(path, (list, tuple)): - path = [path] - - output.append([key, *path]) - - return output - - -def get_project_basic_paths(project_name): - project_settings = get_project_settings(project_name) - folder_structure = ( - project_settings["global"]["project_folder_structure"] - ) - if not folder_structure: - return [] - - if isinstance(folder_structure, str): - folder_structure = json.loads(folder_structure) - return _list_path_items(folder_structure) - - def clear_metadata_from_settings(values): """Remove all metadata keys from loaded settings.""" if isinstance(values, dict): From 181c1faf002b8b8953b3fa76585c95d84a4a206e Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 11 Sep 2021 17:26:38 +0800 Subject: [PATCH 185/716] add project archive confirm setting attribute --- openpype/settings/defaults/project_anatomy/attributes.json | 3 ++- .../projects_schema/schemas/schema_anatomy_attributes.json | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_anatomy/attributes.json b/openpype/settings/defaults/project_anatomy/attributes.json index 387e12bcea..77e6d5e07a 100644 --- a/openpype/settings/defaults/project_anatomy/attributes.json +++ b/openpype/settings/defaults/project_anatomy/attributes.json @@ -22,5 +22,6 @@ "aftereffects/2021", "unreal/4-26" ], - "tools_env": [] + "tools_env": [], + "archive_confirm": "" } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index 7391108a02..dc54fa598e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -69,6 +69,12 @@ "type": "tools-enum", "key": "tools_env", "label": "Tools" + }, + { + "type": "text", + "key": "archive_confirm", + "label": "Archive Project", + "placeholder": "Input project name to confirm archiving." } ] } From 394b714496e27aff5b7effd28a8950e749a9a0e8 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 11 Sep 2021 17:44:01 +0800 Subject: [PATCH 186/716] Launcher ignore archived project Based on avalon-core bfce450f --- openpype/tools/launcher/models.py | 16 +--------------- openpype/tools/launcher/window.py | 1 - 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 4988829c11..bbd419df1c 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -326,8 +326,6 @@ class ProjectModel(QtGui.QStandardItemModel): super(ProjectModel, self).__init__(parent=parent) self.dbcon = dbcon - - self.hide_invisible = False self.project_icon = qtawesome.icon("fa.map", color="white") self._project_names = set() @@ -380,16 +378,4 @@ class ProjectModel(QtGui.QStandardItemModel): self.invisibleRootItem().insertRows(row, items) def get_projects(self): - project_docs = [] - - for project_doc in sorted( - self.dbcon.projects(), key=lambda x: x["name"] - ): - if ( - self.hide_invisible - and not project_doc["data"].get("visible", True) - ): - continue - project_docs.append(project_doc) - - return project_docs + return sorted(self.dbcon.projects(no_archived=True), key=lambda x: x["name"]) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index bd37a9b89c..4331892e94 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -271,7 +271,6 @@ class LauncherWindow(QtWidgets.QDialog): ) project_model = ProjectModel(self.dbcon) - project_model.hide_invisible = True project_handler = ProjectHandler(self.dbcon, project_model) project_panel = ProjectsPanel(project_handler) From 902334d384a9c6fe2fb4a1076298a5358028420d Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 11 Sep 2021 17:44:22 +0800 Subject: [PATCH 187/716] ProjectManager ignore archived project Based on avalon-core bfce450f --- .../project_manager/project_manager/model.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 7ee43a6b61..e31dd2ccfe 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -43,18 +43,14 @@ class ProjectModel(QtGui.QStandardItemModel): none_project.setData(None) project_items.append(none_project) - database = self.dbcon.database project_names = set() - for project_name in database.collection_names(): - # Each collection will have exactly one project document - project_doc = database[project_name].find_one( - {"type": "project"}, - {"name": 1} - ) - if not project_doc: - continue - project_name = project_doc.get("name") + for doc in sorted( + self.dbcon.projects(projection={"name": 1}, no_archived=True), + key=lambda x: x["name"] + ): + + project_name = doc.get("name") if project_name: project_names.add(project_name) project_items.append(QtGui.QStandardItem(project_name)) From 07e248da8912092779b1ac9a96a3c2d7bb9a6a79 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 11 Sep 2021 17:44:57 +0800 Subject: [PATCH 188/716] StandalonePublisher ignore archived project Based on avalon-core bfce450f --- openpype/tools/standalonepublish/widgets/widget_asset.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index c39d71b055..e4fed1b9a7 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -273,8 +273,10 @@ class AssetWidget(QtWidgets.QWidget): def _set_projects(self): project_names = list() - for project in self.dbcon.projects(): - project_name = project.get("name") + + for doc in self.dbcon.projects(projection={"name": 1}, no_archived=True): + + project_name = doc.get("name") if project_name: project_names.append(project_name) @@ -299,7 +301,8 @@ class AssetWidget(QtWidgets.QWidget): def on_project_change(self): projects = list() - for project in self.dbcon.projects(): + + for project in self.dbcon.projects(projection={"name": 1}, no_archived=True): projects.append(project['name']) project_name = self.combo_projects.currentText() if project_name in projects: From 1ec69c37ceb0467947c79fc5218a8d422e93d629 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 11 Sep 2021 17:45:32 +0800 Subject: [PATCH 189/716] Settings ignore archived project Based on avalon-core bfce450f --- .../settings/local_settings/projects_widget.py | 2 +- openpype/tools/settings/settings/widgets.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/tools/settings/local_settings/projects_widget.py b/openpype/tools/settings/local_settings/projects_widget.py index a48c504d59..7d19c37bdf 100644 --- a/openpype/tools/settings/local_settings/projects_widget.py +++ b/openpype/tools/settings/local_settings/projects_widget.py @@ -809,7 +809,7 @@ class ProjectSettingsWidget(QtWidgets.QWidget): self.modules_manager = modules_manager - projects_widget = _ProjectListWidget(self) + projects_widget = _ProjectListWidget(self, no_archived=True) roos_site_widget = RootSiteWidget( modules_manager, project_settings, self ) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 2caf8c33ba..dd95eeb100 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -616,7 +616,7 @@ class ProjectListWidget(QtWidgets.QWidget): default = "< Default >" project_changed = QtCore.Signal() - def __init__(self, parent): + def __init__(self, parent, no_archived=False): self._parent = parent self.current_project = None @@ -645,6 +645,7 @@ class ProjectListWidget(QtWidgets.QWidget): self.project_list = project_list self.dbcon = None + self._no_archived = no_archived def on_item_clicked(self, new_index): new_project_name = new_index.data(QtCore.Qt.DisplayRole) @@ -731,14 +732,13 @@ class ProjectListWidget(QtWidgets.QWidget): self.current_project = None if self.dbcon: - database = self.dbcon.database - for project_name in database.collection_names(): - project_doc = database[project_name].find_one( - {"type": "project"}, - {"name": 1} - ) - if project_doc: - items.append(project_doc["name"]) + for doc in sorted( + self.dbcon.projects(projection={"name": 1}, + no_archived=self._no_archived), + key=lambda x: x["name"] + ): + items.append(doc["name"]) + for item in items: model.appendRow(QtGui.QStandardItem(item)) From 04d4afa9579d6e14e98087832213598439fe146e Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 19:09:11 +0800 Subject: [PATCH 190/716] evaluate archive flag on saving anatomy --- openpype/settings/entities/root_entities.py | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 05d20ee60b..e0a9ecdb35 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -755,6 +755,31 @@ class ProjectSettings(RootEntity): """ return DEFAULTS_DIR + def settings_value(self): + output = super(ProjectSettings, self).settings_value() + + anatomy = output.get(PROJECT_ANATOMY_KEY) or {} + + # Evaluate project archiving flag + # + archive_confirm = anatomy.get("attributes", {}).get("archive_confirm") + if archive_confirm: + # set flag + if archive_confirm == self.project_name: + self.log.debug( + "Project archiving." + ) + anatomy["attributes"]["archived"] = True + + else: + self.log.debug( + "Project archiving confirmation string not matched." + ) + anatomy["attributes"]["archive_confirm"] = "" + anatomy["attributes"]["archived"] = False + + return output + def _save_studio_values(self): settings_value = self.settings_value() From f817740a92c43df0c43f4d4d5c5d8e301b97c2ea Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 19:13:27 +0800 Subject: [PATCH 191/716] add archived sorting/filtering in Studio Settings --- openpype/tools/settings/settings/widgets.py | 103 +++++++++++++++++--- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index dd95eeb100..6dee90daa9 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -612,10 +612,34 @@ class ProjectListView(QtWidgets.QListView): super(ProjectListView, self).mouseReleaseEvent(event) +class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): + + def __init__(self, *args, **kwargs): + super(ProjectListSortFilterProxy, self).__init__(*args, **kwargs) + self._enable_filter = True + + def filterAcceptsRow(self, source_row, source_parent): + if not self._enable_filter: + return True + + index = self.sourceModel().index(source_row, 0, source_parent) + return bool(index.data(self.filterRole())) + + def is_filter_enabled(self): + return self._enable_filter + + def set_filter_enabled(self, value): + self._enable_filter = value + self.invalidateFilter() + + class ProjectListWidget(QtWidgets.QWidget): default = "< Default >" project_changed = QtCore.Signal() + ProjectSortRole = QtCore.Qt.UserRole + 10 + ProjectFilterRole = QtCore.Qt.UserRole + 11 + def __init__(self, parent, no_archived=False): self._parent = parent @@ -625,8 +649,17 @@ class ProjectListWidget(QtWidgets.QWidget): self.setObjectName("ProjectListWidget") label_widget = QtWidgets.QLabel("Projects") + project_list = ProjectListView(self) - project_list.setModel(QtGui.QStandardItemModel()) + project_model = QtGui.QStandardItemModel() + project_proxy = ProjectListSortFilterProxy() + + project_proxy.setFilterRole(self.ProjectFilterRole) + project_proxy.setSortRole(self.ProjectSortRole) + project_proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + + project_proxy.setSourceModel(project_model) + project_list.setModel(project_proxy) # Do not allow editing project_list.setEditTriggers( @@ -640,9 +673,24 @@ class ProjectListWidget(QtWidgets.QWidget): layout.addWidget(label_widget, 0) layout.addWidget(project_list, 1) + if no_archived: + archived_chk = None + else: + archived_chk = QtWidgets.QCheckBox(" Show Archived Project ") + archived_chk.setChecked(not project_proxy.is_filter_enabled()) + + layout.addSpacing(5) + layout.addWidget(archived_chk, 0) + layout.addSpacing(5) + + archived_chk.stateChanged.connect(self.on_archive_vis_changed) + project_list.left_mouse_released_at.connect(self.on_item_clicked) self.project_list = project_list + self.project_proxy = project_proxy + self.project_model = project_model + self.archived_chk = archived_chk self.dbcon = None self._no_archived = no_archived @@ -680,6 +728,14 @@ class ProjectListWidget(QtWidgets.QWidget): else: self.select_project(self.current_project) + def on_archive_vis_changed(self): + if self.archived_chk is None: + # should not happen. + return + + enable_filter = not self.archived_chk.isChecked() + self.project_proxy.set_filter_enabled(enable_filter) + def validate_context_change(self): return not self._parent.entity.has_unsaved_changes @@ -692,12 +748,16 @@ class ProjectListWidget(QtWidgets.QWidget): self.select_project(self.default) def select_project(self, project_name): - model = self.project_list.model() + model = self.project_model + proxy = self.project_proxy + found_items = model.findItems(project_name) if not found_items: found_items = model.findItems(self.default) index = model.indexFromItem(found_items[0]) + index = proxy.mapFromSource(index) + self.project_list.selectionModel().clear() self.project_list.selectionModel().setCurrentIndex( index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent @@ -709,10 +769,10 @@ class ProjectListWidget(QtWidgets.QWidget): selected_project = index.data(QtCore.Qt.DisplayRole) break - model = self.project_list.model() + model = self.project_model model.clear() - items = [self.default] + items = [(self.default, None)] mongo_url = os.environ["OPENPYPE_MONGO"] @@ -732,15 +792,34 @@ class ProjectListWidget(QtWidgets.QWidget): self.current_project = None if self.dbcon: - for doc in sorted( - self.dbcon.projects(projection={"name": 1}, - no_archived=self._no_archived), - key=lambda x: x["name"] - ): - items.append(doc["name"]) - for item in items: - model.appendRow(QtGui.QStandardItem(item)) + for doc in self.dbcon.projects( + projection={"name": 1, "data.archived": 1}, + no_archived=self._no_archived + ): + items.append( + (doc["name"], doc.get("data", {}).get("archived")) + ) + + for project_name, is_archived in items: + visible = not is_archived + + row = QtGui.QStandardItem(project_name) + row.setData(visible, self.ProjectFilterRole) + + if is_archived: + row.setData("~" + project_name, self.ProjectSortRole) + + font = row.font() + font.setItalic(True) + row.setFont(font) + + else: + row.setData(project_name, self.ProjectSortRole) + + model.appendRow(row) + + self.project_proxy.sort(0) self.select_project(selected_project) From 02937e308f8346193df16637f84d0bd3a8ad60ce Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 19:33:43 +0800 Subject: [PATCH 192/716] SyncServer ignore archived project --- .../modules/default_modules/sync_server/tray/widgets.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index c9160733a0..1737b2e0c6 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -34,7 +34,7 @@ class SyncProjectListWidget(ProjectListWidget): """ def __init__(self, sync_server, parent): - super(SyncProjectListWidget, self).__init__(parent) + super(SyncProjectListWidget, self).__init__(parent, no_archived=True) self.sync_server = sync_server self.project_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.project_list.customContextMenuRequested.connect( @@ -49,7 +49,7 @@ class SyncProjectListWidget(ProjectListWidget): return True def refresh(self): - model = self.project_list.model() + model = self.project_model model.clear() project_name = None @@ -70,8 +70,7 @@ class SyncProjectListWidget(ProjectListWidget): QtCore.Qt.DisplayRole ) if not self.current_project: - self.current_project = self.project_list.model().item(0). \ - data(QtCore.Qt.DisplayRole) + self.current_project = model.item(0).data(QtCore.Qt.DisplayRole) if project_name: self.local_site = self.sync_server.get_active_site(project_name) From c234cb907993282c00bff9a31083a7d7b1b5ab64 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 23:21:10 +0800 Subject: [PATCH 193/716] register 'archived' to project anatomy schema So the settings can recognize and won't pop warnings --- openpype/settings/defaults/project_anatomy/attributes.json | 3 ++- .../projects_schema/schemas/schema_anatomy_attributes.json | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_anatomy/attributes.json b/openpype/settings/defaults/project_anatomy/attributes.json index 77e6d5e07a..ac91622726 100644 --- a/openpype/settings/defaults/project_anatomy/attributes.json +++ b/openpype/settings/defaults/project_anatomy/attributes.json @@ -23,5 +23,6 @@ "unreal/4-26" ], "tools_env": [], - "archive_confirm": "" + "archive_confirm": "", + "archived": false } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index dc54fa598e..f70ab2fc5f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -75,6 +75,11 @@ "key": "archive_confirm", "label": "Archive Project", "placeholder": "Input project name to confirm archiving." + }, + { + "type": "boolean", + "key": "archived", + "label": "Is Archived" } ] } From 662e74816b16e108d80b80d55bc726c0859df056 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 23:22:42 +0800 Subject: [PATCH 194/716] reset 'archived' to False if no confirmation string --- openpype/settings/entities/root_entities.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index e0a9ecdb35..5701b60098 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -778,6 +778,10 @@ class ProjectSettings(RootEntity): anatomy["attributes"]["archive_confirm"] = "" anatomy["attributes"]["archived"] = False + else: + if anatomy and "attributes" in anatomy: + anatomy["attributes"]["archived"] = False + return output def _save_studio_values(self): From 921fc00e8f8471d00bb14d571ea77c0d232492b4 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sun, 12 Sep 2021 23:26:53 +0800 Subject: [PATCH 195/716] keep selected project in view when being archived --- openpype/tools/settings/settings/widgets.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 6dee90daa9..d1a8bd8958 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -623,7 +623,10 @@ class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): return True index = self.sourceModel().index(source_row, 0, source_parent) - return bool(index.data(self.filterRole())) + is_active = bool(index.data(self.filterRole())) + is_selected = bool(index.data(ProjectListWidget.ProjectSelectedRole)) + + return is_active or is_selected def is_filter_enabled(self): return self._enable_filter @@ -639,6 +642,7 @@ class ProjectListWidget(QtWidgets.QWidget): ProjectSortRole = QtCore.Qt.UserRole + 10 ProjectFilterRole = QtCore.Qt.UserRole + 11 + ProjectSelectedRole = QtCore.Qt.UserRole + 12 def __init__(self, parent, no_archived=False): self._parent = parent @@ -756,6 +760,8 @@ class ProjectListWidget(QtWidgets.QWidget): found_items = model.findItems(self.default) index = model.indexFromItem(found_items[0]) + model.setData(index, True, self.ProjectSelectedRole) + index = proxy.mapFromSource(index) self.project_list.selectionModel().clear() @@ -806,6 +812,7 @@ class ProjectListWidget(QtWidgets.QWidget): row = QtGui.QStandardItem(project_name) row.setData(visible, self.ProjectFilterRole) + row.setData(False, self.ProjectSelectedRole) if is_archived: row.setData("~" + project_name, self.ProjectSortRole) From 453a813ec1ab2067927fc9ddc1eb4acb37f0b607 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 13 Sep 2021 00:03:12 +0800 Subject: [PATCH 196/716] fix linter --- openpype/tools/launcher/models.py | 3 ++- openpype/tools/standalonepublish/widgets/widget_asset.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index bbd419df1c..53e2c19a3d 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -378,4 +378,5 @@ class ProjectModel(QtGui.QStandardItemModel): self.invisibleRootItem().insertRows(row, items) def get_projects(self): - return sorted(self.dbcon.projects(no_archived=True), key=lambda x: x["name"]) + return sorted(self.dbcon.projects(no_archived=True), + key=lambda x: x["name"]) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index e4fed1b9a7..8ee09d2435 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -274,7 +274,8 @@ class AssetWidget(QtWidgets.QWidget): def _set_projects(self): project_names = list() - for doc in self.dbcon.projects(projection={"name": 1}, no_archived=True): + for doc in self.dbcon.projects(projection={"name": 1}, + no_archived=True): project_name = doc.get("name") if project_name: @@ -302,7 +303,8 @@ class AssetWidget(QtWidgets.QWidget): def on_project_change(self): projects = list() - for project in self.dbcon.projects(projection={"name": 1}, no_archived=True): + for project in self.dbcon.projects(projection={"name": 1}, + no_archived=True): projects.append(project['name']) project_name = self.combo_projects.currentText() if project_name in projects: From a0338cb548b94fccd9ed0d1b799f10d80eb4a9d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 13 Sep 2021 10:40:29 +0200 Subject: [PATCH 197/716] avalon-core update --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index f48fce09c0..b3e4959778 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit f48fce09c0986c1fd7f6731de33907be46b436c5 +Subproject commit b3e49597786c931c13bca207769727d5fc56d5f6 From 500a6d53df5e1023f175d91e1cde8ce5656c0e5e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Sep 2021 11:20:17 +0200 Subject: [PATCH 198/716] connect_with_modules is not abstract method --- openpype/modules/base.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 01c3cebe60..c2b40b7c4a 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -417,7 +417,6 @@ class OpenPypeModule: """ pass - @abstractmethod def connect_with_modules(self, enabled_modules): """Connect with other enabled modules.""" pass @@ -438,10 +437,6 @@ class OpenPypeAddOn(OpenPypeModule): """Initialization is not be required for most of addons.""" pass - def connect_with_modules(self, enabled_modules): - """Do not require to implement connection with modules for addon.""" - pass - class ModulesManager: """Manager of Pype modules helps to load and prepare them to work. From d12509bd8edcd7f6d3c12660fa0ea62e2045a29f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Sep 2021 11:20:43 +0200 Subject: [PATCH 199/716] removed empty implementations of connect_with_modules from modules --- openpype/modules/default_modules/avalon_apps/avalon_app.py | 3 --- .../modules/default_modules/clockify/clockify_module.py | 3 --- .../modules/default_modules/deadline/deadline_module.py | 3 --- .../modules/default_modules/log_viewer/log_view_module.py | 4 ---- openpype/modules/default_modules/muster/muster.py | 3 --- openpype/modules/default_modules/project_manager_action.py | 3 --- .../default_modules/python_console_interpreter/module.py | 3 --- .../default_modules/settings_module/settings_action.py | 6 ------ openpype/modules/default_modules/slack/slack_module.py | 4 ---- .../default_modules/sync_server/sync_server_module.py | 3 --- 10 files changed, 35 deletions(-) diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index 53e06ec90a..eae013c060 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -71,9 +71,6 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes): exc_info=True ) - def connect_with_modules(self, _enabled_modules): - return - def webserver_initialization(self, server_manager): """Implementation of IWebServerRoutes interface.""" diff --git a/openpype/modules/default_modules/clockify/clockify_module.py b/openpype/modules/default_modules/clockify/clockify_module.py index a9e989f4ec..f82ae0d55d 100644 --- a/openpype/modules/default_modules/clockify/clockify_module.py +++ b/openpype/modules/default_modules/clockify/clockify_module.py @@ -94,9 +94,6 @@ class ClockifyModule( "server": [CLOCKIFY_FTRACK_SERVER_PATH] } - def connect_with_modules(self, *_a, **_kw): - return - def clockify_timer_stopped(self): self.bool_timer_run = False # Call `ITimersManager` method diff --git a/openpype/modules/default_modules/deadline/deadline_module.py b/openpype/modules/default_modules/deadline/deadline_module.py index ada5e8225a..1a179e9aaf 100644 --- a/openpype/modules/default_modules/deadline/deadline_module.py +++ b/openpype/modules/default_modules/deadline/deadline_module.py @@ -26,9 +26,6 @@ class DeadlineModule(OpenPypeModule, IPluginPaths): "not specified. Disabling module.")) return - def connect_with_modules(self, *_a, **_kw): - return - def get_plugin_paths(self): """Deadline plugin paths.""" current_dir = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/modules/default_modules/log_viewer/log_view_module.py b/openpype/modules/default_modules/log_viewer/log_view_module.py index bc1a98f4ad..14be6b392e 100644 --- a/openpype/modules/default_modules/log_viewer/log_view_module.py +++ b/openpype/modules/default_modules/log_viewer/log_view_module.py @@ -40,10 +40,6 @@ class LogViewModule(OpenPypeModule, ITrayModule): def tray_exit(self): return - def connect_with_modules(self, _enabled_modules): - """Nothing special.""" - return - def _show_logs_gui(self): if self.window: self.window.show() diff --git a/openpype/modules/default_modules/muster/muster.py b/openpype/modules/default_modules/muster/muster.py index a0e72006af..76d6cbb8f6 100644 --- a/openpype/modules/default_modules/muster/muster.py +++ b/openpype/modules/default_modules/muster/muster.py @@ -54,9 +54,6 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes): """Nothing special for Muster.""" return - def connect_with_modules(self, *_a, **_kw): - return - # Definition of Tray menu def tray_menu(self, parent): """Add **change credentials** option to tray menu.""" diff --git a/openpype/modules/default_modules/project_manager_action.py b/openpype/modules/default_modules/project_manager_action.py index c1f984a8cb..251964a059 100644 --- a/openpype/modules/default_modules/project_manager_action.py +++ b/openpype/modules/default_modules/project_manager_action.py @@ -17,9 +17,6 @@ class ProjectManagerAction(OpenPypeModule, ITrayAction): # Tray attributes self.project_manager_window = None - def connect_with_modules(self, *_a, **_kw): - return - def tray_init(self): """Initialization in tray implementation of ITrayAction.""" self.create_project_manager_window() diff --git a/openpype/modules/default_modules/python_console_interpreter/module.py b/openpype/modules/default_modules/python_console_interpreter/module.py index f4df3fb6d8..8c4a2fba73 100644 --- a/openpype/modules/default_modules/python_console_interpreter/module.py +++ b/openpype/modules/default_modules/python_console_interpreter/module.py @@ -18,9 +18,6 @@ class PythonInterpreterAction(OpenPypeModule, ITrayAction): if self._interpreter_window is not None: self._interpreter_window.save_registry() - def connect_with_modules(self, *args, **kwargs): - pass - def create_interpreter_window(self): """Initializa Settings Qt window.""" if self._interpreter_window: diff --git a/openpype/modules/default_modules/settings_module/settings_action.py b/openpype/modules/default_modules/settings_module/settings_action.py index 7140c57bab..2b4b51e3ad 100644 --- a/openpype/modules/default_modules/settings_module/settings_action.py +++ b/openpype/modules/default_modules/settings_module/settings_action.py @@ -19,9 +19,6 @@ class SettingsAction(OpenPypeModule, ITrayAction): # Tray attributes self.settings_window = None - def connect_with_modules(self, *_a, **_kw): - return - def tray_init(self): """Initialization in tray implementation of ITrayAction.""" self.create_settings_window() @@ -84,9 +81,6 @@ class LocalSettingsAction(OpenPypeModule, ITrayAction): self.settings_window = None self._first_trigger = True - def connect_with_modules(self, *_a, **_kw): - return - def tray_init(self): """Initialization in tray implementation of ITrayAction.""" self.create_settings_window() diff --git a/openpype/modules/default_modules/slack/slack_module.py b/openpype/modules/default_modules/slack/slack_module.py index e3f7b4ad19..9b2976d766 100644 --- a/openpype/modules/default_modules/slack/slack_module.py +++ b/openpype/modules/default_modules/slack/slack_module.py @@ -17,10 +17,6 @@ class SlackIntegrationModule(OpenPypeModule, IPluginPaths, ILaunchHookPaths): slack_settings = modules_settings[self.name] self.enabled = slack_settings["enabled"] - def connect_with_modules(self, _enabled_modules): - """Nothing special.""" - return - def get_launch_hook_paths(self): """Implementation of `ILaunchHookPaths`.""" return os.path.join(SLACK_MODULE_DIR, "launch_hooks") diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 976a349bfa..4c54f25c02 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -680,9 +680,6 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return sites - def connect_with_modules(self, *_a, **kw): - return - def tray_init(self): """ Actual initialization of Sync Server. From a290d77a41b40ac89235e48bb9f1521fc041bc6c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 13 Sep 2021 11:18:25 +0100 Subject: [PATCH 200/716] Fixed Unreal support for templates --- .../unreal/hooks/pre_workfile_preparation.py | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 01b8b6bc05..0c7146634f 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -6,7 +6,9 @@ from pathlib import Path from openpype.lib import ( PreLaunchHook, ApplicationLaunchFailed, - ApplicationNotFound + ApplicationNotFound, + get_workdir_data, + get_workfile_template_key ) from openpype.hosts.unreal.api import lib as unreal_lib @@ -25,13 +27,45 @@ class UnrealPrelaunchHook(PreLaunchHook): self.signature = "( {} )".format(self.__class__.__name__) + def _get_work_filename(self): + # Use last workfile if was found + last_workfile = self.data.get("last_workfile_path") + if last_workfile and os.path.exists(last_workfile): + return os.path.basename(last_workfile) + + # Prepare data for fill data and for getting workfile template key + task_name = self.data["task_name"] + anatomy = self.data["anatomy"] + asset_doc = self.data["asset_doc"] + project_doc = self.data["project_doc"] + + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + + workdir_data = get_workdir_data( + project_doc, asset_doc, task_name, self.host_name + ) + # QUESTION raise exception if version is part of filename template? + workdir_data["version"] = 1 + workdir_data["ext"] = "uproject" + + # Get workfile template key for current context + workfile_template_key = get_workfile_template_key( + task_type, + self.host_name, + project_name=project_doc["name"] + ) + # Fill templates + filled_anatomy = anatomy.format(workdir_data) + + # Return filename + return filled_anatomy[workfile_template_key]["file"] + def execute(self): """Hook entry method.""" - asset_name = self.data["asset_name"] - task_name = self.data["task_name"] workdir = self.launch_context.env["AVALON_WORKDIR"] engine_version = self.app_name.split("/")[-1].replace("-", ".") - unreal_project_name = f"{asset_name}_{task_name}" try: if int(engine_version.split(".")[0]) < 4 and \ int(engine_version.split(".")[1]) < 26: @@ -45,6 +79,8 @@ class UnrealPrelaunchHook(PreLaunchHook): # so lets keep it quite. ... + unreal_project_filename = self._get_work_filename() + unreal_project_name = os.path.splitext(unreal_project_filename)[0] # Unreal is sensitive about project names longer then 20 chars if len(unreal_project_name) > 20: self.log.warning(( @@ -55,7 +91,7 @@ class UnrealPrelaunchHook(PreLaunchHook): # of the project name. This is because project name is then used # in various places inside c++ code and there variable names cannot # start with non-alpha. We append 'P' before project name to solve it. - # 😱 + # 😱 if not unreal_project_name[:1].isalpha(): self.log.warning(( "Project name doesn't start with alphabet " @@ -89,10 +125,10 @@ class UnrealPrelaunchHook(PreLaunchHook): ue4_path = unreal_lib.get_editor_executable_path( Path(detected[engine_version])) - self.launch_context.launch_args.append(ue4_path.as_posix()) + self.launch_context.launch_args = [ue4_path.as_posix()] project_path.mkdir(parents=True, exist_ok=True) - project_file = project_path / f"{unreal_project_name}.uproject" + project_file = project_path / unreal_project_filename if not project_file.is_file(): engine_path = detected[engine_version] self.log.info(( From 0cb2cbb14f39a52befde9a6e7d7f4635594618fc Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 13 Sep 2021 12:27:31 +0200 Subject: [PATCH 201/716] Update openpype/modules/README.md --- openpype/modules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index abc7ed3961..5716324365 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -9,7 +9,7 @@ OpenPype modules should contain separated logic of specific kind of implementati ### TODOs - add module/addon manifest - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.) - - defying that folder is content of a module or an addon + - defining a folder as a content of a module or an addon ## Base class `OpenPypeModule` - abstract class as base for each module From 0adcf3334ad5a119341132c777540a22a5f51f30 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 13 Sep 2021 13:35:24 +0200 Subject: [PATCH 202/716] Added new InventoryAction to import (localize) reference in Maya PYPE-1399 --- openpype/hosts/maya/api/__init__.py | 1 + .../plugins/inventory/import_reference.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 openpype/hosts/maya/plugins/inventory/import_reference.py diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 9219da407f..1c8534d9a5 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -35,6 +35,7 @@ def install(): pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) log.info(PUBLISH_PATH) menu.install() diff --git a/openpype/hosts/maya/plugins/inventory/import_reference.py b/openpype/hosts/maya/plugins/inventory/import_reference.py new file mode 100644 index 0000000000..d389c8733e --- /dev/null +++ b/openpype/hosts/maya/plugins/inventory/import_reference.py @@ -0,0 +1,27 @@ +from maya import cmds + +from avalon import api + + +class ImportReference(api.InventoryAction): + """Imports selected reference inside the file.""" + + label = "Import Reference" + icon = "mouse-pointer" + color = "#d8d8d8" + + def process(self, containers): + references = cmds.ls(type="reference") + + for container in containers: + if container["loader"] != "ReferenceLoader": + print("Not a reference, skipping") + continue + + reference_name = container["namespace"] + "RN" + if reference_name in references: + print("Importing {}".format(reference_name)) + + ref_file = cmds.referenceQuery(reference_name, f=True) + + cmds.file(ref_file, importReference=True) From dfa3f76d2c71975ebcd26335f6d6077968ae0be1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 13 Sep 2021 13:46:37 +0200 Subject: [PATCH 203/716] Added return to force refresh of SceneInventory window --- openpype/hosts/maya/plugins/inventory/import_reference.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/inventory/import_reference.py b/openpype/hosts/maya/plugins/inventory/import_reference.py index d389c8733e..ac97096ee7 100644 --- a/openpype/hosts/maya/plugins/inventory/import_reference.py +++ b/openpype/hosts/maya/plugins/inventory/import_reference.py @@ -25,3 +25,5 @@ class ImportReference(api.InventoryAction): ref_file = cmds.referenceQuery(reference_name, f=True) cmds.file(ref_file, importReference=True) + + return "refresh" From 65b9bcf6e1c43746c9f7038db19602e21dcca925 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 09:45:07 +0200 Subject: [PATCH 204/716] removed arguments for storing credentials and event paths --- website/docs/admin_openpype_commands.md | 7 ++----- website/docs/module_ftrack.md | 5 ----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index d6ccc883b0..7a46ee7906 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -55,7 +55,7 @@ openpype_console tray --debug --- ### `launch` arguments {#eventserver-arguments} You have to set either proper environment variables to provide URL and credentials or use -option to specify them. If you use `--store_credentials` provided credentials will be stored for later use. +option to specify them. | Argument | Description | | --- | --- | @@ -63,16 +63,13 @@ option to specify them. If you use `--store_credentials` provided credentials wi | `--ftrack-url` | URL to ftrack server (can be set with `FTRACK_SERVER`) | | `--ftrack-user` |user name to log in to ftrack (can be set with `FTRACK_API_USER`) | | `--ftrack-api-key` | ftrack api key (can be set with `FTRACK_API_KEY`) | -| `--ftrack-events-path` | path to event server plugins (can be set with `FTRACK_EVENTS_PATH`) | -| `--no-stored-credentials` | will use credential specified with options above | -| `--store-credentials` | will store credentials to file for later use | | `--legacy` | run event server without mongo storing | | `--clockify-api-key` | Clockify API key (can be set with `CLOCKIFY_API_KEY`) | | `--clockify-workspace` | Clockify workspace (can be set with `CLOCKIFY_WORKSPACE`) | To run ftrack event server: ```shell -openpype_console eventserver --ftrack-url= --ftrack-user= --ftrack-api-key= --ftrack-events-path= --no-stored-credentials --store-credentials +openpype_console eventserver --ftrack-url= --ftrack-user= --ftrack-api-key= ``` --- diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index 005270b3b9..cafee628c1 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -51,10 +51,7 @@ There are specific launch arguments for event server. With `openpype_console eve - **`--ftrack-user "your.username"`** : Ftrack Username - **`--ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee"`** : User's API key -- **`--store-crededentials`** : Entered credentials will be stored for next launch with this argument _(It is not needed to enter **ftrackuser** and **ftrackapikey** args on next launch)_ -- **`--no-stored-credentials`** : Stored credentials are loaded first so if you want to change credentials use this argument - `--ftrack-url "https://yourdomain.ftrackapp.com/"` : Ftrack server URL _(it is not needed to enter if you have set `FTRACK_SERVER` in OpenPype' environments)_ -- `--ftrack-events-path "//Paths/To/Events/"` : Paths to events folder. May contain multiple paths separated by `;`. _(it is not needed to enter if you have set `FTRACK_EVENTS_PATH` in OpenPype' environments)_ So if you want to use OpenPype's environments then you can launch event server for first time with these arguments `openpype_console.exe eventserver --ftrack-user "my.username" --ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `openpype_console.exe eventserver`. @@ -64,8 +61,6 @@ So if you want to use OpenPype's environments then you can launch event server f - `FTRACK_API_USER` - Username _("your.username")_ - `FTRACK_API_KEY` - User's API key _("00000aaa-11bb-22cc-33dd-444444eeeee")_ - `FTRACK_SERVER` - Ftrack server url _(")_ -- `FTRACK_EVENTS_PATH` - Paths to events _("//Paths/To/Events/")_ - We do not recommend you this way. From 2e5800cf8c2bb355175d189ec3468b42aeb835d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 09:48:16 +0200 Subject: [PATCH 205/716] updated how to prepare shell scripts --- website/docs/module_ftrack.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index cafee628c1..8e3806828d 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -98,10 +98,12 @@ Event server should **not** run more than once! It may cause major issues. `sudo vi /opt/openpype/run_event_server.sh` - add content to the file: ```sh -#!/usr/bin/env -export OPENPYPE_DEBUG=3 -pushd /mnt/pipeline/prod/openpype-setup -. openpype_console eventserver --ftrack-user --ftrack-api-key +#!/usr/bin/env bash +export OPENPYPE_DEBUG=1 +export OPENPYPE_MONGO= + +pushd /mnt/path/to/openpype +./openpype_console eventserver --ftrack-user --ftrack-api-key ``` - change file permission: `sudo chmod 0755 /opt/openpype/run_event_server.sh` @@ -141,9 +143,11 @@ WantedBy=multi-user.target - add content to the service file: ```sh @echo off -set OPENPYPE_DEBUG=3 -pushd \\path\to\file\ -openpype_console.exe eventserver --ftrack-user --ftrack-api-key +set OPENPYPE_DEBUG=1 +set OPENPYPE_MONGO= + +pushd \\path\to\openpype +openpype_console.exe eventserver --ftrack-user --ftrack-api-key ``` - download and install `nssm.cc` - create Windows service according to nssm.cc manual From e7046b09d7f7373bbe9e10931afde8dbe1646974 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:30:37 +0200 Subject: [PATCH 206/716] synchronization logic is encapsulated in `synchronization` method --- .../action_sync_to_avalon.py | 22 ++++++++++------- .../action_sync_to_avalon.py | 24 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py index d449c4b7df..aa5b95b207 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -52,17 +52,21 @@ class SyncToAvalonServer(ServerAction): return False def launch(self, session, in_entities, event): + project_entity = self.get_project_from_entity(in_entities[0]) + project_name = project_entity["full_name"] + result = self.synchronization( + session, in_entities, event, project_name + ) + + return result + + def synchronization(self, session, in_entities, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) - # Get ftrack project - if in_entities[0].entity_type.lower() == "project": - ft_project_name = in_entities[0]["full_name"] - else: - ft_project_name = in_entities[0]["project"]["full_name"] try: - output = self.entities_factory.launch_setup(ft_project_name) + output = self.entities_factory.launch_setup(project_name) if output is not None: return output @@ -72,7 +76,7 @@ class SyncToAvalonServer(ServerAction): time_2 = time.time() # This must happen before all filtering!!! - self.entities_factory.prepare_avalon_entities(ft_project_name) + self.entities_factory.prepare_avalon_entities(project_name) time_3 = time.time() self.entities_factory.filter_by_ignore_sync() @@ -118,7 +122,7 @@ class SyncToAvalonServer(ServerAction): report = self.entities_factory.report() if report and report.get("items"): default_title = "Synchronization report ({}):".format( - ft_project_name + project_name ) self.show_interface( items=report["items"], @@ -135,7 +139,6 @@ class SyncToAvalonServer(ServerAction): "Synchronization failed due to code error", exc_info=True ) msg = "An error has happened during synchronization" - title = "Synchronization report ({}):".format(ft_project_name) items = [] items.append({ "type": "label", @@ -160,6 +163,7 @@ class SyncToAvalonServer(ServerAction): report = self.entities_factory.report() except Exception: pass + title = "Synchronization report ({}):".format(project_name) _items = report.get("items", []) if _items: diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index d6ca561bbe..a57bb819a4 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -63,17 +63,23 @@ class SyncToAvalonLocal(BaseAction): return is_valid def launch(self, session, in_entities, event): + project_entity = self.get_project_from_entity(in_entities[0]) + project_name = project_entity["full_name"] + + result = self.synchronization( + session, in_entities, event, project_name + ) + + + return result + + def synchronization(self, session, in_entities, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) - # Get ftrack project - if in_entities[0].entity_type.lower() == "project": - ft_project_name = in_entities[0]["full_name"] - else: - ft_project_name = in_entities[0]["project"]["full_name"] try: - output = self.entities_factory.launch_setup(ft_project_name) + output = self.entities_factory.launch_setup(project_name) if output is not None: return output @@ -83,7 +89,7 @@ class SyncToAvalonLocal(BaseAction): time_2 = time.time() # This must happen before all filtering!!! - self.entities_factory.prepare_avalon_entities(ft_project_name) + self.entities_factory.prepare_avalon_entities(project_name) time_3 = time.time() self.entities_factory.filter_by_ignore_sync() @@ -129,7 +135,7 @@ class SyncToAvalonLocal(BaseAction): report = self.entities_factory.report() if report and report.get("items"): default_title = "Synchronization report ({}):".format( - ft_project_name + project_name ) self.show_interface( items=report["items"], @@ -146,7 +152,6 @@ class SyncToAvalonLocal(BaseAction): "Synchronization failed due to code error", exc_info=True ) msg = "An error occurred during synchronization" - title = "Synchronization report ({}):".format(ft_project_name) items = [] items.append({ "type": "label", @@ -181,6 +186,7 @@ class SyncToAvalonLocal(BaseAction): return {"success": True, "message": msg} + title = "Synchronization report ({}):".format(project_name) finally: try: self.entities_factory.dbcon.uninstall() From 2bd9ef0b53d1a95df767057cc2cf79c835b3dccf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:35:10 +0200 Subject: [PATCH 207/716] exception handling moved from 'synchronization' to 'launch' --- .../action_sync_to_avalon.py | 75 ++++++++----------- .../action_sync_to_avalon.py | 75 ++++++++----------- 2 files changed, 61 insertions(+), 89 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py index aa5b95b207..7f9074907a 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -54,13 +54,40 @@ class SyncToAvalonServer(ServerAction): def launch(self, session, in_entities, event): project_entity = self.get_project_from_entity(in_entities[0]) project_name = project_entity["full_name"] - result = self.synchronization( - session, in_entities, event, project_name - ) + + try: + result = self.synchronization(event, project_name) + + except Exception as exc: + self.log.error( + "Synchronization failed due to code error", exc_info=True + ) + msg = "An error has happened during synchronization" + title = "Synchronization report ({}):".format(project_name) + items = [] + items.append({ + "type": "label", + "value": "# {}".format(msg) + }) + + report = {} + try: + report = self.entities_factory.report() + except Exception: + pass + + _items = report.get("items") or [] + if _items: + items.append(self.entities_factory.report_splitter) + items.extend(_items) + + self.show_interface(items, title, event, submit_btn_label="Ok") + + return {"success": True, "message": msg} return result - def synchronization(self, session, in_entities, event, project_name): + def synchronization(self, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) @@ -134,46 +161,6 @@ class SyncToAvalonServer(ServerAction): "message": "Synchronization Finished" } - except Exception: - self.log.error( - "Synchronization failed due to code error", exc_info=True - ) - msg = "An error has happened during synchronization" - items = [] - items.append({ - "type": "label", - "value": "# {}".format(msg) - }) - items.append({ - "type": "label", - "value": "## Traceback of the error" - }) - items.append({ - "type": "label", - "value": "

    {}

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

    {}

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

    Download report from job for more information.

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

    Download report from job for more information.

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

    {}

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

    {subset}

    " + "

    {version}

    " + "Comment
    " + "{comment}

    " + + "Created
    " + "{created}

    " + + "Source
    " + "{source}" + ).format(**data)) + + def contextMenuEvent(self, event): + """Context menu with additional actions""" + menu = self.createStandardContextMenu() + + # Add additional actions when any text so we can assume + # the version is set. + if self.toPlainText().strip(): + + menu.addSeparator() + action = QtWidgets.QAction("Copy source path to clipboard", + menu) + action.triggered.connect(self.on_copy_source) + menu.addAction(action) + + action = QtWidgets.QAction("Copy raw data to clipboard", + menu) + action.triggered.connect(self.on_copy_raw) + menu.addAction(action) + + menu.exec_(event.globalPos()) + del menu + + def on_copy_source(self): + """Copy formatted source path to clipboard""" + source = self.data.get("source", None) + if not source: + return + + path = source.format(root=api.registered_root()) + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(path) + + def on_copy_raw(self): + """Copy raw version data to clipboard + + The data is string formatted with `pprint.pformat`. + + """ + raw = self.data.get("raw", None) + if not raw: + return + + raw_text = pprint.pformat(raw) + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(raw_text) + + +class ThumbnailWidget(QtWidgets.QLabel): + + aspect_ratio = (16, 9) + max_width = 300 + + def __init__(self, dbcon, parent=None): + super(ThumbnailWidget, self).__init__(parent) + self.dbcon = dbcon + + self.current_thumb_id = None + self.current_thumbnail = None + + self.setAlignment(QtCore.Qt.AlignCenter) + + # TODO get res path much better way + loader_path = os.path.dirname(os.path.abspath(__file__)) + avalon_path = os.path.dirname(os.path.dirname(loader_path)) + default_pix_path = os.path.join( + os.path.dirname(avalon_path), + "res", "tools", "images", "default_thumbnail.png" + ) + self.default_pix = QtGui.QPixmap(default_pix_path) + + def height(self): + width = self.width() + asp_w, asp_h = self.aspect_ratio + + return (width / asp_w) * asp_h + + def width(self): + width = super(ThumbnailWidget, self).width() + if width > self.max_width: + width = self.max_width + return width + + def set_pixmap(self, pixmap=None): + if not pixmap: + pixmap = self.default_pix + self.current_thumb_id = None + + self.current_thumbnail = pixmap + + pixmap = self.scale_pixmap(pixmap) + self.setPixmap(pixmap) + + def resizeEvent(self, _event): + if not self.current_thumbnail: + return + cur_pix = self.scale_pixmap(self.current_thumbnail) + self.setPixmap(cur_pix) + + def scale_pixmap(self, pixmap): + return pixmap.scaled( + self.width(), self.height(), QtCore.Qt.KeepAspectRatio + ) + + def set_thumbnail(self, entity=None): + if not entity: + self.set_pixmap() + return + + if isinstance(entity, (list, tuple)): + if len(entity) == 1: + entity = entity[0] + else: + self.set_pixmap() + return + + thumbnail_id = entity.get("data", {}).get("thumbnail_id") + if thumbnail_id == self.current_thumb_id: + if self.current_thumbnail is None: + self.set_pixmap() + return + + self.current_thumb_id = thumbnail_id + if not thumbnail_id: + self.set_pixmap() + return + + thumbnail_ent = self.dbcon.find_one( + {"type": "thumbnail", "_id": thumbnail_id} + ) + if not thumbnail_ent: + return + + thumbnail_bin = pipeline.get_thumbnail_binary( + thumbnail_ent, "thumbnail", self.dbcon + ) + if not thumbnail_bin: + self.set_pixmap() + return + + thumbnail = QtGui.QPixmap() + thumbnail.loadFromData(thumbnail_bin) + + self.set_pixmap(thumbnail) + + +class VersionWidget(QtWidgets.QWidget): + """A Widget that display information about a specific version""" + def __init__(self, dbcon, parent=None): + super(VersionWidget, self).__init__(parent=parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + label = QtWidgets.QLabel("Version", self) + data = VersionTextEdit(dbcon, self) + data.setReadOnly(True) + + layout.addWidget(label) + layout.addWidget(data) + + self.data = data + + def set_version(self, version_doc): + self.data.set_version(version_doc) + + +class FamilyListWidget(QtWidgets.QListWidget): + """A Widget that lists all available families""" + + NameRole = QtCore.Qt.UserRole + 1 + active_changed = QtCore.Signal(list) + + def __init__(self, dbcon, family_config_cache, parent=None): + super(FamilyListWidget, self).__init__(parent=parent) + + self.family_config_cache = family_config_cache + self.dbcon = dbcon + + multi_select = QtWidgets.QAbstractItemView.ExtendedSelection + self.setSelectionMode(multi_select) + self.setAlternatingRowColors(True) + # Enable RMB menu + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_right_mouse_menu) + + self.itemChanged.connect(self._on_item_changed) + + def refresh(self): + """Refresh the listed families. + + This gets all unique families and adds them as checkable items to + the list. + + """ + + families = [] + if self.dbcon.Session.get("AVALON_PROJECT"): + result = list(self.dbcon.aggregate([ + {"$match": { + "type": "subset" + }}, + {"$project": { + "family": {"$arrayElemAt": ["$data.families", 0]} + }}, + {"$group": { + "_id": "family_group", + "families": {"$addToSet": "$family"} + }} + ])) + if result: + families = result[0]["families"] + + # Rebuild list + self.blockSignals(True) + self.clear() + for name in sorted(families): + family = self.family_config_cache.family_config(name) + if family.get("hideFilter"): + continue + + label = family.get("label", name) + icon = family.get("icon", None) + + # TODO: This should be more managable by the artist + # Temporarily implement support for a default state in the project + # configuration + state = family.get("state", True) + state = QtCore.Qt.Checked if state else QtCore.Qt.Unchecked + + item = QtWidgets.QListWidgetItem(parent=self) + item.setText(label) + item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) + item.setData(self.NameRole, name) + item.setCheckState(state) + + if icon: + item.setIcon(icon) + + self.addItem(item) + self.blockSignals(False) + + self.active_changed.emit(self.get_filters()) + + def get_filters(self): + """Return the checked family items""" + + items = [self.item(i) for i in + range(self.count())] + + return [item.data(self.NameRole) for item in items if + item.checkState() == QtCore.Qt.Checked] + + def _on_item_changed(self): + self.active_changed.emit(self.get_filters()) + + def _set_checkstate_all(self, state): + _state = QtCore.Qt.Checked if state is True else QtCore.Qt.Unchecked + self.blockSignals(True) + for i in range(self.count()): + item = self.item(i) + item.setCheckState(_state) + self.blockSignals(False) + self.active_changed.emit(self.get_filters()) + + def show_right_mouse_menu(self, pos): + """Build RMB menu under mouse at current position (within widget)""" + + # Get mouse position + globalpos = self.viewport().mapToGlobal(pos) + + menu = QtWidgets.QMenu(self) + + # Add enable all action + state_checked = QtWidgets.QAction(menu, text="Enable All") + state_checked.triggered.connect( + lambda: self._set_checkstate_all(True)) + # Add disable all action + state_unchecked = QtWidgets.QAction(menu, text="Disable All") + state_unchecked.triggered.connect( + lambda: self._set_checkstate_all(False)) + + menu.addAction(state_checked) + menu.addAction(state_unchecked) + + menu.exec_(globalpos) + + +class RepresentationWidget(QtWidgets.QWidget): + load_started = QtCore.Signal() + load_ended = QtCore.Signal() + + default_widths = ( + ("name", 120), + ("subset", 125), + ("asset", 125), + ("active_site", 85), + ("remote_site", 85) + ) + + commands = {'active': 'Download', 'remote': 'Upload'} + + def __init__(self, dbcon, tool_name=None, parent=None): + super(RepresentationWidget, self).__init__(parent=parent) + self.dbcon = dbcon + self.tool_name = tool_name + + headers = [item[0] for item in self.default_widths] + + model = RepresentationModel(self.dbcon, headers, []) + + proxy_model = RepresentationSortProxyModel(self) + proxy_model.setSourceModel(model) + + label = QtWidgets.QLabel("Representations", self) + + tree_view = DeselectableTreeView() + tree_view.setModel(proxy_model) + tree_view.setAllColumnsShowFocus(True) + tree_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + tree_view.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection) + tree_view.setSortingEnabled(True) + tree_view.sortByColumn(1, QtCore.Qt.AscendingOrder) + tree_view.setAlternatingRowColors(True) + tree_view.setIndentation(20) + tree_view.setStyleSheet(""" + QTreeView::item{ + padding: 5px 1px; + border: 0px; + } + """) + tree_view.collapseAll() + + for column_name, width in self.default_widths: + idx = model.Columns.index(column_name) + tree_view.setColumnWidth(idx, width) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(label) + layout.addWidget(tree_view) + + # self.itemChanged.connect(self._on_item_changed) + tree_view.customContextMenuRequested.connect(self.on_context_menu) + + self.tree_view = tree_view + self.model = model + self.proxy_model = proxy_model + + self.sync_server_enabled = False + actual_project = dbcon.Session["AVALON_PROJECT"] + self.on_project_change(actual_project) + + self.model.refresh() + + def on_project_change(self, project_name): + """ + Called on each project change in parent widget. + + Checks if Sync Server is enabled for a project, pushes changes to + model. + """ + enabled = False + if project_name: + self.model.reset_sync_server(project_name) + if self.model.sync_server: + enabled_proj = self.model.sync_server.get_enabled_projects() + enabled = project_name in enabled_proj + + self.sync_server_enabled = enabled + lib.change_visibility(self.model, self.tree_view, + "active_site", enabled) + lib.change_visibility(self.model, self.tree_view, + "remote_site", enabled) + + def _repre_contexts_for_loaders_filter(self, items): + repre_ids = [] + for item in items: + repre_ids.append(item["_id"]) + + repre_docs = list(self.dbcon.find( + { + "type": "representation", + "_id": {"$in": repre_ids} + }, + { + "name": 1, + "parent": 1 + } + )) + version_ids = [ + repre_doc["parent"] + for repre_doc in repre_docs + ] + version_docs = self.dbcon.find({ + "_id": {"$in": version_ids} + }) + + version_docs_by_id = {} + version_docs_by_subset_id = collections.defaultdict(list) + for version_doc in version_docs: + version_id = version_doc["_id"] + subset_id = version_doc["parent"] + version_docs_by_id[version_id] = version_doc + version_docs_by_subset_id[subset_id].append(version_doc) + + subset_docs = list(self.dbcon.find( + { + "_id": {"$in": list(version_docs_by_subset_id.keys())}, + "type": "subset" + }, + { + "schema": 1, + "data.families": 1 + } + )) + subset_docs_by_id = { + subset_doc["_id"]: subset_doc + for subset_doc in subset_docs + } + repre_context_by_id = {} + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + + version_doc = version_docs_by_id[version_id] + repre_context_by_id[repre_doc["_id"]] = { + "representation": repre_doc, + "version": version_doc, + "subset": subset_docs_by_id[version_doc["parent"]] + } + return repre_context_by_id + + def on_context_menu(self, point): + """Shows menu with loader actions on Right-click. + + Registered actions are filtered by selection and help of + `loaders_from_representation` from avalon api. Intersection of actions + is shown when more subset is selected. When there are not available + actions for selected subsets then special action is shown (works as + info message to user): "*No compatible loaders for your selection" + + """ + point_index = self.tree_view.indexAt(point) + if not point_index.isValid(): + return + + # Get selected subsets without groups + selection = self.tree_view.selectionModel() + rows = selection.selectedRows(column=0) + + items = lib.get_selected_items(rows, self.model.ItemRole) + + selected_side = self._get_selected_side(point_index, rows) + + # Get all representation->loader combinations available for the + # index under the cursor, so we can list the user the options. + available_loaders = api.discover(api.Loader) + + filtered_loaders = [] + for loader in available_loaders: + # Skip subset loaders + if api.SubsetLoader in inspect.getmro(loader): + continue + + if ( + tools_lib.is_sync_loader(loader) + and not self.sync_server_enabled + ): + continue + + filtered_loaders.append(loader) + + if self.tool_name: + filtered_loaders = lib.remove_tool_name_from_loaders( + filtered_loaders, self.tool_name + ) + + loaders = list() + already_added_loaders = set() + label_already_in_menu = set() + + repre_context_by_id = ( + self._repre_contexts_for_loaders_filter(items) + ) + + for item in items: + repre_context = repre_context_by_id[item["_id"]] + for loader in pipeline.loaders_from_repre_context( + filtered_loaders, + repre_context + ): + if tools_lib.is_sync_loader(loader): + both_unavailable = item["active_site_progress"] <= 0 and \ + item["remote_site_progress"] <= 0 + if both_unavailable: + continue + + for selected_side in self.commands.keys(): + item = item.copy() + item["custom_label"] = None + label = None + selected_site_progress = item.get( + "{}_site_progress".format(selected_side), -1) + + # only remove if actually present + if tools_lib.is_remove_site_loader(loader): + label = "Remove {}".format(selected_side) + if selected_site_progress < 1: + continue + + if tools_lib.is_add_site_loader(loader): + label = self.commands[selected_side] + if selected_site_progress >= 0: + label = 'Re-{} {}'.format(label, selected_side) + + if not label: + continue + + item["selected_side"] = selected_side + item["custom_label"] = label + + if label not in label_already_in_menu: + loaders.append((item, loader)) + already_added_loaders.add(loader) + label_already_in_menu.add(label) + + else: + item = item.copy() + item["custom_label"] = None + + if loader not in already_added_loaders: + loaders.append((item, loader)) + already_added_loaders.add(loader) + + loaders = lib.sort_loaders(loaders) + + menu = OptionalMenu(self) + if not loaders: + action = lib.get_no_loader_action(menu) + menu.addAction(action) + else: + repre_contexts = pipeline.get_repres_contexts( + repre_context_by_id.keys(), self.dbcon) + menu = lib.add_representation_loaders_to_menu(loaders, menu, + repre_contexts) + + self._process_action(items, menu, point) + + def _process_action(self, items, menu, point): + """ + Show the context action menu and process selected + + Args: + items(dict): menu items + menu(OptionalMenu) + point(PointIndex) + """ + global_point = self.tree_view.mapToGlobal(point) + action = menu.exec_(global_point) + + if not action or not action.data(): + return + + self.load_started.emit() + + # Find the representation name and loader to trigger + action_representation, loader = action.data() + repre_ids = [] + data_by_repre_id = {} + selected_side = action_representation.get("selected_side") + + for item in items: + if tools_lib.is_sync_loader(loader): + site_name = "{}_site_name".format(selected_side) + data = { + "_id": item.get("_id"), + "site_name": item.get(site_name), + "project_name": self.dbcon.Session["AVALON_PROJECT"] + } + + if not data["site_name"]: + continue + + data_by_repre_id[data["_id"]] = data + + repre_ids.append(item.get("_id")) + + repre_contexts = pipeline.get_repres_contexts(repre_ids, + self.dbcon) + options = lib.get_options(action, loader, self, + list(repre_contexts.values())) + + errors = _load_representations_by_loader( + loader, repre_contexts, + options=options, data_by_repre_id=data_by_repre_id) + + self.model.refresh() + + self.load_ended.emit() + + if errors: + box = LoadErrorMessageBox(errors) + box.show() + + def _get_optional_labels(self, loaders, selected_side): + """Each loader could have specific label + + Args: + loaders (tuple of dict, dict): (item, loader) + selected_side(string): active or remote + + Returns: + (dict) {loader: string} + """ + optional_labels = {} + if selected_side: + if selected_side == 'active': + txt = "Localize" + else: + txt = "Sync to Remote" + optional_labels = {loader: txt for _, loader in loaders + if tools_lib.is_sync_loader(loader)} + return optional_labels + + def _get_selected_side(self, point_index, rows): + """Returns active/remote label according to column in 'point_index'""" + selected_side = None + if self.sync_server_enabled: + if rows: + source_index = self.proxy_model.mapToSource(point_index) + selected_side = self.model.data(source_index, + self.model.SiteSideRole) + return selected_side + + def set_version_ids(self, version_ids): + self.model.set_version_ids(version_ids) + + def _set_download(self): + pass + + def change_visibility(self, column_name, visible): + """ + Hides or shows particular 'column_name'. + + "asset" and "subset" columns should be visible only in multiselect + """ + lib.change_visibility(self.model, self.tree_view, column_name, visible) + + +def _load_representations_by_loader(loader, repre_contexts, + options, + data_by_repre_id=None): + """Loops through list of repre_contexts and loads them with one loader + + Args: + loader (cls of api.Loader) - not initialized yet + repre_contexts (dicts) - full info about selected representations + (containing repre_doc, version_doc, subset_doc, project info) + options (dict) - qargparse arguments to fill OptionDialog + data_by_repre_id (dict) - additional data applicable on top of + options to provide dynamic values + """ + error_info = [] + + if options is None: # not load when cancelled + return + + for repre_context in repre_contexts.values(): + try: + if data_by_repre_id: + _id = repre_context["representation"]["_id"] + data = data_by_repre_id.get(_id) + options.update(data) + pipeline.load_with_repre_context( + loader, + repre_context, + options=options + ) + except pipeline.IncompatibleLoaderError as exc: + print(exc) + error_info.append(( + "Incompatible Loader", + None, + repre_context["representation"]["name"], + repre_context["subset"]["name"], + repre_context["version"]["name"] + )) + + except Exception as exc: + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + error_info.append(( + str(exc), + formatted_traceback, + repre_context["representation"]["name"], + repre_context["subset"]["name"], + repre_context["version"]["name"] + )) + return error_info + + +def _load_subsets_by_loader(loader, subset_contexts, options, + subset_version_docs=None): + """ + Triggers load with SubsetLoader type of loaders + + Args: + loader (SubsetLoder): + subset_contexts (list): + options (dict): + subset_version_docs (dict): {subset_id: version_doc} + """ + error_info = [] + + if options is None: # not load when cancelled + return + + if loader.is_multiple_contexts_compatible: + subset_names = [] + for context in subset_contexts: + subset_name = context.get("subset", {}).get("name") or "N/A" + subset_names.append(subset_name) + + context["version"] = subset_version_docs[context["subset"]["_id"]] + try: + pipeline.load_with_subset_contexts( + loader, + subset_contexts, + options=options + ) + except Exception as exc: + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join( + traceback.format_exception( + exc_type, exc_value, exc_traceback + ) + ) + error_info.append(( + str(exc), + formatted_traceback, + None, + ", ".join(subset_names), + None + )) + else: + for subset_context in subset_contexts: + subset_name = subset_context.get("subset", {}).get("name") or "N/A" + + version_doc = subset_version_docs[subset_context["subset"]["_id"]] + subset_context["version"] = version_doc + try: + pipeline.load_with_subset_context( + loader, + subset_context, + options=options + ) + except Exception as exc: + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "\n".join( + traceback.format_exception( + exc_type, exc_value, exc_traceback + ) + ) + error_info.append(( + str(exc), + formatted_traceback, + None, + subset_name, + None + )) + + return error_info diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/tools/utils/delegates.py b/openpype/tools/utils/delegates.py new file mode 100644 index 0000000000..608762b538 --- /dev/null +++ b/openpype/tools/utils/delegates.py @@ -0,0 +1,448 @@ +import time +from datetime import datetime +import logging +import numbers + +import Qt +from Qt import QtWidgets, QtGui, QtCore + +from avalon.lib import HeroVersionType +from .models import ( + AssetModel, + TreeModel +) +from . import lib + +if Qt.__binding__ == "PySide": + from PySide.QtGui import QStyleOptionViewItemV4 +elif Qt.__binding__ == "PyQt4": + from PyQt4.QtGui import QStyleOptionViewItemV4 + +log = logging.getLogger(__name__) + + +class AssetDelegate(QtWidgets.QItemDelegate): + bar_height = 3 + + def sizeHint(self, option, index): + result = super(AssetDelegate, self).sizeHint(option, index) + height = result.height() + result.setHeight(height + self.bar_height) + + return result + + def paint(self, painter, option, index): + # Qt4 compat + if Qt.__binding__ in ("PySide", "PyQt4"): + option = QStyleOptionViewItemV4(option) + + painter.save() + + item_rect = QtCore.QRect(option.rect) + item_rect.setHeight(option.rect.height() - self.bar_height) + + subset_colors = index.data(AssetModel.subsetColorsRole) + subset_colors_width = 0 + if subset_colors: + subset_colors_width = option.rect.width() / len(subset_colors) + + subset_rects = [] + counter = 0 + for subset_c in subset_colors: + new_color = None + new_rect = None + if subset_c: + new_color = QtGui.QColor(*subset_c) + + new_rect = QtCore.QRect( + option.rect.left() + (counter * subset_colors_width), + option.rect.top() + ( + option.rect.height() - self.bar_height + ), + subset_colors_width, + self.bar_height + ) + subset_rects.append((new_color, new_rect)) + counter += 1 + + # Background + bg_color = QtGui.QColor(60, 60, 60) + if option.state & QtWidgets.QStyle.State_Selected: + if len(subset_colors) == 0: + item_rect.setTop(item_rect.top() + (self.bar_height / 2)) + if option.state & QtWidgets.QStyle.State_MouseOver: + bg_color.setRgb(70, 70, 70) + else: + item_rect.setTop(item_rect.top() + (self.bar_height / 2)) + if option.state & QtWidgets.QStyle.State_MouseOver: + bg_color.setAlpha(100) + else: + bg_color.setAlpha(0) + + # # -- When not needed to do a rounded corners (easier and without painter restore): + # painter.fillRect( + # item_rect, + # QtGui.QBrush(bg_color) + # ) + pen = painter.pen() + pen.setStyle(QtCore.Qt.NoPen) + pen.setWidth(0) + painter.setPen(pen) + painter.setBrush(QtGui.QBrush(bg_color)) + painter.drawRoundedRect(option.rect, 3, 3) + + if option.state & QtWidgets.QStyle.State_Selected: + for color, subset_rect in subset_rects: + if not color or not subset_rect: + continue + painter.fillRect(subset_rect, QtGui.QBrush(color)) + + painter.restore() + painter.save() + + # Icon + icon_index = index.model().index( + index.row(), index.column(), index.parent() + ) + # - Default icon_rect if not icon + icon_rect = QtCore.QRect( + item_rect.left(), + item_rect.top(), + # To make sure it's same size all the time + option.rect.height() - self.bar_height, + option.rect.height() - self.bar_height + ) + icon = index.model().data(icon_index, QtCore.Qt.DecorationRole) + + if icon: + mode = QtGui.QIcon.Normal + if not (option.state & QtWidgets.QStyle.State_Enabled): + mode = QtGui.QIcon.Disabled + elif option.state & QtWidgets.QStyle.State_Selected: + mode = QtGui.QIcon.Selected + + if isinstance(icon, QtGui.QPixmap): + icon = QtGui.QIcon(icon) + option.decorationSize = icon.size() / icon.devicePixelRatio() + + elif isinstance(icon, QtGui.QColor): + pixmap = QtGui.QPixmap(option.decorationSize) + pixmap.fill(icon) + icon = QtGui.QIcon(pixmap) + + elif isinstance(icon, QtGui.QImage): + icon = QtGui.QIcon(QtGui.QPixmap.fromImage(icon)) + option.decorationSize = icon.size() / icon.devicePixelRatio() + + elif isinstance(icon, QtGui.QIcon): + state = QtGui.QIcon.Off + if option.state & QtWidgets.QStyle.State_Open: + state = QtGui.QIcon.On + actualSize = option.icon.actualSize( + option.decorationSize, mode, state + ) + option.decorationSize = QtCore.QSize( + min(option.decorationSize.width(), actualSize.width()), + min(option.decorationSize.height(), actualSize.height()) + ) + + state = QtGui.QIcon.Off + if option.state & QtWidgets.QStyle.State_Open: + state = QtGui.QIcon.On + + icon.paint( + painter, icon_rect, + QtCore.Qt.AlignLeft, mode, state + ) + + # Text + text_rect = QtCore.QRect( + icon_rect.left() + icon_rect.width() + 2, + item_rect.top(), + item_rect.width(), + item_rect.height() + ) + + painter.drawText( + text_rect, QtCore.Qt.AlignVCenter, + index.data(QtCore.Qt.DisplayRole) + ) + + painter.restore() + + +class VersionDelegate(QtWidgets.QStyledItemDelegate): + """A delegate that display version integer formatted as version string.""" + + version_changed = QtCore.Signal() + first_run = False + lock = False + + def __init__(self, dbcon, *args, **kwargs): + self.dbcon = dbcon + super(VersionDelegate, self).__init__(*args, **kwargs) + + def displayText(self, value, locale): + if isinstance(value, HeroVersionType): + return lib.format_version(value, True) + assert isinstance(value, numbers.Integral), ( + "Version is not integer. \"{}\" {}".format(value, str(type(value))) + ) + return lib.format_version(value) + + def paint(self, painter, option, index): + fg_color = index.data(QtCore.Qt.ForegroundRole) + if fg_color: + if isinstance(fg_color, QtGui.QBrush): + fg_color = fg_color.color() + elif isinstance(fg_color, QtGui.QColor): + pass + else: + fg_color = None + + if not fg_color: + return super(VersionDelegate, self).paint(painter, option, index) + + if option.widget: + style = option.widget.style() + else: + style = QtWidgets.QApplication.style() + + style.drawControl( + style.CE_ItemViewItem, option, painter, option.widget + ) + + painter.save() + + text = self.displayText( + index.data(QtCore.Qt.DisplayRole), option.locale + ) + pen = painter.pen() + pen.setColor(fg_color) + painter.setPen(pen) + + text_rect = style.subElementRect(style.SE_ItemViewItemText, option) + text_margin = style.proxy().pixelMetric( + style.PM_FocusFrameHMargin, option, option.widget + ) + 1 + + painter.drawText( + text_rect.adjusted(text_margin, 0, - text_margin, 0), + option.displayAlignment, + text + ) + + painter.restore() + + def createEditor(self, parent, option, index): + item = index.data(TreeModel.ItemRole) + if item.get("isGroup") or item.get("isMerged"): + return + + editor = QtWidgets.QComboBox(parent) + + def commit_data(): + if not self.first_run: + self.commitData.emit(editor) # Update model data + self.version_changed.emit() # Display model data + editor.currentIndexChanged.connect(commit_data) + + self.first_run = True + self.lock = False + + return editor + + def setEditorData(self, editor, index): + if self.lock: + # Only set editor data once per delegation + return + + editor.clear() + + # Current value of the index + item = index.data(TreeModel.ItemRole) + value = index.data(QtCore.Qt.DisplayRole) + if item["version_document"]["type"] != "hero_version": + assert isinstance(value, numbers.Integral), ( + "Version is not integer" + ) + + # Add all available versions to the editor + parent_id = item["version_document"]["parent"] + version_docs = list(self.dbcon.find( + { + "type": "version", + "parent": parent_id + }, + sort=[("name", 1)] + )) + + hero_version_doc = self.dbcon.find_one( + { + "type": "hero_version", + "parent": parent_id + }, { + "name": 1, + "data.tags": 1, + "version_id": 1 + } + ) + + doc_for_hero_version = None + + selected = None + items = [] + for version_doc in version_docs: + version_tags = version_doc["data"].get("tags") or [] + if "deleted" in version_tags: + continue + + if ( + hero_version_doc + and doc_for_hero_version is None + and hero_version_doc["version_id"] == version_doc["_id"] + ): + doc_for_hero_version = version_doc + + label = lib.format_version(version_doc["name"]) + item = QtGui.QStandardItem(label) + item.setData(version_doc, QtCore.Qt.UserRole) + items.append(item) + + if version_doc["name"] == value: + selected = item + + if hero_version_doc and doc_for_hero_version: + version_name = doc_for_hero_version["name"] + label = lib.format_version(version_name, True) + if isinstance(value, HeroVersionType): + index = len(version_docs) + hero_version_doc["name"] = HeroVersionType(version_name) + + item = QtGui.QStandardItem(label) + item.setData(hero_version_doc, QtCore.Qt.UserRole) + items.append(item) + + # Reverse items so latest versions be upper + items = list(reversed(items)) + for item in items: + editor.model().appendRow(item) + + index = 0 + if selected: + index = selected.row() + + # Will trigger index-change signal + editor.setCurrentIndex(index) + self.first_run = False + self.lock = True + + def setModelData(self, editor, model, index): + """Apply the integer version back in the model""" + version = editor.itemData(editor.currentIndex()) + model.setData(index, version["name"]) + + +def pretty_date(t, now=None, strftime="%b %d %Y %H:%M"): + """Parse datetime to readable timestamp + + Within first ten seconds: + - "just now", + Within first minute ago: + - "%S seconds ago" + Within one hour ago: + - "%M minutes ago". + Within one day ago: + - "%H:%M hours ago" + Else: + "%Y-%m-%d %H:%M:%S" + + """ + + assert isinstance(t, datetime) + if now is None: + now = datetime.now() + assert isinstance(now, datetime) + diff = now - t + + second_diff = diff.seconds + day_diff = diff.days + + # future (consider as just now) + if day_diff < 0: + return "just now" + + # history + if day_diff == 0: + if second_diff < 10: + return "just now" + if second_diff < 60: + return str(second_diff) + " seconds ago" + if second_diff < 120: + return "a minute ago" + if second_diff < 3600: + return str(second_diff // 60) + " minutes ago" + if second_diff < 86400: + minutes = (second_diff % 3600) // 60 + hours = second_diff // 3600 + return "{0}:{1:02d} hours ago".format(hours, minutes) + + return t.strftime(strftime) + + +def pretty_timestamp(t, now=None): + """Parse timestamp to user readable format + + >>> pretty_timestamp("20170614T151122Z", now="20170614T151123Z") + 'just now' + + >>> pretty_timestamp("20170614T151122Z", now="20170614T171222Z") + '2:01 hours ago' + + Args: + t (str): The time string to parse. + now (str, optional) + + Returns: + str: human readable "recent" date. + + """ + + if now is not None: + try: + now = time.strptime(now, "%Y%m%dT%H%M%SZ") + now = datetime.fromtimestamp(time.mktime(now)) + except ValueError as e: + log.warning("Can't parse 'now' time format: {0} {1}".format(t, e)) + return None + + if isinstance(t, float): + dt = datetime.fromtimestamp(t) + else: + # Parse the time format as if it is `str` result from + # `pyblish.lib.time()` which usually is stored in Avalon database. + try: + t = time.strptime(t, "%Y%m%dT%H%M%SZ") + except ValueError as e: + log.warning("Can't parse time format: {0} {1}".format(t, e)) + return None + dt = datetime.fromtimestamp(time.mktime(t)) + + # prettify + return pretty_date(dt, now=now) + + +class PrettyTimeDelegate(QtWidgets.QStyledItemDelegate): + """A delegate that displays a timestamp as a pretty date. + + This displays dates like `pretty_date`. + + """ + + def displayText(self, value, locale): + + if value is None: + # Ignore None value + return + + return pretty_timestamp(value) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py new file mode 100644 index 0000000000..e83f663b2e --- /dev/null +++ b/openpype/tools/utils/lib.py @@ -0,0 +1,622 @@ +import os +import sys +import contextlib +import collections + +from Qt import QtWidgets, QtCore, QtGui + +from avalon import io, api, style +from avalon.vendor import qtawesome + +self = sys.modules[__name__] +self._jobs = dict() + + +class SharedObjects: + # Variable for family cache in global context + # QUESTION is this safe? More than one tool can refresh at the same time. + family_cache = None + + +def global_family_cache(): + if SharedObjects.family_cache is None: + SharedObjects.family_cache = FamilyConfigCache(io) + return SharedObjects.family_cache + + +def format_version(value, hero_version=False): + """Formats integer to displayable version name""" + label = "v{0:03d}".format(value) + if not hero_version: + return label + return "[{}]".format(label) + + +@contextlib.contextmanager +def application(): + app = QtWidgets.QApplication.instance() + + if not app: + print("Starting new QApplication..") + app = QtWidgets.QApplication(sys.argv) + yield app + app.exec_() + else: + print("Using existing QApplication..") + yield app + + +def defer(delay, func): + """Append artificial delay to `func` + + This aids in keeping the GUI responsive, but complicates logic + when producing tests. To combat this, the environment variable ensures + that every operation is synchonous. + + Arguments: + delay (float): Delay multiplier; default 1, 0 means no delay + func (callable): Any callable + + """ + + delay *= float(os.getenv("PYBLISH_DELAY", 1)) + if delay > 0: + return QtCore.QTimer.singleShot(delay, func) + else: + return func() + + +def schedule(func, time, channel="default"): + """Run `func` at a later `time` in a dedicated `channel` + + Given an arbitrary function, call this function after a given + timeout. It will ensure that only one "job" is running within + the given channel at any one time and cancel any currently + running job if a new job is submitted before the timeout. + + """ + + try: + self._jobs[channel].stop() + except (AttributeError, KeyError, RuntimeError): + pass + + timer = QtCore.QTimer() + timer.setSingleShot(True) + timer.timeout.connect(func) + timer.start(time) + + self._jobs[channel] = timer + + +@contextlib.contextmanager +def dummy(): + """Dummy context manager + + Usage: + >> with some_context() if False else dummy(): + .. pass + + """ + + yield + + +def iter_model_rows(model, column, include_root=False): + """Iterate over all row indices in a model""" + indices = [QtCore.QModelIndex()] # start iteration at root + + for index in indices: + # Add children to the iterations + child_rows = model.rowCount(index) + for child_row in range(child_rows): + child_index = model.index(child_row, column, index) + indices.append(child_index) + + if not include_root and not index.isValid(): + continue + + yield index + + +@contextlib.contextmanager +def preserve_states(tree_view, + column=0, + role=None, + preserve_expanded=True, + preserve_selection=True, + expanded_role=QtCore.Qt.DisplayRole, + selection_role=QtCore.Qt.DisplayRole): + """Preserves row selection in QTreeView by column's data role. + This function is created to maintain the selection status of + the model items. When refresh is triggered the items which are expanded + will stay expanded and vise versa. + tree_view (QWidgets.QTreeView): the tree view nested in the application + column (int): the column to retrieve the data from + role (int): the role which dictates what will be returned + Returns: + None + """ + # When `role` is set then override both expanded and selection roles + if role: + expanded_role = role + selection_role = role + + model = tree_view.model() + selection_model = tree_view.selectionModel() + flags = selection_model.Select | selection_model.Rows + + expanded = set() + + if preserve_expanded: + for index in iter_model_rows( + model, column=column, include_root=False + ): + if tree_view.isExpanded(index): + value = index.data(expanded_role) + expanded.add(value) + + selected = None + + if preserve_selection: + selected_rows = selection_model.selectedRows() + if selected_rows: + selected = set(row.data(selection_role) for row in selected_rows) + + try: + yield + finally: + if expanded: + for index in iter_model_rows( + model, column=0, include_root=False + ): + value = index.data(expanded_role) + is_expanded = value in expanded + # skip if new index was created meanwhile + if is_expanded is None: + continue + tree_view.setExpanded(index, is_expanded) + + if selected: + # Go through all indices, select the ones with similar data + for index in iter_model_rows( + model, column=column, include_root=False + ): + value = index.data(selection_role) + state = value in selected + if state: + tree_view.scrollTo(index) # Ensure item is visible + selection_model.select(index, flags) + + +@contextlib.contextmanager +def preserve_expanded_rows(tree_view, column=0, role=None): + """Preserves expanded row in QTreeView by column's data role. + + This function is created to maintain the expand vs collapse status of + the model items. When refresh is triggered the items which are expanded + will stay expanded and vise versa. + + Arguments: + tree_view (QWidgets.QTreeView): the tree view which is + nested in the application + column (int): the column to retrieve the data from + role (int): the role which dictates what will be returned + + Returns: + None + + """ + if role is None: + role = QtCore.Qt.DisplayRole + model = tree_view.model() + + expanded = set() + + for index in iter_model_rows(model, column=column, include_root=False): + if tree_view.isExpanded(index): + value = index.data(role) + expanded.add(value) + + try: + yield + finally: + if not expanded: + return + + for index in iter_model_rows(model, column=column, include_root=False): + value = index.data(role) + state = value in expanded + if state: + tree_view.expand(index) + else: + tree_view.collapse(index) + + +@contextlib.contextmanager +def preserve_selection(tree_view, column=0, role=None, current_index=True): + """Preserves row selection in QTreeView by column's data role. + + This function is created to maintain the selection status of + the model items. When refresh is triggered the items which are expanded + will stay expanded and vise versa. + + tree_view (QWidgets.QTreeView): the tree view nested in the application + column (int): the column to retrieve the data from + role (int): the role which dictates what will be returned + + Returns: + None + + """ + if role is None: + role = QtCore.Qt.DisplayRole + model = tree_view.model() + selection_model = tree_view.selectionModel() + flags = selection_model.Select | selection_model.Rows + + if current_index: + current_index_value = tree_view.currentIndex().data(role) + else: + current_index_value = None + + selected_rows = selection_model.selectedRows() + if not selected_rows: + yield + return + + selected = set(row.data(role) for row in selected_rows) + try: + yield + finally: + if not selected: + return + + # Go through all indices, select the ones with similar data + for index in iter_model_rows(model, column=column, include_root=False): + value = index.data(role) + state = value in selected + if state: + tree_view.scrollTo(index) # Ensure item is visible + selection_model.select(index, flags) + + if current_index_value and value == current_index_value: + selection_model.setCurrentIndex( + index, selection_model.NoUpdate + ) + + +class FamilyConfigCache: + default_color = "#0091B2" + _default_icon = None + _default_item = None + + def __init__(self, dbcon): + self.dbcon = dbcon + self.family_configs = {} + + @classmethod + def default_icon(cls): + if cls._default_icon is None: + cls._default_icon = qtawesome.icon( + "fa.folder", color=cls.default_color + ) + return cls._default_icon + + @classmethod + def default_item(cls): + if cls._default_item is None: + cls._default_item = {"icon": cls.default_icon()} + return cls._default_item + + def family_config(self, family_name): + """Get value from config with fallback to default""" + return self.family_configs.get(family_name, self.default_item()) + + def refresh(self): + """Get the family configurations from the database + + The configuration must be stored on the project under `config`. + For example: + + {"config": { + "families": [ + {"name": "avalon.camera", label: "Camera", "icon": "photo"}, + {"name": "avalon.anim", label: "Animation", "icon": "male"}, + ] + }} + + It is possible to override the default behavior and set specific + families checked. For example we only want the families imagesequence + and camera to be visible in the Loader. + + # This will turn every item off + api.data["familyStateDefault"] = False + + # Only allow the imagesequence and camera + api.data["familyStateToggled"] = ["imagesequence", "camera"] + + """ + + self.family_configs.clear() + + families = [] + + # Update the icons from the project configuration + project_name = self.dbcon.Session.get("AVALON_PROJECT") + if project_name: + project_doc = self.dbcon.find_one( + {"type": "project"}, + projection={"config.families": True} + ) + + if not project_doc: + print(( + "Project \"{}\" not found!" + " Can't refresh family icons cache." + ).format(project_name)) + else: + families = project_doc["config"].get("families") or [] + + # Check if any family state are being overwritten by the configuration + default_state = api.data.get("familiesStateDefault", True) + toggled = set(api.data.get("familiesStateToggled") or []) + + # Replace icons with a Qt icon we can use in the user interfaces + for family in families: + name = family["name"] + # Set family icon + icon = family.get("icon", None) + if icon: + family["icon"] = qtawesome.icon( + "fa.{}".format(icon), + color=self.default_color + ) + else: + family["icon"] = self.default_icon() + + # Update state + if name in toggled: + state = True + else: + state = default_state + family["state"] = state + + self.family_configs[name] = family + + return self.family_configs + + +class GroupsConfig: + # Subset group item's default icon and order + _default_group_config = None + + def __init__(self, dbcon): + self.dbcon = dbcon + self.groups = {} + + @classmethod + def default_group_config(cls): + if cls._default_group_config is None: + cls._default_group_config = { + "icon": qtawesome.icon( + "fa.object-group", + color=style.colors.default + ), + "order": 0 + } + return cls._default_group_config + + def refresh(self): + """Get subset group configurations from the database + + The 'group' configuration must be stored in the project `config` field. + See schema `config-1.0.json` + + """ + # Clear cached groups + self.groups.clear() + + group_configs = [] + project_name = self.dbcon.Session.get("AVALON_PROJECT") + if project_name: + # Get pre-defined group name and apperance from project config + project_doc = self.dbcon.find_one( + {"type": "project"}, + projection={"config.groups": True} + ) + + if project_doc: + group_configs = project_doc["config"].get("groups") or [] + else: + print("Project not found! \"{}\"".format(project_name)) + + # Build pre-defined group configs + for config in group_configs: + name = config["name"] + icon = "fa." + config.get("icon", "object-group") + color = config.get("color", style.colors.default) + order = float(config.get("order", 0)) + + self.groups[name] = { + "icon": qtawesome.icon(icon, color=color), + "order": order + } + + return self.groups + + def ordered_groups(self, group_names): + # default order zero included + _orders = set([0]) + for config in self.groups.values(): + _orders.add(config["order"]) + + # Remap order to list index + orders = sorted(_orders) + + _groups = list() + for name in group_names: + # Get group config + config = self.groups.get(name) or self.default_group_config() + # Base order + remapped_order = orders.index(config["order"]) + + data = { + "name": name, + "icon": config["icon"], + "_order": remapped_order, + } + + _groups.append(data) + + # Sort by tuple (base_order, name) + # If there are multiple groups in same order, will sorted by name. + ordered_groups = sorted( + _groups, key=lambda _group: (_group.pop("_order"), _group["name"]) + ) + + total = len(ordered_groups) + order_temp = "%0{}d".format(len(str(total))) + + # Update sorted order to config + for index, group_data in enumerate(ordered_groups): + order = index + inverse_order = total - index + + # Format orders into fixed length string for groups sorting + group_data["order"] = order_temp % order + group_data["inverseOrder"] = order_temp % inverse_order + + return ordered_groups + + def active_groups(self, asset_ids, include_predefined=True): + """Collect all active groups from each subset""" + # Collect groups from subsets + group_names = set( + self.dbcon.distinct( + "data.subsetGroup", + {"type": "subset", "parent": {"$in": asset_ids}} + ) + ) + if include_predefined: + # Ensure all predefined group configs will be included + group_names.update(self.groups.keys()) + + return self.ordered_groups(group_names) + + def split_subsets_for_groups(self, subset_docs, grouping): + """Collect all active groups from each subset""" + subset_docs_without_group = collections.defaultdict(list) + subset_docs_by_group = collections.defaultdict(dict) + for subset_doc in subset_docs: + subset_name = subset_doc["name"] + if grouping: + group_name = subset_doc["data"].get("subsetGroup") + if group_name: + if subset_name not in subset_docs_by_group[group_name]: + subset_docs_by_group[group_name][subset_name] = [] + + subset_docs_by_group[group_name][subset_name].append( + subset_doc + ) + continue + + subset_docs_without_group[subset_name].append(subset_doc) + + ordered_groups = self.ordered_groups(subset_docs_by_group.keys()) + + return ordered_groups, subset_docs_without_group, subset_docs_by_group + + +def create_qthread(func, *args, **kwargs): + class Thread(QtCore.QThread): + def run(self): + func(*args, **kwargs) + return Thread() + + +def get_repre_icons(): + try: + from openpype_modules import sync_server + except Exception: + # Backwards compatibility + from openpype.modules import sync_server + + resource_path = os.path.join( + os.path.dirname(sync_server.sync_server_module.__file__), + "providers", "resources" + ) + icons = {} + # TODO get from sync module + for provider in ['studio', 'local_drive', 'gdrive']: + pix_url = "{}/{}.png".format(resource_path, provider) + icons[provider] = QtGui.QIcon(pix_url) + + return icons + + +def get_progress_for_repre(doc, active_site, remote_site): + """ + Calculates average progress for representation. + + If site has created_dt >> fully available >> progress == 1 + + Could be calculated in aggregate if it would be too slow + Args: + doc(dict): representation dict + Returns: + (dict) with active and remote sites progress + {'studio': 1.0, 'gdrive': -1} - gdrive site is not present + -1 is used to highlight the site should be added + {'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not + uploaded yet + """ + progress = {active_site: -1, + remote_site: -1} + if not doc: + return progress + + files = {active_site: 0, remote_site: 0} + doc_files = doc.get("files") or [] + for doc_file in doc_files: + if not isinstance(doc_file, dict): + continue + + sites = doc_file.get("sites") or [] + for site in sites: + if ( + # Pype 2 compatibility + not isinstance(site, dict) + # Check if site name is one of progress sites + or site["name"] not in progress + ): + continue + + files[site["name"]] += 1 + norm_progress = max(progress[site["name"]], 0) + if site.get("created_dt"): + progress[site["name"]] = norm_progress + 1 + elif site.get("progress"): + progress[site["name"]] = norm_progress + site["progress"] + else: # site exists, might be failed, do not add again + progress[site["name"]] = 0 + + # for example 13 fully avail. files out of 26 >> 13/26 = 0.5 + avg_progress = {} + avg_progress[active_site] = \ + progress[active_site] / max(files[active_site], 1) + avg_progress[remote_site] = \ + progress[remote_site] / max(files[remote_site], 1) + return avg_progress + + +def is_sync_loader(loader): + return is_remove_site_loader(loader) or is_add_site_loader(loader) + + +def is_remove_site_loader(loader): + return hasattr(loader, "remove_site_on_representation") + + +def is_add_site_loader(loader): + return hasattr(loader, "add_site_to_representation") diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py new file mode 100644 index 0000000000..d09d16d898 --- /dev/null +++ b/openpype/tools/utils/models.py @@ -0,0 +1,626 @@ +import re +import time +import logging +import collections + +import Qt +from Qt import QtCore, QtGui +from avalon.vendor import qtawesome +from avalon import style, io +from . import lib + +log = logging.getLogger(__name__) + + +class TreeModel(QtCore.QAbstractItemModel): + + Columns = list() + ItemRole = QtCore.Qt.UserRole + 1 + item_class = None + + def __init__(self, parent=None): + super(TreeModel, self).__init__(parent) + self._root_item = self.ItemClass() + + @property + def ItemClass(self): + if self.item_class is not None: + return self.item_class + return Item + + def rowCount(self, parent=None): + if parent is None or not parent.isValid(): + parent_item = self._root_item + else: + parent_item = parent.internalPointer() + return parent_item.childCount() + + def columnCount(self, parent): + return len(self.Columns) + + def data(self, index, role): + if not index.isValid(): + return None + + if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: + item = index.internalPointer() + column = index.column() + + key = self.Columns[column] + return item.get(key, None) + + if role == self.ItemRole: + return index.internalPointer() + + def setData(self, index, value, role=QtCore.Qt.EditRole): + """Change the data on the items. + + Returns: + bool: Whether the edit was successful + """ + + if index.isValid(): + if role == QtCore.Qt.EditRole: + + item = index.internalPointer() + column = index.column() + key = self.Columns[column] + item[key] = value + + # passing `list()` for PyQt5 (see PYSIDE-462) + if Qt.__binding__ in ("PyQt4", "PySide"): + self.dataChanged.emit(index, index) + else: + self.dataChanged.emit(index, index, [role]) + + # must return true if successful + return True + + return False + + def setColumns(self, keys): + assert isinstance(keys, (list, tuple)) + self.Columns = keys + + def headerData(self, section, orientation, role): + + if role == QtCore.Qt.DisplayRole: + if section < len(self.Columns): + return self.Columns[section] + + super(TreeModel, self).headerData(section, orientation, role) + + def flags(self, index): + flags = QtCore.Qt.ItemIsEnabled + + item = index.internalPointer() + if item.get("enabled", True): + flags |= QtCore.Qt.ItemIsSelectable + + return flags + + def parent(self, index): + + item = index.internalPointer() + parent_item = item.parent() + + # If it has no parents we return invalid + if parent_item == self._root_item or not parent_item: + return QtCore.QModelIndex() + + return self.createIndex(parent_item.row(), 0, parent_item) + + def index(self, row, column, parent=None): + """Return index for row/column under parent""" + + if parent is None or not parent.isValid(): + parent_item = self._root_item + else: + parent_item = parent.internalPointer() + + child_item = parent_item.child(row) + if child_item: + return self.createIndex(row, column, child_item) + else: + return QtCore.QModelIndex() + + def add_child(self, item, parent=None): + if parent is None: + parent = self._root_item + + parent.add_child(item) + + def column_name(self, column): + """Return column key by index""" + + if column < len(self.Columns): + return self.Columns[column] + + def clear(self): + self.beginResetModel() + self._root_item = self.ItemClass() + self.endResetModel() + + +class Item(dict): + """An item that can be represented in a tree view using `TreeModel`. + + The item can store data just like a regular dictionary. + + >>> data = {"name": "John", "score": 10} + >>> item = Item(data) + >>> assert item["name"] == "John" + + """ + + def __init__(self, data=None): + super(Item, self).__init__() + + self._children = list() + self._parent = None + + if data is not None: + assert isinstance(data, dict) + self.update(data) + + def childCount(self): + return len(self._children) + + def child(self, row): + + if row >= len(self._children): + log.warning("Invalid row as child: {0}".format(row)) + return + + return self._children[row] + + def children(self): + return self._children + + def parent(self): + return self._parent + + def row(self): + """ + Returns: + int: Index of this item under parent""" + if self._parent is not None: + siblings = self.parent().children() + return siblings.index(self) + return -1 + + def add_child(self, child): + """Add a child to this item""" + child._parent = self + self._children.append(child) + + +class TasksModel(TreeModel): + """A model listing the tasks combined for a list of assets""" + + Columns = ["name", "count"] + + def __init__(self, dbcon, parent=None): + super(TasksModel, self).__init__(parent=parent) + self.dbcon = dbcon + self._num_assets = 0 + self._icons = { + "__default__": qtawesome.icon( + "fa.male", + color=style.colors.default + ), + "__no_task__": qtawesome.icon( + "fa.exclamation-circle", + color=style.colors.mid + ) + } + + self._refresh_task_icons() + + def _refresh_task_icons(self): + # Get the project configured icons from database + project = self.dbcon.find_one({"type": "project"}, {"config.tasks"}) + tasks = project["config"].get("tasks", {}) + for task_name, task in tasks.items(): + icon_name = task.get("icon", None) + if icon_name: + icon = qtawesome.icon( + "fa.{}".format(icon_name), + color=style.colors.default + ) + self._icons[task_name] = icon + + def set_assets(self, asset_ids=None, asset_docs=None): + """Set assets to track by their database id + + Arguments: + asset_ids (list): List of asset ids. + asset_docs (list): List of asset entities from MongoDB. + + """ + + if asset_docs is None and asset_ids is not None: + # prepare filter query + _filter = {"type": "asset", "_id": {"$in": asset_ids}} + _projection = {"data.tasks"} + + # find assets in db by query + asset_docs = list(self.dbcon.find(_filter, _projection)) + db_assets_ids = [asset["_id"] for asset in asset_docs] + + # check if all assets were found + not_found = [ + str(a_id) for a_id in asset_ids if a_id not in db_assets_ids + ] + + assert not not_found, "Assets not found by id: {0}".format( + ", ".join(not_found) + ) + + if asset_docs is None: + asset_docs = list() + + self._num_assets = len(asset_docs) + + tasks = collections.Counter() + for asset_doc in asset_docs: + asset_tasks = asset_doc.get("data", {}).get("tasks", {}) + tasks.update(asset_tasks.keys()) + + self.clear() + self.beginResetModel() + + default_icon = self._icons["__default__"] + + if not tasks: + no_task_icon = self._icons["__no_task__"] + item = Item({ + "name": "No task", + "count": 0, + "icon": no_task_icon, + "enabled": False, + }) + + self.add_child(item) + + else: + for task, count in sorted(tasks.items()): + icon = self._icons.get(task, default_icon) + + item = Item({ + "name": task, + "count": count, + "icon": icon + }) + + self.add_child(item) + + self.endResetModel() + + def headerData(self, section, orientation, role): + # Override header for count column to show amount of assets + # it is listing the tasks for + if role == QtCore.Qt.DisplayRole: + if orientation == QtCore.Qt.Horizontal: + if section == 0: + return "Tasks" + elif section == 1: # count column + return "count ({0})".format(self._num_assets) + + return super(TasksModel, self).headerData(section, orientation, role) + + def data(self, index, role): + if not index.isValid(): + return + + # Add icon to the first column + if role == QtCore.Qt.DecorationRole: + if index.column() == 0: + return index.internalPointer()["icon"] + + return super(TasksModel, self).data(index, role) + + +class AssetModel(TreeModel): + """A model listing assets in the silo in the active project. + + The assets are displayed in a treeview, they are visually parented by + a `visualParent` field in the database containing an `_id` to a parent + asset. + + """ + + Columns = ["label"] + Name = 0 + Deprecated = 2 + ObjectId = 3 + + DocumentRole = QtCore.Qt.UserRole + 2 + ObjectIdRole = QtCore.Qt.UserRole + 3 + subsetColorsRole = QtCore.Qt.UserRole + 4 + + doc_fetched = QtCore.Signal(bool) + refreshed = QtCore.Signal(bool) + + # Asset document projection + asset_projection = { + "type": 1, + "schema": 1, + "name": 1, + "silo": 1, + "data.visualParent": 1, + "data.label": 1, + "data.tags": 1, + "data.icon": 1, + "data.color": 1, + "data.deprecated": 1 + } + + def __init__(self, dbcon=None, parent=None, asset_projection=None): + super(AssetModel, self).__init__(parent=parent) + if dbcon is None: + dbcon = io + self.dbcon = dbcon + self.asset_colors = {} + + # Projections for Mongo queries + # - let ability to modify them if used in tools that require more than + # defaults + if asset_projection: + self.asset_projection = asset_projection + + self.asset_projection = asset_projection + + self._doc_fetching_thread = None + self._doc_fetching_stop = False + self._doc_payload = {} + + self.doc_fetched.connect(self.on_doc_fetched) + + self.refresh() + + def _add_hierarchy(self, assets, parent=None, silos=None): + """Add the assets that are related to the parent as children items. + + This method does *not* query the database. These instead are queried + in a single batch upfront as an optimization to reduce database + queries. Resulting in up to 10x speed increase. + + Args: + assets (dict): All assets in the currently active silo stored + by key/value + + Returns: + None + + """ + # Reset colors + self.asset_colors = {} + + if silos: + # WARNING: Silo item "_id" is set to silo value + # mainly because GUI issue with perserve selection and expanded row + # and because of easier hierarchy parenting (in "assets") + for silo in silos: + item = Item({ + "_id": silo, + "name": silo, + "label": silo, + "type": "silo" + }) + self.add_child(item, parent=parent) + self._add_hierarchy(assets, parent=item) + + parent_id = parent["_id"] if parent else None + current_assets = assets.get(parent_id, list()) + + for asset in current_assets: + # get label from data, otherwise use name + data = asset.get("data", {}) + label = data.get("label", asset["name"]) + tags = data.get("tags", []) + + # store for the asset for optimization + deprecated = "deprecated" in tags + + item = Item({ + "_id": asset["_id"], + "name": asset["name"], + "label": label, + "type": asset["type"], + "tags": ", ".join(tags), + "deprecated": deprecated, + "_document": asset + }) + self.add_child(item, parent=parent) + + # Add asset's children recursively if it has children + if asset["_id"] in assets: + self._add_hierarchy(assets, parent=item) + + self.asset_colors[asset["_id"]] = [] + + def on_doc_fetched(self, was_stopped): + if was_stopped: + self.stop_fetch_thread() + return + + self.beginResetModel() + + assets_by_parent = self._doc_payload.get("assets_by_parent") + silos = self._doc_payload.get("silos") + if assets_by_parent is not None: + # Build the hierarchical tree items recursively + self._add_hierarchy( + assets_by_parent, + parent=None, + silos=silos + ) + + self.endResetModel() + + has_content = bool(assets_by_parent) or bool(silos) + self.refreshed.emit(has_content) + + self.stop_fetch_thread() + + def fetch(self): + self._doc_payload = self._fetch() or {} + # Emit doc fetched only if was not stopped + self.doc_fetched.emit(self._doc_fetching_stop) + + def _fetch(self): + if not self.dbcon.Session.get("AVALON_PROJECT"): + return + + project_doc = self.dbcon.find_one( + {"type": "project"}, + {"_id": True} + ) + if not project_doc: + return + + # Get all assets sorted by name + db_assets = self.dbcon.find( + {"type": "asset"}, + self.asset_projection + ).sort("name", 1) + + # Group the assets by their visual parent's id + assets_by_parent = collections.defaultdict(list) + for asset in db_assets: + if self._doc_fetching_stop: + return + parent_id = asset.get("data", {}).get("visualParent") + assets_by_parent[parent_id].append(asset) + + return { + "assets_by_parent": assets_by_parent, + "silos": None + } + + def stop_fetch_thread(self): + if self._doc_fetching_thread is not None: + self._doc_fetching_stop = True + while self._doc_fetching_thread.isRunning(): + time.sleep(0.001) + self._doc_fetching_thread = None + + def refresh(self, force=False): + """Refresh the data for the model.""" + # Skip fetch if there is already other thread fetching documents + if self._doc_fetching_thread is not None: + if not force: + return + self.stop_fetch_thread() + + # Clear model items + self.clear() + + # Fetch documents from mongo + # Restart payload + self._doc_payload = {} + self._doc_fetching_stop = False + self._doc_fetching_thread = lib.create_qthread(self.fetch) + self._doc_fetching_thread.start() + + def flags(self, index): + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + def setData(self, index, value, role=QtCore.Qt.EditRole): + if not index.isValid(): + return False + + if role == self.subsetColorsRole: + asset_id = index.data(self.ObjectIdRole) + self.asset_colors[asset_id] = value + + if Qt.__binding__ in ("PyQt4", "PySide"): + self.dataChanged.emit(index, index) + else: + self.dataChanged.emit(index, index, [role]) + + return True + + return super(AssetModel, self).setData(index, value, role) + + def data(self, index, role): + if not index.isValid(): + return + + item = index.internalPointer() + if role == QtCore.Qt.DecorationRole: + column = index.column() + if column == self.Name: + # Allow a custom icon and custom icon color to be defined + data = item.get("_document", {}).get("data", {}) + icon = data.get("icon", None) + if icon is None and item.get("type") == "silo": + icon = "database" + color = data.get("color", style.colors.default) + + if icon is None: + # Use default icons if no custom one is specified. + # If it has children show a full folder, otherwise + # show an open folder + has_children = self.rowCount(index) > 0 + icon = "folder" if has_children else "folder-o" + + # Make the color darker when the asset is deprecated + if item.get("deprecated", False): + color = QtGui.QColor(color).darker(250) + + try: + key = "fa.{0}".format(icon) # font-awesome key + icon = qtawesome.icon(key, color=color) + return icon + except Exception as exception: + # Log an error message instead of erroring out completely + # when the icon couldn't be created (e.g. invalid name) + log.error(exception) + + return + + if role == QtCore.Qt.ForegroundRole: # font color + if "deprecated" in item.get("tags", []): + return QtGui.QColor(style.colors.light).darker(250) + + if role == self.ObjectIdRole: + return item.get("_id", None) + + if role == self.DocumentRole: + return item.get("_document", None) + + if role == self.subsetColorsRole: + asset_id = item.get("_id", None) + return self.asset_colors.get(asset_id) or [] + + return super(AssetModel, self).data(index, role) + + +class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): + """Filters to the regex if any of the children matches allow parent""" + def filterAcceptsRow(self, row, parent): + regex = self.filterRegExp() + if not regex.isEmpty(): + pattern = regex.pattern() + model = self.sourceModel() + source_index = model.index(row, self.filterKeyColumn(), parent) + if source_index.isValid(): + # Check current index itself + key = model.data(source_index, self.filterRole()) + if re.search(pattern, key, re.IGNORECASE): + return True + + # Check children + rows = model.rowCount(source_index) + for i in range(rows): + if self.filterAcceptsRow(i, source_index): + return True + + # Otherwise filter it + return False + + return super( + RecursiveSortFilterProxyModel, self + ).filterAcceptsRow(row, parent) diff --git a/openpype/tools/utils/views.py b/openpype/tools/utils/views.py new file mode 100644 index 0000000000..bed5655647 --- /dev/null +++ b/openpype/tools/utils/views.py @@ -0,0 +1,86 @@ +import os +from avalon import style +from Qt import QtWidgets, QtCore, QtGui, QtSvg + + +class DeselectableTreeView(QtWidgets.QTreeView): + """A tree view that deselects on clicking on an empty area in the view""" + + def mousePressEvent(self, event): + + index = self.indexAt(event.pos()) + if not index.isValid(): + # clear the selection + self.clearSelection() + # clear the current index + self.setCurrentIndex(QtCore.QModelIndex()) + + QtWidgets.QTreeView.mousePressEvent(self, event) + + +class TreeViewSpinner(QtWidgets.QTreeView): + size = 160 + + def __init__(self, parent=None): + super(TreeViewSpinner, self).__init__(parent=parent) + + loading_image_path = os.path.join( + os.path.dirname(os.path.abspath(style.__file__)), + "svg", + "spinner-200.svg" + ) + self.spinner = QtSvg.QSvgRenderer(loading_image_path) + + self.is_loading = False + self.is_empty = True + + def paint_loading(self, event): + rect = event.rect() + rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight()) + rect.moveTo( + rect.x() + rect.width() / 2 - self.size / 2, + rect.y() + rect.height() / 2 - self.size / 2 + ) + rect.setSize(QtCore.QSizeF(self.size, self.size)) + painter = QtGui.QPainter(self.viewport()) + self.spinner.render(painter, rect) + + def paint_empty(self, event): + painter = QtGui.QPainter(self.viewport()) + rect = event.rect() + rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight()) + qtext_opt = QtGui.QTextOption( + QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter + ) + painter.drawText(rect, "No Data", qtext_opt) + + def paintEvent(self, event): + if self.is_loading: + self.paint_loading(event) + elif self.is_empty: + self.paint_empty(event) + else: + super(TreeViewSpinner, self).paintEvent(event) + + +class AssetsView(TreeViewSpinner, DeselectableTreeView): + """Item view. + This implements a context menu. + """ + + def __init__(self): + super(AssetsView, self).__init__() + self.setIndentation(15) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setHeaderHidden(True) + + def mousePressEvent(self, event): + index = self.indexAt(event.pos()) + if not index.isValid(): + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers == QtCore.Qt.ShiftModifier: + return + elif modifiers == QtCore.Qt.ControlModifier: + return + + super(AssetsView, self).mousePressEvent(event) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py new file mode 100644 index 0000000000..153f012ff6 --- /dev/null +++ b/openpype/tools/utils/widgets.py @@ -0,0 +1,499 @@ +import logging +import time + +from . import lib + +from Qt import QtWidgets, QtCore, QtGui +from avalon.vendor import qtawesome, qargparse + +from avalon import style, io + +from .models import AssetModel, RecursiveSortFilterProxyModel +from .views import AssetsView +from .delegates import AssetDelegate + +log = logging.getLogger(__name__) + + +class AssetWidget(QtWidgets.QWidget): + """A Widget to display a tree of assets with filter + + To list the assets of the active project: + >>> # widget = AssetWidget() + >>> # widget.refresh() + >>> # widget.show() + + """ + + refresh_triggered = QtCore.Signal() # on model refresh + refreshed = QtCore.Signal() + selection_changed = QtCore.Signal() # on view selection change + current_changed = QtCore.Signal() # on view current index change + + def __init__(self, dbcon, multiselection=False, parent=None): + super(AssetWidget, self).__init__(parent=parent) + + self.dbcon = dbcon + + self.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + + # Tree View + model = AssetModel(dbcon=self.dbcon, parent=self) + proxy = RecursiveSortFilterProxyModel() + proxy.setSourceModel(model) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + + view = AssetsView() + view.setModel(proxy) + if multiselection: + asset_delegate = AssetDelegate() + view.setSelectionMode(view.ExtendedSelection) + view.setItemDelegate(asset_delegate) + + # Header + header = QtWidgets.QHBoxLayout() + + icon = qtawesome.icon("fa.arrow-down", color=style.colors.light) + set_current_asset_btn = QtWidgets.QPushButton(icon, "") + set_current_asset_btn.setToolTip("Go to Asset from current Session") + # Hide by default + set_current_asset_btn.setVisible(False) + + icon = qtawesome.icon("fa.refresh", color=style.colors.light) + refresh = QtWidgets.QPushButton(icon, "") + refresh.setToolTip("Refresh items") + + filter = QtWidgets.QLineEdit() + filter.textChanged.connect(proxy.setFilterFixedString) + filter.setPlaceholderText("Filter assets..") + + header.addWidget(filter) + header.addWidget(set_current_asset_btn) + header.addWidget(refresh) + + # Layout + layout.addLayout(header) + layout.addWidget(view) + + # Signals/Slots + selection = view.selectionModel() + selection.selectionChanged.connect(self.selection_changed) + selection.currentChanged.connect(self.current_changed) + refresh.clicked.connect(self.refresh) + set_current_asset_btn.clicked.connect(self.set_current_session_asset) + + self.set_current_asset_btn = set_current_asset_btn + self.model = model + self.proxy = proxy + self.view = view + + self.model_selection = {} + + def set_current_asset_btn_visibility(self, visible=None): + """Hide set current asset button. + + Not all tools support using of current context asset. + """ + if visible is None: + visible = not self.set_current_asset_btn.isVisible() + self.set_current_asset_btn.setVisible(visible) + + def _refresh_model(self): + # Store selection + self._store_model_selection() + time_start = time.time() + + self.set_loading_state( + loading=True, + empty=True + ) + + def on_refreshed(has_item): + self.set_loading_state(loading=False, empty=not has_item) + self._restore_model_selection() + self.model.refreshed.disconnect() + self.refreshed.emit() + print("Duration: %.3fs" % (time.time() - time_start)) + + # Connect to signal + self.model.refreshed.connect(on_refreshed) + # Trigger signal before refresh is called + self.refresh_triggered.emit() + # Refresh model + self.model.refresh() + + def refresh(self): + self._refresh_model() + + def get_active_asset(self): + """Return the asset item of the current selection.""" + current = self.view.currentIndex() + return current.data(self.model.ItemRole) + + def get_active_asset_document(self): + """Return the asset document of the current selection.""" + current = self.view.currentIndex() + return current.data(self.model.DocumentRole) + + def get_active_index(self): + return self.view.currentIndex() + + def get_selected_assets(self): + """Return the documents of selected assets.""" + selection = self.view.selectionModel() + rows = selection.selectedRows() + assets = [row.data(self.model.DocumentRole) for row in rows] + + # NOTE: skip None object assumed they are silo (backwards comp.) + return [asset for asset in assets if asset] + + def select_assets(self, assets, expand=True, key="name"): + """Select assets by item key. + + Args: + assets (list): List of asset values that can be found under + specified `key` + expand (bool): Whether to also expand to the asset in the view + key (string): Key that specifies where to look for `assets` values + + Returns: + None + + Default `key` is "name" in that case `assets` should contain single + asset name or list of asset names. (It is good idea to use "_id" key + instead of name in that case `assets` must contain `ObjectId` object/s) + It is expected that each value in `assets` will be found only once. + If the filters according to the `key` and `assets` correspond to + the more asset, only the first found will be selected. + + """ + + if not isinstance(assets, (tuple, list)): + assets = [assets] + + # convert to list - tuple cant be modified + assets = set(assets) + + # Clear selection + selection_model = self.view.selectionModel() + selection_model.clearSelection() + + # Select + mode = selection_model.Select | selection_model.Rows + for index in lib.iter_model_rows( + self.proxy, column=0, include_root=False + ): + # stop iteration if there are no assets to process + if not assets: + break + + value = index.data(self.model.ItemRole).get(key) + if value not in assets: + continue + + # Remove processed asset + assets.discard(value) + + selection_model.select(index, mode) + if expand: + # Expand parent index + self.view.expand(self.proxy.parent(index)) + + # Set the currently active index + self.view.setCurrentIndex(index) + + def set_loading_state(self, loading, empty): + if self.view.is_loading != loading: + if loading: + self.view.spinner.repaintNeeded.connect( + self.view.viewport().update + ) + else: + self.view.spinner.repaintNeeded.disconnect() + + self.view.is_loading = loading + self.view.is_empty = empty + + def _store_model_selection(self): + index = self.view.currentIndex() + current = None + if index and index.isValid(): + current = index.data(self.model.ObjectIdRole) + + expanded = set() + model = self.view.model() + for index in lib.iter_model_rows( + model, column=0, include_root=False + ): + if self.view.isExpanded(index): + value = index.data(self.model.ObjectIdRole) + expanded.add(value) + + selection_model = self.view.selectionModel() + + selected = None + selected_rows = selection_model.selectedRows() + if selected_rows: + selected = set( + row.data(self.model.ObjectIdRole) + for row in selected_rows + ) + + self.model_selection = { + "expanded": expanded, + "selected": selected, + "current": current + } + + def _restore_model_selection(self): + model = self.view.model() + not_set = object() + expanded = self.model_selection.pop("expanded", not_set) + selected = self.model_selection.pop("selected", not_set) + current = self.model_selection.pop("current", not_set) + + if ( + expanded is not_set + or selected is not_set + or current is not_set + ): + return + + if expanded: + for index in lib.iter_model_rows( + model, column=0, include_root=False + ): + is_expanded = index.data(self.model.ObjectIdRole) in expanded + self.view.setExpanded(index, is_expanded) + + if not selected and not current: + self.set_current_session_asset() + return + + current_index = None + selected_indexes = [] + # Go through all indices, select the ones with similar data + for index in lib.iter_model_rows( + model, column=0, include_root=False + ): + object_id = index.data(self.model.ObjectIdRole) + if object_id in selected: + selected_indexes.append(index) + + if not current_index and object_id == current: + current_index = index + + if current_index: + self.view.setCurrentIndex(current_index) + + if not selected_indexes: + return + selection_model = self.view.selectionModel() + flags = selection_model.Select | selection_model.Rows + for index in selected_indexes: + # Ensure item is visible + self.view.scrollTo(index) + selection_model.select(index, flags) + + def set_current_session_asset(self): + asset_name = self.dbcon.Session.get("AVALON_ASSET") + if asset_name: + self.select_assets([asset_name]) + + +class OptionalMenu(QtWidgets.QMenu): + """A subclass of `QtWidgets.QMenu` to work with `OptionalAction` + + This menu has reimplemented `mouseReleaseEvent`, `mouseMoveEvent` and + `leaveEvent` to provide better action hightlighting and triggering for + actions that were instances of `QtWidgets.QWidgetAction`. + + """ + + def mouseReleaseEvent(self, event): + """Emit option clicked signal if mouse released on it""" + active = self.actionAt(event.pos()) + if active and active.use_option: + option = active.widget.option + if option.is_hovered(event.globalPos()): + option.clicked.emit() + super(OptionalMenu, self).mouseReleaseEvent(event) + + def mouseMoveEvent(self, event): + """Add highlight to active action""" + active = self.actionAt(event.pos()) + for action in self.actions(): + action.set_highlight(action is active, event.globalPos()) + super(OptionalMenu, self).mouseMoveEvent(event) + + def leaveEvent(self, event): + """Remove highlight from all actions""" + for action in self.actions(): + action.set_highlight(False) + super(OptionalMenu, self).leaveEvent(event) + + +class OptionalAction(QtWidgets.QWidgetAction): + """Menu action with option box + + A menu action like Maya's menu item with option box, implemented by + subclassing `QtWidgets.QWidgetAction`. + + """ + + def __init__(self, label, icon, use_option, parent): + super(OptionalAction, self).__init__(parent) + self.label = label + self.icon = icon + self.use_option = use_option + self.option_tip = "" + self.optioned = False + + def createWidget(self, parent): + widget = OptionalActionWidget(self.label, parent) + self.widget = widget + + if self.icon: + widget.setIcon(self.icon) + + if self.use_option: + widget.option.clicked.connect(self.on_option) + widget.option.setToolTip(self.option_tip) + else: + widget.option.setVisible(False) + + return widget + + def set_option_tip(self, options): + sep = "\n\n" + mak = (lambda opt: opt["name"] + " :\n " + opt["help"]) + self.option_tip = sep.join(mak(opt) for opt in options) + + def on_option(self): + self.optioned = True + + def set_highlight(self, state, global_pos=None): + body = self.widget.body + option = self.widget.option + + role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window + body.setBackgroundRole(role) + body.setAutoFillBackground(state) + + if not self.use_option: + return + + state = option.is_hovered(global_pos) + role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window + option.setBackgroundRole(role) + option.setAutoFillBackground(state) + + +class OptionalActionWidget(QtWidgets.QWidget): + """Main widget class for `OptionalAction`""" + + def __init__(self, label, parent=None): + super(OptionalActionWidget, self).__init__(parent) + + body = QtWidgets.QWidget() + body.setStyleSheet("background: transparent;") + + icon = QtWidgets.QLabel() + label = QtWidgets.QLabel(label) + option = OptionBox(body) + + icon.setFixedSize(24, 16) + option.setFixedSize(30, 30) + + layout = QtWidgets.QHBoxLayout(body) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(2) + layout.addWidget(icon) + layout.addWidget(label) + layout.addSpacing(6) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(6, 1, 2, 1) + layout.setSpacing(0) + layout.addWidget(body) + layout.addWidget(option) + + body.setMouseTracking(True) + label.setMouseTracking(True) + option.setMouseTracking(True) + self.setMouseTracking(True) + self.setFixedHeight(32) + + self.icon = icon + self.label = label + self.option = option + self.body = body + + # (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke. + # See https://stackoverflow.com/q/52838690/4145300 + label.setStyle(QtWidgets.QStyleFactory.create("Plastique")) + + def setIcon(self, icon): + pixmap = icon.pixmap(16, 16) + self.icon.setPixmap(pixmap) + + +class OptionBox(QtWidgets.QLabel): + """Option box widget class for `OptionalActionWidget`""" + + clicked = QtCore.Signal() + + def __init__(self, parent): + super(OptionBox, self).__init__(parent) + + self.setAlignment(QtCore.Qt.AlignCenter) + + icon = qtawesome.icon("fa.sticky-note-o", color="#c6c6c6") + pixmap = icon.pixmap(18, 18) + self.setPixmap(pixmap) + + self.setStyleSheet("background: transparent;") + + def is_hovered(self, global_pos): + if global_pos is None: + return False + pos = self.mapFromGlobal(global_pos) + return self.rect().contains(pos) + + +class OptionDialog(QtWidgets.QDialog): + """Option dialog shown by option box""" + + def __init__(self, parent=None): + super(OptionDialog, self).__init__(parent) + self.setModal(True) + self._options = dict() + + def create(self, options): + parser = qargparse.QArgumentParser(arguments=options) + + decision = QtWidgets.QWidget() + accept = QtWidgets.QPushButton("Accept") + cancel = QtWidgets.QPushButton("Cancel") + + layout = QtWidgets.QHBoxLayout(decision) + layout.addWidget(accept) + layout.addWidget(cancel) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(parser) + layout.addWidget(decision) + + accept.clicked.connect(self.accept) + cancel.clicked.connect(self.reject) + parser.changed.connect(self.on_changed) + + def on_changed(self, argument): + self._options[argument["name"]] = argument.read() + + def parse(self): + return self._options.copy() From 658583fb19568bd683cd54f51329f395c4124d90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:12:16 +0200 Subject: [PATCH 291/716] added missing settings key for delete old versions action --- .../ftrack/event_handlers_user/action_delete_old_versions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_old_versions.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_old_versions.py index 063f086e9c..c66d1819ac 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_old_versions.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_old_versions.py @@ -23,6 +23,8 @@ class DeleteOldVersions(BaseAction): ) icon = statics_icon("ftrack", "action_icons", "OpenPypeAdmin.svg") + settings_key = "delete_old_versions" + dbcon = AvalonMongoDB() inteface_title = "Choose your preferences" From 07bc5c5c32eed74f1f605838d5bacc114f1db766 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:18:53 +0200 Subject: [PATCH 292/716] use loader from openpype in houdini --- openpype/hosts/houdini/startup/MainMenuCommon.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index 77ee182e7c..76585085e2 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -15,8 +15,8 @@ creator.show() From b66eadef3b9424b528a9b1d43c93fd522d1a8258 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:28:51 +0200 Subject: [PATCH 293/716] use openpype loader and library loader in fusion --- openpype/hosts/fusion/api/menu.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 3f04bf839b..9093aa9e5e 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -10,8 +10,10 @@ from .pipeline import ( from avalon.tools import ( creator, - loader, sceneinventory, +) +from openpype.tools import ( + loader, libraryloader ) From 2e33f024071c18aa0ee0eecc72d3eaa359346802 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:57:33 +0200 Subject: [PATCH 294/716] use openpype workfiles tools in hosts --- openpype/hosts/fusion/api/pipeline.py | 2 +- openpype/hosts/hiero/api/pipeline.py | 6 ++---- openpype/hosts/maya/api/__init__.py | 2 +- openpype/hosts/nuke/api/lib.py | 2 +- openpype/hosts/resolve/api/pipeline.py | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 0095530087..688e75f6fe 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -3,7 +3,7 @@ Basic avalon integration """ import os -from avalon.tools import workfiles +from openpype.tools import workfiles from avalon import api as avalon from pyblish import api as pyblish from openpype.api import Logger diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index ab7e2bdabf..12f6923de7 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -4,10 +4,8 @@ Basic avalon integration import os import contextlib from collections import OrderedDict -from avalon.tools import ( - workfiles, - publish as _publish -) +from avalon.tools import publish as _publish +from openpype.tools import workfiles from avalon.pipeline import AVALON_CONTAINER_ID from avalon import api as avalon from avalon import schema diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 9219da407f..db4dbf29c5 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -8,7 +8,7 @@ from avalon import api as avalon from avalon import pipeline from avalon.maya import suspended_refresh from avalon.maya.pipeline import IS_HEADLESS -from avalon.tools import workfiles +from openpype.tools import workfiles from pyblish import api as pyblish from openpype.lib import any_outdated import openpype.hosts.maya diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 257bf8d64e..34cf34392e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -7,7 +7,7 @@ from collections import OrderedDict from avalon import api, io, lib -from avalon.tools import workfiles +from openpype.tools import workfiles import avalon.nuke from avalon.nuke import lib as anlib from avalon.nuke import ( diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index a659ac7e51..80249310e8 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -4,7 +4,7 @@ Basic avalon integration import os import contextlib from collections import OrderedDict -from avalon.tools import workfiles +from openpype.tools import workfiles from avalon import api as avalon from avalon import schema from avalon.pipeline import AVALON_CONTAINER_ID From 9aae8f9f489540aa74d4bbc1a9f195ad47139081 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 11:57:46 +0200 Subject: [PATCH 295/716] use openpype loader --- openpype/hosts/hiero/api/menu.py | 5 +++-- openpype/hosts/maya/api/customize.py | 2 +- openpype/hosts/resolve/api/menu.py | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/hiero/api/menu.py b/openpype/hosts/hiero/api/menu.py index ab49251093..bcd78aa5bb 100644 --- a/openpype/hosts/hiero/api/menu.py +++ b/openpype/hosts/hiero/api/menu.py @@ -41,7 +41,8 @@ def menu_install(): apply_colorspace_project, apply_colorspace_clips ) # here is the best place to add menu - from avalon.tools import cbloader, creator, sceneinventory + from avalon.tools import creator, sceneinventory + from openpype.tools import loader from avalon.vendor.Qt import QtGui menu_name = os.environ['AVALON_LABEL'] @@ -90,7 +91,7 @@ def menu_install(): loader_action = menu.addAction("Load ...") loader_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png")) - loader_action.triggered.connect(cbloader.show) + loader_action.triggered.connect(loader.show) sceneinventory_action = menu.addAction("Manage ...") sceneinventory_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png")) diff --git a/openpype/hosts/maya/api/customize.py b/openpype/hosts/maya/api/customize.py index 22945471b7..a84412963b 100644 --- a/openpype/hosts/maya/api/customize.py +++ b/openpype/hosts/maya/api/customize.py @@ -79,7 +79,7 @@ def override_toolbox_ui(): log.warning("Could not import SceneInventory tool") try: - import avalon.tools.loader as loader + import openpype.tools.loader as loader except Exception: log.warning("Could not import Loader tool") diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index e7be3fc963..c639fd2db8 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -10,11 +10,13 @@ from .pipeline import ( from avalon.tools import ( creator, - loader, sceneinventory, - libraryloader, subsetmanager ) +from openpype.tools import ( + loader, + libraryloader, +) def load_stylesheet(): From 54e11a52ea76f28558190ab18e84e68a60de094a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 12:01:38 +0200 Subject: [PATCH 296/716] use pretty_timestamp from openpype tools utils --- openpype/modules/default_modules/sync_server/tray/models.py | 2 +- openpype/modules/default_modules/sync_server/tray/widgets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/models.py b/openpype/modules/default_modules/sync_server/tray/models.py index 8c86d3b98f..c2c63c68ea 100644 --- a/openpype/modules/default_modules/sync_server/tray/models.py +++ b/openpype/modules/default_modules/sync_server/tray/models.py @@ -5,7 +5,7 @@ from bson.objectid import ObjectId from Qt import QtCore from Qt.QtCore import Qt -from avalon.tools.delegates import pretty_timestamp +from openpype.tools.utils.delegates import pretty_timestamp from avalon.vendor import qtawesome from openpype.lib import PypeLogger diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index c9160733a0..c9b58ebe7c 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -14,7 +14,7 @@ from openpype.tools.settings import ( from openpype.api import get_local_site_id from openpype.lib import PypeLogger -from avalon.tools.delegates import pretty_timestamp +from openpype.tools.utils.delegates import pretty_timestamp from avalon.vendor import qtawesome from .models import ( From 548100e04fa901bd8b7f2f58e9ecbaced2a7c6f0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 12:01:51 +0200 Subject: [PATCH 297/716] library loader in tray is using openpype library loader --- openpype/modules/default_modules/avalon_apps/avalon_app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index b3b7dd8484..4459fa2cac 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -55,11 +55,11 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes): def tray_init(self): # Add library tool try: - from avalon.tools.libraryloader import app - from avalon import style from Qt import QtGui + from avalon import style + from openpype.tools.libraryloader import LibraryLoaderWindow - self.libraryloader = app.Window( + self.libraryloader = LibraryLoaderWindow( icon=QtGui.QIcon(resources.get_openpype_icon_filepath()), show_projects=True, show_libraries=True From cf5bf6f49c262452b4bd86b719eb884f740c246f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 12:14:49 +0200 Subject: [PATCH 298/716] fixed few hounds --- openpype/tools/loader/__main__.py | 10 ++++------ openpype/tools/loader/widgets.py | 8 +++++--- openpype/tools/utils/delegates.py | 3 ++- openpype/tools/utils/widgets.py | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/openpype/tools/loader/__main__.py b/openpype/tools/loader/__main__.py index 27794b9bc5..146ba7fd10 100644 --- a/openpype/tools/loader/__main__.py +++ b/openpype/tools/loader/__main__.py @@ -1,10 +1,12 @@ -"""Main entrypoint for standalone debugging""" -""" +"""Main entrypoint for standalone debugging + Used for running 'avalon.tool.loader.__main__' as a module (-m), useful for debugging without need to start host. Modify AVALON_MONGO accordingly """ +import os +import sys from . import cli @@ -17,7 +19,6 @@ def my_exception_hook(exctype, value, traceback): if __name__ == '__main__': - import os os.environ["AVALON_MONGO"] = "mongodb://localhost:27017" os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017" os.environ["AVALON_DB"] = "avalon" @@ -26,9 +27,6 @@ if __name__ == '__main__': os.environ["AVALON_CONFIG"] = "pype" os.environ["AVALON_ASSET"] = "Jungle" - - import sys - # Set the exception hook to our wrapping function sys.excepthook = my_exception_hook diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 9479558026..91e6f20518 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -8,7 +8,7 @@ import collections from Qt import QtWidgets, QtCore, QtGui -from avalon import api, io, pipeline +from avalon import api, pipeline from avalon.lib import HeroVersionType from openpype.tools.utils import lib as tools_lib @@ -1173,8 +1173,10 @@ class RepresentationWidget(QtWidgets.QWidget): repre_context ): if tools_lib.is_sync_loader(loader): - both_unavailable = item["active_site_progress"] <= 0 and \ - item["remote_site_progress"] <= 0 + both_unavailable = ( + item["active_site_progress"] <= 0 + and item["remote_site_progress"] <= 0 + ) if both_unavailable: continue diff --git a/openpype/tools/utils/delegates.py b/openpype/tools/utils/delegates.py index 608762b538..1827bc7e9b 100644 --- a/openpype/tools/utils/delegates.py +++ b/openpype/tools/utils/delegates.py @@ -79,7 +79,8 @@ class AssetDelegate(QtWidgets.QItemDelegate): else: bg_color.setAlpha(0) - # # -- When not needed to do a rounded corners (easier and without painter restore): + # When not needed to do a rounded corners (easier and without + # painter restore): # painter.fillRect( # item_rect, # QtGui.QBrush(bg_color) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index 153f012ff6..b9b542c123 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -6,7 +6,7 @@ from . import lib from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome, qargparse -from avalon import style, io +from avalon import style from .models import AssetModel, RecursiveSortFilterProxyModel from .views import AssetsView From 354fdd1cdc36ee6236527596f2c12b422bc4f39e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 12:18:54 +0200 Subject: [PATCH 299/716] removed unused TaskModel --- openpype/tools/utils/models.py | 126 --------------------------------- 1 file changed, 126 deletions(-) diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index d09d16d898..c5e1ce1b12 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -195,132 +195,6 @@ class Item(dict): self._children.append(child) -class TasksModel(TreeModel): - """A model listing the tasks combined for a list of assets""" - - Columns = ["name", "count"] - - def __init__(self, dbcon, parent=None): - super(TasksModel, self).__init__(parent=parent) - self.dbcon = dbcon - self._num_assets = 0 - self._icons = { - "__default__": qtawesome.icon( - "fa.male", - color=style.colors.default - ), - "__no_task__": qtawesome.icon( - "fa.exclamation-circle", - color=style.colors.mid - ) - } - - self._refresh_task_icons() - - def _refresh_task_icons(self): - # Get the project configured icons from database - project = self.dbcon.find_one({"type": "project"}, {"config.tasks"}) - tasks = project["config"].get("tasks", {}) - for task_name, task in tasks.items(): - icon_name = task.get("icon", None) - if icon_name: - icon = qtawesome.icon( - "fa.{}".format(icon_name), - color=style.colors.default - ) - self._icons[task_name] = icon - - def set_assets(self, asset_ids=None, asset_docs=None): - """Set assets to track by their database id - - Arguments: - asset_ids (list): List of asset ids. - asset_docs (list): List of asset entities from MongoDB. - - """ - - if asset_docs is None and asset_ids is not None: - # prepare filter query - _filter = {"type": "asset", "_id": {"$in": asset_ids}} - _projection = {"data.tasks"} - - # find assets in db by query - asset_docs = list(self.dbcon.find(_filter, _projection)) - db_assets_ids = [asset["_id"] for asset in asset_docs] - - # check if all assets were found - not_found = [ - str(a_id) for a_id in asset_ids if a_id not in db_assets_ids - ] - - assert not not_found, "Assets not found by id: {0}".format( - ", ".join(not_found) - ) - - if asset_docs is None: - asset_docs = list() - - self._num_assets = len(asset_docs) - - tasks = collections.Counter() - for asset_doc in asset_docs: - asset_tasks = asset_doc.get("data", {}).get("tasks", {}) - tasks.update(asset_tasks.keys()) - - self.clear() - self.beginResetModel() - - default_icon = self._icons["__default__"] - - if not tasks: - no_task_icon = self._icons["__no_task__"] - item = Item({ - "name": "No task", - "count": 0, - "icon": no_task_icon, - "enabled": False, - }) - - self.add_child(item) - - else: - for task, count in sorted(tasks.items()): - icon = self._icons.get(task, default_icon) - - item = Item({ - "name": task, - "count": count, - "icon": icon - }) - - self.add_child(item) - - self.endResetModel() - - def headerData(self, section, orientation, role): - # Override header for count column to show amount of assets - # it is listing the tasks for - if role == QtCore.Qt.DisplayRole: - if orientation == QtCore.Qt.Horizontal: - if section == 0: - return "Tasks" - elif section == 1: # count column - return "count ({0})".format(self._num_assets) - - return super(TasksModel, self).headerData(section, orientation, role) - - def data(self, index, role): - if not index.isValid(): - return - - # Add icon to the first column - if role == QtCore.Qt.DecorationRole: - if index.column() == 0: - return index.internalPointer()["icon"] - - return super(TasksModel, self).data(index, role) - - class AssetModel(TreeModel): """A model listing assets in the silo in the active project. From e3ba7e9c15ee9f1e361390683db2bc877178b7c4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 17 Sep 2021 12:53:08 +0200 Subject: [PATCH 300/716] Removal of unwanted change --- .../defaults/project_settings/nuke.json | 10 ++++++ .../schemas/schema_nuke_publish.json | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 136f1d6b42..c1c3e77684 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -96,6 +96,16 @@ }, "ExtractSlateFrame": { "viewer_lut_raw": false + }, + "IncrementScriptVersion": { + "optional": true, + "active": true, + "families": [ + "workfile", + "render", + "render.local", + "render.farm" + ] } }, "load": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 782179cfd1..d354ff15f8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -152,6 +152,38 @@ "label": "Viewer LUT raw" } ] + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Integrators" + }, + { + "type": "dict", + "collapsible": false, + "key": "IncrementScriptVersion", + "label": "IncrementScriptVersion", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "list", + "key": "families", + "object_type": "text", + "label": "Trigger on families" + } + ] } ] } From 0a033f4dbe2f3981097d3ca7623e655d939ff245 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 17 Sep 2021 13:28:49 +0200 Subject: [PATCH 301/716] Lowercased task type --- .../plugins/publish/collect_published_files.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 434f82d3ea..7e9b98956a 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -177,9 +177,11 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): (family, [families], subset_template_name, tags) tuple AssertionError if not matching family found """ - if task_type: - task_type = task_type.capitalize() - task_obj = settings.get(task_type) + task_type = task_type.lower() + lower_cased_task_types = {} + for t_type, task in settings.items(): + lower_cased_task_types[t_type.lower()] = task + task_obj = lower_cased_task_types.get(task_type) assert task_obj, "No family configuration for '{}'".format(task_type) found_family = None From 61cf8c8ecb3fcf01e32f06d14c32e770b36ab904 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 13:59:44 +0200 Subject: [PATCH 302/716] use right model for getting selection index --- openpype/tools/workfiles/app.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 3d2633f8dc..6fff0d0278 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -376,6 +376,9 @@ class TasksWidget(QtWidgets.QWidget): task (str): Name of the task to select. """ + task_view_model = self._tasks_view.model() + if not task_view_model: + return # Clear selection selection_model = self._tasks_view.selectionModel() @@ -383,8 +386,8 @@ class TasksWidget(QtWidgets.QWidget): # Select the task mode = selection_model.Select | selection_model.Rows - for row in range(self._tasks_model.rowCount()): - index = self._tasks_model.index(row, 0) + for row in range(task_view_model.rowCount()): + index = task_view_model.index(row, 0) name = index.data(TASK_NAME_ROLE) if name == task_name: selection_model.select(index, mode) From 9e72c62b201f2b95ec32b57ad0a366789bc2f9c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 13:59:56 +0200 Subject: [PATCH 303/716] fix usage of get_openpype_version function --- openpype/lib/pype_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index 8fe8701908..33715e369d 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -64,7 +64,7 @@ def is_running_staging(): Returns: bool: True if openpype version containt 'staging'. """ - if "staging" in get_pype_version(): + if "staging" in get_openpype_version(): return True return False From 83b6f112c6f4c8d2cf68fae854dc658513256f79 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 14:33:28 +0200 Subject: [PATCH 304/716] added default thumbnail image to loader --- .../tools/loader/images/default_thumbnail.png | Bin 0 -> 4018 bytes openpype/tools/loader/widgets.py | 7 +++---- 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 openpype/tools/loader/images/default_thumbnail.png diff --git a/openpype/tools/loader/images/default_thumbnail.png b/openpype/tools/loader/images/default_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..97bd958e0daf01e740db9f426e1f9b3e22484c87 GIT binary patch literal 4018 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 aQ8TBK`_#VgnlokXr%hW!U;VAB9rQ1Cc_|D4 literal 0 HcmV?d00001 diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 91e6f20518..39d162613a 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -744,11 +744,10 @@ class ThumbnailWidget(QtWidgets.QLabel): self.setAlignment(QtCore.Qt.AlignCenter) # TODO get res path much better way - loader_path = os.path.dirname(os.path.abspath(__file__)) - avalon_path = os.path.dirname(os.path.dirname(loader_path)) default_pix_path = os.path.join( - os.path.dirname(avalon_path), - "res", "tools", "images", "default_thumbnail.png" + os.path.dirname(os.path.abspath(__file__)), + "images", + "default_thumbnail.png" ) self.default_pix = QtGui.QPixmap(default_pix_path) From d960ec7e2344ec401af2a17b451f01ede624625b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 14:52:30 +0200 Subject: [PATCH 305/716] defined family view, model and proxy model --- openpype/tools/libraryloader/app.py | 4 +- openpype/tools/loader/app.py | 4 +- openpype/tools/loader/widgets.py | 180 +++++++++++++++++----------- 3 files changed, 112 insertions(+), 76 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 362d05cce6..3f7979ff1c 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -9,7 +9,7 @@ from openpype.tools.utils import lib as tools_lib from openpype.tools.loader.widgets import ( ThumbnailWidget, VersionWidget, - FamilyListWidget, + FamilyListView, RepresentationWidget ) from openpype.tools.utils.widgets import AssetWidget @@ -65,7 +65,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): assets = AssetWidget( self.dbcon, multiselection=True, parent=self ) - families = FamilyListWidget( + families = FamilyListView( self.dbcon, self.family_config_cache, parent=self ) subsets = LibrarySubsetWidget( diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 381d6b25d8..4beebe43b8 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -11,7 +11,7 @@ from openpype.tools.utils import lib from .widgets import ( SubsetWidget, VersionWidget, - FamilyListWidget, + FamilyListView, ThumbnailWidget, RepresentationWidget, OverlayFrame @@ -64,7 +64,7 @@ class LoaderWidow(QtWidgets.QDialog): assets = AssetWidget(io, multiselection=True, parent=self) assets.set_current_asset_btn_visibility(True) - families = FamilyListWidget(io, self.family_config_cache, self) + families = FamilyListView(io, self.family_config_cache, self) subsets = SubsetWidget( io, self.groups_config, diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 39d162613a..2953179509 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -846,36 +846,17 @@ class VersionWidget(QtWidgets.QWidget): self.data.set_version(version_doc) -class FamilyListWidget(QtWidgets.QListWidget): - """A Widget that lists all available families""" +class FamilyModel(QtGui.QStandardItemModel): + def __init__(self, dbcon, family_config_cache): + super(FamilyModel, self).__init__() - NameRole = QtCore.Qt.UserRole + 1 - active_changed = QtCore.Signal(list) - - def __init__(self, dbcon, family_config_cache, parent=None): - super(FamilyListWidget, self).__init__(parent=parent) - - self.family_config_cache = family_config_cache self.dbcon = dbcon + self.family_config_cache = family_config_cache - multi_select = QtWidgets.QAbstractItemView.ExtendedSelection - self.setSelectionMode(multi_select) - self.setAlternatingRowColors(True) - # Enable RMB menu - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.show_right_mouse_menu) - - self.itemChanged.connect(self._on_item_changed) + self._items_by_family = {} def refresh(self): - """Refresh the listed families. - - This gets all unique families and adds them as checkable items to - the list. - - """ - - families = [] + families = set() if self.dbcon.Session.get("AVALON_PROJECT"): result = list(self.dbcon.aggregate([ {"$match": { @@ -890,81 +871,136 @@ class FamilyListWidget(QtWidgets.QListWidget): }} ])) if result: - families = result[0]["families"] + families = set(result[0]["families"]) - # Rebuild list + root_item = self.invisibleRootItem() self.blockSignals(True) - self.clear() - for name in sorted(families): - family = self.family_config_cache.family_config(name) - if family.get("hideFilter"): + for family in tuple(self._items_by_family.keys()): + if family not in families: + item = self._items_by_family.pop(family) + root_item.removeRow(item.row()) + self.blockSignals(False) + + new_items = [] + for family in families: + if family in self._items_by_family: continue - label = family.get("label", name) - icon = family.get("icon", None) + family_config = self.family_config_cache.family_config(family) + if family_config.get("hideFilter"): + continue - # TODO: This should be more managable by the artist - # Temporarily implement support for a default state in the project - # configuration - state = family.get("state", True) - state = QtCore.Qt.Checked if state else QtCore.Qt.Unchecked + label = family_config.get("label", family) + icon = family_config.get("icon", None) - item = QtWidgets.QListWidgetItem(parent=self) - item.setText(label) - item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) - item.setData(self.NameRole, name) + if family_config.get("state", True): + state = QtCore.Qt.Checked + else: + state = QtCore.Qt.Unchecked + + item = QtGui.QStandardItem(label) + item.setFlags( + QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsUserCheckable + ) item.setCheckState(state) if icon: item.setIcon(icon) - self.addItem(item) - self.blockSignals(False) + new_items.append(item) - self.active_changed.emit(self.get_filters()) + if new_items: + root_item.appendRows(new_items) - def get_filters(self): + +class FamilyListView(QtWidgets.QListView): + active_changed = QtCore.Signal(list) + + def __init__(self, dbcon, family_config_cache, parent=None): + super(FamilyListView, self).__init__(parent=parent) + + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setAlternatingRowColors(True) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + family_model = FamilyModel(dbcon, family_config_cache) + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setDynamicSortFilter(True) + proxy_model.setSourceModel(family_model) + + self.setModel(proxy_model) + + family_model.dataChanged.connect(self._on_data_change) + self.customContextMenuRequested.connect(self._on_context_menu) + + self._family_model = family_model + self._proxy_model = proxy_model + + def refresh(self): + self._family_model.refresh() + + self.active_changed.emit(self.get_enabled_families()) + + def get_enabled_families(self): """Return the checked family items""" + model = self.model() + checked_families = [] + for row in range(model.rowCount()): + index = model.index(row, 0) + if index.data(QtCore.Qt.CheckStateRole) == QtCore.Qt.Checked: + family = index.data(QtCore.Qt.DisplayRole) + checked_families.append(family) - items = [self.item(i) for i in - range(self.count())] + return checked_families - return [item.data(self.NameRole) for item in items if - item.checkState() == QtCore.Qt.Checked] + def set_all_unchecked(self): + self._set_all_checkstate(False) - def _on_item_changed(self): - self.active_changed.emit(self.get_filters()) + def set_all_checked(self): + self._set_all_checkstate(True) + + def _set_all_checkstate(self, checked): + if checked: + state = QtCore.Qt.Checked + else: + state = QtCore.Qt.Unchecked - def _set_checkstate_all(self, state): - _state = QtCore.Qt.Checked if state is True else QtCore.Qt.Unchecked self.blockSignals(True) - for i in range(self.count()): - item = self.item(i) - item.setCheckState(_state) + + model = self._family_model + for row in range(model.rowCount()): + index = model.index(row, 0) + if index.data(QtCore.Qt.CheckStateRole) != state: + model.setData(index, state, QtCore.Qt.CheckStateRole) + self.blockSignals(False) - self.active_changed.emit(self.get_filters()) - def show_right_mouse_menu(self, pos): + self.active_changed.emit(self.get_enabled_families()) + + def _on_data_change(self, *_args): + self.active_changed.emit(self.get_enabled_families()) + + def _on_context_menu(self, pos): """Build RMB menu under mouse at current position (within widget)""" - - # Get mouse position - globalpos = self.viewport().mapToGlobal(pos) - menu = QtWidgets.QMenu(self) # Add enable all action - state_checked = QtWidgets.QAction(menu, text="Enable All") - state_checked.triggered.connect( - lambda: self._set_checkstate_all(True)) + action_check_all = QtWidgets.QAction(menu) + action_check_all.setText("Enable All") + action_check_all.triggered.connect(self.set_all_checked) # Add disable all action - state_unchecked = QtWidgets.QAction(menu, text="Disable All") - state_unchecked.triggered.connect( - lambda: self._set_checkstate_all(False)) + action_uncheck_all = QtWidgets.QAction(menu) + action_uncheck_all.setText("Disable All") + action_uncheck_all.triggered.connect(self.set_all_unchecked) - menu.addAction(state_checked) - menu.addAction(state_unchecked) + menu.addAction(action_check_all) + menu.addAction(action_uncheck_all) - menu.exec_(globalpos) + # Get mouse position + global_pos = self.viewport().mapToGlobal(pos) + menu.exec_(global_pos) class RepresentationWidget(QtWidgets.QWidget): From 767142b764194aa232a60310c969c7d42a9e1ef4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 15:04:50 +0200 Subject: [PATCH 306/716] added simpler checkstate changes with space, enter and backspace --- openpype/tools/loader/widgets.py | 63 +++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 2953179509..21018671be 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -945,7 +945,7 @@ class FamilyListView(QtWidgets.QListView): def get_enabled_families(self): """Return the checked family items""" - model = self.model() + model = self._family_model checked_families = [] for row in range(model.rowCount()): index = model.index(row, 0) @@ -956,29 +956,54 @@ class FamilyListView(QtWidgets.QListView): return checked_families def set_all_unchecked(self): - self._set_all_checkstate(False) + self._set_checkstates(False, self._get_all_indexes()) def set_all_checked(self): - self._set_all_checkstate(True) + self._set_checkstates(True, self._get_all_indexes()) - def _set_all_checkstate(self, checked): - if checked: + def _get_all_indexes(self): + indexes = [] + model = self._family_model + for row in range(model.rowCount()): + index = model.index(row, 0) + indexes.append(index) + return indexes + + def _set_checkstates(self, checked, indexes): + if not indexes: + return + + if checked is None: + state = None + elif checked: state = QtCore.Qt.Checked else: state = QtCore.Qt.Unchecked self.blockSignals(True) - model = self._family_model - for row in range(model.rowCount()): - index = model.index(row, 0) - if index.data(QtCore.Qt.CheckStateRole) != state: - model.setData(index, state, QtCore.Qt.CheckStateRole) + for index in indexes: + index_state = index.data(QtCore.Qt.CheckStateRole) + if index_state == state: + continue + + new_state = state + if new_state is None: + if index_state == QtCore.Qt.Checked: + new_state = QtCore.Qt.Unchecked + else: + new_state = QtCore.Qt.Checked + + index.model().setData(index, new_state, QtCore.Qt.CheckStateRole) self.blockSignals(False) self.active_changed.emit(self.get_enabled_families()) + def _change_selection_state(self, checked): + indexes = self.selectionModel().selectedIndexes() + self._set_checkstates(checked, indexes) + def _on_data_change(self, *_args): self.active_changed.emit(self.get_enabled_families()) @@ -1002,6 +1027,24 @@ class FamilyListView(QtWidgets.QListView): global_pos = self.viewport().mapToGlobal(pos) menu.exec_(global_pos) + def event(self, event): + if not event.type() == QtCore.QEvent.KeyPress: + pass + + elif event.key() == QtCore.Qt.Key_Space: + self._change_selection_state(None) + return True + + elif event.key() == QtCore.Qt.Key_Backspace: + self._change_selection_state(False) + return True + + elif event.key() == QtCore.Qt.Key_Return: + self._change_selection_state(True) + return True + + return super(FamilyListView, self).event(event) + class RepresentationWidget(QtWidgets.QWidget): load_started = QtCore.Signal() From ab98bf5358fc8529535481f3acf5e41a7616cdb4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 15:23:16 +0200 Subject: [PATCH 307/716] added filtering model --- openpype/tools/loader/widgets.py | 50 +++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 21018671be..0c61db2623 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -915,6 +915,46 @@ class FamilyModel(QtGui.QStandardItemModel): root_item.appendRows(new_items) +class FamilyProxyFiler(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(FamilyProxyFiler, self).__init__(*args, **kwargs) + + self._filtering_enabled = False + self._enabled_families = set() + + def set_enabled_families(self, families): + if self._enabled_families == families: + return + + self._enabled_families = families + if self._filtering_enabled: + self.invalidateFilter() + + def is_filter_enabled(self): + return self._filtering_enabled + + def set_filter_enabled(self, enabled=None): + if enabled is None: + enabled = not self._filtering_enabled + if self._filtering_enabled == enabled: + return + + self._filtering_enabled = enabled + self.invalidateFilter() + + def filterAcceptsRow(self, row, parent): + if not self._filtering_enabled: + return True + + if not self._enabled_families: + return False + + index = self.sourceModel().index(row, self.filterKeyColumn(), parent) + if index.data(QtCore.Qt.DisplayRole) in self._enabled_families: + return True + return False + + class FamilyListView(QtWidgets.QListView): active_changed = QtCore.Signal(list) @@ -926,7 +966,7 @@ class FamilyListView(QtWidgets.QListView): self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) family_model = FamilyModel(dbcon, family_config_cache) - proxy_model = QtCore.QSortFilterProxyModel() + proxy_model = FamilyProxyFiler() proxy_model.setDynamicSortFilter(True) proxy_model.setSourceModel(family_model) @@ -938,6 +978,14 @@ class FamilyListView(QtWidgets.QListView): self._family_model = family_model self._proxy_model = proxy_model + def set_enabled_families(self, families): + self._proxy_model.set_enabled_families(families) + + self.set_enabled_family_filtering(True) + + def set_enabled_family_filtering(self, enabled=None): + self._proxy_model.set_filter_enabled(enabled) + def refresh(self): self._family_model.refresh() From e402c9c51f83ac4abcda56c5b006e086e50acd4b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 17 Sep 2021 16:18:36 +0200 Subject: [PATCH 308/716] Added possibility to configure of synchronization of workfile version with selected families --- .../plugins/publish/precollect_instances.py | 13 ++++----- .../defaults/project_settings/nuke.json | 8 +++++- .../schemas/schema_nuke_publish.json | 27 ++++++++++++++++--- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index c2c25d0627..d9aec14dc2 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -13,7 +13,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): hosts = ["nuke", "nukeassist"] # presets - sync_workfile_version = False + sync_workfile_version_on_families = [] def process(self, context): asset_data = io.find_one({ @@ -120,11 +120,12 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): # sync workfile version _families_test = [family] + families self.log.debug("__ _families_test: `{}`".format(_families_test)) - if not next((f for f in _families_test - if "prerender" in f), - None) and self.sync_workfile_version: - # get version to instance for integration - instance.data['version'] = instance.context.data['version'] + for family_test in _families_test: + if family_test in self.sync_workfile_version_on_families: + self.log.debug("Syncing version with workfile for '{}'" + .format(family_test)) + # get version to instance for integration + instance.data['version'] = instance.context.data['version'] instance.data.update({ "subset": subset, diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 136f1d6b42..6ee7c2cd39 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -30,7 +30,13 @@ }, "publish": { "PreCollectNukeInstances": { - "sync_workfile_version": true + "sync_workfile_version_on_families": [ + "nukenodes", + "camera", + "gizmo", + "source", + "render" + ] }, "ValidateContainers": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 782179cfd1..2772c5f3a6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -16,9 +16,30 @@ "is_group": true, "children": [ { - "type": "boolean", - "key": "sync_workfile_version", - "label": "Sync Version from workfile" + "type": "enum", + "key": "sync_workfile_version_on_families", + "label": "Sync workfile version for families", + "multiselection": true, + "enum_items": [ + { + "nukenodes": "nukenodes" + }, + { + "camera": "camera" + }, + { + "gizmo": "gizmo" + }, + { + "source": "source" + }, + { + "prerender": "prerender" + }, + { + "render": "render" + } + ] } ] }, From 7696fbd2de0ddb239f9d257da4def93478580fb2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:21:43 +0200 Subject: [PATCH 309/716] pass refreshed to subset widget --- openpype/tools/loader/app.py | 17 +++++++---------- openpype/tools/loader/widgets.py | 2 ++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 4beebe43b8..5cb0bf41a9 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -146,6 +146,7 @@ class LoaderWidow(QtWidgets.QDialog): assets.view.clicked.connect(self.on_assetview_click) subsets.active_changed.connect(self.on_subsetschanged) subsets.version_changed.connect(self.on_versionschanged) + subsets.refreshed.connect(self._on_subset_refresh) subsets.load_started.connect(self._on_load_start) subsets.load_ended.connect(self._on_load_end) @@ -215,6 +216,12 @@ class LoaderWidow(QtWidgets.QDialog): def _hide_overlay(self): self._overlay_frame.setVisible(False) + def _on_subset_refresh(self, has_item): + subsets_widget = self.data["widgets"]["subsets"] + familis_widget = self.data["widgets"]["families"] + + subsets_widget.set_loading_state(loading=False, empty=not has_item) + def _on_load_end(self): # Delay hiding as click events happened during loading should be # blocked @@ -264,8 +271,6 @@ class LoaderWidow(QtWidgets.QDialog): def _assetschanged(self): """Selected assets have changed""" - t1 = time.time() - assets_widget = self.data["widgets"]["assets"] subsets_widget = self.data["widgets"]["subsets"] subsets_model = subsets_widget.model @@ -283,14 +288,6 @@ class LoaderWidow(QtWidgets.QDialog): empty=True ) - def on_refreshed(has_item): - empty = not has_item - subsets_widget.set_loading_state(loading=False, empty=empty) - subsets_model.refreshed.disconnect() - self.echo("Duration: %.3fs" % (time.time() - t1)) - - subsets_model.refreshed.connect(on_refreshed) - subsets_model.set_assets(asset_ids) subsets_widget.view.setColumnHidden( subsets_model.Columns.index("asset"), diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 0c61db2623..5a04cbac8f 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -122,6 +122,7 @@ class SubsetWidget(QtWidgets.QWidget): version_changed = QtCore.Signal() # version state changed for a subset load_started = QtCore.Signal() load_ended = QtCore.Signal() + refreshed = QtCore.Signal(bool) default_widths = ( ("subset", 200), @@ -242,6 +243,7 @@ class SubsetWidget(QtWidgets.QWidget): self.filter.textChanged.connect(self.proxy.setFilterRegExp) self.filter.textChanged.connect(self.view.expandAll) + model.refreshed.connect(self.refreshed) self.model.refresh() From aad79964a621ac95ed9ae1f79118cbe814c9035c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:21:53 +0200 Subject: [PATCH 310/716] fixed subset projection --- openpype/tools/loader/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 253341f70d..184d488efc 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -128,7 +128,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): "name": 1, "parent": 1, "schema": 1, - "families": 1, + "data.families": 1, "data.subsetGroup": 1 } From 2e871a7f136055be1abadcc343d4280feed0d508 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:22:10 +0200 Subject: [PATCH 311/716] store subset families --- openpype/tools/loader/model.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 184d488efc..1668fc4a27 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -70,7 +70,6 @@ class BaseRepresentationModel(object): class SubsetsModel(TreeModel, BaseRepresentationModel): - doc_fetched = QtCore.Signal() refreshed = QtCore.Signal(bool) @@ -354,10 +353,16 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): }, self.subset_doc_projection ) - for subset in subset_docs: + subset_families = set() + for subset_doc in subset_docs: if self._doc_fetching_stop: return - subset_docs_by_id[subset["_id"]] = subset + + families = subset_doc.get("data", {}).get("families") + if families: + subset_families.add(families[0]) + + subset_docs_by_id[subset_doc["_id"]] = subset_doc subset_ids = list(subset_docs_by_id.keys()) _pipeline = [ @@ -428,6 +433,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): self._doc_payload = { "asset_docs_by_id": asset_docs_by_id, "subset_docs_by_id": subset_docs_by_id, + "subset_families": subset_families, "last_versions_by_subset_id": last_versions_by_subset_id } From e6abb640d732f6e4e267c172a4ebc1f14aa464bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:22:25 +0200 Subject: [PATCH 312/716] added ability to return families of current subsets --- openpype/tools/loader/app.py | 2 ++ openpype/tools/loader/model.py | 3 +++ openpype/tools/loader/widgets.py | 3 +++ 3 files changed, 8 insertions(+) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 5cb0bf41a9..f36248b0c0 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -221,6 +221,8 @@ class LoaderWidow(QtWidgets.QDialog): familis_widget = self.data["widgets"]["families"] subsets_widget.set_loading_state(loading=False, empty=not has_item) + families = subsets_widget.get_subsets_families() + familis_widget.set_enabled_families(families) def _on_load_end(self): # Delay hiding as click events happened during loading should be diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 1668fc4a27..0ad8e88593 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -190,6 +190,9 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): self._grouping = state self.on_doc_fetched() + def get_subsets_families(self): + return self._doc_payload.get("subset_families") or set() + def setData(self, index, value, role=QtCore.Qt.EditRole): # Trigger additional edit when `version` column changed # because it also updates the information in other columns diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 5a04cbac8f..22d5f8ec3a 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -247,6 +247,9 @@ class SubsetWidget(QtWidgets.QWidget): self.model.refresh() + def get_subsets_families(self): + return self.model.get_subsets_families() + def set_family_filters(self, families): self.family_proxy.setFamiliesFilter(families) From 0a2ff39c4a1cdc478198e7a6c6441c11510a383d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 17 Sep 2021 16:18:36 +0200 Subject: [PATCH 313/716] Added possibility to configure of synchronization of workfile version with selected families --- .../plugins/publish/precollect_instances.py | 13 ++++----- .../defaults/project_settings/nuke.json | 8 +++++- .../schemas/schema_nuke_publish.json | 27 ++++++++++++++++--- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index 75d0b4f9a9..5c30df9a62 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -13,7 +13,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): hosts = ["nuke", "nukeassist"] # presets - sync_workfile_version = False + sync_workfile_version_on_families = [] def process(self, context): asset_data = io.find_one({ @@ -120,11 +120,12 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): # sync workfile version _families_test = [family] + families self.log.debug("__ _families_test: `{}`".format(_families_test)) - if not next((f for f in _families_test - if "prerender" in f), - None) and self.sync_workfile_version: - # get version to instance for integration - instance.data['version'] = instance.context.data['version'] + for family_test in _families_test: + if family_test in self.sync_workfile_version_on_families: + self.log.debug("Syncing version with workfile for '{}'" + .format(family_test)) + # get version to instance for integration + instance.data['version'] = instance.context.data['version'] instance.data.update({ "subset": subset, diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 136f1d6b42..6ee7c2cd39 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -30,7 +30,13 @@ }, "publish": { "PreCollectNukeInstances": { - "sync_workfile_version": true + "sync_workfile_version_on_families": [ + "nukenodes", + "camera", + "gizmo", + "source", + "render" + ] }, "ValidateContainers": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 782179cfd1..2772c5f3a6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -16,9 +16,30 @@ "is_group": true, "children": [ { - "type": "boolean", - "key": "sync_workfile_version", - "label": "Sync Version from workfile" + "type": "enum", + "key": "sync_workfile_version_on_families", + "label": "Sync workfile version for families", + "multiselection": true, + "enum_items": [ + { + "nukenodes": "nukenodes" + }, + { + "camera": "camera" + }, + { + "gizmo": "gizmo" + }, + { + "source": "source" + }, + { + "prerender": "prerender" + }, + { + "render": "render" + } + ] } ] }, From 5c1f34a7eb4d2dd29230b407e52aeeb452b8d475 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:37:26 +0200 Subject: [PATCH 314/716] families filtering by asset added to library loader --- openpype/tools/libraryloader/app.py | 43 ++++++++++++++--------------- openpype/tools/loader/app.py | 11 ++++---- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 3f7979ff1c..6dbe47301c 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -151,6 +151,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): assets.view.clicked.connect(self.on_assetview_click) subsets.active_changed.connect(self.on_subsetschanged) subsets.version_changed.connect(self.on_versionschanged) + subsets.refreshed.connect(self._on_subset_refresh) self.combo_projects.currentTextChanged.connect(self.on_project_change) self.sync_server = sync_server @@ -242,6 +243,12 @@ class LibraryLoaderWindow(QtWidgets.QDialog): "Config `%s` has no function `install`" % _config.__name__ ) + subsets = self.data["widgets"]["subsets"] + representations = self.data["widgets"]["representations"] + + subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) + representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) + self.family_config_cache.refresh() self.groups_config.refresh() @@ -252,12 +259,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): title = "{} - {}".format(self.tool_title, project_name) self.setWindowTitle(title) - subsets = self.data["widgets"]["subsets"] - subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) - - representations = self.data["widgets"]["representations"] - representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) - @property def current_project(self): if ( @@ -288,6 +289,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self.echo("Fetching version..") tools_lib.schedule(self._versionschanged, 150, channel="mongo") + def _on_subset_refresh(self, has_item): + subsets_widget = self.data["widgets"]["subsets"] + families_view = self.data["widgets"]["families"] + + subsets_widget.set_loading_state(loading=False, empty=not has_item) + families = subsets_widget.get_subsets_families() + families_view.set_enabled_families(families) + def set_context(self, context, refresh=True): self.echo("Setting context: {}".format(context)) lib.schedule( @@ -312,13 +321,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog): assert project_doc, "This is a bug" assets_widget = self.data["widgets"]["assets"] + families_view = self.data["widgets"]["families"] + families_view.set_enabled_families(set()) + families_view.refresh() + assets_widget.model.stop_fetch_thread() assets_widget.refresh() assets_widget.setFocus() - families = self.data["widgets"]["families"] - families.refresh() - def clear_assets_underlines(self): last_asset_ids = self.data["state"]["assetIds"] if not last_asset_ids: @@ -337,8 +347,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): def _assetschanged(self): """Selected assets have changed""" - t1 = time.time() - assets_widget = self.data["widgets"]["assets"] subsets_widget = self.data["widgets"]["subsets"] subsets_model = subsets_widget.model @@ -365,14 +373,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): empty=True ) - def on_refreshed(has_item): - empty = not has_item - subsets_widget.set_loading_state(loading=False, empty=empty) - subsets_model.refreshed.disconnect() - self.echo("Duration: %.3fs" % (time.time() - t1)) - - subsets_model.refreshed.connect(on_refreshed) - subsets_model.set_assets(asset_ids) subsets_widget.view.setColumnHidden( subsets_model.Columns.index("asset"), @@ -386,9 +386,8 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self.data["state"]["assetIds"] = asset_ids representations = self.data["widgets"]["representations"] - representations.set_version_ids([]) # reset repre list - - self.echo("Duration: %.3fs" % (time.time() - t1)) + # reset repre list + representations.set_version_ids([]) def _subsetschanged(self): asset_ids = self.data["state"]["assetIds"] diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index f36248b0c0..cce05c1d3e 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -218,11 +218,11 @@ class LoaderWidow(QtWidgets.QDialog): def _on_subset_refresh(self, has_item): subsets_widget = self.data["widgets"]["subsets"] - familis_widget = self.data["widgets"]["families"] + families_view = self.data["widgets"]["families"] subsets_widget.set_loading_state(loading=False, empty=not has_item) families = subsets_widget.get_subsets_families() - familis_widget.set_enabled_families(families) + families_view.set_enabled_families(families) def _on_load_end(self): # Delay hiding as click events happened during loading should be @@ -247,8 +247,8 @@ class LoaderWidow(QtWidgets.QDialog): assets_widget.refresh() assets_widget.setFocus() - families = self.data["widgets"]["families"] - families.refresh() + families_view = self.data["widgets"]["families"] + families_view.refresh() def clear_assets_underlines(self): """Clear colors from asset data to remove colored underlines @@ -303,7 +303,8 @@ class LoaderWidow(QtWidgets.QDialog): self.data["state"]["assetIds"] = asset_ids representations = self.data["widgets"]["representations"] - representations.set_version_ids([]) # reset repre list + # reset repre list + representations.set_version_ids([]) def _subsetschanged(self): asset_ids = self.data["state"]["assetIds"] From 0639cfa35f9a330b52aa7b5c282b38d6db2a6a0f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:42:52 +0200 Subject: [PATCH 315/716] fixed duplication of families --- openpype/tools/loader/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 22d5f8ec3a..79a31a787f 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -915,6 +915,7 @@ class FamilyModel(QtGui.QStandardItemModel): item.setIcon(icon) new_items.append(item) + self._items_by_family[family] = item if new_items: root_item.appendRows(new_items) From 152549bd4f34b25e1b67d7cac92c571a188818bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 17:24:00 +0200 Subject: [PATCH 316/716] disable projects view if defaults are modified --- .../tools/settings/settings/categories.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index c420a8cdc5..be2264340b 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -609,14 +609,23 @@ class ProjectWidget(SettingsCategoryWidget): self.project_list_widget.refresh() def _on_reset_crash(self): - self.project_list_widget.setEnabled(False) + self._set_enabled_project_list(False) super(ProjectWidget, self)._on_reset_crash() def _on_reset_success(self): - if not self.project_list_widget.isEnabled(): - self.project_list_widget.setEnabled(True) + self._set_enabled_project_list(True) super(ProjectWidget, self)._on_reset_success() + def _set_enabled_project_list(self, enabled): + if ( + enabled + and self.modify_defaults_checkbox + and self.modify_defaults_checkbox.isChecked() + ): + enabled = False + if self.project_list_widget.isEnabled() != enabled: + self.project_list_widget.setEnabled(enabled) + def _create_root_entity(self): self.entity = ProjectSettings(change_state=False) self.entity.on_change_callbacks.append(self._on_entity_change) @@ -637,7 +646,8 @@ class ProjectWidget(SettingsCategoryWidget): if self.modify_defaults_checkbox: self.modify_defaults_checkbox.setEnabled(True) - self.project_list_widget.setEnabled(True) + + self._set_enabled_project_list(True) except DefaultsNotDefined: if not self.modify_defaults_checkbox: @@ -646,7 +656,7 @@ class ProjectWidget(SettingsCategoryWidget): self.entity.set_defaults_state() self.modify_defaults_checkbox.setChecked(True) self.modify_defaults_checkbox.setEnabled(False) - self.project_list_widget.setEnabled(False) + self._set_enabled_project_list(False) except StudioDefaultsNotDefined: self.select_default_project() @@ -666,8 +676,10 @@ class ProjectWidget(SettingsCategoryWidget): def _on_modify_defaults(self): if self.modify_defaults_checkbox.isChecked(): + self._set_enabled_project_list(False) if not self.entity.is_in_defaults_state(): self.reset() else: + self._set_enabled_project_list(True) if not self.entity.is_in_studio_state(): self.reset() From 8c0a9add2de64db00f8590a187b5db5a04b8d080 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 17:24:14 +0200 Subject: [PATCH 317/716] added better colors for disabled view --- openpype/tools/settings/settings/style/style.css | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css index d9d85a481e..32259af30c 100644 --- a/openpype/tools/settings/settings/style/style.css +++ b/openpype/tools/settings/settings/style/style.css @@ -146,6 +146,15 @@ QSlider::handle:vertical { border: 1px solid #464b54; background: #21252B; } + +#ProjectListWidget QListView:disabled { + background: #282C34; +} + +#ProjectListWidget QListView::item:disabled { + color: #4e5254; +} + #ProjectListWidget QLabel { background: transparent; font-weight: bold; @@ -249,8 +258,6 @@ QTabBar::tab:!selected:hover { background: #333840; } - - QTabBar::tab:first:selected { margin-left: 0; } From aadd769ef44485d23f6b1729009da07969e1284f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 17:27:22 +0200 Subject: [PATCH 318/716] keep selected color unchanged even if view loose focus --- openpype/tools/settings/settings/style/style.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css index 32259af30c..b77b575204 100644 --- a/openpype/tools/settings/settings/style/style.css +++ b/openpype/tools/settings/settings/style/style.css @@ -412,12 +412,15 @@ QHeaderView::section { font-weight: bold; } -QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { +QAbstractItemView::item:pressed { background: #78879b; color: #FFFFFF; } -QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { +QAbstractItemView::item:selected:active { + background: #3d8ec9; +} +QAbstractItemView::item:selected:!active { background: #3d8ec9; } From 11ad2a87b7fb87e8ff4ff0d35e1675b62ca0b497 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:36:51 +0200 Subject: [PATCH 319/716] FamiliesFilterProxyModel does not need family config --- openpype/tools/loader/model.py | 7 +------ openpype/tools/loader/widgets.py | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 0ad8e88593..6e9c7bf220 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -860,10 +860,9 @@ class SubsetFilterProxyModel(GroupMemberFilterProxyModel): class FamiliesFilterProxyModel(GroupMemberFilterProxyModel): """Filters to specified families""" - def __init__(self, family_config_cache, *args, **kwargs): + def __init__(self, *args, **kwargs): super(FamiliesFilterProxyModel, self).__init__(*args, **kwargs) self._families = set() - self.family_config_cache = family_config_cache def familyFilter(self): return self._families @@ -895,10 +894,6 @@ class FamiliesFilterProxyModel(GroupMemberFilterProxyModel): if not family: return True - family_config = self.family_config_cache.family_config(family) - if family_config.get("hideFilter"): - return False - # We want to keep the families which are not in the list return family in self._families diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 79a31a787f..6d29dee6ec 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -159,7 +159,7 @@ class SubsetWidget(QtWidgets.QWidget): grouping=enable_grouping ) proxy = SubsetFilterProxyModel() - family_proxy = FamiliesFilterProxyModel(family_config_cache) + family_proxy = FamiliesFilterProxyModel() family_proxy.setSourceModel(proxy) subset_filter = QtWidgets.QLineEdit() From eb9b88068c656cf7ec2d73032b4b174950e33388 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:37:11 +0200 Subject: [PATCH 320/716] refresh family configu during refresh of family model --- openpype/tools/loader/widgets.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 6d29dee6ec..650879ac86 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -879,12 +879,13 @@ class FamilyModel(QtGui.QStandardItemModel): families = set(result[0]["families"]) root_item = self.invisibleRootItem() - self.blockSignals(True) + for family in tuple(self._items_by_family.keys()): if family not in families: item = self._items_by_family.pop(family) root_item.removeRow(item.row()) - self.blockSignals(False) + + self.family_config_cache.refresh() new_items = [] for family in families: @@ -892,8 +893,6 @@ class FamilyModel(QtGui.QStandardItemModel): continue family_config = self.family_config_cache.family_config(family) - if family_config.get("hideFilter"): - continue label = family_config.get("label", family) icon = family_config.get("icon", None) From 84cab2bcdd24f04522b1552ad429613c8c05a6d9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:37:24 +0200 Subject: [PATCH 321/716] removed unused global_family_cache --- openpype/tools/utils/lib.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index e83f663b2e..c402b1f169 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -12,18 +12,6 @@ self = sys.modules[__name__] self._jobs = dict() -class SharedObjects: - # Variable for family cache in global context - # QUESTION is this safe? More than one tool can refresh at the same time. - family_cache = None - - -def global_family_cache(): - if SharedObjects.family_cache is None: - SharedObjects.family_cache = FamilyConfigCache(io) - return SharedObjects.family_cache - - def format_version(value, hero_version=False): """Formats integer to displayable version name""" label = "v{0:03d}".format(value) From 9433ffe049216f6f549d98ef3dcfb896831253ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:37:56 +0200 Subject: [PATCH 322/716] modified family config to suit more for settings --- openpype/tools/utils/lib.py | 119 +++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 50 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index c402b1f169..db34389434 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -5,9 +5,13 @@ import collections from Qt import QtWidgets, QtCore, QtGui -from avalon import io, api, style +import avalon.api +from avalon import style from avalon.vendor import qtawesome +from openpype.api import get_project_settings +from openpype.lib import filter_profiles + self = sys.modules[__name__] self._jobs = dict() @@ -277,11 +281,12 @@ def preserve_selection(tree_view, column=0, role=None, current_index=True): class FamilyConfigCache: default_color = "#0091B2" _default_icon = None - _default_item = None def __init__(self, dbcon): self.dbcon = dbcon self.family_configs = {} + self._family_filters_set = False + self._require_refresh = True @classmethod def default_icon(cls): @@ -293,15 +298,29 @@ class FamilyConfigCache: @classmethod def default_item(cls): - if cls._default_item is None: - cls._default_item = {"icon": cls.default_icon()} - return cls._default_item + return { + "icon": cls.default_icon() + } def family_config(self, family_name): """Get value from config with fallback to default""" - return self.family_configs.get(family_name, self.default_item()) + if self._require_refresh: + self._refresh() - def refresh(self): + item = self.family_configs.get(family_name) + if not item: + item = self.default_item() + if self._family_filters_set: + item["state"] = False + return item + + def refresh(self, force=False): + self._require_refresh = True + + if force: + self._refresh() + + def _refresh(self): """Get the family configurations from the database The configuration must be stored on the project under `config`. @@ -317,62 +336,62 @@ class FamilyConfigCache: It is possible to override the default behavior and set specific families checked. For example we only want the families imagesequence and camera to be visible in the Loader. - - # This will turn every item off - api.data["familyStateDefault"] = False - - # Only allow the imagesequence and camera - api.data["familyStateToggled"] = ["imagesequence", "camera"] - """ + self._require_refresh = False + self._family_filters_set = False self.family_configs.clear() - - families = [] + # Skip if we're not in host context + if not avalon.api.registered_host(): + return # Update the icons from the project configuration project_name = self.dbcon.Session.get("AVALON_PROJECT") - if project_name: - project_doc = self.dbcon.find_one( - {"type": "project"}, - projection={"config.families": True} + asset_name = self.dbcon.Session.get("AVALON_ASSET") + task_name = self.dbcon.Session.get("AVALON_TASK") + if not all((project_name, asset_name, task_name)): + return + + matching_item = None + project_settings = get_project_settings(project_name) + profiles = ( + project_settings + ["global"] + ["tools"] + ["loader"] + ["family_filter_profiles"] + ) + if profiles: + asset_doc = self.dbcon.find_one( + {"type": "asset", "name": asset_name}, + {"data.tasks": True} ) + tasks_info = asset_doc.get("data", {}).get("tasks") or {} + task_type = tasks_info.get(task_name, {}).get("type") + profiles_filter = { + "task_types": task_type, + "hosts": os.environ["AVALON_APP"] + } + matching_item = filter_profiles(profiles, profiles_filter) - if not project_doc: - print(( - "Project \"{}\" not found!" - " Can't refresh family icons cache." - ).format(project_name)) - else: - families = project_doc["config"].get("families") or [] + families = [] + if matching_item: + families = matching_item["filter_families"] - # Check if any family state are being overwritten by the configuration - default_state = api.data.get("familiesStateDefault", True) - toggled = set(api.data.get("familiesStateToggled") or []) + if not families: + return + + self._family_filters_set = True # Replace icons with a Qt icon we can use in the user interfaces for family in families: - name = family["name"] - # Set family icon - icon = family.get("icon", None) - if icon: - family["icon"] = qtawesome.icon( - "fa.{}".format(icon), - color=self.default_color - ) - else: - family["icon"] = self.default_icon() + family_info = { + "name": family, + "icon": self.default_icon(), + "state": True + } - # Update state - if name in toggled: - state = True - else: - state = default_state - family["state"] = state - - self.family_configs[name] = family - - return self.family_configs + self.family_configs[family] = family_info class GroupsConfig: From 7ccc1bc01077bab2f1afc004cdfcf86568c0ebbd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:43:07 +0200 Subject: [PATCH 323/716] added initial settings for families --- .../defaults/project_settings/global.json | 9 ++++ .../schemas/schema_global_tools.json | 42 +++++++++++++++++++ .../schemas/template_publish_families.json | 20 +++++++++ 3 files changed, 71 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index a53ae14914..6a61f2f5c3 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -287,6 +287,15 @@ "textures" ] } + }, + "loader": { + "family_filter_profiles": [ + { + "hosts": [], + "task_types": [], + "filter_families": [] + } + ] } }, "project_folder_structure": "{\"__project_root__\": {\"prod\": {}, \"resources\": {\"footage\": {\"plates\": {}, \"offline\": {}}, \"audio\": {}, \"art_dept\": {}}, \"editorial\": {}, \"assets[ftrack.Library]\": {\"characters[ftrack]\": {}, \"locations[ftrack]\": {}}, \"shots[ftrack.Sequence]\": {\"scripts\": {}, \"editorial[ftrack.Folder]\": {}}}}", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 245560f115..8382bfe3f6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -190,6 +190,48 @@ } } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "loader", + "label": "Loader", + "children": [ + { + "type": "list", + "key": "family_filter_profiles", + "label": "Family filtering", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "type": "task-types-enum", + "key": "task_types", + "label": "Task types" + }, + { + "type": "splitter" + }, + { + "type": "template", + "name": "template_publish_families", + "template_data": { + "key": "filter_families", + "label": "Filter families", + "multiselection": true + } + } + ] + } + } + ] } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json new file mode 100644 index 0000000000..edec3bad3d --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json @@ -0,0 +1,20 @@ +[ + { + "__default_values__": { + "multiselection": true + } + }, + { + "key": "{key}", + "label": "{label}", + "multiselection": "{multiselection}", + "type": "enum", + "enum_items": [ + {"family1": "family1"}, + {"family2": "family2"}, + {"family3": "family3"}, + {"family4": "family4"}, + {"family5": "family5"} + ] + } +] From 1bbfa72b18440b52735d7c782fa79d00948fb41e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:57:05 +0200 Subject: [PATCH 324/716] added more suitable families --- .../schemas/template_publish_families.json | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json index edec3bad3d..9db1427562 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json @@ -10,11 +10,23 @@ "multiselection": "{multiselection}", "type": "enum", "enum_items": [ - {"family1": "family1"}, - {"family2": "family2"}, - {"family3": "family3"}, - {"family4": "family4"}, - {"family5": "family5"} + {"action": "action"}, + {"animation": "animation"}, + {"audio": "audio"}, + {"camera": "camera"}, + {"editorial": "editorial"}, + {"layout": "layout"}, + {"look": "look"}, + {"mayaAscii": "mayaAscii"}, + {"model": "model"}, + {"pointcache": "pointcache"}, + {"reference": "reference"}, + {"render": "render"}, + {"review": "review"}, + {"rig": "rig"}, + {"setdress": "setdress"}, + {"workfile": "workfile"}, + {"xgen": "xgen"} ] } ] From 622dfa0d4fbbabec98f6fbb242f06629ad361c38 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:19:03 +0200 Subject: [PATCH 325/716] use SharedObject class for jobs --- openpype/tools/utils/lib.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index db34389434..00f64211b8 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -12,9 +12,6 @@ from avalon.vendor import qtawesome from openpype.api import get_project_settings from openpype.lib import filter_profiles -self = sys.modules[__name__] -self._jobs = dict() - def format_version(value, hero_version=False): """Formats integer to displayable version name""" @@ -58,6 +55,10 @@ def defer(delay, func): return func() +class SharedObjects: + jobs = {} + + def schedule(func, time, channel="default"): """Run `func` at a later `time` in a dedicated `channel` @@ -69,7 +70,7 @@ def schedule(func, time, channel="default"): """ try: - self._jobs[channel].stop() + SharedObjects.jobs[channel].stop() except (AttributeError, KeyError, RuntimeError): pass @@ -78,7 +79,7 @@ def schedule(func, time, channel="default"): timer.timeout.connect(func) timer.start(time) - self._jobs[channel] = timer + SharedObjects.jobs[channel] = timer @contextlib.contextmanager From 424c76e3ea42a84eed6b5772ba2f52988492cefa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:19:22 +0200 Subject: [PATCH 326/716] replaced default item with just using new dictionary --- openpype/tools/utils/lib.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 00f64211b8..8454dad0e5 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -91,7 +91,6 @@ def dummy(): .. pass """ - yield @@ -297,11 +296,6 @@ class FamilyConfigCache: ) return cls._default_icon - @classmethod - def default_item(cls): - return { - "icon": cls.default_icon() - } def family_config(self, family_name): """Get value from config with fallback to default""" @@ -310,7 +304,9 @@ class FamilyConfigCache: item = self.family_configs.get(family_name) if not item: - item = self.default_item() + item = { + "icon": self.default_icon() + } if self._family_filters_set: item["state"] = False return item From 62b975dde29e5cbce88336b7d6be0b8705934666 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:26:52 +0200 Subject: [PATCH 327/716] update family filters on context change --- openpype/tools/loader/app.py | 5 ++++- openpype/tools/loader/widgets.py | 27 ++++++++++++++------------- openpype/tools/utils/lib.py | 7 +++---- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index cce05c1d3e..18e94b7474 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -232,8 +232,11 @@ class LoaderWidow(QtWidgets.QDialog): # ------------------------------ def on_context_task_change(self, *args, **kwargs): - # Change to context asset on context change assets_widget = self.data["widgets"]["assets"] + families_view = self.data["widgets"]["families"] + # Refresh families config + families_view.refresh() + # Change to context asset on context change assets_widget.select_assets(io.Session["AVALON_ASSET"]) def _refresh(self): diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 650879ac86..e94942e7b7 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -889,11 +889,7 @@ class FamilyModel(QtGui.QStandardItemModel): new_items = [] for family in families: - if family in self._items_by_family: - continue - family_config = self.family_config_cache.family_config(family) - label = family_config.get("label", family) icon = family_config.get("icon", None) @@ -902,20 +898,25 @@ class FamilyModel(QtGui.QStandardItemModel): else: state = QtCore.Qt.Unchecked - item = QtGui.QStandardItem(label) - item.setFlags( - QtCore.Qt.ItemIsEnabled - | QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsUserCheckable - ) + if family not in self._items_by_family: + item = QtGui.QStandardItem(label) + item.setFlags( + QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsUserCheckable + ) + + else: + item = self._items_by_family[label] + item.setData(QtCore.Qt.DisplayRole, label) + new_items.append(item) + self._items_by_family[family] = item + item.setCheckState(state) if icon: item.setIcon(icon) - new_items.append(item) - self._items_by_family[family] = item - if new_items: root_item.appendRows(new_items) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 8454dad0e5..d01dbbd169 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -296,7 +296,6 @@ class FamilyConfigCache: ) return cls._default_icon - def family_config(self, family_name): """Get value from config with fallback to default""" if self._require_refresh: @@ -343,9 +342,9 @@ class FamilyConfigCache: return # Update the icons from the project configuration - project_name = self.dbcon.Session.get("AVALON_PROJECT") - asset_name = self.dbcon.Session.get("AVALON_ASSET") - task_name = self.dbcon.Session.get("AVALON_TASK") + project_name = os.environ.get("AVALON_PROJECT") + asset_name = os.environ.get("AVALON_ASSET") + task_name = os.environ.get("AVALON_TASK") if not all((project_name, asset_name, task_name)): return From 7ab717b03f6bf42fd1888d143f3609a612d917e7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:30:00 +0200 Subject: [PATCH 328/716] fix class name --- openpype/tools/loader/__init__.py | 4 ++-- openpype/tools/loader/app.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/tools/loader/__init__.py b/openpype/tools/loader/__init__.py index c7bd6148a7..a5fda8f018 100644 --- a/openpype/tools/loader/__init__.py +++ b/openpype/tools/loader/__init__.py @@ -1,11 +1,11 @@ from .app import ( - LoaderWidow, + LoaderWindow, show, cli, ) __all__ = ( - "LoaderWidow", + "LoaderWindow", "show", "cli", ) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 381d6b25d8..5db7a3bcb1 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -34,13 +34,13 @@ def on_context_task_change(*args, **kwargs): pipeline.on("taskChanged", on_context_task_change) -class LoaderWidow(QtWidgets.QDialog): +class LoaderWindow(QtWidgets.QDialog): """Asset loader interface""" tool_name = "loader" def __init__(self, parent=None): - super(LoaderWidow, self).__init__(parent) + super(LoaderWindow, self).__init__(parent) title = "Asset Loader 2.1" project_name = api.Session.get("AVALON_PROJECT") if project_name: @@ -169,11 +169,11 @@ class LoaderWidow(QtWidgets.QDialog): self.resize(1300, 700) def resizeEvent(self, event): - super(LoaderWidow, self).resizeEvent(event) + super(LoaderWindow, self).resizeEvent(event) self._overlay_frame.resize(self.size()) def moveEvent(self, event): - super(LoaderWidow, self).moveEvent(event) + super(LoaderWindow, self).moveEvent(event) self._overlay_frame.move(0, 0) # ------------------------------- @@ -454,7 +454,7 @@ class LoaderWidow(QtWidgets.QDialog): self.setAttribute(QtCore.Qt.WA_DeleteOnClose) print("Good bye") - return super(LoaderWidow, self).closeEvent(event) + return super(LoaderWindow, self).closeEvent(event) def keyPressEvent(self, event): modifiers = event.modifiers() @@ -466,7 +466,7 @@ class LoaderWidow(QtWidgets.QDialog): self.show_grouping_dialog() return - super(LoaderWidow, self).keyPressEvent(event) + super(LoaderWindow, self).keyPressEvent(event) event.setAccepted(True) # Avoid interfering other widgets def show_grouping_dialog(self): @@ -627,7 +627,7 @@ def show(debug=False, parent=None, use_context=False): module.project = any_project["name"] with lib.application(): - window = LoaderWidow(parent) + window = LoaderWindow(parent) window.setStyleSheet(style.load_stylesheet()) window.show() From e7310036645ae00db4a556ed7ab3de6dbdcb2a8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:30:00 +0200 Subject: [PATCH 329/716] fix class name --- openpype/tools/loader/__init__.py | 4 ++-- openpype/tools/loader/app.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/tools/loader/__init__.py b/openpype/tools/loader/__init__.py index c7bd6148a7..a5fda8f018 100644 --- a/openpype/tools/loader/__init__.py +++ b/openpype/tools/loader/__init__.py @@ -1,11 +1,11 @@ from .app import ( - LoaderWidow, + LoaderWindow, show, cli, ) __all__ = ( - "LoaderWidow", + "LoaderWindow", "show", "cli", ) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 18e94b7474..342a00eded 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -34,13 +34,13 @@ def on_context_task_change(*args, **kwargs): pipeline.on("taskChanged", on_context_task_change) -class LoaderWidow(QtWidgets.QDialog): +class LoaderWindow(QtWidgets.QDialog): """Asset loader interface""" tool_name = "loader" def __init__(self, parent=None): - super(LoaderWidow, self).__init__(parent) + super(LoaderWindow, self).__init__(parent) title = "Asset Loader 2.1" project_name = api.Session.get("AVALON_PROJECT") if project_name: @@ -170,11 +170,11 @@ class LoaderWidow(QtWidgets.QDialog): self.resize(1300, 700) def resizeEvent(self, event): - super(LoaderWidow, self).resizeEvent(event) + super(LoaderWindow, self).resizeEvent(event) self._overlay_frame.resize(self.size()) def moveEvent(self, event): - super(LoaderWidow, self).moveEvent(event) + super(LoaderWindow, self).moveEvent(event) self._overlay_frame.move(0, 0) # ------------------------------- @@ -457,7 +457,7 @@ class LoaderWidow(QtWidgets.QDialog): self.setAttribute(QtCore.Qt.WA_DeleteOnClose) print("Good bye") - return super(LoaderWidow, self).closeEvent(event) + return super(LoaderWindow, self).closeEvent(event) def keyPressEvent(self, event): modifiers = event.modifiers() @@ -469,7 +469,7 @@ class LoaderWidow(QtWidgets.QDialog): self.show_grouping_dialog() return - super(LoaderWidow, self).keyPressEvent(event) + super(LoaderWindow, self).keyPressEvent(event) event.setAccepted(True) # Avoid interfering other widgets def show_grouping_dialog(self): @@ -630,7 +630,7 @@ def show(debug=False, parent=None, use_context=False): module.project = any_project["name"] with lib.application(): - window = LoaderWidow(parent) + window = LoaderWindow(parent) window.setStyleSheet(style.load_stylesheet()) window.show() From 1ca05efbaa134999e0dc3f3ed2b35e3e2feb4a8d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:34:55 +0200 Subject: [PATCH 330/716] hound fixes --- openpype/tools/libraryloader/app.py | 1 - openpype/tools/loader/app.py | 1 - openpype/tools/loader/widgets.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 6dbe47301c..8080c547c9 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -1,5 +1,4 @@ import sys -import time from Qt import QtWidgets, QtCore, QtGui diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 342a00eded..c18b6e798a 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -1,5 +1,4 @@ import sys -import time from Qt import QtWidgets, QtCore from avalon import api, io, style, pipeline diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index e94942e7b7..881e9c206b 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -911,7 +911,7 @@ class FamilyModel(QtGui.QStandardItemModel): item.setData(QtCore.Qt.DisplayRole, label) new_items.append(item) self._items_by_family[family] = item - + item.setCheckState(state) if icon: From 5e40602881c51eb52d162de6e1f9b1c64c57f4f1 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Sep 2021 19:28:14 +0100 Subject: [PATCH 331/716] add new labels to changelog categories --- .github/workflows/prerelease.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 82f9a6ae9d..6c2fd07f78 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -43,11 +43,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - breakingLabel: '**πŸ’₯ Breaking**' - enhancementLabel: '**πŸš€ Enhancements**' - bugsLabel: '**πŸ› Bug fixes**' - deprecatedLabel: '**⚠️ Deprecations**' - addSections: '{"documentation":{"prefix":"### πŸ“– Documentation","labels":["documentation"]},"tests":{"prefix":"### βœ… Testing","labels":["tests"]},"feature":{"prefix":"### πŸ†• New features","labels":["feature"]},}' + addSections: '{"documentation":{"prefix":"### πŸ“– Documentation","labels":["type: documentation"]},"tests":{"prefix":"### βœ… Testing","labels":["tests"]},"feature":{"prefix":"**πŸ†• New features**", "labels":["type: feature"]},"feature":{"prefix":"**πŸ’₯ Breaking**", "labels":["breaking"]},"feature":{"prefix":"**πŸš€ Enhancements**", "labels":["type: enhancement"]},"feature":{"prefix":"**πŸ› Bug fixes**", "labels":["type: bug"]},"feature":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, }' issues: false issuesWoLabels: false sinceTag: "3.0.0" From 1737a8cb6818aab92f76c13b35bdc464afc69574 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Sep 2021 19:40:24 +0100 Subject: [PATCH 332/716] fix wrong keys in changelog categories --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 6c2fd07f78..ddab0e59a8 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -43,7 +43,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"documentation":{"prefix":"### πŸ“– Documentation","labels":["type: documentation"]},"tests":{"prefix":"### βœ… Testing","labels":["tests"]},"feature":{"prefix":"**πŸ†• New features**", "labels":["type: feature"]},"feature":{"prefix":"**πŸ’₯ Breaking**", "labels":["breaking"]},"feature":{"prefix":"**πŸš€ Enhancements**", "labels":["type: enhancement"]},"feature":{"prefix":"**πŸ› Bug fixes**", "labels":["type: bug"]},"feature":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, }' + addSections: '{"documentation":{"prefix":"### πŸ“– Documentation","labels":["type: documentation"]},"tests":{"prefix":"### βœ… Testing","labels":["tests"]},"feature":{"prefix":"**πŸ†• New features**", "labels":["type: feature"]},"breaking":{"prefix":"**πŸ’₯ Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**πŸš€ Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**πŸ› Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}' issues: false issuesWoLabels: false sinceTag: "3.0.0" From 4eb3f5fb66080d2df42201bf96c9ce11e2487c53 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Fri, 17 Sep 2021 18:46:00 +0000 Subject: [PATCH 333/716] [Automated] Bump version --- CHANGELOG.md | 137 +++++++++++++++++++++++++++----------------- openpype/version.py | 2 +- 2 files changed, 85 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1737458b2..3106a878b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,35 +1,72 @@ # Changelog -## [3.4.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.4.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...HEAD) -**Merged pull requests:** +### πŸ“– Documentation -- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972) -- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) +- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) +- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) +- OpenPype: Add version validation and `--headless` mode and update progress πŸ”„ [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) + +**πŸš€ Enhancements** + +- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) +- General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) +- Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) +- Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) +- General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) +- Ftrack: Sync to avalon actions have jobs [\#2015](https://github.com/pypeclub/OpenPype/pull/2015) +- Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009) +- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) +- Settings UI: Number with configurable steps [\#2001](https://github.com/pypeclub/OpenPype/pull/2001) +- Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996) +- Configurable items for providers without Settings [\#1987](https://github.com/pypeclub/OpenPype/pull/1987) +- Global: Example addons [\#1986](https://github.com/pypeclub/OpenPype/pull/1986) +- Settings UI: Number sliders [\#1978](https://github.com/pypeclub/OpenPype/pull/1978) +- Workfiles: Support more workfile templates [\#1966](https://github.com/pypeclub/OpenPype/pull/1966) - Launcher: Fix crashes on action click [\#1964](https://github.com/pypeclub/OpenPype/pull/1964) - Settings: Minor fixes in UI and missing default values [\#1963](https://github.com/pypeclub/OpenPype/pull/1963) - Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962) -- Resolve path when adding to zip [\#1960](https://github.com/pypeclub/OpenPype/pull/1960) -- Bump url-parse from 1.5.1 to 1.5.3 in /website [\#1958](https://github.com/pypeclub/OpenPype/pull/1958) +- Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959) - Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949) -- Global: Define hosts in CollectSceneVersion [\#1948](https://github.com/pypeclub/OpenPype/pull/1948) -- Maya: Add Xgen family support [\#1947](https://github.com/pypeclub/OpenPype/pull/1947) -- Add face sets to exported alembics [\#1942](https://github.com/pypeclub/OpenPype/pull/1942) -- Bump path-parse from 1.0.6 to 1.0.7 in /website [\#1933](https://github.com/pypeclub/OpenPype/pull/1933) -- \#1894 - adds host to template\_name\_profiles for filtering [\#1915](https://github.com/pypeclub/OpenPype/pull/1915) -- Environments: Tool environments in alphabetical order [\#1910](https://github.com/pypeclub/OpenPype/pull/1910) -- Disregard publishing time. [\#1888](https://github.com/pypeclub/OpenPype/pull/1888) -- Feature/webpublisher backend [\#1876](https://github.com/pypeclub/OpenPype/pull/1876) -- Dynamic modules [\#1872](https://github.com/pypeclub/OpenPype/pull/1872) -- Houdini: add Camera, Point Cache, Composite, Redshift ROP and VDB Cache support [\#1821](https://github.com/pypeclub/OpenPype/pull/1821) + +**πŸ› Bug fixes** + +- Workfiles tool: Task selection [\#2040](https://github.com/pypeclub/OpenPype/pull/2040) +- Ftrack: Delete old versions missing settings key [\#2037](https://github.com/pypeclub/OpenPype/pull/2037) +- Nuke: typo on a button [\#2034](https://github.com/pypeclub/OpenPype/pull/2034) +- Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033) +- General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016) +- Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006) +- Nuke thumbnails generated from middle of the sequence [\#1992](https://github.com/pypeclub/OpenPype/pull/1992) +- Nuke: last version from path gets correct version [\#1990](https://github.com/pypeclub/OpenPype/pull/1990) +- nuke, resolve, hiero: precollector order lest then 0.5 [\#1984](https://github.com/pypeclub/OpenPype/pull/1984) +- Last workfile with multiple work templates [\#1981](https://github.com/pypeclub/OpenPype/pull/1981) +- Collectors order [\#1977](https://github.com/pypeclub/OpenPype/pull/1977) +- Stop timer was within validator order range. [\#1975](https://github.com/pypeclub/OpenPype/pull/1975) +- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972) +- Deadline: Houdini plugins in different hierarchy [\#1970](https://github.com/pypeclub/OpenPype/pull/1970) +- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) +- Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961) + +**Merged pull requests:** + +- Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982) +- Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974) +- Bump url-parse from 1.5.1 to 1.5.3 in /website [\#1958](https://github.com/pypeclub/OpenPype/pull/1958) +- CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) -**Merged pull requests:** +**πŸš€ Enhancements** + +- Global: Define hosts in CollectSceneVersion [\#1948](https://github.com/pypeclub/OpenPype/pull/1948) + +**πŸ› Bug fixes** - TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) - Maya: Menu actions fix [\#1945](https://github.com/pypeclub/OpenPype/pull/1945) @@ -40,52 +77,46 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.0-nightly.11...3.3.0) -**Merged pull requests:** +### πŸ“– Documentation + +- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) +- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) + +**πŸ†• New features** + +- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) + +**πŸš€ Enhancements** - Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940) -- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937) -- Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) -- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) -- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) -- Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929) - Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) -- Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926) - Check for missing ✨ Python when using `pyenv` [\#1925](https://github.com/pypeclub/OpenPype/pull/1925) -- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) -- Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922) - Settings: Default values for enum [\#1920](https://github.com/pypeclub/OpenPype/pull/1920) - Settings UI: Modifiable dict view enhance [\#1919](https://github.com/pypeclub/OpenPype/pull/1919) -- standalone: validator asset parents [\#1917](https://github.com/pypeclub/OpenPype/pull/1917) -- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916) -- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914) - submodules: avalon-core update [\#1911](https://github.com/pypeclub/OpenPype/pull/1911) -- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906) -- Add support for multiple Deadline β˜ οΈβž– servers [\#1905](https://github.com/pypeclub/OpenPype/pull/1905) -- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904) -- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903) -- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902) -- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) - Ftrack: Where I run action enhancement [\#1900](https://github.com/pypeclub/OpenPype/pull/1900) - Ftrack: Private project server actions [\#1899](https://github.com/pypeclub/OpenPype/pull/1899) - Support nested studio plugins paths. [\#1898](https://github.com/pypeclub/OpenPype/pull/1898) -- Bug: fixed python detection [\#1893](https://github.com/pypeclub/OpenPype/pull/1893) -- Settings: global validators with options [\#1892](https://github.com/pypeclub/OpenPype/pull/1892) -- Settings: Conditional dict enum positioning [\#1891](https://github.com/pypeclub/OpenPype/pull/1891) -- global: integrate name missing default template [\#1890](https://github.com/pypeclub/OpenPype/pull/1890) -- publisher: editorial plugins fixes [\#1889](https://github.com/pypeclub/OpenPype/pull/1889) -- Expose stop timer through rest api. [\#1886](https://github.com/pypeclub/OpenPype/pull/1886) -- TVPaint: Increment workfile [\#1885](https://github.com/pypeclub/OpenPype/pull/1885) -- Allow Multiple Notes to run on tasks. [\#1882](https://github.com/pypeclub/OpenPype/pull/1882) -- Normalize path returned from Workfiles. [\#1880](https://github.com/pypeclub/OpenPype/pull/1880) -- Prepare for pyside2 [\#1869](https://github.com/pypeclub/OpenPype/pull/1869) -- Filter hosts in settings host-enum [\#1868](https://github.com/pypeclub/OpenPype/pull/1868) -- Local actions with process identifier [\#1867](https://github.com/pypeclub/OpenPype/pull/1867) -- Workfile tool start at host launch support [\#1865](https://github.com/pypeclub/OpenPype/pull/1865) -- Maya: add support for `RedshiftNormalMap` node, fix `tx` linear space πŸš€ [\#1863](https://github.com/pypeclub/OpenPype/pull/1863) -- Workfiles tool event arguments fix [\#1862](https://github.com/pypeclub/OpenPype/pull/1862) -- Maya: support for configurable `dirmap` πŸ—ΊοΈ [\#1859](https://github.com/pypeclub/OpenPype/pull/1859) -- Maya: don't add reference members as connections to the container set πŸ“¦ [\#1855](https://github.com/pypeclub/OpenPype/pull/1855) -- Settings list can use template or schema as object type [\#1815](https://github.com/pypeclub/OpenPype/pull/1815) + +**πŸ› Bug fixes** + +- Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) +- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) +- Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929) +- Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926) +- Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922) +- standalone: validator asset parents [\#1917](https://github.com/pypeclub/OpenPype/pull/1917) +- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916) +- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914) +- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906) +- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904) +- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903) +- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902) + +**Merged pull requests:** + +- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937) +- Add support for multiple Deadline β˜ οΈβž– servers [\#1905](https://github.com/pypeclub/OpenPype/pull/1905) ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) diff --git a/openpype/version.py b/openpype/version.py index 17bd0ff892..3f166f0735 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.0-nightly.4" +__version__ = "3.4.0-nightly.5" From 90cea71a024532d46fbe8b9eca865b19cb07cb37 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Sep 2021 20:06:19 +0100 Subject: [PATCH 334/716] Fix changelog generation for release --- .github/workflows/release.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37e1cb4b15..5d3f301b99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,11 +39,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - breakingLabel: '**πŸ’₯ Breaking**' - enhancementLabel: '**πŸš€ Enhancements**' - bugsLabel: '**πŸ› Bug fixes**' - deprecatedLabel: '**⚠️ Deprecations**' - addSections: '{"documentation":{"prefix":"### πŸ“– Documentation","labels":["documentation"]},"tests":{"prefix":"### βœ… Testing","labels":["tests"]}}' + addSections: '{"tests":{"prefix":"### βœ… Testing","labels":["tests"]},"feature":{"prefix":"**πŸ†• New features**", "labels":["type: feature"]},"breaking":{"prefix":"**πŸ’₯ Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**πŸš€ Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**πŸ› Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]},"documentation":{"prefix":"### πŸ“– Documentation","labels":["type: documentation"]}}' issues: false issuesWoLabels: false sinceTag: "3.0.0" @@ -85,11 +81,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - breakingLabel: '**πŸ’₯ Breaking**' - enhancementLabel: '**πŸš€ Enhancements**' - bugsLabel: '**πŸ› Bug fixes**' - deprecatedLabel: '**⚠️ Deprecations**' - addSections: '{"documentation":{"prefix":"### πŸ“– Documentation","labels":["documentation"]},"tests":{"prefix":"### βœ… Testing","labels":["tests"]}}' + addSections: '{"documentation":{"prefix":"### πŸ“– Documentation","labels":["type: documentation"]},"tests":{"prefix":"### βœ… Testing","labels":["tests"]},"feature":{"prefix":"**πŸ†• New features**", "labels":["type: feature"]},"breaking":{"prefix":"**πŸ’₯ Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**πŸš€ Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**πŸ› Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}' issues: false issuesWoLabels: false sinceTag: ${{ steps.version.outputs.last_release }} From 912b92d00bb6baf39ae9ecc86f27ce86573630af Mon Sep 17 00:00:00 2001 From: OpenPype Date: Fri, 17 Sep 2021 19:17:01 +0000 Subject: [PATCH 335/716] [Automated] Bump version --- CHANGELOG.md | 18 +++++++++--------- openpype/version.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3106a878b0..c5bd5d5554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.4.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.4.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...HEAD) @@ -8,7 +8,10 @@ - Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) - Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) -- OpenPype: Add version validation and `--headless` mode and update progress πŸ”„ [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) + +**πŸ†• New features** + +- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) **πŸš€ Enhancements** @@ -19,7 +22,6 @@ - General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) - Ftrack: Sync to avalon actions have jobs [\#2015](https://github.com/pypeclub/OpenPype/pull/2015) - Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009) -- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) - Settings UI: Number with configurable steps [\#2001](https://github.com/pypeclub/OpenPype/pull/2001) - Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996) - Configurable items for providers without Settings [\#1987](https://github.com/pypeclub/OpenPype/pull/1987) @@ -31,6 +33,7 @@ - Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962) - Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959) - Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949) +- OpenPype: Add version validation and `--headless` mode and update progress πŸ”„ [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) **πŸ› Bug fixes** @@ -38,6 +41,7 @@ - Ftrack: Delete old versions missing settings key [\#2037](https://github.com/pypeclub/OpenPype/pull/2037) - Nuke: typo on a button [\#2034](https://github.com/pypeclub/OpenPype/pull/2034) - Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033) +- FFmpeg: Subprocess arguments as list [\#2032](https://github.com/pypeclub/OpenPype/pull/2032) - General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016) - Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006) - Nuke thumbnails generated from middle of the sequence [\#1992](https://github.com/pypeclub/OpenPype/pull/1992) @@ -55,7 +59,6 @@ - Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982) - Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974) -- Bump url-parse from 1.5.1 to 1.5.3 in /website [\#1958](https://github.com/pypeclub/OpenPype/pull/1958) - CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) @@ -77,14 +80,11 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.0-nightly.11...3.3.0) -### πŸ“– Documentation - -- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) -- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) - **πŸ†• New features** - Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) +- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) +- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) **πŸš€ Enhancements** diff --git a/openpype/version.py b/openpype/version.py index 3f166f0735..faf171c92b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.0-nightly.5" +__version__ = "3.4.0-nightly.6" From b201acdda477d82a461641667f237616a1ae2644 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Fri, 17 Sep 2021 19:32:05 +0000 Subject: [PATCH 336/716] [Automated] Release --- CHANGELOG.md | 23 ++++++++--------------- openpype/version.py | 2 +- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5bd5d5554..ffeb09a531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,8 @@ # Changelog -## [3.4.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.4.0](https://github.com/pypeclub/OpenPype/tree/3.4.0) (2021-09-17) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...HEAD) - -### πŸ“– Documentation - -- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) -- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...3.4.0) **πŸ†• New features** @@ -26,12 +21,14 @@ - Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996) - Configurable items for providers without Settings [\#1987](https://github.com/pypeclub/OpenPype/pull/1987) - Global: Example addons [\#1986](https://github.com/pypeclub/OpenPype/pull/1986) +- Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982) - Settings UI: Number sliders [\#1978](https://github.com/pypeclub/OpenPype/pull/1978) - Workfiles: Support more workfile templates [\#1966](https://github.com/pypeclub/OpenPype/pull/1966) - Launcher: Fix crashes on action click [\#1964](https://github.com/pypeclub/OpenPype/pull/1964) - Settings: Minor fixes in UI and missing default values [\#1963](https://github.com/pypeclub/OpenPype/pull/1963) - Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962) - Global: Settings defined by Addons/Modules [\#1959](https://github.com/pypeclub/OpenPype/pull/1959) +- CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) - Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949) - OpenPype: Add version validation and `--headless` mode and update progress πŸ”„ [\#1939](https://github.com/pypeclub/OpenPype/pull/1939) @@ -50,25 +47,21 @@ - Last workfile with multiple work templates [\#1981](https://github.com/pypeclub/OpenPype/pull/1981) - Collectors order [\#1977](https://github.com/pypeclub/OpenPype/pull/1977) - Stop timer was within validator order range. [\#1975](https://github.com/pypeclub/OpenPype/pull/1975) +- Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974) - Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972) - Deadline: Houdini plugins in different hierarchy [\#1970](https://github.com/pypeclub/OpenPype/pull/1970) - Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) - Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961) -**Merged pull requests:** +### πŸ“– Documentation -- Standalone Publisher: Extract harmony zip handle workfile template [\#1982](https://github.com/pypeclub/OpenPype/pull/1982) -- Ftrack: arrow submodule has https url source [\#1974](https://github.com/pypeclub/OpenPype/pull/1974) -- CI: change release numbering triggers [\#1954](https://github.com/pypeclub/OpenPype/pull/1954) +- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) +- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) -**πŸš€ Enhancements** - -- Global: Define hosts in CollectSceneVersion [\#1948](https://github.com/pypeclub/OpenPype/pull/1948) - **πŸ› Bug fixes** - TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) diff --git a/openpype/version.py b/openpype/version.py index faf171c92b..2d9c68a032 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.0-nightly.6" +__version__ = "3.4.0" From 4faf6d11b71255191a7a98ad5c71a2f5afd68a80 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Sep 2021 23:09:20 +0100 Subject: [PATCH 337/716] get release type from labels on github PR --- .github/workflows/prerelease.yml | 4 ++-- tools/ci_tools.py | 40 ++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index ddab0e59a8..0fb07be79d 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -20,12 +20,12 @@ jobs: python-version: 3.7 - name: Install Python requirements - run: pip install gitpython semver + run: pip install gitpython semver PyGithub - name: πŸ”Ž Determine next version type id: version_type run: | - TYPE=$(python ./tools/ci_tools.py --bump) + TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.GITHUB_TOKEN }}) echo ::set-output name=type::$TYPE diff --git a/tools/ci_tools.py b/tools/ci_tools.py index 3c1aaae991..d087ee08e5 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -3,7 +3,31 @@ import sys from semver import VersionInfo from git import Repo from optparse import OptionParser +from github import Github +import os +def get_release_type_github(Log, github_token): + # print(Log) + minor_labels = ["type: feature", "type: deprecated"] + patch_labels = ["type: enhancement", "type: bug"] + + g = Github(github_token) + repo = g.get_repo("pypeclub/ci-testing") + + for line in Log.splitlines(): + print(line) + match = re.search("pull request #(\d+)", line) + if match: + pr_number = match.group(1) + pr = repo.get_pull(int(pr_number)) + for label in pr.labels: + print(label.name) + if label.name in minor_labels: + return ("minor") + elif label.name in patch_labels: + return("patch") + return None + def remove_prefix(text, prefix): return text[text.startswith(prefix) and len(prefix):] @@ -36,7 +60,7 @@ def get_log_since_tag(version): def release_type(log): regex_minor = ["feature/", "(feat)"] - regex_patch = ["bugfix/", "fix/", "(fix)", "enhancement/"] + regex_patch = ["bugfix/", "fix/", "(fix)", "enhancement/", "update"] for reg in regex_minor: if re.search(reg, log): return "minor" @@ -135,17 +159,23 @@ def main(): parser.add_option("-l", "--lastversion", dest="lastversion", action="store", help="work with explicit version") + parser.add_option("-g", "--github_token", + dest="github_token", action="store", + help="github token") (options, args) = parser.parse_args() if options.bump: - last_CI, last_CI_tag = get_last_version("CI") last_release, last_release_tag = get_last_version("release") - bump_type_CI = release_type(get_log_since_tag(last_CI_tag)) - bump_type_release = release_type(get_log_since_tag(last_release_tag)) - if bump_type_CI is None or bump_type_release is None: + bump_type_release = get_release_type_github( + get_log_since_tag(last_release_tag), + options.github_token + ) + if bump_type_release is None: print("skip") + else: + print(bump_type_release) if options.nightly: next_tag_v = calculate_next_nightly() From da5fca2aac36da5c40827bd377bf1a4081603765 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Sep 2021 23:10:33 +0100 Subject: [PATCH 338/716] get correct repo --- tools/ci_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci_tools.py b/tools/ci_tools.py index d087ee08e5..3e20c1b21c 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -12,7 +12,7 @@ def get_release_type_github(Log, github_token): patch_labels = ["type: enhancement", "type: bug"] g = Github(github_token) - repo = g.get_repo("pypeclub/ci-testing") + repo = g.get_repo("pypeclub/OpenPype") for line in Log.splitlines(): print(line) From 463fbe93bc7bcaa3d139808f4f3f16e9db91c8e7 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 18 Sep 2021 03:38:17 +0000 Subject: [PATCH 339/716] [Automated] Bump version --- CHANGELOG.md | 26 ++++++++++++++++++-------- openpype/version.py | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffeb09a531..2ca8de17ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ # Changelog +## [3.5.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...HEAD) + +**πŸš€ Enhancements** + +- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) + ## [3.4.0](https://github.com/pypeclub/OpenPype/tree/3.4.0) (2021-09-17) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...3.4.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0) + +### πŸ“– Documentation + +- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) +- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) **πŸ†• New features** @@ -53,15 +66,14 @@ - Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) - Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961) -### πŸ“– Documentation - -- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) -- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) - ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) +**πŸš€ Enhancements** + +- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) + **πŸ› Bug fixes** - TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) @@ -82,7 +94,6 @@ **πŸš€ Enhancements** - Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940) -- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) - Check for missing ✨ Python when using `pyenv` [\#1925](https://github.com/pypeclub/OpenPype/pull/1925) - Settings: Default values for enum [\#1920](https://github.com/pypeclub/OpenPype/pull/1920) - Settings UI: Modifiable dict view enhance [\#1919](https://github.com/pypeclub/OpenPype/pull/1919) @@ -94,7 +105,6 @@ **πŸ› Bug fixes** - Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) -- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) - Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929) - Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926) - Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922) diff --git a/openpype/version.py b/openpype/version.py index 2d9c68a032..f8ed9c7c2f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.0" +__version__ = "3.5.0-nightly.1" From f1960cd240491b4b2dfed1e80a18cee8774424e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:17:12 +0200 Subject: [PATCH 340/716] function to create deffered value change timer --- openpype/tools/settings/settings/lib.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 openpype/tools/settings/settings/lib.py diff --git a/openpype/tools/settings/settings/lib.py b/openpype/tools/settings/settings/lib.py new file mode 100644 index 0000000000..577aaa5671 --- /dev/null +++ b/openpype/tools/settings/settings/lib.py @@ -0,0 +1,18 @@ +from Qt import QtCore + +# Offset of value change trigger in ms +VALUE_CHANGE_OFFSET_MS = 300 + + +def create_deffered_value_change_timer(callback): + """Deffer value change callback. + + UI won't trigger all callbacks on each value change but after predefined + time. Timer is reset on each start so callback is triggered after user + finish editing. + """ + timer = QtCore.QTimer() + timer.setSingleShot(True) + timer.setInterval(VALUE_CHANGE_OFFSET_MS) + timer.timeout.connect(callback) + return timer From ab30681017cd973c177e7fb03f9deb4893866e55 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:18:10 +0200 Subject: [PATCH 341/716] InputWidget has value change timer all the time --- openpype/tools/settings/settings/base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 8235cf8642..ab6b27bdaf 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -3,6 +3,7 @@ import json from Qt import QtWidgets, QtGui, QtCore from openpype.tools.settings import CHILD_OFFSET from .widgets import ExpandingWidget +from .lib import create_deffered_value_change_timer class BaseWidget(QtWidgets.QWidget): @@ -329,6 +330,20 @@ class BaseWidget(QtWidgets.QWidget): class InputWidget(BaseWidget): + def __init__(self, *args, **kwargs): + super(InputWidget, self).__init__(*args, **kwargs) + + # Input widgets have always timer available (but may not be used). + self._value_change_timer = create_deffered_value_change_timer( + self._on_value_change_timer + ) + + def start_value_timer(self): + self._value_change_timer.start() + + def _on_value_change_timer(self): + pass + def create_ui(self): if self.entity.use_label_wrap: label = None From b4669e9ca6f7af1285faf4bd90515a0cc9db60f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:18:41 +0200 Subject: [PATCH 342/716] use value change deffer in basic input widgets --- openpype/tools/settings/settings/item_widgets.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index da74c2adc5..a28bee8d36 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -400,7 +400,9 @@ class TextWidget(InputWidget): def _on_value_change(self): if self.ignore_input_changes: return + self.start_value_timer() + def _on_value_change_timer(self): self.entity.set(self.input_value()) @@ -474,6 +476,9 @@ class NumberWidget(InputWidget): if self.ignore_input_changes: return + self.start_value_timer() + + def _on_value_change_timer(self): value = self.input_field.value() if self._slider_widget is not None and not self._ignore_input_change: self._ignore_slider_change = True @@ -571,7 +576,9 @@ class RawJsonWidget(InputWidget): def _on_value_change(self): if self.ignore_input_changes: return + self.start_value_timer() + def _on_value_change_timer(self): self._is_invalid = self.input_field.has_invalid_value() if not self.is_invalid: self.entity.set(self.input_field.json_value()) @@ -786,4 +793,7 @@ class PathInputWidget(InputWidget): def _on_value_change(self): if self.ignore_input_changes: return + self.start_value_timer() + + def _on_value_change_timer(self): self.entity.set(self.input_value()) From a1709c9f6cd4b39945d27db06b90b3662011a44b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:19:03 +0200 Subject: [PATCH 343/716] modifiable dictionary has offset key change --- openpype/tools/settings/settings/dict_mutable_widget.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index ba86fe82dd..21cd5c8962 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -3,6 +3,7 @@ from uuid import uuid4 from Qt import QtWidgets, QtCore, QtGui from .base import BaseWidget +from .lib import create_deffered_value_change_timer from .widgets import ( ExpandingWidget, IconButton @@ -284,6 +285,8 @@ class ModifiableDictItem(QtWidgets.QWidget): self.confirm_btn = None + self._key_change_timer = create_deffered_value_change_timer(self._on_timeout) + if collapsible_key: self.create_collapsible_ui() else: @@ -516,6 +519,10 @@ class ModifiableDictItem(QtWidgets.QWidget): if self.ignore_input_changes: return + self._key_change_timer.start() + + def _on_timeout(self): + key = self.key_value() is_key_duplicated = self.entity_widget.validate_key_duplication( self.temp_key, key, self ) From fd17935a6935e986f690aa55a75b2ddc56e126f9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:25:24 +0200 Subject: [PATCH 344/716] fix hound --- openpype/tools/settings/settings/dict_mutable_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 21cd5c8962..cfb9d4a4b1 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -285,7 +285,9 @@ class ModifiableDictItem(QtWidgets.QWidget): self.confirm_btn = None - self._key_change_timer = create_deffered_value_change_timer(self._on_timeout) + self._key_change_timer = create_deffered_value_change_timer( + self._on_timeout + ) if collapsible_key: self.create_collapsible_ui() From e086e2127678d42c466eca1e79e7cf4a5b90b2cb Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 20 Sep 2021 09:08:06 +0100 Subject: [PATCH 345/716] use github labels for nightly releases too --- tools/ci_tools.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tools/ci_tools.py b/tools/ci_tools.py index 3e20c1b21c..69f5158bb3 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -93,7 +93,7 @@ def bump_file_versions(version): file_regex_replace(filename, regex, pyproject_version) -def calculate_next_nightly(token="nightly"): +def calculate_next_nightly(type="nightly", github_token=None): last_prerelease, last_pre_tag = get_last_version("CI") last_pre_v = VersionInfo.parse(last_prerelease) last_pre_v_finalized = last_pre_v.finalize_version() @@ -102,7 +102,10 @@ def calculate_next_nightly(token="nightly"): last_release, last_release_tag = get_last_version("release") last_release_v = VersionInfo.parse(last_release) - bump_type = release_type(get_log_since_tag(last_release)) + bump_type = get_release_type_github( + get_log_since_tag(last_release_tag), + github_token + ) if not bump_type: return None @@ -110,10 +113,10 @@ def calculate_next_nightly(token="nightly"): # print(next_release_v) if next_release_v > last_pre_v_finalized: - next_tag = next_release_v.bump_prerelease(token=token).__str__() + next_tag = next_release_v.bump_prerelease(token=type).__str__() return next_tag elif next_release_v == last_pre_v_finalized: - next_tag = last_pre_v.bump_prerelease(token=token).__str__() + next_tag = last_pre_v.bump_prerelease(token=type).__str__() return next_tag def finalize_latest_nightly(): @@ -149,10 +152,10 @@ def main(): help="finalize latest prerelease to a release") parser.add_option("-p", "--prerelease", dest="prerelease", action="store", - help="define prerelease token") + help="define prerelease type") parser.add_option("-f", "--finalize", dest="finalize", action="store", - help="define prerelease token") + help="define prerelease type") parser.add_option("-v", "--version", dest="version", action="store", help="work with explicit version") @@ -178,7 +181,7 @@ def main(): print(bump_type_release) if options.nightly: - next_tag_v = calculate_next_nightly() + next_tag_v = calculate_next_nightly(github_token=options.github_token) print(next_tag_v) bump_file_versions(next_tag_v) From e86675a5afa388b5f156019ff783e7ff6bb68180 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 20 Sep 2021 10:32:36 +0200 Subject: [PATCH 346/716] Removed unwanted configuration of families Plugin should run only when publishing workfile, configuration of families doesn't make sense. --- .../nuke/plugins/publish/increment_script_version.py | 2 +- openpype/settings/defaults/project_settings/nuke.json | 8 +------- .../projects_schema/schemas/schema_nuke_publish.json | 6 ------ 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/increment_script_version.py b/openpype/hosts/nuke/plugins/publish/increment_script_version.py index 47fccb9125..f55ed21ee2 100644 --- a/openpype/hosts/nuke/plugins/publish/increment_script_version.py +++ b/openpype/hosts/nuke/plugins/publish/increment_script_version.py @@ -9,7 +9,7 @@ class IncrementScriptVersion(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder + 0.9 label = "Increment Script Version" optional = True - families = ["workfile", "render", "render.local", "render.farm"] + families = ["workfile"] hosts = ['nuke'] def process(self, context): diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index c1c3e77684..467849cc36 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -99,13 +99,7 @@ }, "IncrementScriptVersion": { "optional": true, - "active": true, - "families": [ - "workfile", - "render", - "render.local", - "render.farm" - ] + "active": true } }, "load": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index d354ff15f8..df5015b551 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -176,12 +176,6 @@ "type": "boolean", "key": "active", "label": "Active" - }, - { - "type": "list", - "key": "families", - "object_type": "text", - "label": "Trigger on families" } ] } From f31ec7bbf3e35365ec6a265237e77becce0f9412 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 20 Sep 2021 11:32:40 +0200 Subject: [PATCH 347/716] Removed shell flag in subprocess call Shell flag causes issue when ffmpeg is called with a list of arguments --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 2 +- .../plugins/publish/extract_trim_video_audio.py | 2 +- openpype/plugins/publish/extract_jpeg_exr.py | 3 ++- openpype/plugins/publish/extract_review.py | 2 +- openpype/plugins/publish/extract_review_slate.py | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index cdbfe942f0..62e2cf7328 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -108,7 +108,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # run subprocess self.log.debug("Executing: {}".format(" ".join(subprocess_args))) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_args, logger=self.log ) # remove thumbnail key from origin repre diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py index 1cbf186a6c..c18de5bc1c 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py @@ -84,7 +84,7 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): joined_args = " ".join(ffmpeg_args) self.log.info(f"Processing: {joined_args}") openpype.api.run_subprocess( - ffmpeg_args, shell=True, logger=self.log + ffmpeg_args, logger=self.log ) repre = { diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 31e58025d5..25fb3b7b41 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -122,13 +122,14 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.debug("{}".format(subprocess_command)) try: # temporary until oiiotool is supported cross platform run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_args, logger=self.log ) except RuntimeError as exp: if "Compression" in str(exp): self.log.debug("Unsupported compression on input files. " + "Skipping!!!") return + self.log.warning("Conversion crashed", exc_info=True) raise if "representations" not in instance.data: diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index ecc49a8da6..bdcb595197 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -228,7 +228,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_args, logger=self.log ) # delete files added to fill gaps diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 4d26fd1ebc..fbd57bdf36 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -209,7 +209,7 @@ class ExtractReviewSlate(openpype.api.Extractor): "Slate Executing: {}".format(" ".join(slate_subprocess_args)) ) openpype.api.run_subprocess( - slate_subprocess_args, shell=True, logger=self.log + slate_subprocess_args, logger=self.log ) # create ffmpeg concat text file path @@ -244,7 +244,7 @@ class ExtractReviewSlate(openpype.api.Extractor): "Executing concat: {}".format(" ".join(concat_args)) ) openpype.api.run_subprocess( - concat_args, shell=True, logger=self.log + concat_args, logger=self.log ) self.log.debug("__ repre[tags]: {}".format(repre["tags"])) From 7bb87a14a2221ad0c85ad89e263cc23bb5de90ea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 11:45:30 +0200 Subject: [PATCH 348/716] fixed filling of families --- openpype/tools/loader/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 881e9c206b..d8c42250c7 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -905,12 +905,12 @@ class FamilyModel(QtGui.QStandardItemModel): | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable ) + new_items.append(item) + self._items_by_family[family] = item else: item = self._items_by_family[label] item.setData(QtCore.Qt.DisplayRole, label) - new_items.append(item) - self._items_by_family[family] = item item.setCheckState(state) @@ -942,7 +942,7 @@ class FamilyProxyFiler(QtCore.QSortFilterProxyModel): def set_filter_enabled(self, enabled=None): if enabled is None: enabled = not self._filtering_enabled - if self._filtering_enabled == enabled: + elif self._filtering_enabled == enabled: return self._filtering_enabled = enabled From 30cadfd6ad989317710f2668643e6df4e9e902bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 13:09:01 +0200 Subject: [PATCH 349/716] don't use 'split_command_to_list' which may break paths if they are incorrectly used --- .../plugins/publish/extract_thumbnail.py | 5 +--- openpype/lib/__init__.py | 2 -- openpype/lib/execute.py | 30 ------------------- openpype/plugins/publish/extract_jpeg_exr.py | 4 +-- .../publish/extract_otio_audio_tracks.py | 6 +--- openpype/plugins/publish/extract_review.py | 8 ++--- .../plugins/publish/extract_review_slate.py | 8 ++--- 7 files changed, 8 insertions(+), 55 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index cdbfe942f0..d5eb0a8a45 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -101,14 +101,11 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): jpeg_items.append("\"{}\"".format(full_thumbnail_path)) subprocess_jpeg = " ".join(jpeg_items) - subprocess_args = openpype.lib.split_command_to_list( - subprocess_jpeg - ) # run subprocess self.log.debug("Executing: {}".format(" ".join(subprocess_args))) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_jpeg, shell=True, logger=self.log ) # remove thumbnail key from origin repre diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 4cf4a2f8ef..74004a1239 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -27,7 +27,6 @@ from .execute import ( get_pype_execute_args, execute, run_subprocess, - split_command_to_list, path_to_subprocess_arg, CREATE_NO_WINDOW ) @@ -174,7 +173,6 @@ __all__ = [ "get_pype_execute_args", "execute", "run_subprocess", - "split_command_to_list", "path_to_subprocess_arg", "CREATE_NO_WINDOW", diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 3e5b6d3853..a1111fba29 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -147,36 +147,6 @@ def path_to_subprocess_arg(path): return subprocess.list2cmdline([path]) -def split_command_to_list(string_command): - """Split string subprocess command to list. - - Should be able to split complex subprocess command to separated arguments: - `"C:\\ffmpeg folder\\ffmpeg.exe" -i \"D:\\input.mp4\\" \"D:\\output.mp4\"` - - Should result into list: - `["C:\ffmpeg folder\ffmpeg.exe", "-i", "D:\input.mp4", "D:\output.mp4"]` - - This may be required on few versions of python where subprocess can handle - only list of arguments. - - To be able do that is using `shlex` python module. - - Args: - string_command(str): Full subprocess command. - - Returns: - list: Command separated into individual arguments. - """ - if not string_command: - return [] - - kwargs = {} - # Use 'posix' argument only on windows - if platform.system().lower() == "windows": - kwargs["posix"] = False - return shlex.split(string_command, **kwargs) - - def get_pype_execute_args(*args): """Arguments to run pype command. diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 31e58025d5..725afb57e7 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -5,7 +5,6 @@ from openpype.lib import ( get_ffmpeg_tool_path, run_subprocess, - split_command_to_list, path_to_subprocess_arg, should_decompress, @@ -116,13 +115,12 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): jpeg_items.append(path_to_subprocess_arg(full_output_path)) subprocess_command = " ".join(jpeg_items) - subprocess_args = split_command_to_list(subprocess_command) # run subprocess self.log.debug("{}".format(subprocess_command)) try: # temporary until oiiotool is supported cross platform run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_command, shell=True, logger=self.log ) except RuntimeError as exp: if "Compression" in str(exp): diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 2cdc072ffd..9750a6df22 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -3,7 +3,6 @@ import pyblish import openpype.api from openpype.lib import ( get_ffmpeg_tool_path, - split_command_to_list, path_to_subprocess_arg ) import tempfile @@ -62,13 +61,10 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): cmd += self.create_cmd(audio_inputs) cmd += path_to_subprocess_arg(audio_temp_fpath) - # Split command to list for subprocess - cmd_list = split_command_to_list(cmd) - # run subprocess self.log.debug("Executing: {}".format(cmd)) openpype.api.run_subprocess( - cmd_list, logger=self.log + cmd, logger=self.log ) # remove empty diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index ecc49a8da6..f5d6789dd4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -14,7 +14,6 @@ from openpype.lib import ( get_ffmpeg_tool_path, ffprobe_streams, - split_command_to_list, path_to_subprocess_arg, should_decompress, @@ -220,15 +219,12 @@ class ExtractReview(pyblish.api.InstancePlugin): raise NotImplementedError subprcs_cmd = " ".join(ffmpeg_args) - subprocess_args = split_command_to_list(subprcs_cmd) # run subprocess - self.log.debug( - "Executing: {}".format(" ".join(subprocess_args)) - ) + self.log.debug("Executing: {}".format(subprcs_cmd)) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprcs_cmd, shell=True, logger=self.log ) # delete files added to fill gaps diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 4d26fd1ebc..aed146bb69 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -200,16 +200,14 @@ class ExtractReviewSlate(openpype.api.Extractor): " ".join(input_args), " ".join(output_args) ] - slate_subprocess_args = openpype.lib.split_command_to_list( - " ".join(slate_args) - ) + slate_subprocess_cmd = " ".join(slate_args) # run slate generation subprocess self.log.debug( - "Slate Executing: {}".format(" ".join(slate_subprocess_args)) + "Slate Executing: {}".format(slate_subprocess_cmd) ) openpype.api.run_subprocess( - slate_subprocess_args, shell=True, logger=self.log + slate_subprocess_cmd, shell=True, logger=self.log ) # create ffmpeg concat text file path From 2506d0f0d6e4e7a419f078f2b2b644a8f1a31627 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 20 Sep 2021 17:44:02 +0200 Subject: [PATCH 350/716] Changed setting schema to include enabled pill --- openpype/settings/defaults/project_settings/nuke.json | 1 + .../projects_schema/schemas/schema_nuke_publish.json | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 467849cc36..fb10d30f67 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -98,6 +98,7 @@ "viewer_lut_raw": false }, "IncrementScriptVersion": { + "enabled": true, "optional": true, "active": true } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index df5015b551..f385f6149f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -162,11 +162,17 @@ }, { "type": "dict", - "collapsible": false, + "collapsible": true, + "checkbox_key": "enabled", "key": "IncrementScriptVersion", "label": "IncrementScriptVersion", "is_group": true, "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "boolean", "key": "optional", From d0958304fab082b270b82bbbf1bcd5c3c866c613 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 17:48:53 +0200 Subject: [PATCH 351/716] fixed method arguments order --- openpype/tools/loader/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index d8c42250c7..6b94fc6e44 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -910,7 +910,7 @@ class FamilyModel(QtGui.QStandardItemModel): else: item = self._items_by_family[label] - item.setData(QtCore.Qt.DisplayRole, label) + item.setData(label, QtCore.Qt.DisplayRole) item.setCheckState(state) From 595f441947a6bb149a349dc9212fc8bcdaa0c00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 20 Sep 2021 18:21:44 +0200 Subject: [PATCH 352/716] remove submodules --- openpype/modules/ftrack/python2_vendor/arrow | 1 - openpype/modules/ftrack/python2_vendor/ftrack-python-api | 1 - 2 files changed, 2 deletions(-) delete mode 160000 openpype/modules/ftrack/python2_vendor/arrow delete mode 160000 openpype/modules/ftrack/python2_vendor/ftrack-python-api diff --git a/openpype/modules/ftrack/python2_vendor/arrow b/openpype/modules/ftrack/python2_vendor/arrow deleted file mode 160000 index b746fedf72..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b746fedf7286c3755a46f07ab72f4c414cd41fc0 diff --git a/openpype/modules/ftrack/python2_vendor/ftrack-python-api b/openpype/modules/ftrack/python2_vendor/ftrack-python-api deleted file mode 160000 index d277f474ab..0000000000 --- a/openpype/modules/ftrack/python2_vendor/ftrack-python-api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d277f474ab016e7b53479c36af87cb861d0cc53e From 885b84fc1584d3648eefeedd08f2aa4dff35d904 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 19:19:15 +0200 Subject: [PATCH 353/716] ftrack module does not check IFtrackEventHandlerPaths --- .../default_modules/ftrack/ftrack_module.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 3732e762b4..cfce38d125 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -8,8 +8,7 @@ from openpype_interfaces import ( ITrayModule, IPluginPaths, ILaunchHookPaths, - ISettingsChangeListener, - IFtrackEventHandlerPaths + ISettingsChangeListener ) from openpype.settings import SaveWarningExc @@ -81,9 +80,17 @@ class FtrackModule( def connect_with_modules(self, enabled_modules): for module in enabled_modules: - if not isinstance(module, IFtrackEventHandlerPaths): + if not hasattr(module, "get_event_handler_paths"): continue - paths_by_type = module.get_event_handler_paths() or {} + + try: + paths_by_type = module.get_event_handler_paths() + except Exception: + continue + + if not isinstance(paths_by_type, dict): + continue + for key, value in paths_by_type.items(): if not value: continue From 9bf53533cd4e1f4afe6b127241bb15d75e301800 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 19:19:20 +0200 Subject: [PATCH 354/716] removed IFtrackEventHandlerPaths --- .../modules/default_modules/ftrack/interfaces.py | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 openpype/modules/default_modules/ftrack/interfaces.py diff --git a/openpype/modules/default_modules/ftrack/interfaces.py b/openpype/modules/default_modules/ftrack/interfaces.py deleted file mode 100644 index 16ce0d2e62..0000000000 --- a/openpype/modules/default_modules/ftrack/interfaces.py +++ /dev/null @@ -1,12 +0,0 @@ -from abc import abstractmethod -from openpype.modules import OpenPypeInterface - - -class IFtrackEventHandlerPaths(OpenPypeInterface): - """Other modules interface to return paths to ftrack event handlers. - - Expected output is dictionary with "server" and "user" keys. - """ - @abstractmethod - def get_event_handler_paths(self): - pass From db095da94a4dc1b22a893286476e633fdf3f1b00 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 19:19:31 +0200 Subject: [PATCH 355/716] clockify is not using IFtrackEventHandlerPaths --- .../modules/default_modules/clockify/clockify_module.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/clockify/clockify_module.py b/openpype/modules/default_modules/clockify/clockify_module.py index 0de62d8ba4..5f3c247413 100644 --- a/openpype/modules/default_modules/clockify/clockify_module.py +++ b/openpype/modules/default_modules/clockify/clockify_module.py @@ -10,16 +10,14 @@ from .constants import ( from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITrayModule, - IPluginPaths, - IFtrackEventHandlerPaths + IPluginPaths ) class ClockifyModule( OpenPypeModule, ITrayModule, - IPluginPaths, - IFtrackEventHandlerPaths + IPluginPaths ): name = "clockify" @@ -94,7 +92,7 @@ class ClockifyModule( } def get_event_handler_paths(self): - """Implementaton of IFtrackEventHandlerPaths to get plugin paths.""" + """Function for Ftrack module to add ftrack event handler paths.""" return { "user": [CLOCKIFY_FTRACK_USER_PATH], "server": [CLOCKIFY_FTRACK_SERVER_PATH] From ba6934946f585899da148c924359d30e262312a6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 19:20:21 +0200 Subject: [PATCH 356/716] removed unused functions --- openpype/modules/default_modules/ftrack/lib/__init__.py | 4 +--- openpype/modules/default_modules/ftrack/lib/settings.py | 9 --------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/lib/__init__.py b/openpype/modules/default_modules/ftrack/lib/__init__.py index 9dc2d67279..433a1f7881 100644 --- a/openpype/modules/default_modules/ftrack/lib/__init__.py +++ b/openpype/modules/default_modules/ftrack/lib/__init__.py @@ -5,8 +5,7 @@ from .constants import ( CUST_ATTR_TOOLS, CUST_ATTR_APPLICATIONS ) -from . settings import ( - get_ftrack_url_from_settings, +from .settings import ( get_ftrack_event_mongo_info ) from .custom_attributes import ( @@ -31,7 +30,6 @@ __all__ = ( "CUST_ATTR_TOOLS", "CUST_ATTR_APPLICATIONS", - "get_ftrack_url_from_settings", "get_ftrack_event_mongo_info", "default_custom_attributes_definition", diff --git a/openpype/modules/default_modules/ftrack/lib/settings.py b/openpype/modules/default_modules/ftrack/lib/settings.py index 027356edc6..bf44981de0 100644 --- a/openpype/modules/default_modules/ftrack/lib/settings.py +++ b/openpype/modules/default_modules/ftrack/lib/settings.py @@ -1,13 +1,4 @@ import os -from openpype.api import get_system_settings - - -def get_ftrack_settings(): - return get_system_settings()["modules"]["ftrack"] - - -def get_ftrack_url_from_settings(): - return get_ftrack_settings()["ftrack_server"] def get_ftrack_event_mongo_info(): From 886188a489f739c6609c9c7df35d86c8c5a0eff5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 19:23:55 +0200 Subject: [PATCH 357/716] change expected method name to 'get_ftrack_event_handler_paths' --- openpype/modules/default_modules/clockify/clockify_module.py | 2 +- openpype/modules/default_modules/ftrack/ftrack_module.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/clockify/clockify_module.py b/openpype/modules/default_modules/clockify/clockify_module.py index 5f3c247413..932ce87c36 100644 --- a/openpype/modules/default_modules/clockify/clockify_module.py +++ b/openpype/modules/default_modules/clockify/clockify_module.py @@ -91,7 +91,7 @@ class ClockifyModule( "actions": [actions_path] } - def get_event_handler_paths(self): + def get_ftrack_event_handler_paths(self): """Function for Ftrack module to add ftrack event handler paths.""" return { "user": [CLOCKIFY_FTRACK_USER_PATH], diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index cfce38d125..c73f9b100d 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -80,11 +80,11 @@ class FtrackModule( def connect_with_modules(self, enabled_modules): for module in enabled_modules: - if not hasattr(module, "get_event_handler_paths"): + if not hasattr(module, "get_ftrack_event_handler_paths"): continue try: - paths_by_type = module.get_event_handler_paths() + paths_by_type = module.get_ftrack_event_handler_paths() except Exception: continue From 5b8832f4e3f1faa0d44a655fa4261f4ea2c5b94a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Sep 2021 22:23:38 +0000 Subject: [PATCH 358/716] Bump prismjs from 1.24.0 to 1.25.0 in /website Bumps [prismjs](https://github.com/PrismJS/prism) from 1.24.0 to 1.25.0. - [Release notes](https://github.com/PrismJS/prism/releases) - [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md) - [Commits](https://github.com/PrismJS/prism/compare/v1.24.0...v1.25.0) --- updated-dependencies: - dependency-name: prismjs dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index b4c12edeb6..066d156d97 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -6594,9 +6594,9 @@ prism-react-renderer@^1.1.1: integrity sha512-GHqzxLYImx1iKN1jJURcuRoA/0ygCcNhfGw1IT8nPIMzarmKQ3Nc+JcG0gi8JXQzuh0C5ShE4npMIoqNin40hg== prismjs@^1.23.0: - version "1.24.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.0.tgz#0409c30068a6c52c89ef7f1089b3ca4de56be2ac" - integrity sha512-SqV5GRsNqnzCL8k5dfAjCNhUrF3pR0A9lTDSCUZeh/LIshheXJEaP0hwLz2t4XHivd2J/v2HR+gRnigzeKe3cQ== + version "1.25.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756" + integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg== process-nextick-args@~2.0.0: version "2.0.1" From a19ffc1e56e17f765c416ffa3a8ee821ae229d14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 10:39:38 +0200 Subject: [PATCH 359/716] added shell=True back where command is executed as string --- openpype/plugins/publish/extract_jpeg_exr.py | 2 +- openpype/plugins/publish/extract_otio_audio_tracks.py | 2 +- openpype/plugins/publish/extract_review.py | 2 +- openpype/plugins/publish/extract_review_slate.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 48db35801e..3c08c1862d 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -120,7 +120,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.debug("{}".format(subprocess_command)) try: # temporary until oiiotool is supported cross platform run_subprocess( - subprocess_command, logger=self.log + subprocess_command, shell=True, logger=self.log ) except RuntimeError as exp: if "Compression" in str(exp): diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 9750a6df22..be0bae5cdc 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -64,7 +64,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # run subprocess self.log.debug("Executing: {}".format(cmd)) openpype.api.run_subprocess( - cmd, logger=self.log + cmd, shell=True, logger=self.log ) # remove empty diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index a6d652f00b..f5d6789dd4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -224,7 +224,7 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("Executing: {}".format(subprcs_cmd)) openpype.api.run_subprocess( - subprcs_cmd, logger=self.log + subprcs_cmd, shell=True, logger=self.log ) # delete files added to fill gaps diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index c3f8c78c61..7002168cdb 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -207,7 +207,7 @@ class ExtractReviewSlate(openpype.api.Extractor): "Slate Executing: {}".format(slate_subprocess_cmd) ) openpype.api.run_subprocess( - slate_subprocess_cmd, logger=self.log + slate_subprocess_cmd, shell=True, logger=self.log ) # create ffmpeg concat text file path From 4343cf7e27dae1566522e956327220374090f00e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 10:41:00 +0200 Subject: [PATCH 360/716] one more plugin where ffmpeg command is used as string --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index ab079f6c9c..24690cb840 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -105,7 +105,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # run subprocess self.log.debug("Executing: {}".format(subprocess_jpeg)) openpype.api.run_subprocess( - subprocess_jpeg, logger=self.log + subprocess_jpeg, shell=True, logger=self.log ) # remove thumbnail key from origin repre From 1bea470dd0e4c50f78a8dd971bd32ddcb4890376 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 21 Sep 2021 11:14:35 +0200 Subject: [PATCH 361/716] add support for pyenv on windows --- tools/build_win_installer.ps1 | 42 ++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/tools/build_win_installer.ps1 b/tools/build_win_installer.ps1 index a0832e0135..49fa803742 100644 --- a/tools/build_win_installer.ps1 +++ b/tools/build_win_installer.ps1 @@ -105,6 +105,46 @@ $env:BUILD_VERSION = $openpype_version iscc +Write-Host ">>> " -NoNewline -ForegroundColor green +Write-Host "Detecting host Python ... " -NoNewline +$python = "python" +if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { + $pyenv_python = & pyenv which python + if (Test-Path -PathType Leaf -Path "$($pyenv_python)") { + $python = $pyenv_python + } +} +if (-not (Get-Command $python -ErrorAction SilentlyContinue)) { + Write-Host "!!! Python not detected" -ForegroundColor red + Set-Location -Path $current_dir + Exit-WithCode 1 +} +$version_command = @' +import sys +print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1])) +'@ + +$p = & $python -c $version_command +$env:PYTHON_VERSION = $p +$m = $p -match '(\d+)\.(\d+)' +if(-not $m) { + Write-Host "!!! Cannot determine version" -ForegroundColor red + Set-Location -Path $current_dir + Exit-WithCode 1 +} +# We are supporting python 3.7 only +if (($matches[1] -lt 3) -or ($matches[2] -lt 7)) { + Write-Host "FAILED Version [ $p ] is old and unsupported" -ForegroundColor red + Set-Location -Path $current_dir + Exit-WithCode 1 +} elseif (($matches[1] -eq 3) -and ($matches[2] -gt 7)) { + Write-Host "WARNING Version [ $p ] is unsupported, use at your own risk." -ForegroundColor yellow + Write-Host "*** " -NoNewline -ForegroundColor yellow + Write-Host "OpenPype supports only Python 3.7" -ForegroundColor white +} else { + Write-Host "OK [ $p ]" -ForegroundColor green +} + Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Creating OpenPype installer ... " -ForegroundColor white @@ -114,7 +154,7 @@ from distutils.util import get_platform print('exe.{}-{}'.format(get_platform(), sys.version[0:3])) "@ -$build_dir = & python -c $build_dir_command +$build_dir = & $python -c $build_dir_command Write-Host "Build directory ... ${build_dir}" -ForegroundColor white $env:BUILD_DIR = $build_dir From ce98319ef264612f7b9eb19f58d634deafb8544c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 11:45:20 +0200 Subject: [PATCH 362/716] Nuke adding proxy mode validator --- .../plugins/publish/validate_proxy_mode.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py diff --git a/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py b/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py new file mode 100644 index 0000000000..9c6ca03ffd --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py @@ -0,0 +1,33 @@ +import pyblish +import nuke + + +class FixProxyMode(pyblish.api.Action): + """ + Togger off proxy switch OFF + """ + + label = "Proxy toggle to OFF" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + rootNode = nuke.root() + rootNode["proxy"].setValue(False) + + +@pyblish.api.log +class ValidateProxyMode(pyblish.api.ContextPlugin): + """Validate active proxy mode""" + + order = pyblish.api.ValidatorOrder + label = "Validate Proxy Mode" + hosts = ["nuke"] + actions = [FixProxyMode] + + def process(self, context): + + rootNode = nuke.root() + isProxy = rootNode["proxy"].value() + + assert not isProxy, "Proxy mode should be toggled OFF" From e24b142962b90d25f1bf371292c2966cdf953f36 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 12:53:23 +0200 Subject: [PATCH 363/716] added startup validations of ffmpeg and oiio tool --- start.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/start.py b/start.py index 00f9a50cbb..a02c06661f 100644 --- a/start.py +++ b/start.py @@ -96,6 +96,7 @@ Attributes: import os import re import sys +import platform import traceback import subprocess import site @@ -339,6 +340,60 @@ def set_modules_environments(): os.environ.update(env) +def is_tool(name): + try: + import os.errno as errno + except ImportError: + import errno + + try: + devnull = open(os.devnull, "w") + subprocess.Popen( + [name], stdout=devnull, stderr=devnull + ).communicate() + except OSError as exc: + if exc.errno == errno.ENOENT: + return False + return True + + +def _startup_validations(): + """Validations before OpenPype starts.""" + _validate_thirdparty_binaries() + + +def _validate_thirdparty_binaries(): + """Check existence of thirdpart executables.""" + low_platform = platform.system().lower() + binary_vendors_dir = os.path.join( + os.environ["OPENPYPE_ROOT"], + "vendor", + "bin" + ) + + error_msg = ( + "Missing binary dependency {}. Please fetch thirdparty dependencies." + ) + # Validate existence of FFmpeg + ffmpeg_dir = os.path.join(binary_vendors_dir, "ffmpeg", low_platform) + if low_platform == "windows": + ffmpeg_dir = os.path.join(ffmpeg_dir, "bin") + ffmpeg_executable = os.path.join(ffmpeg_dir, "ffmpeg") + if not is_tool(ffmpeg_executable): + raise RuntimeError(error_msg.format("FFmpeg")) + + # Validate existence of OpenImageIO (not on MacOs) + if low_platform != "darwin": + oiio_tool_path = os.path.join( + binary_vendors_dir, + "oiio", + low_platform, + "oiio_tool" + ) + if not is_tool(oiio_tool_path): + raise RuntimeError(error_msg.format("OpenImageIO")) + + def _process_arguments() -> tuple: """Process command line arguments. @@ -767,6 +822,11 @@ def boot(): # ------------------------------------------------------------------------ os.environ["OPENPYPE_ROOT"] = OPENPYPE_ROOT + # ------------------------------------------------------------------------ + # Do necessary startup validations + # ------------------------------------------------------------------------ + _startup_validations() + # ------------------------------------------------------------------------ # Play animation # ------------------------------------------------------------------------ From cda626d76fce511be8524b5358db6b37a977684f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 12:55:01 +0200 Subject: [PATCH 364/716] removed validator of ffmpeg --- .../publish/validate_ffmpeg_installed.py | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 openpype/plugins/publish/validate_ffmpeg_installed.py diff --git a/openpype/plugins/publish/validate_ffmpeg_installed.py b/openpype/plugins/publish/validate_ffmpeg_installed.py deleted file mode 100644 index a5390a07b2..0000000000 --- a/openpype/plugins/publish/validate_ffmpeg_installed.py +++ /dev/null @@ -1,34 +0,0 @@ -import pyblish.api -import os -import subprocess -import openpype.lib -try: - import os.errno as errno -except ImportError: - import errno - - -class ValidateFFmpegInstalled(pyblish.api.ContextPlugin): - """Validate availability of ffmpeg tool in PATH""" - - order = pyblish.api.ValidatorOrder - label = 'Validate ffmpeg installation' - optional = True - - def is_tool(self, name): - try: - devnull = open(os.devnull, "w") - subprocess.Popen( - [name], stdout=devnull, stderr=devnull - ).communicate() - except OSError as e: - if e.errno == errno.ENOENT: - return False - return True - - def process(self, context): - ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") - self.log.info("ffmpeg path: `{}`".format(ffmpeg_path)) - if self.is_tool("{}".format(ffmpeg_path)) is False: - self.log.error("ffmpeg not found in PATH") - raise RuntimeError('ffmpeg not installed.') From 7fe3fd1d11a66de9dbfbc479bd91cd0cb7d35013 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 14:06:25 +0200 Subject: [PATCH 365/716] show tkinter message box if validation crashes --- start.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/start.py b/start.py index a02c06661f..8b4ee97b09 100644 --- a/start.py +++ b/start.py @@ -359,7 +359,23 @@ def is_tool(name): def _startup_validations(): """Validations before OpenPype starts.""" - _validate_thirdparty_binaries() + try: + _validate_thirdparty_binaries() + except Exception as exc: + if os.environ.get("OPENPYPE_HEADLESS_MODE"): + raise + + from tkinter import Tk + from tkinter.messagebox import showerror + + root = Tk() + root.withdraw() + showerror( + "Startup validations didn't pass", + str(exc) + ) + root.destroy() + sys.exit(1) def _validate_thirdparty_binaries(): From 38bc581b574d9740e0b2f2865b582638f04ce73f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 14:41:43 +0200 Subject: [PATCH 366/716] nuke: securing plugin is loading only sequence representation --- openpype/hosts/nuke/plugins/load/load_sequence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/nuke/plugins/load/load_sequence.py b/openpype/hosts/nuke/plugins/load/load_sequence.py index 5f2128b10f..003b406ee7 100644 --- a/openpype/hosts/nuke/plugins/load/load_sequence.py +++ b/openpype/hosts/nuke/plugins/load/load_sequence.py @@ -76,6 +76,8 @@ class LoadSequence(api.Loader): file = file.replace("\\", "/") repr_cont = context["representation"]["context"] + assert repr_cont.get("frame"), "Representation is not sequence" + if "#" not in file: frame = repr_cont.get("frame") if frame: @@ -170,6 +172,7 @@ class LoadSequence(api.Loader): assert read_node.Class() == "Read", "Must be Read" repr_cont = representation["context"] + assert repr_cont.get("frame"), "Representation is not sequence" file = api.get_representation_path(representation) From 9df99db56ee338caa1b0af39c2b4434247917a24 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 14:42:12 +0200 Subject: [PATCH 367/716] standalone: jpg renamed to thumbnail --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 24690cb840..23f0b104c8 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -116,7 +116,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # create new thumbnail representation representation = { - 'name': 'jpg', + 'name': 'thumbnail', 'ext': 'jpg', 'files': filename, "stagingDir": staging_dir, From ce07679ead6414639f81872eee75f4f7b9c6e6a5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 18:02:38 +0200 Subject: [PATCH 368/716] fixing global plugin orders --- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_workfile.py | 2 ++ openpype/plugins/publish/collect_hierarchy.py | 2 +- openpype/plugins/publish/collect_otio_frame_ranges.py | 2 +- openpype/plugins/publish/collect_otio_review.py | 2 +- openpype/plugins/publish/collect_otio_subset_resources.py | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 936ea2be58..85b4e273d5 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -131,7 +131,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): self.create_shot_instance(context, **data) self.log.info("Creating instance: {}".format(instance)) - self.log.debug( + self.log.info( "_ instance.data: {}".format(pformat(instance.data))) if not with_audio: diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index ff5d516065..4f164acc91 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -7,6 +7,8 @@ from pprint import pformat from openpype.hosts.hiero.otio import hiero_export from Qt.QtGui import QPixmap import tempfile +reload(hiero_export) + class PrecollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" diff --git a/openpype/plugins/publish/collect_hierarchy.py b/openpype/plugins/publish/collect_hierarchy.py index 1aa10fcb9b..f7d1c6b4be 100644 --- a/openpype/plugins/publish/collect_hierarchy.py +++ b/openpype/plugins/publish/collect_hierarchy.py @@ -13,7 +13,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): """ label = "Collect Hierarchy" - order = pyblish.api.CollectorOrder - 0.57 + order = pyblish.api.CollectorOrder - 0.47 families = ["shot"] hosts = ["resolve", "hiero"] diff --git a/openpype/plugins/publish/collect_otio_frame_ranges.py b/openpype/plugins/publish/collect_otio_frame_ranges.py index e1b8b95a46..a35ef47e79 100644 --- a/openpype/plugins/publish/collect_otio_frame_ranges.py +++ b/openpype/plugins/publish/collect_otio_frame_ranges.py @@ -18,7 +18,7 @@ class CollectOcioFrameRanges(pyblish.api.InstancePlugin): Adding timeline and source ranges to instance data""" label = "Collect OTIO Frame Ranges" - order = pyblish.api.CollectorOrder - 0.58 + order = pyblish.api.CollectorOrder - 0.48 families = ["shot", "clip"] hosts = ["resolve", "hiero"] diff --git a/openpype/plugins/publish/collect_otio_review.py b/openpype/plugins/publish/collect_otio_review.py index e78ccc032c..10ceafdcca 100644 --- a/openpype/plugins/publish/collect_otio_review.py +++ b/openpype/plugins/publish/collect_otio_review.py @@ -20,7 +20,7 @@ class CollectOcioReview(pyblish.api.InstancePlugin): """Get matching otio track from defined review layer""" label = "Collect OTIO Review" - order = pyblish.api.CollectorOrder - 0.57 + order = pyblish.api.CollectorOrder - 0.47 families = ["clip"] hosts = ["resolve", "hiero"] diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 010430a303..dd670ff850 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -18,7 +18,7 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): """Get Resources for a subset version""" label = "Collect OTIO Subset Resources" - order = pyblish.api.CollectorOrder - 0.57 + order = pyblish.api.CollectorOrder - 0.47 families = ["clip"] hosts = ["resolve", "hiero"] From 0abede9e5d06bbd5431574159dcfab7fd2a89b24 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 18:24:32 +0200 Subject: [PATCH 369/716] hiero: otio is not ignoring disabled and offline clips --- openpype/hosts/hiero/otio/hiero_export.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/hiero/otio/hiero_export.py b/openpype/hosts/hiero/otio/hiero_export.py index ccc05d5fd7..af4322e3d9 100644 --- a/openpype/hosts/hiero/otio/hiero_export.py +++ b/openpype/hosts/hiero/otio/hiero_export.py @@ -378,6 +378,17 @@ def add_otio_metadata(otio_item, media_source, **kwargs): def create_otio_timeline(): + def set_prev_item(itemindex, track_item): + # Add Gap if needed + if itemindex == 0: + # if it is first track item at track then add + # it to previouse item + return track_item + + else: + # get previouse item + return track_item.parent().items()[itemindex - 1] + # get current timeline self.timeline = hiero.ui.activeSequence() self.project_fps = self.timeline.framerate().toFloat() @@ -396,14 +407,6 @@ def create_otio_timeline(): type(track), track.name()) for itemindex, track_item in enumerate(track): - # skip offline track items - if not track_item.isMediaPresent(): - continue - - # skip if track item is disabled - if not track_item.isEnabled(): - continue - # Add Gap if needed if itemindex == 0: # if it is first track item at track then add From 8cd3821ee4e000863bc7337dda5e486bea6103cd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 18:26:45 +0200 Subject: [PATCH 370/716] hound: suggestion --- openpype/hosts/hiero/plugins/publish/precollect_workfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 4f164acc91..7db155048f 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -7,7 +7,6 @@ from pprint import pformat from openpype.hosts.hiero.otio import hiero_export from Qt.QtGui import QPixmap import tempfile -reload(hiero_export) class PrecollectWorkfile(pyblish.api.ContextPlugin): From 1cd8aec44c07c75c264110b0a9bfecb035e093d2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 22 Sep 2021 03:07:20 +0200 Subject: [PATCH 371/716] 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 c2a51824d589175cbcb65f5150ddb6e7d95acc9c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 09:47:15 +0200 Subject: [PATCH 372/716] add more required libraries to centos docker --- Dockerfile.centos7 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index 0095ddff53..8b87654775 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -35,12 +35,15 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n openssl-devel \ openssl-libs \ tk-devel libffi-devel \ - qt5-qtbase-devel \ patchelf \ automake \ autoconf \ ncurses \ ncurses-devel \ + qt5-qtbase-devel \ + libxcb libxcb-devel \ + xcb-util xcb-util-devel \ + libxkbcommon-devel libxkbcommon-x11-devel && yum clean all # we need to build our own patchelf From 3d1f4fcdd42b8f87c0ebf728dc47e6d5815dee6a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 10:20:31 +0200 Subject: [PATCH 373/716] fixed endline --- Dockerfile.centos7 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index 8b87654775..67d45ce3b2 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -43,7 +43,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n qt5-qtbase-devel \ libxcb libxcb-devel \ xcb-util xcb-util-devel \ - libxkbcommon-devel libxkbcommon-x11-devel + libxkbcommon-devel libxkbcommon-x11-devel \ && yum clean all # we need to build our own patchelf From 0b4cd5885801487f92f622335f4c774738bcc8f7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 10:48:48 +0200 Subject: [PATCH 374/716] fix oiio executable name --- start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.py b/start.py index 8b4ee97b09..451db03a54 100644 --- a/start.py +++ b/start.py @@ -404,7 +404,7 @@ def _validate_thirdparty_binaries(): binary_vendors_dir, "oiio", low_platform, - "oiio_tool" + "oiiotool" ) if not is_tool(oiio_tool_path): raise RuntimeError(error_msg.format("OpenImageIO")) From 81f743bd21fcb16254c08f759fac209b5a3b8931 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 11:28:32 +0200 Subject: [PATCH 375/716] tkinter message is visible in taskbar --- start.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/start.py b/start.py index 451db03a54..f3adabd942 100644 --- a/start.py +++ b/start.py @@ -365,16 +365,20 @@ def _startup_validations(): if os.environ.get("OPENPYPE_HEADLESS_MODE"): raise - from tkinter import Tk + import tkinter from tkinter.messagebox import showerror - root = Tk() - root.withdraw() + root = tkinter.Tk() + root.attributes("-alpha", 0.0) + root.wm_state("iconic") + if platform.system().lower() != "windows": + root.withdraw() + showerror( "Startup validations didn't pass", str(exc) ) - root.destroy() + root.withdraw() sys.exit(1) From 48e120b25c687886d6ad45f52d69906f4fda63ba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 13:32:37 +0200 Subject: [PATCH 376/716] fix typo --- .../modules/default_modules/timers_manager/timers_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index e2c421bcfe..47ba0b4059 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -209,7 +209,7 @@ class TimersManager(OpenPypeModule, ITrayService, IIdleManager): self.widget_user_idle.refresh_context() self.is_running = False - self.timer_stopper(None) + self.timer_stopped(None) def connect_with_modules(self, enabled_modules): for module in enabled_modules: From 39c9df52b818938bf02f38c35277cf4dec92ab34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Sep 2021 12:28:52 +0000 Subject: [PATCH 377/716] Bump axios from 0.21.1 to 0.21.4 in /website Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.4. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.4) --- updated-dependencies: - dependency-name: axios dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 066d156d97..ae40005384 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2175,11 +2175,11 @@ autoprefixer@^10.0.2, autoprefixer@^10.2.5: postcss-value-parser "^4.1.0" axios@^0.21.1: - version "0.21.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" - integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== dependencies: - follow-redirects "^1.10.0" + follow-redirects "^1.14.0" babel-loader@^8.2.2: version "8.2.2" @@ -3982,10 +3982,10 @@ flux@^4.0.1: fbemitter "^3.0.0" fbjs "^3.0.0" -follow-redirects@^1.0.0, follow-redirects@^1.10.0: - version "1.13.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" - integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== +follow-redirects@^1.0.0, follow-redirects@^1.14.0: + version "1.14.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" + integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g== for-in@^1.0.2: version "1.0.2" From 950da8749efaed7eeb17ab9511de1c7ddd1075ed Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Sep 2021 16:11:43 +0200 Subject: [PATCH 378/716] Fix - project lists refresh each show up event Fix can_edit method --- .../modules/default_modules/sync_server/tray/app.py | 1 + .../default_modules/sync_server/tray/widgets.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/app.py b/openpype/modules/default_modules/sync_server/tray/app.py index 0299edb2eb..5298c7be1d 100644 --- a/openpype/modules/default_modules/sync_server/tray/app.py +++ b/openpype/modules/default_modules/sync_server/tray/app.py @@ -97,6 +97,7 @@ class SyncServerWindow(QtWidgets.QDialog): def showEvent(self, event): self.representationWidget.model.set_project( self.projects.current_project) + self.projects.refresh() self._set_running(True) super().showEvent(event) diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index e2009bd219..4fc5723f42 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -65,6 +65,7 @@ class SyncProjectListWidget(QtWidgets.QWidget): self.current_project = None self.project_name = None self.local_site = None + self.remote_site = None self.icons = {} def _on_index_change(self, new_idx, _old_idx): @@ -99,6 +100,11 @@ class SyncProjectListWidget(QtWidgets.QWidget): if project_name: self.local_site = self.sync_server.get_active_site(project_name) + self.remote_site = self.sync_server.get_remote_site(project_name) + + def _can_edit(self): + """Returns true if some site is user local site, eg. could edit""" + return get_local_site_id() in (self.local_site, self.remote_site) def _get_icon(self, status): if not self.icons.get(status): @@ -122,9 +128,7 @@ class SyncProjectListWidget(QtWidgets.QWidget): menu = QtWidgets.QMenu(self) actions_mapping = {} - can_edit = self.model.can_edit - - if can_edit: + if self._can_edit(): if self.sync_server.is_project_paused(self.project_name): action = QtWidgets.QAction("Unpause") actions_mapping[action] = self._unpause From a10f2379e5aa0ef9f9768631d71bc57923fd7201 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 17:16:50 +0200 Subject: [PATCH 379/716] define openpype_root in build_dependencies --- tools/build_dependencies.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/build_dependencies.py b/tools/build_dependencies.py index e5a430e220..dcdae3814d 100644 --- a/tools/build_dependencies.py +++ b/tools/build_dependencies.py @@ -93,18 +93,18 @@ _print("Getting venv site-packages ...") assert site_pkg, "No venv site-packages are found." _print(f"Working with: {site_pkg}", 2) - -build_dir = "exe.{}-{}".format(get_platform(), sys.version[0:3]) +openpype_root = Path(os.path.dirname(__file__)).parent # create full path if platform.system().lower() == "darwin": - build_dir = Path(os.path.dirname(__file__)).parent.joinpath( + build_dir = openpype_root.joinpath( "build", "OpenPype.app", "Contents", "MacOS") else: - build_dir = Path(os.path.dirname(__file__)).parent / "build" / build_dir + build_subdir = "exe.{}-{}".format(get_platform(), sys.version[0:3]) + build_dir = openpype_root / "build" / build_subdir _print(f"Using build at {build_dir}", 2) if not build_dir.exists(): From cd52b1e1805649c5f242a03e661dc51bbf9aabc0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 17:17:30 +0200 Subject: [PATCH 380/716] added rpath modifications using patchelf --- tools/build_dependencies.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tools/build_dependencies.py b/tools/build_dependencies.py index dcdae3814d..05ae07d406 100644 --- a/tools/build_dependencies.py +++ b/tools/build_dependencies.py @@ -23,6 +23,7 @@ import sys import site from distutils.util import get_platform import platform +import subprocess from pathlib import Path import shutil import blessed @@ -145,6 +146,29 @@ if platform.system().lower() == "windows": _print("Could not find {}".format(src), 1) sys.exit(1) +# On Linux use rpath from source libraries in destination libraries +if platform.system().lower() == "linux": + src_pyside_dir = openpype_root / "vendor" / "python" / "PySide2" + dst_pyside_dir = build_dir / "vendor" / "python" / "PySide2" + src_rpath_per_so_file = {} + for filepath in src_pyside_dir.glob("*.so"): + filename = filepath.name + rpath = ( + subprocess.check_output(["patchelf", "--print-rpath", filepath]) + .decode("utf-8") + .strip() + ) + src_rpath_per_so_file[filename] = rpath + + for filepath in dst_pyside_dir.glob("*.so"): + filename = filepath.name + if filename not in src_rpath_per_so_file: + continue + src_rpath = src_rpath_per_so_file[filename] + subprocess.check_call( + ["patchelf", "--set-rpath", src_rpath, filepath] + ) + to_delete = [] # _print("Finding duplicates ...") deps_items = list(deps_dir.iterdir()) From da57732b226a4ca37d96a0c0de1165babb6ba611 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 17:23:12 +0200 Subject: [PATCH 381/716] removed PyQt special code --- tools/build_dependencies.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tools/build_dependencies.py b/tools/build_dependencies.py index 05ae07d406..1798b7ca8f 100644 --- a/tools/build_dependencies.py +++ b/tools/build_dependencies.py @@ -136,15 +136,6 @@ progress_bar.close() # iterate over frozen libs and create list to delete libs_dir = build_dir / "lib" -# On Windows "python3.dll" is needed for PyQt5 from the build. -if platform.system().lower() == "windows": - src = Path(libs_dir / "PyQt5" / "python3.dll") - dst = Path(deps_dir / "PyQt5" / "python3.dll") - if src.exists(): - shutil.copyfile(src, dst) - else: - _print("Could not find {}".format(src), 1) - sys.exit(1) # On Linux use rpath from source libraries in destination libraries if platform.system().lower() == "linux": From 7d2d621c9ab589d1b34638fec182e2939290f77c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 22 Sep 2021 18:07:48 +0200 Subject: [PATCH 382/716] 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 39eb48abe31909222ccc996fedb6fe53158557d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 18:52:27 +0200 Subject: [PATCH 383/716] removed pyqt5 from poetry.lock --- poetry.lock | 60 ----------------------------------------------------- 1 file changed, 60 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6dae442c9d..968fda3d7b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -975,34 +975,6 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "pyqt5" -version = "5.15.4" -description = "Python bindings for the Qt cross platform application toolkit" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -PyQt5-Qt5 = ">=5.15" -PyQt5-sip = ">=12.8,<13" - -[[package]] -name = "pyqt5-qt5" -version = "5.15.2" -description = "The subset of a Qt installation needed by PyQt5." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pyqt5-sip" -version = "12.9.0" -description = "The sip module support for PyQt5" -category = "main" -optional = false -python-versions = ">=3.5" - [[package]] name = "pyrsistent" version = "0.18.0" @@ -2299,38 +2271,6 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] -pyqt5 = [ - {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-abi3-macosx_10_13_intel.whl", hash = "sha256:8c0848ba790a895801d5bfd171da31cad3e551dbcc4e59677a3b622de2ceca98"}, - {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:883a549382fc22d29a0568f3ef20b38c8e7ab633a59498ac4eb63a3bf36d3fd3"}, - {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-none-win32.whl", hash = "sha256:a88526a271e846e44779bb9ad7a738c6d3c4a9d01e15a128ecfc6dd4696393b7"}, - {file = "PyQt5-5.15.4-cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:213bebd51821ed89b4d5b35bb10dbe67564228b3568f463a351a08e8b1677025"}, - {file = "PyQt5-5.15.4.tar.gz", hash = "sha256:2a69597e0dd11caabe75fae133feca66387819fc9bc050f547e5551bce97e5be"}, -] -pyqt5-qt5 = [ - {file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"}, - {file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"}, - {file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"}, - {file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"}, -] -pyqt5-sip = [ - {file = "PyQt5_sip-12.9.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:d85002238b5180bce4b245c13d6face848faa1a7a9e5c6e292025004f2fd619a"}, - {file = "PyQt5_sip-12.9.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:83c3220b1ca36eb8623ba2eb3766637b19eb0ce9f42336ad8253656d32750c0a"}, - {file = "PyQt5_sip-12.9.0-cp36-cp36m-win32.whl", hash = "sha256:d8b2bdff7bbf45bc975c113a03b14fd669dc0c73e1327f02706666a7dd51a197"}, - {file = "PyQt5_sip-12.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:69a3ad4259172e2b1aa9060de211efac39ddd734a517b1924d9c6c0cc4f55f96"}, - {file = "PyQt5_sip-12.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42274a501ab4806d2c31659170db14c282b8313d2255458064666d9e70d96206"}, - {file = "PyQt5_sip-12.9.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a8701892a01a5a2a4720872361197cc80fdd5f49c8482d488ddf38c9c84f055"}, - {file = "PyQt5_sip-12.9.0-cp37-cp37m-win32.whl", hash = "sha256:ac57d796c78117eb39edd1d1d1aea90354651efac9d3590aac67fa4983f99f1f"}, - {file = "PyQt5_sip-12.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4347bd81d30c8e3181e553b3734f91658cfbdd8f1a19f254777f906870974e6d"}, - {file = "PyQt5_sip-12.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c446971c360a0a1030282a69375a08c78e8a61d568bfd6dab3dcc5cf8817f644"}, - {file = "PyQt5_sip-12.9.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fc43f2d7c438517ee33e929e8ae77132749c15909afab6aeece5fcf4147ffdb5"}, - {file = "PyQt5_sip-12.9.0-cp38-cp38-win32.whl", hash = "sha256:055581c6fed44ba4302b70eeb82e979ff70400037358908f251cd85cbb3dbd93"}, - {file = "PyQt5_sip-12.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:c5216403d4d8d857ec4a61f631d3945e44fa248aa2415e9ee9369ab7c8a4d0c7"}, - {file = "PyQt5_sip-12.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a25b9843c7da6a1608f310879c38e6434331aab1dc2fe6cb65c14f1ecf33780e"}, - {file = "PyQt5_sip-12.9.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:dd05c768c2b55ffe56a9d49ce6cc77cdf3d53dbfad935258a9e347cbfd9a5850"}, - {file = "PyQt5_sip-12.9.0-cp39-cp39-win32.whl", hash = "sha256:4f8e05fe01d54275877c59018d8e82dcdd0bc5696053a8b830eecea3ce806121"}, - {file = "PyQt5_sip-12.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:b09f4cd36a4831229fb77c424d89635fa937d97765ec90685e2f257e56a2685a"}, - {file = "PyQt5_sip-12.9.0.tar.gz", hash = "sha256:d3e4489d7c2b0ece9d203ae66e573939f7f60d4d29e089c9f11daa17cfeaae32"}, -] pyrsistent = [ {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, From 0bbea713fb6357708798aa530a07d7ef40b0cda0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 18:58:14 +0200 Subject: [PATCH 384/716] added current develop lock file --- poetry.lock | 684 +++++++++++++++++++++------------------------------- 1 file changed, 271 insertions(+), 413 deletions(-) diff --git a/poetry.lock b/poetry.lock index 968fda3d7b..e43c788d74 100644 --- a/poetry.lock +++ b/poetry.lock @@ -80,7 +80,7 @@ python-dateutil = ">=2.7.0" [[package]] name = "astroid" -version = "2.7.3" +version = "2.5.6" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -89,7 +89,6 @@ python-versions = "~=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} wrapt = ">=1.11,<1.13" [[package]] @@ -147,11 +146,11 @@ pytz = ">=2015.7" [[package]] name = "blessed" -version = "1.18.1" +version = "1.18.0" description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." category = "main" optional = false -python-versions = ">=2.7" +python-versions = "*" [package.dependencies] jinxed = {version = ">=0.5.4", markers = "platform_system == \"Windows\""} @@ -176,7 +175,7 @@ python-versions = "*" [[package]] name = "cffi" -version = "1.14.6" +version = "1.14.5" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -193,17 +192,6 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[[package]] -name = "charset-normalizer" -version = "2.0.4" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = ">=3.5.0" - -[package.extras] -unicode_backport = ["unicodedata2"] - [[package]] name = "click" version = "7.1.2" @@ -265,7 +253,7 @@ toml = ["toml"] [[package]] name = "cryptography" -version = "3.4.8" +version = "3.4.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -284,20 +272,15 @@ test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pret [[package]] name = "cx-freeze" -version = "6.7" +version = "6.6" description = "Create standalone executables from Python scripts" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -cx-logging = {version = ">=3.0", markers = "sys_platform == \"win32\""} -importlib-metadata = ">=4.3.1" - -[package.source] -type = "legacy" -url = "https://distribute.openpype.io/wheels" -reference = "openpype" +cx-Logging = {version = ">=3.0", markers = "sys_platform == \"win32\""} +importlib-metadata = ">=3.1.1" [[package]] name = "cx-logging" @@ -403,19 +386,19 @@ smmap = ">=3.0.1,<5" [[package]] name = "gitpython" -version = "3.1.20" +version = "3.1.17" description = "Python Git Library" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.5" [package.dependencies] gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} +typing-extensions = {version = ">=3.7.4.0", markers = "python_version < \"3.8\""} [[package]] name = "google-api-core" -version = "1.31.2" +version = "1.30.0" description = "Google API client core library" category = "main" optional = false @@ -453,7 +436,7 @@ uritemplate = ">=3.0.0,<4dev" [[package]] name = "google-auth" -version = "1.35.0" +version = "1.31.0" description = "Google Authentication Library" category = "main" optional = false @@ -510,11 +493,11 @@ pyparsing = ">=2.4.2,<3" [[package]] name = "idna" -version = "3.2" +version = "2.10" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "imagesize" @@ -526,7 +509,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.8.1" +version = "4.5.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -538,8 +521,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -perf = ["ipython"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -551,17 +533,16 @@ python-versions = "*" [[package]] name = "isort" -version = "5.9.3" +version = "5.8.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.6,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] -plugins = ["setuptools"] [[package]] name = "jedi" @@ -579,15 +560,14 @@ testing = ["colorama", "docopt", "pytest (>=3.1.0)"] [[package]] name = "jeepney" -version = "0.7.1" +version = "0.6.0" description = "Low-level, pure Python DBus protocol wrapper." category = "main" optional = false python-versions = ">=3.6" [package.extras] -test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] -trio = ["trio", "async-generator"] +test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio"] [[package]] name = "jinja2" @@ -715,11 +695,11 @@ reference = "openpype" [[package]] name = "packaging" -version = "21.0" +version = "20.9" description = "Core utilities for Python packages" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" @@ -738,7 +718,7 @@ testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "pathlib2" -version = "2.3.6" +version = "2.3.5" description = "Object-oriented filesystem paths" category = "main" optional = false @@ -749,38 +729,25 @@ six = "*" [[package]] name = "pillow" -version = "8.3.2" +version = "8.2.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.6" -[[package]] -name = "platformdirs" -version = "2.3.0" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] - [[package]] name = "pluggy" -version = "1.0.0" +version = "0.13.1" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] [[package]] name = "prefixed" @@ -882,7 +849,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.10.0" +version = "2.9.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -890,23 +857,22 @@ python-versions = ">=3.5" [[package]] name = "pylint" -version = "2.10.2" +version = "2.8.3" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] -astroid = ">=2.7.2,<2.8" +astroid = "2.5.6" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" -platformdirs = ">=2.2.0" toml = ">=0.7.1" [[package]] name = "pymongo" -version = "3.12.0" +version = "3.11.4" description = "Python driver for MongoDB " category = "main" optional = false @@ -914,9 +880,9 @@ python-versions = "*" [package.extras] aws = ["pymongo-auth-aws (<2.0.0)"] -encryption = ["pymongocrypt (>=1.1.0,<2.0.0)"] +encryption = ["pymongocrypt (<2.0.0)"] gssapi = ["pykerberos"] -ocsp = ["pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)", "certifi"] +ocsp = ["pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] snappy = ["python-snappy"] srv = ["dnspython (>=1.16.0,<1.17.0)"] tls = ["ipaddress"] @@ -977,15 +943,15 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pyrsistent" -version = "0.18.0" +version = "0.17.3" description = "Persistent/Functional/Immutable data structures" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.5" [[package]] name = "pytest" -version = "6.2.5" +version = "6.2.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -998,7 +964,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" +pluggy = ">=0.12,<1.0.0a1" py = ">=1.8.2" toml = "*" @@ -1023,21 +989,21 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale [[package]] name = "pytest-print" -version = "0.3.0" +version = "0.2.1" description = "pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout)" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -pytest = ">=6" +pytest = ">=3.0.0" [package.extras] -test = ["coverage (>=5)"] +test = ["coverage (>=5)", "pytest (>=4)"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.8.1" description = "Extensions to the standard Python datetime module" category = "main" optional = false @@ -1048,7 +1014,7 @@ six = ">=1.5" [[package]] name = "python-xlib" -version = "0.31" +version = "0.30" description = "Python X Library" category = "main" optional = false @@ -1091,7 +1057,7 @@ python-versions = "*" [[package]] name = "qt.py" -version = "1.3.6" +version = "1.3.3" description = "Python 2 & 3 compatibility wrapper around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5." category = "main" optional = false @@ -1112,21 +1078,21 @@ sphinx = ">=1.3.1" [[package]] name = "requests" -version = "2.26.0" +version = "2.25.1" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.27" [package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "rsa" @@ -1169,15 +1135,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "slack-sdk" -version = "3.10.1" +version = "3.6.0" description = "The Slack API Platform SDK for Python" category = "main" optional = false python-versions = ">=3.6.0" [package.extras] -optional = ["aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "SQLAlchemy (>=1,<2)", "websockets (>=9.1,<10)", "websocket-client (>=1,<2)"] -testing = ["pytest (>=5.4,<6)", "pytest-asyncio (<1)", "Flask-Sockets (>=0.2,<1)", "Flask (>=1,<2)", "Werkzeug (<2)", "pytest-cov (>=2,<3)", "codecov (>=2,<3)", "flake8 (>=3,<4)", "black (==21.7b0)", "psutil (>=5,<6)", "databases (>=0.3)", "boto3 (<=2)", "moto (<2)"] +optional = ["aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "SQLAlchemy (>=1,<2)", "websockets (>=9.1,<10)", "websocket-client (>=0.57,<1)"] +testing = ["pytest (>=5.4,<6)", "pytest-asyncio (<1)", "Flask-Sockets (>=0.2,<1)", "pytest-cov (>=2,<3)", "codecov (>=2,<3)", "flake8 (>=3,<4)", "black (==21.5b1)", "psutil (>=5,<6)", "databases (>=0.3)"] [[package]] name = "smmap" @@ -1205,7 +1171,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "4.1.2" +version = "4.0.2" description = "Python documentation generator" category = "dev" optional = false @@ -1224,14 +1190,14 @@ requests = ">=2.5.0" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-htmlhelp = "*" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" +sphinxcontrib-serializinghtml = "*" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.900)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"] test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] @@ -1373,7 +1339,7 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "3.10.0.2" +version = "3.10.0.0" description = "Backported and Experimental Type Hints for Python 3.5+" category = "main" optional = false @@ -1389,7 +1355,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "urllib3" -version = "1.26.6" +version = "1.26.5" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1459,7 +1425,7 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.5.0" +version = "3.4.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1467,12 +1433,12 @@ python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "ca2a0258a784674ff489a07d0dc8dd2a22373ee39add02cb4676898b8a6993a1" +content-hash = "8875d530ae66f9763b5b0cb84d9d35edc184ef5c141b63d38bf1ff5a1226e556" [metadata.files] acre = [] @@ -1536,8 +1502,8 @@ arrow = [ {file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"}, ] astroid = [ - {file = "astroid-2.7.3-py3-none-any.whl", hash = "sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e"}, - {file = "astroid-2.7.3.tar.gz", hash = "sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c"}, + {file = "astroid-2.5.6-py3-none-any.whl", hash = "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e"}, + {file = "astroid-2.5.6.tar.gz", hash = "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"}, ] async-timeout = [ {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, @@ -1560,8 +1526,8 @@ babel = [ {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] blessed = [ - {file = "blessed-1.18.1-py2.py3-none-any.whl", hash = "sha256:dd7c0d33db9a2e7f597b446996484d0ed46e1586239db064fb5025008937dcae"}, - {file = "blessed-1.18.1.tar.gz", hash = "sha256:8b09936def6bc06583db99b65636b980075733e13550cb6af262ce724a55da23"}, + {file = "blessed-1.18.0-py2.py3-none-any.whl", hash = "sha256:5b5e2f0563d5a668c282f3f5946f7b1abb70c85829461900e607e74d7725106e"}, + {file = "blessed-1.18.0.tar.gz", hash = "sha256:1312879f971330a1b7f2c6341f2ae7e2cbac244bfc9d0ecfbbecd4b0293bc755"}, ] cachetools = [ {file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"}, @@ -1572,60 +1538,48 @@ certifi = [ {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] cffi = [ - {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, - {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, - {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, - {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, - {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, - {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, - {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, - {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, - {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, - {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, - {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, - {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, - {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, - {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, - {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, - {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, - {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, - {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, - {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, - {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, - {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, - {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, - {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, - {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, - {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, + {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, + {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, + {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, + {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, + {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, + {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, + {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, + {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-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-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-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-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"}, ] chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] -charset-normalizer = [ - {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, - {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, -] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, @@ -1701,25 +1655,30 @@ coverage = [ {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] cryptography = [ - {file = "cryptography-3.4.8-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14"}, - {file = "cryptography-3.4.8-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"}, - {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e"}, - {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085"}, - {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b"}, - {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb"}, - {file = "cryptography-3.4.8-cp36-abi3-win32.whl", hash = "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7"}, - {file = "cryptography-3.4.8-cp36-abi3-win_amd64.whl", hash = "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc"}, - {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5"}, - {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af"}, - {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a"}, - {file = "cryptography-3.4.8-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06"}, - {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498"}, - {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7"}, - {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9"}, - {file = "cryptography-3.4.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e"}, - {file = "cryptography-3.4.8.tar.gz", hash = "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c"}, + {file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"}, + {file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"}, + {file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"}, + {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-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.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, +] +cx-freeze = [ + {file = "cx_Freeze-6.6-cp36-cp36m-win32.whl", hash = "sha256:b3d3a6bcd1a07c50b4e1c907f14842642156110e63a99cd5c73b8a24751e9b97"}, + {file = "cx_Freeze-6.6-cp36-cp36m-win_amd64.whl", hash = "sha256:1935266ec644ea4f7e584985f44cefc0622a449a09980d990833a1a2afcadac8"}, + {file = "cx_Freeze-6.6-cp37-cp37m-win32.whl", hash = "sha256:1eac2b0f254319cc641ce25bd83337effd7936092562fde701f3ffb40e0274ec"}, + {file = "cx_Freeze-6.6-cp37-cp37m-win_amd64.whl", hash = "sha256:2bc46ef6d510811b6002f34a3ae4cbfdea44e18644febd2a404d3ee8e48a9fc4"}, + {file = "cx_Freeze-6.6-cp38-cp38-win32.whl", hash = "sha256:46eb50ebc46f7ae236d16c6a52671ab0f7bb479bea668da19f4b6de3cc413e9e"}, + {file = "cx_Freeze-6.6-cp38-cp38-win_amd64.whl", hash = "sha256:8c3b00476ce385bb58595bffce55aed031e5a6e16ab6e14d8bee9d1d569e46c3"}, + {file = "cx_Freeze-6.6-cp39-cp39-win32.whl", hash = "sha256:6e9340cbcf52d4836980ecc83ddba4f7704ff6654dd41168c146b74f512977ce"}, + {file = "cx_Freeze-6.6-cp39-cp39-win_amd64.whl", hash = "sha256:2fcf1c8b77ae5c06f45be3a9aff79e1dd808c0d624e97561f840dec5ea9b214a"}, + {file = "cx_Freeze-6.6.tar.gz", hash = "sha256:c4af8ad3f7e7d71e291c1dec5d0fb26bbe92df834b098ed35434c901fbd6762f"}, ] -cx-freeze = [] cx-logging = [ {file = "cx_Logging-3.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9fcd297e5c51470521c47eff0f86ba844aeca6be97e13c3e2114ebdf03fa3c96"}, {file = "cx_Logging-3.0-cp36-cp36m-win32.whl", hash = "sha256:0df4be47c5022cc54316949e283403214568ef599817ced0c0972183d6d4fabb"}, @@ -1766,20 +1725,20 @@ gitdb = [ {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, ] gitpython = [ - {file = "GitPython-3.1.20-py3-none-any.whl", hash = "sha256:b1e1c269deab1b08ce65403cf14e10d2ef1f6c89e33ea7c5e5bb0222ea593b8a"}, - {file = "GitPython-3.1.20.tar.gz", hash = "sha256:df0e072a200703a65387b0cfdf0466e3bab729c0458cf6b7349d0e9877636519"}, + {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"}, + {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"}, ] google-api-core = [ - {file = "google-api-core-1.31.2.tar.gz", hash = "sha256:8500aded318fdb235130bf183c726a05a9cb7c4b09c266bd5119b86cdb8a4d10"}, - {file = "google_api_core-1.31.2-py2.py3-none-any.whl", hash = "sha256:384459a0dc98c1c8cd90b28dc5800b8705e0275a673a7144a513ae80fc77950b"}, + {file = "google-api-core-1.30.0.tar.gz", hash = "sha256:0724d354d394b3d763bc10dfee05807813c5210f0bd9b8e2ddf6b6925603411c"}, + {file = "google_api_core-1.30.0-py2.py3-none-any.whl", hash = "sha256:92cd9e9f366e84bfcf2524e34d2dc244906c645e731962617ba620da1620a1e0"}, ] google-api-python-client = [ {file = "google-api-python-client-1.12.8.tar.gz", hash = "sha256:f3b9684442eec2cfe9f9bb48e796ef919456b82142c7528c5fd527e5224f08bb"}, {file = "google_api_python_client-1.12.8-py2.py3-none-any.whl", hash = "sha256:3c4c4ca46b5c21196bec7ee93453443e477d82cbfa79234d1ce0645f81170eaf"}, ] google-auth = [ - {file = "google-auth-1.35.0.tar.gz", hash = "sha256:b7033be9028c188ee30200b204ea00ed82ea1162e8ac1df4aa6ded19a191d88e"}, - {file = "google_auth-1.35.0-py2.py3-none-any.whl", hash = "sha256:997516b42ecb5b63e8d80f5632c1a61dddf41d2a4c2748057837e06e00014258"}, + {file = "google-auth-1.31.0.tar.gz", hash = "sha256:154f7889c5d679a6f626f36adb12afbd4dbb0a9a04ec575d989d6ba79c4fd65e"}, + {file = "google_auth-1.31.0-py2.py3-none-any.whl", hash = "sha256:6d47c79b5d09fbc7e8355fd9594cc4cf65fdde5d401c63951eaac4baa1ba2ae1"}, ] google-auth-httplib2 = [ {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, @@ -1794,32 +1753,32 @@ httplib2 = [ {file = "httplib2-0.19.1.tar.gz", hash = "sha256:0b12617eeca7433d4c396a100eaecfa4b08ee99aa881e6df6e257a7aad5d533d"}, ] idna = [ - {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, - {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] imagesize = [ {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, - {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, + {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, + {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, - {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, + {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, + {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, ] jedi = [ {file = "jedi-0.13.3-py2.py3-none-any.whl", hash = "sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c"}, {file = "jedi-0.13.3.tar.gz", hash = "sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b"}, ] jeepney = [ - {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"}, - {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"}, + {file = "jeepney-0.6.0-py3-none-any.whl", hash = "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae"}, + {file = "jeepney-0.6.0.tar.gz", hash = "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657"}, ] jinja2 = [ {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, @@ -1865,22 +1824,12 @@ 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"}, @@ -1889,21 +1838,14 @@ 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"}, @@ -1913,9 +1855,6 @@ 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"}, @@ -1965,79 +1904,56 @@ multidict = [ ] opentimelineio = [] packaging = [ - {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, - {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] parso = [ {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, ] pathlib2 = [ - {file = "pathlib2-2.3.6-py2.py3-none-any.whl", hash = "sha256:3a130b266b3a36134dcc79c17b3c7ac9634f083825ca6ea9d8f557ee6195c9c8"}, - {file = "pathlib2-2.3.6.tar.gz", hash = "sha256:7d8bcb5555003cdf4a8d2872c538faa3a0f5d20630cb360e518ca3b981795e5f"}, + {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, + {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, ] pillow = [ - {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, - {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, - {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, - {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, - {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, - {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, - {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, - {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, - {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, - {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, - {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, - {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, - {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, - {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, - {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, - {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, - {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, - {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, - {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, - {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, - {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, - {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, -] -platformdirs = [ - {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, - {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, + {file = "Pillow-8.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9"}, + {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b"}, + {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b"}, + {file = "Pillow-8.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9"}, + {file = "Pillow-8.2.0-cp36-cp36m-win32.whl", hash = "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727"}, + {file = "Pillow-8.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f"}, + {file = "Pillow-8.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d"}, + {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a"}, + {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9"}, + {file = "Pillow-8.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388"}, + {file = "Pillow-8.2.0-cp37-cp37m-win32.whl", hash = "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5"}, + {file = "Pillow-8.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2"}, + {file = "Pillow-8.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4"}, + {file = "Pillow-8.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812"}, + {file = "Pillow-8.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178"}, + {file = "Pillow-8.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb"}, + {file = "Pillow-8.2.0-cp38-cp38-win32.whl", hash = "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232"}, + {file = "Pillow-8.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797"}, + {file = "Pillow-8.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5"}, + {file = "Pillow-8.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484"}, + {file = "Pillow-8.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602"}, + {file = "Pillow-8.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2"}, + {file = "Pillow-8.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef"}, + {file = "Pillow-8.2.0-cp39-cp39-win32.whl", hash = "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713"}, + {file = "Pillow-8.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c"}, + {file = "Pillow-8.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9"}, + {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9"}, + {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c"}, + {file = "Pillow-8.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b"}, + {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"}, + {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"}, + {file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"}, + {file = "Pillow-8.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8b56553c0345ad6dcb2e9b433ae47d67f95fc23fe28a0bde15a120f25257e291"}, + {file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"}, ] pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] prefixed = [ {file = "prefixed-0.3.2-py2.py3-none-any.whl", hash = "sha256:5e107306462d63f2f03c529dbf11b0026fdfec621a9a008ca639d71de22995c3"}, @@ -2062,13 +1978,9 @@ 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"}, ] @@ -2131,112 +2043,78 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pygments = [ - {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, - {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, + {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, + {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, ] pylint = [ - {file = "pylint-2.10.2-py3-none-any.whl", hash = "sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852"}, - {file = "pylint-2.10.2.tar.gz", hash = "sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1"}, + {file = "pylint-2.8.3-py3-none-any.whl", hash = "sha256:792b38ff30903884e4a9eab814ee3523731abd3c463f3ba48d7b627e87013484"}, + {file = "pylint-2.8.3.tar.gz", hash = "sha256:0a049c5d47b629d9070c3932d13bff482b12119b6a241a93bc460b0be16953c8"}, ] pymongo = [ - {file = "pymongo-3.12.0-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:072ba7cb65c8aa4d5c5659bf6722ee85781c9d7816dc00679b8b6f3dff1ddafc"}, - {file = "pymongo-3.12.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d6e11ffd43184d529d6752d6dcb62b994f903038a17ea2168ef1910c96324d26"}, - {file = "pymongo-3.12.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7412a36798966624dc4c57d64aa43c2d1100b348abd98daaac8e99e57d87e1d7"}, - {file = "pymongo-3.12.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e8a82e35d52ad6f867e88096a1a2b9bdc7ec4d5e65c7b4976a248bf2d1a32a93"}, - {file = "pymongo-3.12.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dcd3d0009fbb6e454d729f8b22d0063bd9171c31a55e0f0271119bd4f2700023"}, - {file = "pymongo-3.12.0-cp27-cp27m-win32.whl", hash = "sha256:1bc6fe7279ff40c6818db002bf5284aa03ec181ea1b1ceaeee33c289d412afa7"}, - {file = "pymongo-3.12.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e2b7670c0c8c6b501464150dd49dd0d6be6cb7f049e064124911cec5514fa19e"}, - {file = "pymongo-3.12.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:316c1b8723afa9870567cd6dff35d440b2afeda53aa13da6c5ab85f98ed6f5ca"}, - {file = "pymongo-3.12.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:255a35bf29185f44b412e31a927d9dcedda7c2c380127ecc4fbf2f61b72fa978"}, - {file = "pymongo-3.12.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ffbae429ba9e42d0582d3ac63fdb410338892468a2107d8ff68228ec9a39a0ed"}, - {file = "pymongo-3.12.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c188db6cf9e14dbbb42f5254292be96f05374a35e7dfa087cc2140f0ff4f10f6"}, - {file = "pymongo-3.12.0-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:6fb3f85870ae26896bb44e67db94045f2ebf00c5d41e6b66cdcbb5afd644fc18"}, - {file = "pymongo-3.12.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:aaa038eafb7186a4abbb311fcf20724be9363645882bbce540bef4797e812a7a"}, - {file = "pymongo-3.12.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:7d98ce3c42921bb91566121b658e0d9d59a9082a9bd6f473190607ff25ab637f"}, - {file = "pymongo-3.12.0-cp34-cp34m-win32.whl", hash = "sha256:b0a0cf39f589e52d801fdef418305562bc030cdf8929217463c8433c65fd5c2f"}, - {file = "pymongo-3.12.0-cp34-cp34m-win_amd64.whl", hash = "sha256:ceae3ab9e11a27aaab42878f1d203600dfd24f0e43678b47298219a0f10c0d30"}, - {file = "pymongo-3.12.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:5e574664f1468872cd40f74e4811e22b1aa4de9399d6bcfdf1ee6ea94c017fcf"}, - {file = "pymongo-3.12.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73b400fdc22de84bae0dbf1a22613928a41612ec0a3d6ed47caf7ad4d3d0f2ff"}, - {file = "pymongo-3.12.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:cbf8672edeb7b7128c4a939274801f0e32bbf5159987815e3d1eace625264a46"}, - {file = "pymongo-3.12.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:a634a4730ce0b0934ed75e45beba730968e12b4dafbb22f69b3b2f616d9e644e"}, - {file = "pymongo-3.12.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:c55782a55f4a013a78ac5b6ee4b8731a192dea7ab09f1b6b3044c96d5128edd4"}, - {file = "pymongo-3.12.0-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:11f9e0cfc84ade088a38df2708d0b958bb76360181df1b2e1e1a41beaa57952b"}, - {file = "pymongo-3.12.0-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:186104a94d39b8412f8e3de385acd990a628346a4402d4f3a288a82b8660bd22"}, - {file = "pymongo-3.12.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:70761fd3c576b027eec882b43ee0a8e5b22ff9c20cdf4d0400e104bc29e53e34"}, - {file = "pymongo-3.12.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:333bfad77aa9cd11711febfb75eed0bb537a1d022e1c252714dad38993590240"}, - {file = "pymongo-3.12.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fa8957e9a1b202cb45e6b839c241cd986c897be1e722b81d2f32e9c6aeee80b0"}, - {file = "pymongo-3.12.0-cp35-cp35m-win32.whl", hash = "sha256:4ba0def4abef058c0e5101e05e3d5266e6fffb9795bbf8be0fe912a7361a0209"}, - {file = "pymongo-3.12.0-cp35-cp35m-win_amd64.whl", hash = "sha256:a0e5dff6701fa615f165306e642709e1c1550d5b237c5a7a6ea299886828bd50"}, - {file = "pymongo-3.12.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:b542d56ed1b8d5cf3bb36326f814bd2fbe8812dfd2582b80a15689ea433c0e35"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a325600c83e61e3c9cebc0c2b1c8c4140fa887f789085075e8f44c8ff2547eb9"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:48d5bc80ab0af6b60c4163c5617f5cd23f2f880d7600940870ea5055816af024"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5cab230e7cabdae9ff23c12271231283efefb944c1b79bed79a91beb65ba547"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d73e10772152605f6648ba4410318594f1043bbfe36d2fadee7c4b8912eff7c5"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:b1c4874331ab960429caca81acb9d2932170d66d6d6f87e65dc4507a85aca152"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:a3566acfbcde46911c52810374ecc0354fdb841284a3efef6ff7105bc007e9a8"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:b3b5b3cbc3fdf4fcfa292529df2a85b5d9c7053913a739d3069af1e12e12219f"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd3854148005c808c485c754a184c71116372263709958b42aefbef2e5dd373a"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f55c1ddcc1f6050b07d468ce594f55dbf6107b459e16f735d26818d7be1e9538"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ced944dcdd561476deef7cb7bfd4987c69fffbfeff6d02ca4d5d4fd592d559b7"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78ecb8d42f50d393af912bfb1fb1dcc9aabe9967973efb49ee577e8f1cea494c"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1970cfe2aec1bf74b40cf30c130ad10cd968941694630386db33e1d044c22a2e"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8bf42d3b32f586f4c9e37541769993783a534ad35531ce8a4379f6fa664fba9"}, - {file = "pymongo-3.12.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bc9ac81e73573516070d24ce15da91281922811f385645df32bd3c8a45ab4684"}, - {file = "pymongo-3.12.0-cp36-cp36m-win32.whl", hash = "sha256:d04ca462cb99077e6c059e97c072957caf2918e6e4191e3161c01c439e0193de"}, - {file = "pymongo-3.12.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f2acf9bbcd514e901f82c4ca6926bbd2ae61716728f110b4343eb0a69612d018"}, - {file = "pymongo-3.12.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:b754240daafecd9d5fce426b0fbaaed03f4ebb130745c8a4ae9231fffb8d75e5"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:af586e85144023686fb0af09c8cdf672484ea182f352e7ceead3d832de381e1b"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fe5872ce6f9627deac8314bdffd3862624227c3de4c17ef0cc78bbf0402999eb"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f6977a520bd96e097c8a37a8cbb9faa1ea99d21bf84190195056e25f688af73d"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:2dbfbbded947a83a3dffc2bd1ec4750c17e40904692186e2c55a3ad314ca0222"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:a752ecd1a26000a6d67be7c9a2e93801994a8b3f866ac95b672fbc00225ca91a"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:1bab889ae7640eba739f67fcbf8eff252dddc60d4495e6ddd3a87cd9a95fdb52"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:f94c7d22fb36b184734dded7345a04ec5f95130421c775b8b0c65044ef073f34"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec5ca7c0007ce268048bbe0ffc6846ed1616cf3d8628b136e81d5e64ff3f52a2"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c72d08acdf573455b2b9d2b75b8237654841d63a48bc2327dc102c6ee89b75a"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6ea08758b6673610b3c5bdf47189286cf9c58b1077558706a2f6f8744922527"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d5ec90276f71af3a29917b30f2aec2315a2759b5f8d45b3b63a07ca8a070a3"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:625befa3bc9b40746a749115cc6a15bf20b9bd7597ca55d646205b479a2c99c7"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d1131562ddc2ea8a446f66c2648d7dabec2b3816fc818528eb978a75a6d23b2e"}, - {file = "pymongo-3.12.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eee42a1cc06565f6b21caa1f504ec15e07de7ebfd520ab57f8cb3308bc118e22"}, - {file = "pymongo-3.12.0-cp37-cp37m-win32.whl", hash = "sha256:94d38eba4d1b5eb3e6bfece0651b855a35c44f32fd91f512ab4ba41b8c0d3e66"}, - {file = "pymongo-3.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e018a4921657c2d3f89c720b7b90b9182e277178a04a7e9542cc79d7d787ca51"}, - {file = "pymongo-3.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c6a9948916a7bbcc6d3a9f6fb75db1acb5546078023bfb3db6efabcd5a67527"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e9faf8d4712d5ea301d74abfcf6dafe4b7f4af7936e91f283b0ad7bf69ed3e3a"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cc2894fe91f31a513860238ede69fe47fada21f9e7ddfe73f7f9fef93a971e41"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:053b4ebf91c7395d1fcd2ce6a9edff0024575b7b2de6781554a4114448a8adc9"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:39dafa2eaf577d1969f289dc9a44501859a1897eb45bd589e93ce843fc610800"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:246ec420e4c8744fceb4e259f906211b9c198e1f345e6158dcd7cbad3737e11e"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:208debdcf76ed39ebf24f38509f50dc1c100e31e8653817fedb8e1f867850a13"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:18290649759f9db660972442aa606f845c368db9b08c4c73770f6da14113569b"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657ad80de8ec9ed656f28844efc801a0802961e8c6a85038d97ff6f555ef4919"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b772bab31cbd9cb911e41e1a611ebc9497f9a32a7348e2747c38210f75c00f41"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2399a85b54f68008e483b2871f4a458b4c980469c7fe921595ede073e4844f1e"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e66780f14c2efaf989cd3ac613b03ee6a8e3a0ba7b96c0bb14adca71a427e55"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02dc0b0f48ed3cd06c13b7e31b066bf91e00dac5f8147b0a0a45f9009bfab857"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:070a4ef689c9438a999ec3830e69b208ff0d12251846e064d947f97d819d1d05"}, - {file = "pymongo-3.12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:db93608a246da44d728842b8fa9e45aa9782db76955f634a707739a8d53ff544"}, - {file = "pymongo-3.12.0-cp38-cp38-win32.whl", hash = "sha256:5af390fa9faf56c93252dab09ea57cd020c9123aa921b63a0ed51832fdb492e7"}, - {file = "pymongo-3.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:a2239556ff7241584ce57be1facf25081669bb457a9e5cbe68cce4aae6567aa1"}, - {file = "pymongo-3.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cda9e628b1315beec8341e8c04aac9a0b910650b05e0751e42e399d5694aeacb"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:845a8b83798b2fb11b09928413cb32692866bfbc28830a433d9fa4c8c3720dd0"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:da8288bc4a7807c6715416deed1c57d94d5e03e93537889e002bf985be503f1a"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a9ba2a63777027b06b116e1ea8248e66fd1bedc2c644f93124b81a91ddbf6d88"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:9a13661681d17e43009bb3e85e837aa1ec5feeea1e3654682a01b8821940f8b3"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:6b89dc51206e4971c5568c797991eaaef5dc2a6118d67165858ad11752dba055"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:701e08457183da70ed96b35a6b43e6ba1df0b47c837b063cde39a1fbe1aeda81"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:e7a33322e08021c37e89cae8ff06327503e8a1719e97c69f32c31cbf6c30d72c"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd1f49f949a658c4e8f81ed73f9aad25fcc7d4f62f767f591e749e30038c4e1d"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6d055f01b83b1a4df8bb0c61983d3bdffa913764488910af3620e5c2450bf83"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd6ff2192f34bd622883c745a56f492b1c9ccd44e14953e8051c33024a2947d5"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19d4bd0fc29aa405bb1781456c9cfff9fceabb68543741eb17234952dbc2bbb0"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24f8aeec4d6b894a6128844e50ff423dd02462ee83addf503c598ee3a80ddf3d"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b6055e0ef451ff73c93d0348d122a0750dddf323b9361de5835dac2f6cf7fc1"}, - {file = "pymongo-3.12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6261bee7c5abadeac7497f8f1c43e521da78dd13b0a2439f526a7b0fc3788824"}, - {file = "pymongo-3.12.0-cp39-cp39-win32.whl", hash = "sha256:2e92aa32300a0b5e4175caec7769f482b292769807024a86d674b3f19b8e3755"}, - {file = "pymongo-3.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ce83f17f641a62a4dfb0ba1b8a3c1ced7c842f511b5450d90c030c7828e3693"}, - {file = "pymongo-3.12.0-py2.7-macosx-10.14-intel.egg", hash = "sha256:d1740776b70367277323fafb76bcf09753a5cc9824f5d705bac22a34ff3668ea"}, - {file = "pymongo-3.12.0.tar.gz", hash = "sha256:b88d1742159bc93a078733f9789f563cef26f5e370eba810476a71aa98e5fbc2"}, + {file = "pymongo-3.11.4-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:b7efc7e7049ef366777cfd35437c18a4166bb50a5606a1c840ee3b9624b54fc9"}, + {file = "pymongo-3.11.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:517ba47ca04a55b1f50ee8df9fd97f6c37df5537d118fb2718952b8623860466"}, + {file = "pymongo-3.11.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:225c61e08fe517aede7912937939e09adf086c8e6f7e40d4c85ad678c2c2aea3"}, + {file = "pymongo-3.11.4-cp27-cp27m-win32.whl", hash = "sha256:e4e9db78b71db2b1684ee4ecc3e32c4600f18cdf76e6b9ae03e338e52ee4b168"}, + {file = "pymongo-3.11.4-cp27-cp27m-win_amd64.whl", hash = "sha256:8e0004b0393d72d76de94b4792a006cb960c1c65c7659930fbf9a81ce4341982"}, + {file = "pymongo-3.11.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:fedf0dee7a412ca6d1d6d92c158fe9cbaa8ea0cae90d268f9ccc0744de7a97d0"}, + {file = "pymongo-3.11.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f947b359cc4769af8b49be7e37af01f05fcf15b401da2528021148e4a54426d1"}, + {file = "pymongo-3.11.4-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:3a3498a8326111221560e930f198b495ea6926937e249f475052ffc6893a6680"}, + {file = "pymongo-3.11.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:9a4f6e0b01df820ba9ed0b4e618ca83a1c089e48d4f268d0e00dcd49893d4549"}, + {file = "pymongo-3.11.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d65bac5f6724d9ea6f0b5a0f0e4952fbbf209adcf6b5583b54c54bd2fcd74dc0"}, + {file = "pymongo-3.11.4-cp34-cp34m-win32.whl", hash = "sha256:15b083d1b789b230e5ac284442d9ecb113c93f3785a6824f748befaab803b812"}, + {file = "pymongo-3.11.4-cp34-cp34m-win_amd64.whl", hash = "sha256:f08665d3cc5abc2f770f472a9b5f720a9b3ab0b8b3bb97c7c1487515e5653d39"}, + {file = "pymongo-3.11.4-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:977b1d4f868986b4ba5d03c317fde4d3b66e687d74473130cd598e3103db34fa"}, + {file = "pymongo-3.11.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:510cd3bfabb63a07405b7b79fae63127e34c118b7531a2cbbafc7a24fd878594"}, + {file = "pymongo-3.11.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:071552b065e809d24c5653fcc14968cfd6fde4e279408640d5ac58e3353a3c5f"}, + {file = "pymongo-3.11.4-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:f4ba58157e8ae33ee86fadf9062c506e535afd904f07f9be32731f4410a23b7f"}, + {file = "pymongo-3.11.4-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:b413117210fa6d92664c3d860571e8e8727c3e8f2ff197276c5d0cb365abd3ad"}, + {file = "pymongo-3.11.4-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:08b8723248730599c9803ae4c97b8f3f76c55219104303c88cb962a31e3bb5ee"}, + {file = "pymongo-3.11.4-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:8a41fdc751dc4707a4fafb111c442411816a7c225ebb5cadb57599534b5d5372"}, + {file = "pymongo-3.11.4-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:f664ed7613b8b18f0ce5696b146776266a038c19c5cd6efffa08ecc189b01b73"}, + {file = "pymongo-3.11.4-cp35-cp35m-win32.whl", hash = "sha256:5c36428cc4f7fae56354db7f46677fd21222fc3cb1e8829549b851172033e043"}, + {file = "pymongo-3.11.4-cp35-cp35m-win_amd64.whl", hash = "sha256:d0a70151d7de8a3194cdc906bcc1a42e14594787c64b0c1c9c975e5a2af3e251"}, + {file = "pymongo-3.11.4-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:9b9298964389c180a063a9e8bac8a80ed42de11d04166b20249bfa0a489e0e0f"}, + {file = "pymongo-3.11.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b2f41261b648cf5dee425f37ff14f4ad151c2f24b827052b402637158fd056ef"}, + {file = "pymongo-3.11.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e02beaab433fd1104b2804f909e694cfbdb6578020740a9051597adc1cd4e19f"}, + {file = "pymongo-3.11.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:8898f6699f740ca93a0879ed07d8e6db02d68af889d0ebb3d13ab017e6b1af1e"}, + {file = "pymongo-3.11.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:62c29bc36a6d9be68fe7b5aaf1e120b4aa66a958d1e146601fcd583eb12cae7b"}, + {file = "pymongo-3.11.4-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:424799c71ff435094e5fb823c40eebb4500f0e048133311e9c026467e8ccebac"}, + {file = "pymongo-3.11.4-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:3551912f5c34d8dd7c32c6bb00ae04192af47f7b9f653608f107d19c1a21a194"}, + {file = "pymongo-3.11.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:5db59223ed1e634d842a053325f85f908359c6dac9c8ddce8ef145061fae7df8"}, + {file = "pymongo-3.11.4-cp36-cp36m-win32.whl", hash = "sha256:fea5cb1c63efe1399f0812532c7cf65458d38fd011be350bc5021dfcac39fba8"}, + {file = "pymongo-3.11.4-cp36-cp36m-win_amd64.whl", hash = "sha256:d4e62417e89b717a7bcd8576ac3108cd063225942cc91c5b37ff5465fdccd386"}, + {file = "pymongo-3.11.4-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:4c7e8c8e1e1918dcf6a652ac4b9d87164587c26fd2ce5dd81e73a5ab3b3d492f"}, + {file = "pymongo-3.11.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38a7b5140a48fc91681cdb5cb95b7cd64640b43d19259fdd707fa9d5a715f2b2"}, + {file = "pymongo-3.11.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:aff3656af2add93f290731a6b8930b23b35c0c09569150130a58192b3ec6fc61"}, + {file = "pymongo-3.11.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:03be7ad107d252bb7325d4af6309fdd2c025d08854d35f0e7abc8bf048f4245e"}, + {file = "pymongo-3.11.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:6060794aac9f7b0644b299f46a9c6cbc0bc470bd01572f4134df140afd41ded6"}, + {file = "pymongo-3.11.4-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:73326b211e7410c8bd6a74500b1e3f392f39cf10862e243d00937e924f112c01"}, + {file = "pymongo-3.11.4-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:20d75ea11527331a2980ab04762a9d960bcfea9475c54bbeab777af880de61cd"}, + {file = "pymongo-3.11.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:3135dd574ef1286189f3f04a36c8b7a256376914f8cbbce66b94f13125ded858"}, + {file = "pymongo-3.11.4-cp37-cp37m-win32.whl", hash = "sha256:7c97554ea521f898753d9773891d0347ebfaddcc1dee2ad94850b163171bf1f1"}, + {file = "pymongo-3.11.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a08c8b322b671857c81f4c30cd3c8df2895fd3c0e9358714f39e0ef8fb327702"}, + {file = "pymongo-3.11.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f3d851af3852f16ad4adc7ee054fd9c90a7a5063de94d815b7f6a88477b9f4c6"}, + {file = "pymongo-3.11.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3bfc7689a1bacb9bcd2f2d5185d99507aa29f667a58dd8adaa43b5a348139e46"}, + {file = "pymongo-3.11.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b8f94acd52e530a38f25e4d5bf7ddfdd4bea9193e718f58419def0d4406b58d3"}, + {file = "pymongo-3.11.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e4b631688dfbdd61b5610e20b64b99d25771c6d52d9da73349342d2a0f11c46a"}, + {file = "pymongo-3.11.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:474e21d0e07cd09679e357d1dac76e570dab86665e79a9d3354b10a279ac6fb3"}, + {file = "pymongo-3.11.4-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:421d13523d11c57f57f257152bc4a6bb463aadf7a3918e9c96fefdd6be8dbfb8"}, + {file = "pymongo-3.11.4-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:0cabfc297f4cf921f15bc789a8fbfd7115eb9f813d3f47a74b609894bc66ab0d"}, + {file = "pymongo-3.11.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:fe4189846448df013cd9df11bba38ddf78043f8c290a9f06430732a7a8601cce"}, + {file = "pymongo-3.11.4-cp38-cp38-win32.whl", hash = "sha256:eb4d176394c37a76e8b0afe54b12d58614a67a60a7f8c0dd3a5afbb013c01092"}, + {file = "pymongo-3.11.4-cp38-cp38-win_amd64.whl", hash = "sha256:fffff7bfb6799a763d3742c59c6ee7ffadda21abed557637bc44ed1080876484"}, + {file = "pymongo-3.11.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:13acf6164ead81c9fc2afa0e1ea6d6134352973ce2bb35496834fee057063c04"}, + {file = "pymongo-3.11.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d360e5d5dd3d55bf5d1776964625018d85b937d1032bae1926dd52253decd0db"}, + {file = "pymongo-3.11.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:0aaf4d44f1f819360f9432df538d54bbf850f18152f34e20337c01b828479171"}, + {file = "pymongo-3.11.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:08bda7b2c522ff9f1e554570da16298271ebb0c56ab9699446aacba249008988"}, + {file = "pymongo-3.11.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:1a994a42f49dab5b6287e499be7d3d2751776486229980d8857ad53b8333d469"}, + {file = "pymongo-3.11.4-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:161fcd3281c42f644aa8dec7753cca2af03ce654e17d76da4f0dab34a12480ca"}, + {file = "pymongo-3.11.4-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:78f07961f4f214ea8e80be63cffd5cc158eb06cd922ffbf6c7155b11728f28f9"}, + {file = "pymongo-3.11.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ad31f184dcd3271de26ab1f9c51574afb99e1b0e484ab1da3641256b723e4994"}, + {file = "pymongo-3.11.4-cp39-cp39-win32.whl", hash = "sha256:5e606846c049ed40940524057bfdf1105af6066688c0e6a1a3ce2038589bae70"}, + {file = "pymongo-3.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:3491c7de09e44eded16824cb58cf9b5cc1dc6f066a0bb7aa69929d02aa53b828"}, + {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"}, ] pynput = [ {file = "pynput-1.7.3-py2.py3-none-any.whl", hash = "sha256:fea5777454f896bd79d35393088cd29a089f3b2da166f0848a922b1d5a807d4f"}, @@ -2272,47 +2150,27 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pyrsistent = [ - {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2"}, - {file = "pyrsistent-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427"}, - {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef"}, - {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c"}, - {file = "pyrsistent-0.18.0-cp38-cp38-win32.whl", hash = "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78"}, - {file = "pyrsistent-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b"}, - {file = "pyrsistent-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4"}, - {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680"}, - {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426"}, - {file = "pyrsistent-0.18.0-cp39-cp39-win32.whl", hash = "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b"}, - {file = "pyrsistent-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea"}, - {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, + {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, ] pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] pytest-print = [ - {file = "pytest_print-0.3.0-py2.py3-none-any.whl", hash = "sha256:53fb0f71d371f137ac2e7171d92f204eb45055580e8c7920df619d9b2ee45359"}, - {file = "pytest_print-0.3.0.tar.gz", hash = "sha256:769f1b1b0943b2941dbeeaac6985766e76b341130ed538f88c23ebcd7087b90d"}, + {file = "pytest_print-0.2.1-py2.py3-none-any.whl", hash = "sha256:2cfcdeee8b398457d3e3488f1fde5f8303b404c30187be5fcb4c7818df5f4529"}, + {file = "pytest_print-0.2.1.tar.gz", hash = "sha256:8f61e5bb2d031ee88d19a5a7695a0c863caee7b1478f1a82d080c2128b76ad83"}, ] python-dateutil = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, ] python-xlib = [ - {file = "python-xlib-0.31.tar.gz", hash = "sha256:74d83a081f532bc07f6d7afcd6416ec38403d68f68b9b9dc9e1f28fbf2d799e9"}, - {file = "python_xlib-0.31-py2.py3-none-any.whl", hash = "sha256:1ec6ce0de73d9e6592ead666779a5732b384e5b8fb1f1886bd0a81cafa477759"}, + {file = "python-xlib-0.30.tar.gz", hash = "sha256:74131418faf9e7b83178c71d9d80297fbbd678abe99ae9258f5a20cd027acb5f"}, + {file = "python_xlib-0.30-py2.py3-none-any.whl", hash = "sha256:c4c92cd47e07588b2cbc7d52de18407b2902c3812d7cdec39cd2177b060828e2"}, ] python3-xlib = [ {file = "python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8"}, @@ -2338,16 +2196,16 @@ pywin32-ctypes = [ {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] "qt.py" = [ - {file = "Qt.py-1.3.6-py2.py3-none-any.whl", hash = "sha256:7edf6048d07a6924707506b5ba34a6e05d66dde9a3f4e3a62f9996ccab0b91c7"}, - {file = "Qt.py-1.3.6.tar.gz", hash = "sha256:0d78656a2f814602eee304521c7bf5da0cec414818b3833712c77524294c404a"}, + {file = "Qt.py-1.3.3-py2.py3-none-any.whl", hash = "sha256:9e3f5417187c98d246918a9b27a9e1f8055e089bdb2b063a2739986bc19a3d2e"}, + {file = "Qt.py-1.3.3.tar.gz", hash = "sha256:601606127f70be9adc82c248d209d696cccbd1df242c24d3fb1a9e399f3ecaf1"}, ] recommonmark = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, ] requests = [ - {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, - {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] rsa = [ {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"}, @@ -2366,8 +2224,8 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] slack-sdk = [ - {file = "slack_sdk-3.10.1-py2.py3-none-any.whl", hash = "sha256:f17b71a578e94204d9033bffded634475f4ca0a6274c6c7a4fd8a9cb0ac7cd8b"}, - {file = "slack_sdk-3.10.1.tar.gz", hash = "sha256:2b4dde7728eb4ff5a581025d204578ccff25a5d8f0fe11ae175e3ce6e074434f"}, + {file = "slack_sdk-3.6.0-py2.py3-none-any.whl", hash = "sha256:e1b257923a1ef88b8620dd3abff94dc5b3eee16ef37975d101ba9e60123ac3af"}, + {file = "slack_sdk-3.6.0.tar.gz", hash = "sha256:195f044e02a2844579a7a26818ce323e85dde8de224730c859644918d793399e"}, ] smmap = [ {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, @@ -2382,8 +2240,8 @@ speedcopy = [ {file = "speedcopy-2.1.0.tar.gz", hash = "sha256:8bb1a6c735900b83901a7be84ba2175ed3887c13c6786f97dea48f2ea7d504c2"}, ] sphinx = [ - {file = "Sphinx-4.1.2-py3-none-any.whl", hash = "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544"}, - {file = "Sphinx-4.1.2.tar.gz", hash = "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13"}, + {file = "Sphinx-4.0.2-py3-none-any.whl", hash = "sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4"}, + {file = "Sphinx-4.0.2.tar.gz", hash = "sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c"}, ] sphinx-qt-documentation = [ {file = "sphinx_qt_documentation-0.3-py3-none-any.whl", hash = "sha256:bee247cb9e4fc03fc496d07adfdb943100e1103320c3e5e820e0cfa7c790d9b6"}, @@ -2461,17 +2319,17 @@ typed-ast = [ {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, - {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, - {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] uritemplate = [ {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"}, ] urllib3 = [ - {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, - {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, + {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, + {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, @@ -2528,6 +2386,6 @@ yarl = [ {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, ] zipp = [ - {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, - {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, + {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, + {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, ] From 7a34b728e33ba709faee0e2d3767aeac2b419109 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 19:18:34 +0200 Subject: [PATCH 385/716] change cx freeze --- poetry.lock | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index e43c788d74..357623b74d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -272,15 +272,20 @@ test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pret [[package]] name = "cx-freeze" -version = "6.6" +version = "6.7" description = "Create standalone executables from Python scripts" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -cx-Logging = {version = ">=3.0", markers = "sys_platform == \"win32\""} -importlib-metadata = ">=3.1.1" +cx-logging = {version = ">=3.0", markers = "sys_platform == \"win32\""} +importlib-metadata = ">=4.3.1" + +[package.source] +type = "legacy" +url = "https://distribute.openpype.io/wheels" +reference = "openpype" [[package]] name = "cx-logging" @@ -1668,17 +1673,7 @@ cryptography = [ {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, ] -cx-freeze = [ - {file = "cx_Freeze-6.6-cp36-cp36m-win32.whl", hash = "sha256:b3d3a6bcd1a07c50b4e1c907f14842642156110e63a99cd5c73b8a24751e9b97"}, - {file = "cx_Freeze-6.6-cp36-cp36m-win_amd64.whl", hash = "sha256:1935266ec644ea4f7e584985f44cefc0622a449a09980d990833a1a2afcadac8"}, - {file = "cx_Freeze-6.6-cp37-cp37m-win32.whl", hash = "sha256:1eac2b0f254319cc641ce25bd83337effd7936092562fde701f3ffb40e0274ec"}, - {file = "cx_Freeze-6.6-cp37-cp37m-win_amd64.whl", hash = "sha256:2bc46ef6d510811b6002f34a3ae4cbfdea44e18644febd2a404d3ee8e48a9fc4"}, - {file = "cx_Freeze-6.6-cp38-cp38-win32.whl", hash = "sha256:46eb50ebc46f7ae236d16c6a52671ab0f7bb479bea668da19f4b6de3cc413e9e"}, - {file = "cx_Freeze-6.6-cp38-cp38-win_amd64.whl", hash = "sha256:8c3b00476ce385bb58595bffce55aed031e5a6e16ab6e14d8bee9d1d569e46c3"}, - {file = "cx_Freeze-6.6-cp39-cp39-win32.whl", hash = "sha256:6e9340cbcf52d4836980ecc83ddba4f7704ff6654dd41168c146b74f512977ce"}, - {file = "cx_Freeze-6.6-cp39-cp39-win_amd64.whl", hash = "sha256:2fcf1c8b77ae5c06f45be3a9aff79e1dd808c0d624e97561f840dec5ea9b214a"}, - {file = "cx_Freeze-6.6.tar.gz", hash = "sha256:c4af8ad3f7e7d71e291c1dec5d0fb26bbe92df834b098ed35434c901fbd6762f"}, -] +cx-freeze = [] cx-logging = [ {file = "cx_Logging-3.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9fcd297e5c51470521c47eff0f86ba844aeca6be97e13c3e2114ebdf03fa3c96"}, {file = "cx_Logging-3.0-cp36-cp36m-win32.whl", hash = "sha256:0df4be47c5022cc54316949e283403214568ef599817ced0c0972183d6d4fabb"}, From 0e9ece13e97fc9abec2ad82276db423fd0cdd476 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Sep 2021 19:18:50 +0200 Subject: [PATCH 386/716] define requests version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9cbf4e5383..8d329cc2b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ jinxed = [ python3-xlib = { version="*", markers = "sys_platform == 'linux'"} enlighten = "^1.9.0" slack-sdk = "^3.6.0" +requests = "2.25.1" [tool.poetry.dev-dependencies] flake8 = "^3.7" @@ -86,7 +87,6 @@ wheel = "*" enlighten = "*" # cool terminal progress bars toml = "^0.10.2" # for parsing pyproject.toml - [tool.poetry.urls] "Bug Tracker" = "https://github.com/pypeclub/openpype/issues" "Discussions" = "https://github.com/pypeclub/openpype/discussions" From 97ed419fc93fe134d950591891c1c1b325923c5d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 23 Sep 2021 09:11:29 +0100 Subject: [PATCH 387/716] echo tag name in ci --- .github/workflows/prerelease.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 0fb07be79d..60ce608b21 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -76,6 +76,7 @@ jobs: git add . git commit -m "[Automated] Bump version" tag_name="CI/${{ steps.version.outputs.next_tag }}" + echo $tag_name git tag -a $tag_name -m "nightly build" - name: Push to protected main branch From bbf4964235fba34f63513a4af0c7b0b6535a3c97 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 23 Sep 2021 09:23:32 +0100 Subject: [PATCH 388/716] update to latest avalon-core --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 1e94241ffe..8aee68fa10 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 1e94241ffe2dd7ce65ca66b08e452ffc03180235 +Subproject commit 8aee68fa10ab4d79be1a91e7728a609748e7c3c6 From e6093a7835146c28ff5166ffc8b399ae977540cf Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 23 Sep 2021 09:24:31 +0100 Subject: [PATCH 389/716] (fix) double print in ci tag --- tools/ci_tools.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/ci_tools.py b/tools/ci_tools.py index 69f5158bb3..bbe0c699d1 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -15,13 +15,11 @@ def get_release_type_github(Log, github_token): repo = g.get_repo("pypeclub/OpenPype") for line in Log.splitlines(): - print(line) match = re.search("pull request #(\d+)", line) if match: pr_number = match.group(1) pr = repo.get_pull(int(pr_number)) for label in pr.labels: - print(label.name) if label.name in minor_labels: return ("minor") elif label.name in patch_labels: From cee7541fd0fe251ba6ed3409030cd19d459ff30e Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 23 Sep 2021 08:29:15 +0000 Subject: [PATCH 390/716] [Automated] Bump version --- CHANGELOG.md | 51 ++++++++++++++++++++++++++------------------- openpype/version.py | 2 +- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca8de17ae..78d6ca2dc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,38 @@ # Changelog -## [3.5.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.4.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...HEAD) +**πŸ†• New features** + +- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008) + **πŸš€ Enhancements** +- General: Startup validations [\#2054](https://github.com/pypeclub/OpenPype/pull/2054) +- Nuke: proxy mode validator [\#2052](https://github.com/pypeclub/OpenPype/pull/2052) +- Ftrack: Removed ftrack interface [\#2049](https://github.com/pypeclub/OpenPype/pull/2049) +- Settings UI: Deffered set value on entity [\#2044](https://github.com/pypeclub/OpenPype/pull/2044) +- Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043) +- Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042) +- Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) - Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) +- Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) +- TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024) +- Feature Maya import asset from scene inventory [\#2018](https://github.com/pypeclub/OpenPype/pull/2018) + +**πŸ› Bug fixes** + +- Timers manger: Typo fix [\#2058](https://github.com/pypeclub/OpenPype/pull/2058) +- Hiero: Editorial fixes [\#2057](https://github.com/pypeclub/OpenPype/pull/2057) +- Differentiate jpg sequences from thumbnail [\#2056](https://github.com/pypeclub/OpenPype/pull/2056) +- FFmpeg: Split command to list does not work [\#2046](https://github.com/pypeclub/OpenPype/pull/2046) +- Removed shell flag in subprocess call [\#2045](https://github.com/pypeclub/OpenPype/pull/2045) + +**Merged pull requests:** + +- Bump prismjs from 1.24.0 to 1.25.0 in /website [\#2050](https://github.com/pypeclub/OpenPype/pull/2050) ## [3.4.0](https://github.com/pypeclub/OpenPype/tree/3.4.0) (2021-09-17) @@ -25,6 +51,7 @@ - Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) - General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) +- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) - Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) - General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) @@ -70,10 +97,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) -**πŸš€ Enhancements** - -- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) - **πŸ› Bug fixes** - TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946) @@ -89,37 +112,23 @@ - Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) - Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) -- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) **πŸš€ Enhancements** - Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940) +- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927) - Check for missing ✨ Python when using `pyenv` [\#1925](https://github.com/pypeclub/OpenPype/pull/1925) -- Settings: Default values for enum [\#1920](https://github.com/pypeclub/OpenPype/pull/1920) -- Settings UI: Modifiable dict view enhance [\#1919](https://github.com/pypeclub/OpenPype/pull/1919) -- submodules: avalon-core update [\#1911](https://github.com/pypeclub/OpenPype/pull/1911) -- Ftrack: Where I run action enhancement [\#1900](https://github.com/pypeclub/OpenPype/pull/1900) -- Ftrack: Private project server actions [\#1899](https://github.com/pypeclub/OpenPype/pull/1899) -- Support nested studio plugins paths. [\#1898](https://github.com/pypeclub/OpenPype/pull/1898) **πŸ› Bug fixes** - Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935) +- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930) - Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929) - Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926) -- Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922) -- standalone: validator asset parents [\#1917](https://github.com/pypeclub/OpenPype/pull/1917) -- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916) -- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914) -- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906) -- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904) -- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903) -- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902) **Merged pull requests:** - Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937) -- Add support for multiple Deadline β˜ οΈβž– servers [\#1905](https://github.com/pypeclub/OpenPype/pull/1905) ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) diff --git a/openpype/version.py b/openpype/version.py index f8ed9c7c2f..3582fb27e2 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.5.0-nightly.1" +__version__ = "3.4.1-nightly.1" From 0c8af1f7b6e7b3e27e0b9230abb4ecddc92d4bf2 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 23 Sep 2021 11:10:36 +0200 Subject: [PATCH 391/716] 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 392/716] 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 393/716] 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 394/716] 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 395/716] fix syntax --- openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py index 7798cbab4e..01705e8b13 100644 --- a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py @@ -2,6 +2,7 @@ import pyblish.api import maya.cmds as cmds import openpype.api + class ValidateLoadedPlugin(pyblish.api.ContextPlugin): """Ensure there are no unauthorized loaded plugins""" From 53888bb5105fb507b5a8d6b9b5374c10eb454e73 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 23 Sep 2021 11:34:23 +0100 Subject: [PATCH 396/716] fix missing import --- .github/workflows/release.yml | 2 +- tools/ci_tools.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d3f301b99..3f85525c26 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: with: python-version: 3.7 - name: Install Python requirements - run: pip install gitpython semver + run: pip install gitpython semver PyGithub - name: πŸ’‰ Inject new version into files id: version diff --git a/tools/ci_tools.py b/tools/ci_tools.py index bbe0c699d1..337b19a346 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -14,16 +14,21 @@ def get_release_type_github(Log, github_token): g = Github(github_token) repo = g.get_repo("pypeclub/OpenPype") + labels = set() for line in Log.splitlines(): match = re.search("pull request #(\d+)", line) if match: pr_number = match.group(1) pr = repo.get_pull(int(pr_number)) for label in pr.labels: - if label.name in minor_labels: - return ("minor") - elif label.name in patch_labels: - return("patch") + labels.add(label.name) + + if any(label in labels for label in minor_labels): + return "minor" + + if any(label in labels for label in patch_labels): + return "path" + return None From 14d4d5e6f2b6ac11cb0f23cacfa2a266780ac77e Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 23 Sep 2021 10:40:44 +0000 Subject: [PATCH 397/716] [Automated] Release --- CHANGELOG.md | 24 ++++++++++-------------- openpype/version.py | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78d6ca2dc3..d2ae3c9eae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,8 @@ # Changelog -## [3.4.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...HEAD) - -**πŸ†• New features** - -- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.0...3.4.1) **πŸš€ Enhancements** @@ -17,10 +13,11 @@ - Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043) - Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042) - Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) -- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) +- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024) - Feature Maya import asset from scene inventory [\#2018](https://github.com/pypeclub/OpenPype/pull/2018) +- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008) **πŸ› Bug fixes** @@ -38,11 +35,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0) -### πŸ“– Documentation - -- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) -- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) - **πŸ†• New features** - Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) @@ -50,8 +42,8 @@ **πŸš€ Enhancements** - Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041) +- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) - General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036) -- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028) - Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022) - Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019) - General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017) @@ -93,6 +85,11 @@ - Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967) - Global: ExtractJpeg can handle filepaths with spaces [\#1961](https://github.com/pypeclub/OpenPype/pull/1961) +### πŸ“– Documentation + +- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) +- Nuke Quick Start / Tutorial [\#1952](https://github.com/pypeclub/OpenPype/pull/1952) + ## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1) @@ -111,7 +108,6 @@ **πŸ†• New features** - Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932) -- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923) **πŸš€ Enhancements** diff --git a/openpype/version.py b/openpype/version.py index 3582fb27e2..0e52014a56 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.4.1-nightly.1" +__version__ = "3.4.1" From 3661d11976a4839a97c7d6db4e303e3d8bd418b3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Sep 2021 12:58:44 +0200 Subject: [PATCH 398/716] Fix - concurrent change in Settings wont trigger exception --- openpype/modules/default_modules/sync_server/tray/app.py | 1 + .../modules/default_modules/sync_server/tray/widgets.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/openpype/modules/default_modules/sync_server/tray/app.py b/openpype/modules/default_modules/sync_server/tray/app.py index 5298c7be1d..0996cbc468 100644 --- a/openpype/modules/default_modules/sync_server/tray/app.py +++ b/openpype/modules/default_modules/sync_server/tray/app.py @@ -84,6 +84,7 @@ class SyncServerWindow(QtWidgets.QDialog): self.pause_btn.setAutoDefault(False) self.pause_btn.setDefault(False) repres.message_generated.connect(self._update_message) + self.projects.message_generated.connect(self._update_message) self.representationWidget = repres diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index 4fc5723f42..b0730c9c8d 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -30,6 +30,7 @@ class SyncProjectListWidget(QtWidgets.QWidget): Lists all projects that are synchronized to choose from """ project_changed = QtCore.Signal() + message_generated = QtCore.Signal(str) def __init__(self, sync_server, parent): super(SyncProjectListWidget, self).__init__(parent) @@ -71,6 +72,12 @@ class SyncProjectListWidget(QtWidgets.QWidget): def _on_index_change(self, new_idx, _old_idx): project_name = new_idx.data(QtCore.Qt.DisplayRole) + if not self.sync_server.get_sync_project_setting(project_name): + self.message_generated.emit( + "Project {} not active anymore".format(project_name)) + self.refresh() + return + self.current_project = project_name self.project_changed.emit() From fd7438c2aca6e8effcbf5b46e54c2d8e1c95eeb4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 14:02:30 +0200 Subject: [PATCH 399/716] 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 400/716] 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 401/716] 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 402/716] 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 403/716] _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 404/716] 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 405/716] 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 406/716] 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 407/716] 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 408/716] 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 3cff1e38a870cc13bc343c39437c9b92e08c704a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 15:59:07 +0200 Subject: [PATCH 409/716] added info about thirdparty python modules and about PySide2 --- website/docs/dev_build.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/website/docs/dev_build.md b/website/docs/dev_build.md index f71118eba6..bf606ae7c0 100644 --- a/website/docs/dev_build.md +++ b/website/docs/dev_build.md @@ -19,6 +19,7 @@ We use [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze the cod This is outline of build steps. Most of them are done automatically via scripts: - Virtual environment is created using **Poetry** in `.venv` +- Necessary python modules outside of `.venv` are stored to `./vendor/python` (like `PySide2`) - Necessary third-party tools (like [ffmpeg](https://www.ffmpeg.org/), [OpenImageIO](https://github.com/OpenImageIO/oiio) and [usd libraries](https://developer.nvidia.com/usd)) are downloaded to `./vendor/bin` - OpenPype code is frozen with **cx_freeze** to `./build` @@ -55,14 +56,14 @@ For development purposes it is possible to run OpenPype directly from the source To start OpenPype from source you need to 1. Run `.\tools\create_env.ps1` to create virtual environment in `.venv` -2. Run `.\tools\fetch_thirdparty_libs.ps1` to get **ffmpeg**, **oiio** and other tools needed. +2. Run `.\tools\fetch_thirdparty_libs.ps1` to get **PySide2**, **ffmpeg**, **oiio** and other tools needed. 3. Run `.\tools\run_tray.ps1` if you have all required dependencies on your machine you should be greeted with OpenPype igniter window and once you give it your Mongo URL, with OpenPype icon in the system tray. Step 1 and 2 needs to be run only once (or when something was changed). #### To build OpenPype: 1. Run `.\tools\create_env.ps1` to create virtual environment in `.venv` -2. Run `.\tools\fetch_thirdparty_libs.ps1` to get **ffmpeg**, **oiio** and other tools needed. +2. Run `.\tools\fetch_thirdparty_libs.ps1` to get **PySide2**, **ffmpeg**, **oiio** and other tools needed. 3. `.\tools\build.ps1` to build OpenPype to `.\build` @@ -185,7 +186,7 @@ For more information about setting your build environment please refer to [pyenv #### To build Pype: 1. Run `./tools/create_env.sh` to create virtual environment in `./venv` -2. Run `./tools/fetch_thirdparty_libs.sh` to get **ffmpeg**, **oiio** and other tools needed. +2. Run `./tools/fetch_thirdparty_libs.sh` to get **PySide2**, **ffmpeg**, **oiio** and other tools needed. 3. Run `./tools/build.sh` to build pype executables in `.\build\` @@ -273,6 +274,19 @@ pywin32 = { version = "300", markers = "sys_platform == 'win32'" } For more information see [Poetry documentation](https://python-poetry.org/docs/dependency-specification/). +### Python modules as thirdparty +There are some python modules that can be available only in OpenPype and should not be propagated to any subprocess. +Best example is **PySide2** which is required to run OpenPype but can be used only in OpenPype and should not be in PYTHONPATH for most of host applications. +We've decided to separate these breaking dependencies to be able run OpenPype from code and from build the same way. + +:::warning +**PySide2** has handled special cases related to it's build process. +### Linux +- We're fixing rpath of shared objects on linux which is modified during cx freeze processing. +### MacOS +- **QtSql** libraries are removed on MacOS because their dependencies are not available and would require to modify rpath of Postgre library. +::: + ### Binary dependencies To add some binary tool or something that doesn't fit standard Python distribution methods, you can use [fetch_thirdparty_libs](#fetch_thirdparty_libs) script. It will take things defined in From b7fee7f332a76d9d1249b5e1853828b2947e5220 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 16:03:25 +0200 Subject: [PATCH 410/716] remove sql drivers on mac --- tools/fetch_thirdparty_libs.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tools/fetch_thirdparty_libs.py b/tools/fetch_thirdparty_libs.py index 803d092186..b616beab27 100644 --- a/tools/fetch_thirdparty_libs.py +++ b/tools/fetch_thirdparty_libs.py @@ -78,18 +78,28 @@ except AttributeError: _print("No PySide2 version was specified, using latest available.", 2) pyside2_arg = "PySide2" if not pyside2_version else "PySide2{}".format(pyside2_version) # noqa: E501 +python_vendor_dir = openpype_root / "vendor" / "python" try: subprocess.run( [sys.executable, "-m", "pip", "install", "--upgrade", - pyside2_arg, "-t", str(openpype_root / "vendor/python")], + pyside2_arg, "-t", str(python_vendor_dir)], check=True, stdout=subprocess.DEVNULL) except subprocess.CalledProcessError as e: _print("Error during PySide2 installation.", 1) _print(str(e), 1) sys.exit(1) -_print("Processing third-party dependencies ...") +# Remove libraries for QtSql which don't have available libraries +# by default and Postgre library would require to modify rpath of dependency platform_name = platform.system().lower() +if platform_name == "darwin": + pyside2_sqldrivers_dir = ( + python_vendor_dir / "PySide2" / "Qt" / "plugins" / "sqldrivers" + ) + for filepath in pyside2_sqldrivers_dir.iterdir(): + os.remove(str(filepath)) + +_print("Processing third-party dependencies ...") try: thirdparty = pyproject["openpype"]["thirdparty"] From de69cd69c75596da40755d6cb3bc8f9d19ad1f3a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 23 Sep 2021 16:42:54 +0200 Subject: [PATCH 411/716] 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 412/716] =?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 413/716] =?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 c1eeee9fdc81a255f84dd98338d90ea818e2ec77 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 23 Sep 2021 16:49:51 +0100 Subject: [PATCH 414/716] Apply suggestions from code review --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 0c7146634f..880dba5cfb 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -29,9 +29,10 @@ class UnrealPrelaunchHook(PreLaunchHook): def _get_work_filename(self): # Use last workfile if was found - last_workfile = self.data.get("last_workfile_path") - if last_workfile and os.path.exists(last_workfile): - return os.path.basename(last_workfile) + if self.data.get("last_workfile_path"): + last_workfile = Path(self.data.get("last_workfile_path")) + if last_workfile and last_workfile.exists(): + return last_workfile.name # Prepare data for fill data and for getting workfile template key task_name = self.data["task_name"] @@ -91,7 +92,7 @@ class UnrealPrelaunchHook(PreLaunchHook): # of the project name. This is because project name is then used # in various places inside c++ code and there variable names cannot # start with non-alpha. We append 'P' before project name to solve it. - # 😱 + # 😱 if not unreal_project_name[:1].isalpha(): self.log.warning(( "Project name doesn't start with alphabet " From 22664f777d1dece5db6279092549b09303b6d4bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Sep 2021 18:04:41 +0200 Subject: [PATCH 415/716] removed not needed packages --- Dockerfile.centos7 | 3 --- 1 file changed, 3 deletions(-) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index 67d45ce3b2..f3b257e66b 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -41,9 +41,6 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n ncurses \ ncurses-devel \ qt5-qtbase-devel \ - libxcb libxcb-devel \ - xcb-util xcb-util-devel \ - libxkbcommon-devel libxkbcommon-x11-devel \ && yum clean all # we need to build our own patchelf From 85ae0b4b8418137bc25129804e1d8576ea0abf05 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Sep 2021 20:05:26 +0200 Subject: [PATCH 416/716] Fix - better way to handle concurrend deactivate of projects Covers some weird edge cases --- openpype/modules/default_modules/sync_server/tray/app.py | 8 ++++++++ .../modules/default_modules/sync_server/tray/models.py | 4 ++++ .../modules/default_modules/sync_server/tray/widgets.py | 6 ------ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/tray/app.py b/openpype/modules/default_modules/sync_server/tray/app.py index 0996cbc468..fdf8e61965 100644 --- a/openpype/modules/default_modules/sync_server/tray/app.py +++ b/openpype/modules/default_modules/sync_server/tray/app.py @@ -91,10 +91,18 @@ class SyncServerWindow(QtWidgets.QDialog): def _on_project_change(self): if self.projects.current_project is None: return + self.representationWidget.table_view.model().set_project( self.projects.current_project ) + project_name = self.projects.current_project + if not self.sync_server.get_sync_project_setting(project_name): + self.projects.message_generated.emit( + "Project {} not active anymore".format(project_name)) + self.projects.refresh() + return + def showEvent(self, event): self.representationWidget.model.set_project( self.projects.current_project) diff --git a/openpype/modules/default_modules/sync_server/tray/models.py b/openpype/modules/default_modules/sync_server/tray/models.py index 96d09b8786..63c9dfa16e 100644 --- a/openpype/modules/default_modules/sync_server/tray/models.py +++ b/openpype/modules/default_modules/sync_server/tray/models.py @@ -301,6 +301,10 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): """ self._project = project self.sync_server.set_sync_project_settings() + # project might have been deactivated in the meantime + if not self.sync_server.get_sync_project_setting(project): + return + self.active_site = self.sync_server.get_active_site(self.project) self.remote_site = self.sync_server.get_remote_site(self.project) self.refresh() diff --git a/openpype/modules/default_modules/sync_server/tray/widgets.py b/openpype/modules/default_modules/sync_server/tray/widgets.py index b0730c9c8d..5806179f61 100644 --- a/openpype/modules/default_modules/sync_server/tray/widgets.py +++ b/openpype/modules/default_modules/sync_server/tray/widgets.py @@ -72,12 +72,6 @@ class SyncProjectListWidget(QtWidgets.QWidget): def _on_index_change(self, new_idx, _old_idx): project_name = new_idx.data(QtCore.Qt.DisplayRole) - if not self.sync_server.get_sync_project_setting(project_name): - self.message_generated.emit( - "Project {} not active anymore".format(project_name)) - self.refresh() - return - self.current_project = project_name self.project_changed.emit() From da7dda6b6a8b7d8cd240292c73bb7eec641490e5 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 24 Sep 2021 02:35:59 +0800 Subject: [PATCH 417/716] 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 418/716] 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 419/716] 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 420/716] 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 421/716] 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 422/716] [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 423/716] 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 424/716] 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 425/716] 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 426/716] 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 427/716] 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 428/716] 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 429/716] 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 430/716] 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 431/716] 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 432/716] 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 433/716] 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 434/716] 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 435/716] 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 436/716] 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 437/716] 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 438/716] 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 439/716] 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 440/716] 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 441/716] 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 442/716] 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 443/716] 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 444/716] 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 445/716] 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 446/716] 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 447/716] 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 448/716] 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 449/716] 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 450/716] 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 451/716] 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 452/716] 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 453/716] 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 454/716] 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 455/716] 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 456/716] 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 457/716] 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 458/716] 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 459/716] 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 460/716] 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 461/716] 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 462/716] 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 463/716] 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 464/716] 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 465/716] 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 466/716] 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 467/716] 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 468/716] 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 469/716] 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 470/716] 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 471/716] 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 472/716] 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 473/716] 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 474/716] 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 475/716] 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 476/716] 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 477/716] 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 478/716] 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 479/716] 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 480/716] [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 481/716] 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 482/716] 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 483/716] 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 484/716] 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 485/716] 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 486/716] 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 487/716] 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 488/716] 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 489/716] 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 490/716] 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 491/716] Updated documentation --- website/docs/admin_settings_system.md | 3 +++ .../settings/settings_system_general.png | Bin 17313 -> 33586 bytes 2 files changed, 3 insertions(+) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 80154356af..6057ed0830 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -21,6 +21,9 @@ as a naive barier to prevent artists from accidental setting changes. **`Environment`** - Globally applied environment variables that will be appended to any OpenPype process in the studio. +**`Disk mapping`** - Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up. +Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume). + **`Versions Repository`** - Location where automatic update mechanism searches for zip files with OpenPype update packages. To read more about preparing OpenPype for automatic updates go to [Admin Distribute docs](admin_distribute#2-openpype-codebase) diff --git a/website/docs/assets/settings/settings_system_general.png b/website/docs/assets/settings/settings_system_general.png index 4c1452d0d474b8605c6e0438641e2f54593cd906..d04586205d24ee30445a80ce54330b2c3c03e77c 100644 GIT binary patch literal 33586 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 literal 17313 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
    Date: Thu, 30 Sep 2021 15:59:09 +0200 Subject: [PATCH 492/716] 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 493/716] 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 494/716] 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 495/716] 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 496/716] 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 497/716] 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 498/716] 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 499/716] 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 500/716] 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 501/716] 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 502/716] 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 503/716] 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 504/716] 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 505/716] 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 506/716] 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 507/716] 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 508/716] 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 509/716] 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 510/716] 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 511/716] 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 512/716] 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 513/716] 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 514/716] 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 515/716] 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 516/716] 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 517/716] 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 518/716] 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 519/716] 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 520/716] [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 521/716] 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 522/716] 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 523/716] 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 524/716] 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 525/716] 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 526/716] 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 527/716] 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 528/716] 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 529/716] 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 530/716] 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 531/716] 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 532/716] 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 533/716] 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 534/716] 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 535/716] 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 536/716] 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 537/716] 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 538/716] 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 539/716] 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 540/716] 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 541/716] 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 542/716] 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 543/716] 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 544/716] [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 545/716] 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 546/716] 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 1428963e5f42810b8bf33a3d3fba0c105e0e42ca Mon Sep 17 00:00:00 2001 From: karimmozilla Date: Wed, 6 Oct 2021 13:31:33 +0200 Subject: [PATCH 547/716] crop to reformat --- openpype/hosts/nuke/api/lib.py | 4 ++-- openpype/hosts/nuke/plugins/create/create_write_render.py | 6 +++--- .../nuke/plugins/publish/validate_output_resolution.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ab4c992719..87bf137d93 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1031,7 +1031,7 @@ class WorkfileSettings(object): log.error(msg) nuke.message(msg) - bbox = self._asset_entity.get('data', {}).get('crop') + bbox = self._asset_entity.get('data', {}).get('reformat') if bbox: try: @@ -1046,7 +1046,7 @@ class WorkfileSettings(object): ) except Exception as e: bbox = None - msg = ("{}:{} \nFormat:Crop need to be set with dots, " + msg = ("{}:{} \nFormat:Reformat need to be set with dots, " "example: 0.0.1920.1080, " "/nSetting to default").format(__name__, e) log.error(msg) diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index a1381122ee..3a68ce4035 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -99,7 +99,7 @@ class CreateWriteRender(plugin.PypeCreator): "fpath_template": ("{work}/renders/nuke/{subset}" "/{subset}.{frame}.{ext}")}) - # add crop node to cut off all outside of format bounding box + # add reformat node to cut off all outside of format bounding box # get width and height try: width, height = (selected_node.width(), selected_node.height()) @@ -109,8 +109,8 @@ class CreateWriteRender(plugin.PypeCreator): _prenodes = [ { - "name": "Crop01", - "class": "Crop", + "name": "Reformat01", + "class": "Reformat", "knobs": [ ("box", [ 0.0, diff --git a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py index 2563ee929f..27094b8d74 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py +++ b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py @@ -56,8 +56,8 @@ class ValidateOutputResolution(pyblish.api.InstancePlugin): def process(self, instance): - # Skip bounding box check if a crop node exists. - if instance[0].dependencies()[0].Class() == "Crop": + # Skip bounding box check if a reformat node exists. + if instance[0].dependencies()[0].Class() == "Reformat": return msg = "Bounding box is outside the format." From 2681caacd7058de35bbae60e75be5740024c0621 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 6 Oct 2021 13:47:42 +0200 Subject: [PATCH 548/716] 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 cb4dc83fd84aebe87a3dd863556d0a790d7d8bdf Mon Sep 17 00:00:00 2001 From: karimmozilla Date: Wed, 6 Oct 2021 13:49:59 +0200 Subject: [PATCH 549/716] reformat knobs --- .../hosts/nuke/plugins/create/create_write_render.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 3a68ce4035..acc1ee53ff 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -112,12 +112,10 @@ class CreateWriteRender(plugin.PypeCreator): "name": "Reformat01", "class": "Reformat", "knobs": [ - ("box", [ - 0.0, - 0.0, - width, - height - ]) + ("type", 1), + ("box_fixed", 1), + ("box_width", width), + ("box_height", height) ], "dependent": None } From aefbfa638aef9106d7b0d74657efeec031edbaf1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 6 Oct 2021 16:30:01 +0200 Subject: [PATCH 550/716] 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 947139f425218c51618e09d7d1de748521b03039 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 6 Oct 2021 18:14:37 +0200 Subject: [PATCH 551/716] downgrade setuptools --- tools/create_env.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/create_env.sh b/tools/create_env.sh index 4ed6412c43..6fe6cdafb9 100755 --- a/tools/create_env.sh +++ b/tools/create_env.sh @@ -188,10 +188,11 @@ main () { # reinstall these because of bug in poetry? or cx_freeze? # cx_freeze will crash on missing __pychache__ on these but # reinstalling them solves the problem. - echo -e "${BIGreen}>>>${RST} Fixing pycache bug ..." - "$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall setuptools + echo -e "${BIGreen}>>>${RST} Post-venv creation fixes ..." + "$POETRY_HOME/bin/poetry" run pip install setuptools==49.6.0 "$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall wheel "$POETRY_HOME/bin/poetry" run python -m pip install --disable-pip-version-check --force-reinstall pip + "$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall cx_freeze } return_code=0 From 4d612931ef6295ef848ba11f3894c452115e4f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 7 Oct 2021 09:46:16 +0200 Subject: [PATCH 552/716] pyproject parser for shell scripts --- tools/parse_pyproject.py | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tools/parse_pyproject.py diff --git a/tools/parse_pyproject.py b/tools/parse_pyproject.py new file mode 100644 index 0000000000..0aed321fcf --- /dev/null +++ b/tools/parse_pyproject.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +"""Parse pyproject.toml and return its values. + +Useful for shell scripts to know more about OpenPype build. +""" +import os +import blessed +import toml +from pathlib import Path +import click + +term = blessed.Terminal() + + +def _print(msg: str, message_type: int = 0) -> None: + """Print message to console. + + Args: + msg (str): message to print + message_type (int): type of message (0 info, 1 error, 2 note) + + """ + if message_type == 0: + header = term.aquamarine3(">>> ") + elif message_type == 1: + header = term.orangered2("!!! ") + elif message_type == 2: + header = term.tan1("... ") + else: + header = term.darkolivegreen3("--- ") + + print("{}{}".format(header, msg)) + + +@click.command() +@click.argument("key", nargs=-1, type=click.STRING) +def main(key): + _print("Reading build metadata ...") + openpype_root = Path(os.path.dirname(__file__)).parent + py_project = toml.load(openpype_root / "pyproject.toml") + query = key.split(".") + data = py_project + for k in query: + if isinstance(data, dict): + data = data.get(k) + else: + break + print(data) + + +if __name__ == "__main__": + main() From 94d4926084a594cfd1170c7f7cbaaf77a16a1fea Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 7 Oct 2021 12:51:50 +0200 Subject: [PATCH 553/716] 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 554/716] 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 9f9caafb0d3b80e9bf0d2a4e2fae288db7d30056 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 7 Oct 2021 18:18:33 +0200 Subject: [PATCH 555/716] reinstall cx_freeze with downgraded setuptools --- setup.py | 5 ++-- tools/build_dependencies.py | 25 ++++++++++++---- tools/create_env.sh | 4 ++- tools/parse_pyproject.py | 60 +++++++++++++++++-------------------- 4 files changed, 52 insertions(+), 42 deletions(-) diff --git a/setup.py b/setup.py index 55098cb0b4..cd3ed4f82c 100644 --- a/setup.py +++ b/setup.py @@ -59,13 +59,14 @@ includes = [] excludes = [ "openpype" ] -bin_includes = [] +bin_includes = [ + "vendor" +] include_files = [ "igniter", "openpype", "repos", "schema", - "vendor", "LICENSE", "README.md" ] diff --git a/tools/build_dependencies.py b/tools/build_dependencies.py index 1798b7ca8f..75d6e0c315 100644 --- a/tools/build_dependencies.py +++ b/tools/build_dependencies.py @@ -113,7 +113,26 @@ if not build_dir.exists(): _print("Probably freezing of code failed. Check ./build/build.log", 3) sys.exit(1) +def _progress(_base, _names): + progress_bar.update() + return [] + deps_dir = build_dir / "dependencies" +vendor_dir = build_dir / "vendor" +vendor_src = openpype_root / "vendor" + +# copy vendor files +_print("Copying vendor files ...") + +total_files = count_folders(vendor_src) +progress_bar = enlighten.Counter( + total=total_files, desc="Copying vendor files ...", + units="%", color=(64, 128, 222)) + +shutil.copytree(vendor_src.as_posix(), + vendor_dir.as_posix(), + ignore=_progress) +progress_bar.close() # copy all files _print("Copying dependencies ...") @@ -123,12 +142,6 @@ progress_bar = enlighten.Counter( total=total_files, desc="Processing Dependencies", units="%", color=(53, 178, 202)) - -def _progress(_base, _names): - progress_bar.update() - return [] - - shutil.copytree(site_pkg.as_posix(), deps_dir.as_posix(), ignore=_progress) diff --git a/tools/create_env.sh b/tools/create_env.sh index 6fe6cdafb9..917ddc36ba 100755 --- a/tools/create_env.sh +++ b/tools/create_env.sh @@ -189,10 +189,12 @@ main () { # cx_freeze will crash on missing __pychache__ on these but # reinstalling them solves the problem. echo -e "${BIGreen}>>>${RST} Post-venv creation fixes ..." + local openpype_index=$("$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/parse_pyproject.py" tool.poetry.source.0.url) + echo -e "${BIGreen}- ${RST} Using index: ${BIWhite}$openpype_index${RST}" "$POETRY_HOME/bin/poetry" run pip install setuptools==49.6.0 "$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall wheel "$POETRY_HOME/bin/poetry" run python -m pip install --disable-pip-version-check --force-reinstall pip - "$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall cx_freeze + "$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall cx_freeze -i $openpype_index --extra-index-url https://pypi.org/simple } return_code=0 diff --git a/tools/parse_pyproject.py b/tools/parse_pyproject.py index 0aed321fcf..296d73654d 100644 --- a/tools/parse_pyproject.py +++ b/tools/parse_pyproject.py @@ -3,49 +3,43 @@ Useful for shell scripts to know more about OpenPype build. """ +import sys import os -import blessed import toml from pathlib import Path import click -term = blessed.Terminal() - - -def _print(msg: str, message_type: int = 0) -> None: - """Print message to console. - - Args: - msg (str): message to print - message_type (int): type of message (0 info, 1 error, 2 note) - - """ - if message_type == 0: - header = term.aquamarine3(">>> ") - elif message_type == 1: - header = term.orangered2("!!! ") - elif message_type == 2: - header = term.tan1("... ") - else: - header = term.darkolivegreen3("--- ") - - print("{}{}".format(header, msg)) @click.command() -@click.argument("key", nargs=-1, type=click.STRING) -def main(key): - _print("Reading build metadata ...") +@click.argument("keys", nargs=-1, type=click.STRING) +def main(keys): + """Get values from `pyproject.toml`. + + You can specify dot separated keys from `pyproject.toml` + as arguments and this script will return them on separate + lines. If key doesn't exists, None is returned. + + + """ openpype_root = Path(os.path.dirname(__file__)).parent py_project = toml.load(openpype_root / "pyproject.toml") - query = key.split(".") - data = py_project - for k in query: - if isinstance(data, dict): - data = data.get(k) - else: - break - print(data) + for q in keys: + query = q.split(".") + data = py_project + for i, k in enumerate(query): + + if isinstance(data, list): + try: + data = data[int(k)] + except IndexError: + print("None") + sys.exit() + continue + + if isinstance(data, dict): + data = data.get(k) + print(data) if __name__ == "__main__": From 4276f34a301fe65ed1960e9a87935121907a0016 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 7 Oct 2021 23:04:48 +0200 Subject: [PATCH 556/716] fix hound --- tools/build_dependencies.py | 2 ++ tools/parse_pyproject.py | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/build_dependencies.py b/tools/build_dependencies.py index 75d6e0c315..8eaf9d87e3 100644 --- a/tools/build_dependencies.py +++ b/tools/build_dependencies.py @@ -113,10 +113,12 @@ if not build_dir.exists(): _print("Probably freezing of code failed. Check ./build/build.log", 3) sys.exit(1) + def _progress(_base, _names): progress_bar.update() return [] + deps_dir = build_dir / "dependencies" vendor_dir = build_dir / "vendor" vendor_src = openpype_root / "vendor" diff --git a/tools/parse_pyproject.py b/tools/parse_pyproject.py index 296d73654d..dacecd88d0 100644 --- a/tools/parse_pyproject.py +++ b/tools/parse_pyproject.py @@ -10,7 +10,6 @@ from pathlib import Path import click - @click.command() @click.argument("keys", nargs=-1, type=click.STRING) def main(keys): @@ -19,7 +18,6 @@ def main(keys): You can specify dot separated keys from `pyproject.toml` as arguments and this script will return them on separate lines. If key doesn't exists, None is returned. - """ openpype_root = Path(os.path.dirname(__file__)).parent @@ -27,8 +25,8 @@ def main(keys): for q in keys: query = q.split(".") data = py_project - for i, k in enumerate(query): + for k in query: if isinstance(data, list): try: data = data[int(k)] From 8312412cd6105fd18a38a3193cfbc7fe237ca89f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 9 Oct 2021 03:38:41 +0000 Subject: [PATCH 557/716] [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 558/716] 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 c8b1037e99e829be086ab092e94103e5eb115ef6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 11 Oct 2021 16:35:32 +0200 Subject: [PATCH 559/716] added few magic attributes to getattr --- openpype/modules/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 748c7857a9..7779fff6ec 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -48,7 +48,7 @@ class _ModuleClass(object): def __getattr__(self, attr_name): if attr_name not in self.__attributes__: - if attr_name in ("__path__"): + if attr_name in ("__path__", "__file__"): return None raise ImportError("No module named {}.{}".format( self.name, attr_name @@ -104,6 +104,9 @@ class _InterfacesClass(_ModuleClass): """ def __getattr__(self, attr_name): if attr_name not in self.__attributes__: + if attr_name in ("__path__", "__file__"): + return None + # Fake Interface if is not missing self.__attributes__[attr_name] = type( attr_name, From 45b39792e3d18c409e076a0d9788e981cd630001 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 11 Oct 2021 16:35:48 +0200 Subject: [PATCH 560/716] removed boolean from setFocus --- openpype/tools/settings/settings/categories.py | 2 +- openpype/tools/settings/settings/dict_mutable_widget.py | 6 +++--- openpype/tools/settings/settings/list_item_widget.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index be2264340b..5f9051344d 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -508,7 +508,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): first_invalid_item = invalid_items[0] self.scroll_widget.ensureWidgetVisible(first_invalid_item) if first_invalid_item.isVisible(): - first_invalid_item.setFocus(True) + first_invalid_item.setFocus() return False def on_saved(self, saved_tab_widget): diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index cfb9d4a4b1..9afce7259e 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -128,9 +128,9 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): def add_new_item(self, key=None, label=None): input_field = self.entity_widget.add_new_key(key, label) if self.collapsible_key: - self.key_input.setFocus(True) + self.key_input.setFocus() else: - input_field.key_input.setFocus(True) + input_field.key_input.setFocus() return input_field def _on_add_clicked(self): @@ -563,7 +563,7 @@ class ModifiableDictItem(QtWidgets.QWidget): def on_add_clicked(self): widget = self.entity_widget.add_new_key(None, None) - widget.key_input.setFocus(True) + widget.key_input.setFocus() def on_edit_pressed(self): if not self.key_input.isVisible(): diff --git a/openpype/tools/settings/settings/list_item_widget.py b/openpype/tools/settings/settings/list_item_widget.py index 17412a30b9..128af92631 100644 --- a/openpype/tools/settings/settings/list_item_widget.py +++ b/openpype/tools/settings/settings/list_item_widget.py @@ -357,7 +357,7 @@ class ListWidget(InputWidget): new_entity = self.entity.add_new_item(row) input_field = self._input_fields_by_entity_id.get(new_entity.id) if input_field is not None: - input_field.input_field.setFocus(True) + input_field.input_field.setFocus() return new_entity def add_row(self, child_entity, row=None): From e3303f88322290143beffbc3b4b2bf116291bb63 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 11 Oct 2021 16:40:16 +0200 Subject: [PATCH 561/716] avoid garbage collection of clipboard --- openpype/tools/settings/settings/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index ab6b27bdaf..92fffe6f9c 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -214,7 +214,8 @@ class BaseWidget(QtWidgets.QWidget): def _paste_value_actions(self, menu): output = [] # Allow paste of value only if were copied from this UI - mime_data = QtWidgets.QApplication.clipboard().mimeData() + clipboard = QtWidgets.QApplication.clipboard() + mime_data = clipboard.mimeData() mime_value = mime_data.data("application/copy_settings_value") # Skip if there is nothing to do if not mime_value: From 330d61cf00658e591b35fc4c8f48d4543df030a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 11 Oct 2021 17:40:41 +0200 Subject: [PATCH 562/716] 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 563/716] 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 e969cd188395b34b447978ff44989fb0d210ff01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LORRAIN?= Date: Tue, 12 Oct 2021 16:41:02 +0200 Subject: [PATCH 564/716] Add photoshop extractReview plugin options for representations tags --- .../plugins/publish/extract_review.py | 9 +++-- openpype/plugins/publish/extract_burnin.py | 3 +- .../defaults/project_settings/photoshop.json | 12 +++++++ .../schema_project_photoshop.json | 34 ++++++++++++++++++- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 1c53c3a2ef..8c4d05b282 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -17,6 +17,10 @@ class ExtractReview(openpype.api.Extractor): hosts = ["photoshop"] families = ["review"] + # Extract Options + jpg_options = None + mov_options = None + def process(self, instance): staging_dir = self.staging_dir(instance) self.log.info("Outputting image to {}".format(staging_dir)) @@ -53,7 +57,8 @@ class ExtractReview(openpype.api.Extractor): "name": "jpg", "ext": "jpg", "files": output_image, - "stagingDir": staging_dir + "stagingDir": staging_dir, + "tags": self.jpg_options['tags'] }) instance.data["stagingDir"] = staging_dir @@ -97,7 +102,7 @@ class ExtractReview(openpype.api.Extractor): "frameEnd": 1, "fps": 25, "preview": True, - "tags": ["review", "ftrackreview"] + "tags": self.mov_options['tags'] }) # Required for extract_review plugin (L222 onwards). diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 207e696fb1..06eb85c593 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -45,7 +45,8 @@ class ExtractBurnin(openpype.api.Extractor): "aftereffects", "tvpaint", "webpublisher", - "aftereffects" + "aftereffects", + "photoshop" # "resolve" ] optional = True diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 4c36e4bd49..36c30bad6c 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -17,6 +17,18 @@ "png", "jpg" ] + }, + "ExtractReview": { + "jpg_options": { + "tags": [ + ] + }, + "mov_options": { + "tags": [ + "review", + "ftrackreview" + ] + } } }, "workfile_builder": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 3b65f08ac4..6f5577650c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -60,7 +60,39 @@ "object_type": "text" } ] - } + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractReview", + "label": "Extract Review", + "children": [ + { + "type": "dict", + "collapsible": false, + "key": "jpg_options", + "label": "Extracted jpg Options", + "children": [ + { + "type": "schema", + "name": "schema_representation_tags" + } + ] + }, + { + "type": "dict", + "collapsible": false, + "key": "mov_options", + "label": "Extracted mov Options", + "children": [ + { + "type": "schema", + "name": "schema_representation_tags" + } + ] + } + ] + } ] }, { From 50742bba5c6b3d620bf13042ecd61dff352fc544 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Oct 2021 16:45:29 +0200 Subject: [PATCH 565/716] nuke: adding supporting plugin function --- openpype/hosts/nuke/api/plugin.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 0ad98146b1..71329c0d46 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -23,3 +23,19 @@ class PypeCreator(PypeCreatorMixin, avalon.nuke.pipeline.Creator): self.log.error(msg + '\n\nPlease use other subset name!') raise NameError("`{0}: {1}".format(__name__, msg)) return + + +def get_review_presets_config(): + settings = get_current_project_settings() + review_profiles = ( + settings["global"] + ["publish"] + ["ExtractReview"] + ["profiles"] + ) + + outputs = {} + for profile in review_profiles: + outputs.update(profile.get("outputs", {})) + + return [str(name) for name, _prop in outputs.items()] From ad2be29831e483bdbf1c4c61622e40884e23b0a6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Oct 2021 16:45:53 +0200 Subject: [PATCH 566/716] nuke: adding repair inventory action --- .../plugins/inventory/repair_old_loaders.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 openpype/hosts/nuke/plugins/inventory/repair_old_loaders.py diff --git a/openpype/hosts/nuke/plugins/inventory/repair_old_loaders.py b/openpype/hosts/nuke/plugins/inventory/repair_old_loaders.py new file mode 100644 index 0000000000..e7ae51fa86 --- /dev/null +++ b/openpype/hosts/nuke/plugins/inventory/repair_old_loaders.py @@ -0,0 +1,37 @@ +from avalon import api, style +from avalon.nuke import lib as anlib +from openpype.api import ( + Logger) + + +class RepairOldLoaders(api.InventoryAction): + + label = "Repair Old Loaders" + icon = "gears" + color = style.colors.alert + + log = Logger().get_logger(__name__) + + def process(self, containers): + import nuke + new_loader = "LoadClip" + + for cdata in containers: + orig_loader = cdata["loader"] + orig_name = cdata["objectName"] + if orig_loader not in ["LoadSequence", "LoadMov"]: + self.log.warning( + "This repair action is only working on " + "`LoadSequence` and `LoadMov` Loaders") + continue + + new_name = orig_name.replace(orig_loader, new_loader) + node = nuke.toNode(cdata["objectName"]) + + cdata.update({ + "loader": new_loader, + "objectName": new_name + }) + node["name"].setValue(new_name) + # get data from avalon knob + anlib.set_avalon_knob_data(node, cdata) From fcd9ebda39459eccf407875217f920745ef8d5d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Oct 2021 16:46:17 +0200 Subject: [PATCH 567/716] nuke: removing unused code --- .../nuke/plugins/inventory/set_tool_color.py | 68 ------------------- 1 file changed, 68 deletions(-) delete mode 100644 openpype/hosts/nuke/plugins/inventory/set_tool_color.py diff --git a/openpype/hosts/nuke/plugins/inventory/set_tool_color.py b/openpype/hosts/nuke/plugins/inventory/set_tool_color.py deleted file mode 100644 index 7a81444c90..0000000000 --- a/openpype/hosts/nuke/plugins/inventory/set_tool_color.py +++ /dev/null @@ -1,68 +0,0 @@ -# from avalon import api, style -# from avalon.vendor.Qt import QtGui, QtWidgets -# -# import avalon.fusion -# -# -# class FusionSetToolColor(api.InventoryAction): -# """Update the color of the selected tools""" -# -# label = "Set Tool Color" -# icon = "plus" -# color = "#d8d8d8" -# _fallback_color = QtGui.QColor(1.0, 1.0, 1.0) -# -# def process(self, containers): -# """Color all selected tools the selected colors""" -# -# result = [] -# comp = avalon.fusion.get_current_comp() -# -# # Get tool color -# first = containers[0] -# tool = first["_node"] -# color = tool.TileColor -# -# if color is not None: -# qcolor = QtGui.QColor().fromRgbF(color["R"], color["G"], color["B"]) -# else: -# qcolor = self._fallback_color -# -# # Launch pick color -# picked_color = self.get_color_picker(qcolor) -# if not picked_color: -# return -# -# with avalon.fusion.comp_lock_and_undo_chunk(comp): -# for container in containers: -# # Convert color to RGB 0-1 floats -# rgb_f = picked_color.getRgbF() -# rgb_f_table = {"R": rgb_f[0], "G": rgb_f[1], "B": rgb_f[2]} -# -# # Update tool -# tool = container["_node"] -# tool.TileColor = rgb_f_table -# -# result.append(container) -# -# return result -# -# def get_color_picker(self, color): -# """Launch color picker and return chosen color -# -# Args: -# color(QtGui.QColor): Start color to display -# -# Returns: -# QtGui.QColor -# -# """ -# -# color_dialog = QtWidgets.QColorDialog(color) -# color_dialog.setStyleSheet(style.load_stylesheet()) -# -# accepted = color_dialog.exec_() -# if not accepted: -# return -# -# return color_dialog.selectedColor() From 3ed84b3bc932d41c1ae2a49dfb67b81120740cee Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Oct 2021 16:46:54 +0200 Subject: [PATCH 568/716] nuke: fixing inventory action obsolete way of accessing nodes --- openpype/hosts/nuke/plugins/inventory/select_containers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/inventory/select_containers.py b/openpype/hosts/nuke/plugins/inventory/select_containers.py index b420f53431..bd00983172 100644 --- a/openpype/hosts/nuke/plugins/inventory/select_containers.py +++ b/openpype/hosts/nuke/plugins/inventory/select_containers.py @@ -8,10 +8,10 @@ class SelectContainers(api.InventoryAction): color = "#d8d8d8" def process(self, containers): - + import nuke import avalon.nuke - nodes = [i["_node"] for i in containers] + nodes = [nuke.toNode(i["objectName"]) for i in containers] with avalon.nuke.viewer_update_and_undo_stop(): # clear previous_selection From d1029a30426706ef5f4ad169ab49ee116e19cb94 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Oct 2021 16:54:36 +0200 Subject: [PATCH 569/716] Nuke: new way of adding representation from settings to plugins --- .../hosts/nuke/plugins/load/load_image.py | 14 +++++- .../defaults/project_settings/nuke.json | 45 ++----------------- .../schemas/schema_nuke_load.json | 8 +--- .../schemas/template_loader_plugin_nuke.json | 8 +--- 4 files changed, 19 insertions(+), 56 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 9b8bc43d12..2af44d6eba 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -12,7 +12,15 @@ from openpype.hosts.nuke.api.lib import ( class LoadImage(api.Loader): """Load still image into Nuke""" - families = ["render", "source", "plate", "review", "image"] + families = [ + "render2d", + "source", + "plate", + "render", + "prerender", + "review", + "image" + ] representations = ["exr", "dpx", "jpg", "jpeg", "png", "psd", "tiff"] label = "Load Image" @@ -33,6 +41,10 @@ class LoadImage(api.Loader): ) ] + @classmethod + def get_representations(cls): + return cls.representations + cls._representations + def load(self, context, name, namespace, options): from avalon.nuke import ( containerise, diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index dd65df02e5..e3c7834e4a 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -117,16 +117,7 @@ "load": { "LoadImage": { "enabled": true, - "families": [ - "render2d", - "source", - "plate", - "render", - "prerender", - "review", - "image" - ], - "representations": [ + "_representations": [ "exr", "dpx", "jpg", @@ -137,39 +128,9 @@ ], "node_name_template": "{class_name}_{ext}" }, - "LoadMov": { + "LoadClip": { "enabled": true, - "families": [ - "source", - "plate", - "render", - "prerender", - "review" - ], - "representations": [ - "mov", - "review", - "mp4", - "h264" - ], - "node_name_template": "{class_name}_{ext}" - }, - "LoadSequence": { - "enabled": true, - "families": [ - "render2d", - "source", - "plate", - "render", - "prerender", - "review" - ], - "representations": [ - "exr", - "dpx", - "jpg", - "jpeg", - "png" + "_representations": [ ], "node_name_template": "{class_name}_{ext}" } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json index 737843ad98..5bd8337e4c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json @@ -13,12 +13,8 @@ "label": "Image Loader" }, { - "key": "LoadMov", - "label": "Movie Loader" - }, - { - "key": "LoadSequence", - "label": "Image Sequence Loader" + "key": "LoadClip", + "label": "Clip Loader" } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_loader_plugin_nuke.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_loader_plugin_nuke.json index d01691ed5f..7ee8d0bda0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_loader_plugin_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_loader_plugin_nuke.json @@ -13,13 +13,7 @@ }, { "type": "list", - "key": "families", - "label": "Families", - "object_type": "text" - }, - { - "type": "list", - "key": "representations", + "key": "_representations", "label": "Representations", "object_type": "text" }, From 4d1eef14f16518b6bac1966021b4700660f2f333 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Oct 2021 16:55:19 +0200 Subject: [PATCH 570/716] Nuke: new unified plugin for all clip data (sequence, video) --- openpype/hosts/nuke/plugins/load/load_clip.py | 340 ++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 openpype/hosts/nuke/plugins/load/load_clip.py diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py new file mode 100644 index 0000000000..532d3dba2a --- /dev/null +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -0,0 +1,340 @@ +import nuke +from avalon.vendor import qargparse +from avalon import api, io + +from openpype.hosts.nuke.api.lib import ( + get_imageio_input_colorspace +) + +from openpype.hosts.nuke.api.plugin import ( + get_review_presets_config) + + +class LoadClip(api.Loader): + """Load clip into Nuke + + Either it is image sequence or video file. + """ + + families = [ + "source", + "plate", + "render", + "prerender", + "review" + ] + representations = ([ + "exr", "dpx", "mov", + "review", "mp4"] + + get_review_presets_config()) + + label = "Load Clip" + order = -20 + icon = "file-video-o" + color = "white" + + script_start = nuke.root()["first_frame"].value() + + # option gui + defaults = { + "start_at_workfile": True + } + + options = [ + qargparse.Boolean( + "start_at_workfile", + help="Load at workfile start frame", + default=True + ) + ] + + node_name_template = "" + + @classmethod + def get_representations(cls): + return cls.representations + cls._representations + + def load(self, context, name, namespace, options): + from avalon.nuke import ( + containerise, + viewer_update_and_undo_stop + ) + + start_at_workfile = options.get( + "start_at_workfile", self.defaults["start_at_workfile"]) + + version = context['version'] + version_data = version.get("data", {}) + repr_id = context["representation"]["_id"] + + self.log.info("version_data: {}\n".format(version_data)) + self.log.debug( + "Representation id `{}` ".format(repr_id)) + + self.first_frame = int(nuke.root()["first_frame"].getValue()) + self.handle_start = version_data.get("handleStart", 0) + self.handle_end = version_data.get("handleEnd", 0) + + first = version_data.get("frameStart", None) + last = version_data.get("frameEnd", None) + + # Fallback to asset name when namespace is None + if namespace is None: + namespace = context['asset']['name'] + + first -= self.handle_start + last += self.handle_end + + file = self.fname + + if not file: + repr_id = context["representation"]["_id"] + self.log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return + + file = file.replace("\\", "/") + + repr_cont = context["representation"]["context"] + assert repr_cont.get("frame"), "Representation is not sequence" + + if "#" not in file: + frame = repr_cont.get("frame") + if frame: + padding = len(frame) + file = file.replace(frame, "#" * padding) + + name_data = { + "asset": repr_cont["asset"], + "subset": repr_cont["subset"], + "representation": context["representation"]["name"], + "ext": repr_cont["representation"], + "id": context["representation"]["_id"], + "class_name": self.__class__.__name__ + } + + read_name = self.node_name_template.format(**name_data) + + # Create the Loader with the filename path set + read_node = nuke.createNode( + "Read", + "name {}".format(read_name)) + + # to avoid multiple undo steps for rest of process + # we will switch off undo-ing + with viewer_update_and_undo_stop(): + read_node["file"].setValue(file) + + # Set colorspace defined in version data + colorspace = context["version"]["data"].get("colorspace") + if colorspace: + read_node["colorspace"].setValue(str(colorspace)) + + preset_clrsp = get_imageio_input_colorspace(file) + + if preset_clrsp is not None: + read_node["colorspace"].setValue(preset_clrsp) + + # set start frame depending on workfile or version + self.loader_shift(read_node, start_at_workfile) + read_node["origfirst"].setValue(int(first)) + read_node["first"].setValue(int(first)) + read_node["origlast"].setValue(int(last)) + read_node["last"].setValue(int(last)) + + # add additional metadata from the version to imprint Avalon knob + add_keys = ["frameStart", "frameEnd", + "source", "colorspace", "author", "fps", "version", + "handleStart", "handleEnd"] + + data_imprint = {} + for k in add_keys: + if k == 'version': + data_imprint.update({k: context["version"]['name']}) + else: + data_imprint.update( + {k: context["version"]['data'].get(k, str(None))}) + + data_imprint.update({"objectName": read_name}) + + read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) + + if version_data.get("retime", None): + speed = version_data.get("speed", 1) + time_warp_nodes = version_data.get("timewarps", []) + self.make_retimes(speed, time_warp_nodes) + + return containerise(read_node, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + """Update the Loader's path + + Nuke automatically tries to reset some variables when changing + the loader's path to a new file. These automatic changes are to its + inputs: + + """ + + from avalon.nuke import ( + update_container + ) + + read_node = nuke.toNode(container['objectName']) + + assert read_node.Class() == "Read", "Must be Read" + + repr_cont = representation["context"] + assert repr_cont.get("frame"), "Representation is not sequence" + + file = api.get_representation_path(representation) + + if not file: + repr_id = representation["_id"] + self.log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return + + file = file.replace("\\", "/") + + if "#" not in file: + frame = repr_cont.get("frame") + if frame: + padding = len(frame) + file = file.replace(frame, "#" * padding) + + # Get start frame from version data + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + version_data = version.get("data", {}) + + self.first_frame = int(nuke.root()["first_frame"].getValue()) + self.handle_start = version_data.get("handleStart", 0) + self.handle_end = version_data.get("handleEnd", 0) + + first = version_data.get("frameStart") + last = version_data.get("frameEnd") + + if first is None: + self.log.warning( + "Missing start frame for updated version" + "assuming starts at frame 0 for: " + "{} ({})".format(read_node['name'].value(), representation)) + first = 0 + + first -= self.handle_start + last += self.handle_end + + read_node["file"].setValue(file) + + # set start frame depending on workfile or version + self.loader_shift( + read_node, + bool("start at" in read_node['frame_mode'].value())) + + read_node["origfirst"].setValue(int(first)) + read_node["first"].setValue(int(first)) + read_node["origlast"].setValue(int(last)) + read_node["last"].setValue(int(last)) + + updated_dict = {} + updated_dict.update({ + "representation": str(representation["_id"]), + "frameStart": str(first), + "frameEnd": str(last), + "version": str(version.get("name")), + "colorspace": version_data.get("colorspace"), + "source": version_data.get("source"), + "handleStart": str(self.handle_start), + "handleEnd": str(self.handle_end), + "fps": str(version_data.get("fps")), + "author": version_data.get("author"), + "outputDir": version_data.get("outputDir"), + }) + + # change color of read_node + if version.get("name") not in [max_version]: + read_node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) + + if version_data.get("retime", None): + speed = version_data.get("speed", 1) + time_warp_nodes = version_data.get("timewarps", []) + self.make_retimes(speed, time_warp_nodes) + + # Update the imprinted representation + update_container( + read_node, + updated_dict + ) + self.log.info("udated to version: {}".format(version.get("name"))) + + def remove(self, container): + + from avalon.nuke import viewer_update_and_undo_stop + + read_node = nuke.toNode(container['objectName']) + assert read_node.Class() == "Read", "Must be Read" + + with viewer_update_and_undo_stop(): + nuke.delete(read_node) + + def make_retimes(self, speed, time_warp_nodes): + ''' Create all retime and timewarping nodes with coppied animation ''' + if speed != 1: + rtn = nuke.createNode( + "Retime", + "speed {}".format(speed)) + rtn["before"].setValue("continue") + rtn["after"].setValue("continue") + rtn["input.first_lock"].setValue(True) + rtn["input.first"].setValue( + self.first_frame + ) + + if time_warp_nodes != []: + start_anim = self.first_frame + (self.handle_start / speed) + for timewarp in time_warp_nodes: + twn = nuke.createNode(timewarp["Class"], + "name {}".format(timewarp["name"])) + if isinstance(timewarp["lookup"], list): + # if array for animation + twn["lookup"].setAnimated() + for i, value in enumerate(timewarp["lookup"]): + twn["lookup"].setValueAt( + (start_anim + i) + value, + (start_anim + i)) + else: + # if static value `int` + twn["lookup"].setValue(timewarp["lookup"]) + + def loader_shift(self, read_node, workfile_start=False): + """ Set start frame of read node to a workfile start + + Args: + read_node (nuke.Node): The nuke's read node + workfile_start (bool): set workfile start frame if true + + """ + if workfile_start: + read_node['frame_mode'].setValue("start at") + read_node['frame'].setValue(str(self.script_start)) From 6177281a7fca6f1eba796a03078ac60b444ca5f9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Oct 2021 17:01:19 +0200 Subject: [PATCH 571/716] Nuke: improving code of get_representation on loadClip --- openpype/hosts/nuke/plugins/load/load_clip.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 532d3dba2a..97b91b53a3 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -23,10 +23,13 @@ class LoadClip(api.Loader): "prerender", "review" ] - representations = ([ - "exr", "dpx", "mov", - "review", "mp4"] - + get_review_presets_config()) + representations = [ + "exr", + "dpx", + "mov", + "review", + "mp4" + ] label = "Load Clip" order = -20 @@ -52,7 +55,11 @@ class LoadClip(api.Loader): @classmethod def get_representations(cls): - return cls.representations + cls._representations + return ( + cls.representations + + cls._representations + + get_review_presets_config() + ) def load(self, context, name, namespace, options): from avalon.nuke import ( From 9a662f1a5d7b6b4b115ec5fecf5adcb0437a433f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Oct 2021 17:03:33 +0200 Subject: [PATCH 572/716] Nuke: returning back node name template --- openpype/hosts/nuke/plugins/load/load_clip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 97b91b53a3..f4fb765a43 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -51,7 +51,7 @@ class LoadClip(api.Loader): ) ] - node_name_template = "" + node_name_template = "{class_name}_{ext}" @classmethod def get_representations(cls): From 7991c806d645b91921557cf1bd0a62590982d7b9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 12 Oct 2021 17:35:39 +0200 Subject: [PATCH 573/716] Nuke: LoadClip wip integrating video file loading --- openpype/hosts/nuke/plugins/load/load_clip.py | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index f4fb765a43..0bb030564a 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -36,7 +36,7 @@ class LoadClip(api.Loader): icon = "file-video-o" color = "white" - script_start = nuke.root()["first_frame"].value() + script_start = int(nuke.root()["first_frame"].value()) # option gui defaults = { @@ -66,6 +66,9 @@ class LoadClip(api.Loader): containerise, viewer_update_and_undo_stop ) + is_sequence = len(context["representation"]["files"]) <= 1 + + file = self.fname.replace("\\", "/") start_at_workfile = options.get( "start_at_workfile", self.defaults["start_at_workfile"]) @@ -73,44 +76,42 @@ class LoadClip(api.Loader): version = context['version'] version_data = version.get("data", {}) repr_id = context["representation"]["_id"] + colorspace = version_data.get("colorspace") + repr_cont = context["representation"]["context"] self.log.info("version_data: {}\n".format(version_data)) self.log.debug( "Representation id `{}` ".format(repr_id)) - self.first_frame = int(nuke.root()["first_frame"].getValue()) self.handle_start = version_data.get("handleStart", 0) self.handle_end = version_data.get("handleEnd", 0) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) + first -= self.handle_start + last += self.handle_end + + if not is_sequence: + duration = last - first + 1 + first = 1 + last = first + duration + elif "#" not in file: + frame = repr_cont.get("frame") + assert frame, "Representation is not sequence" + + padding = len(frame) + file = file.replace(frame, "#" * padding) # Fallback to asset name when namespace is None if namespace is None: namespace = context['asset']['name'] - first -= self.handle_start - last += self.handle_end - - file = self.fname - if not file: repr_id = context["representation"]["_id"] self.log.warning( "Representation id `{}` is failing to load".format(repr_id)) return - file = file.replace("\\", "/") - - repr_cont = context["representation"]["context"] - assert repr_cont.get("frame"), "Representation is not sequence" - - if "#" not in file: - frame = repr_cont.get("frame") - if frame: - padding = len(frame) - file = file.replace(frame, "#" * padding) - name_data = { "asset": repr_cont["asset"], "subset": repr_cont["subset"], @@ -133,7 +134,6 @@ class LoadClip(api.Loader): read_node["file"].setValue(file) # Set colorspace defined in version data - colorspace = context["version"]["data"].get("colorspace") if colorspace: read_node["colorspace"].setValue(str(colorspace)) @@ -233,7 +233,6 @@ class LoadClip(api.Loader): version_data = version.get("data", {}) - self.first_frame = int(nuke.root()["first_frame"].getValue()) self.handle_start = version_data.get("handleStart", 0) self.handle_end = version_data.get("handleEnd", 0) @@ -315,11 +314,11 @@ class LoadClip(api.Loader): rtn["after"].setValue("continue") rtn["input.first_lock"].setValue(True) rtn["input.first"].setValue( - self.first_frame + self.script_start ) if time_warp_nodes != []: - start_anim = self.first_frame + (self.handle_start / speed) + start_anim = self.script_start + (self.handle_start / speed) for timewarp in time_warp_nodes: twn = nuke.createNode(timewarp["Class"], "name {}".format(timewarp["name"])) From a1cc57d8ef8e2914e7e8a1726f65787a662fb989 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 13 Oct 2021 03:39:34 +0000 Subject: [PATCH 574/716] [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 26790f65f0c51b8ac61a01dbc8cf8bed839242f7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 Oct 2021 10:08:37 +0200 Subject: [PATCH 575/716] openpype tray does not require to use it's qapplication --- openpype/tools/tray/pype_tray.py | 77 +++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 3050e206ce..0f817d7130 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -268,7 +268,6 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): # Set modules self.tray_man = TrayManager(self, self.parent) - self.tray_man.initialize_modules() # Add menu to Context of SystemTrayIcon self.setContextMenu(self.menu) @@ -291,6 +290,17 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): self._doubleclick = False self._click_pos = None + self._initializing_modules = False + + @property + def initializing_modules(self): + return self._initializing_modules + + def initialize_modules(self): + self._initializing_modules = True + self.tray_man.initialize_modules() + self._initializing_modules = False + def _click_timer_timeout(self): self._click_timer.stop() doubleclick = self._doubleclick @@ -334,38 +344,48 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): QtCore.QCoreApplication.exit() -class TrayMainWindow(QtWidgets.QMainWindow): - """ TrayMainWindow is base of Pype application. - - Every widget should have set this window as parent because - QSystemTrayIcon widget is not allowed to be a parent of any widget. - """ - +class PypeTrayStarter(QtCore.QObject): def __init__(self, app): - super(TrayMainWindow, self).__init__() - self.app = app + app.setQuitOnLastWindowClosed(False) + self._app = app + self._splash = None - self.tray_widget = SystemTrayIcon(self) - self.tray_widget.show() + main_window = QtWidgets.QMainWindow() + tray_widget = SystemTrayIcon(main_window) + start_timer = QtCore.QTimer() + start_timer.setInterval(100) + start_timer.start() -class PypeTrayApplication(QtWidgets.QApplication): - """Qt application manages application's control flow.""" + start_timer.timeout.connect(self._on_start_timer) - def __init__(self): - super(PypeTrayApplication, self).__init__(sys.argv) - # Allows to close widgets without exiting app - self.setQuitOnLastWindowClosed(False) + self._main_window = main_window + self._tray_widget = tray_widget + self._timer_counter = 0 + self._start_timer = start_timer - # Sets up splash - splash_widget = self.set_splash() + def _on_start_timer(self): + if self._timer_counter == 0: + self._timer_counter += 1 + splash = self._get_splash() + splash.show() + self._tray_widget.show() - splash_widget.show() - self.processEvents() - self.main_window = TrayMainWindow(self) - splash_widget.hide() + elif self._timer_counter == 1: + self._timer_counter += 1 + self._tray_widget.initialize_modules() - def set_splash(self): + elif not self._tray_widget.initializing_modules: + splash = self._get_splash() + splash.hide() + self._start_timer.stop() + + def _get_splash(self): + if self._splash is None: + self._splash = self._create_splash() + return self._splash + + def _create_splash(self): splash_pix = QtGui.QPixmap(resources.get_openpype_splash_filepath()) splash = QtWidgets.QSplashScreen(splash_pix) splash.setMask(splash_pix.mask()) @@ -377,7 +397,12 @@ class PypeTrayApplication(QtWidgets.QApplication): def main(): - app = PypeTrayApplication() + app = QtWidgets.QApplication.instance() + if not app: + app = QtWidgets.QApplication([]) + + starter = PypeTrayStarter(app) + # TODO remove when pype.exe will have an icon if os.name == "nt": import ctypes From c9b760fcdee596771434c1ce8141198ff0aaa4ed Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 Oct 2021 10:55:21 +0200 Subject: [PATCH 576/716] stop all threads in ftrack --- openpype/modules/default_modules/ftrack/ftrack_module.py | 2 +- openpype/modules/default_modules/ftrack/tray/ftrack_tray.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 5a1fdbc276..73a4dfee82 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -371,7 +371,7 @@ class FtrackModule( return self.tray_module.validate() def tray_exit(self): - return self.tray_module.stop_action_server() + self.tray_module.tray_exit() def set_credentials_to_env(self, username, api_key): os.environ["FTRACK_API_USER"] = username or "" diff --git a/openpype/modules/default_modules/ftrack/tray/ftrack_tray.py b/openpype/modules/default_modules/ftrack/tray/ftrack_tray.py index 34e4646767..c6201a94f6 100644 --- a/openpype/modules/default_modules/ftrack/tray/ftrack_tray.py +++ b/openpype/modules/default_modules/ftrack/tray/ftrack_tray.py @@ -289,6 +289,10 @@ class FtrackTrayWrapper: parent_menu.addMenu(tray_menu) + def tray_exit(self): + self.stop_action_server() + self.stop_timer_thread() + # Definition of visibility of each menu actions def set_menu_visibility(self): self.tray_server_menu.menuAction().setVisible(self.bool_logged) From 339df3a586b3fc7c61e8e9755940d2f775f1e14f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 Oct 2021 10:58:20 +0200 Subject: [PATCH 577/716] wait for idle manager stop --- .../modules/default_modules/timers_manager/timers_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 1199db0611..7687d056f8 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -150,6 +150,7 @@ class TimersManager(OpenPypeModule, ITrayService): def tray_exit(self): if self._idle_manager: self._idle_manager.stop() + self._idle_manager.wait() def start_timer(self, project_name, asset_name, task_name, hierarchy): """ From 3f74b6b3854005ec238a8e3b3d551eed43bb187b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 13 Oct 2021 14:16:16 +0200 Subject: [PATCH 578/716] 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): From 620909d0c7a6c328037cd88cfa312710032ccd68 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Oct 2021 16:25:57 +0200 Subject: [PATCH 579/716] nuke: loadClip with update and retime creating parent nukeLoader --- openpype/hosts/nuke/api/plugin.py | 55 ++++ openpype/hosts/nuke/plugins/load/load_clip.py | 295 ++++++++++-------- 2 files changed, 215 insertions(+), 135 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 71329c0d46..62eadecaf4 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -1,4 +1,10 @@ +import random +import string + import avalon.nuke +from avalon.nuke import lib as anlib +from avalon import api + from openpype.api import ( get_current_project_settings, PypeCreatorMixin @@ -39,3 +45,52 @@ def get_review_presets_config(): outputs.update(profile.get("outputs", {})) return [str(name) for name, _prop in outputs.items()] + + +class NukeLoader(api.Loader): + container_id_knob = "containerId" + container_id = ''.join(random.choice( + string.ascii_uppercase + string.digits) for _ in range(10)) + + def get_container_id(self, node): + id_knob = node.knobs().get(self.container_id_knob) + return id_knob.value() if id_knob else None + + def get_members(self, source): + """Return nodes that has same 'containerId' as `source`""" + source_id = self.get_container_id(source) + return [node for node in nuke.allNodes(recurseGroups=True) + if self.get_container_id(node) == source_id + and node is not source] if source_id else [] + + def set_as_member(self, node): + source_id = self.get_container_id(node) + + if source_id: + node[self.container_id_knob].setValue(self.container_id) + else: + HIDEN_FLAG = 0x00040000 + _knob = anlib.Knobby( + "String_Knob", + self.container_id, + flags=[nuke.READ_ONLY, HIDEN_FLAG]) + knob = _knob.create(self.container_id_knob) + node.addKnob(knob) + + def clear_members(self, parent_node): + members = self.get_members(parent_node) + + dependent_nodes = None + for node in members: + _depndc = [n for n in node.dependent() if n not in members] + if not _depndc: + continue + + dependent_nodes = _depndc + break + + for member in members: + self.log.info("removing node: `{}".format(member.name())) + nuke.delete(member) + + return dependent_nodes diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 0bb030564a..f56120ae0a 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -5,12 +5,18 @@ from avalon import api, io from openpype.hosts.nuke.api.lib import ( get_imageio_input_colorspace ) +from avalon.nuke import ( + containerise, + update_container, + viewer_update_and_undo_stop, + maintained_selection +) +from openpype.hosts.nuke.api import plugin -from openpype.hosts.nuke.api.plugin import ( - get_review_presets_config) +reload(plugin) -class LoadClip(api.Loader): +class LoadClip(plugin.NukeLoader): """Load clip into Nuke Either it is image sequence or video file. @@ -58,15 +64,12 @@ class LoadClip(api.Loader): return ( cls.representations + cls._representations - + get_review_presets_config() + + plugin.get_review_presets_config() ) def load(self, context, name, namespace, options): - from avalon.nuke import ( - containerise, - viewer_update_and_undo_stop - ) - is_sequence = len(context["representation"]["files"]) <= 1 + + is_sequence = len(context["representation"]["files"]) > 1 file = self.fname.replace("\\", "/") @@ -77,6 +80,7 @@ class LoadClip(api.Loader): version_data = version.get("data", {}) repr_id = context["representation"]["_id"] colorspace = version_data.get("colorspace") + iio_colorspace = get_imageio_input_colorspace(file) repr_cont = context["representation"]["context"] self.log.info("version_data: {}\n".format(version_data)) @@ -107,7 +111,6 @@ class LoadClip(api.Loader): namespace = context['asset']['name'] if not file: - repr_id = context["representation"]["_id"] self.log.warning( "Representation id `{}` is failing to load".format(repr_id)) return @@ -127,6 +130,7 @@ class LoadClip(api.Loader): read_node = nuke.createNode( "Read", "name {}".format(read_name)) + self.set_as_member(read_node) # to avoid multiple undo steps for rest of process # we will switch off undo-ing @@ -136,18 +140,11 @@ class LoadClip(api.Loader): # Set colorspace defined in version data if colorspace: read_node["colorspace"].setValue(str(colorspace)) + elif iio_colorspace is not None: + read_node["colorspace"].setValue(iio_colorspace) - preset_clrsp = get_imageio_input_colorspace(file) + self.set_range_to_node(read_node, first, last, start_at_workfile) - if preset_clrsp is not None: - read_node["colorspace"].setValue(preset_clrsp) - - # set start frame depending on workfile or version - self.loader_shift(read_node, start_at_workfile) - read_node["origfirst"].setValue(int(first)) - read_node["first"].setValue(int(first)) - read_node["origlast"].setValue(int(last)) - read_node["last"].setValue(int(last)) # add additional metadata from the version to imprint Avalon knob add_keys = ["frameStart", "frameEnd", @@ -166,17 +163,18 @@ class LoadClip(api.Loader): read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) - if version_data.get("retime", None): - speed = version_data.get("speed", 1) - time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(speed, time_warp_nodes) + container = containerise( + read_node, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) - return containerise(read_node, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__, - data=data_imprint) + if version_data.get("retime", None): + self.make_retimes(read_node, version_data) + + return container def switch(self, container, representation): self.update(container, representation) @@ -190,109 +188,111 @@ class LoadClip(api.Loader): """ - from avalon.nuke import ( - update_container - ) + is_sequence = len(representation["files"]) > 1 read_node = nuke.toNode(container['objectName']) + file = api.get_representation_path(representation).replace("\\", "/") - assert read_node.Class() == "Read", "Must be Read" + start_at_workfile = bool("start at" in read_node['frame_mode'].value()) - repr_cont = representation["context"] - assert repr_cont.get("frame"), "Representation is not sequence" - - file = api.get_representation_path(representation) - - if not file: - repr_id = representation["_id"] - self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) - return - - file = file.replace("\\", "/") - - if "#" not in file: - frame = repr_cont.get("frame") - if frame: - padding = len(frame) - file = file.replace(frame, "#" * padding) - - # Get start frame from version data version = io.find_one({ "type": "version", "_id": representation["parent"] }) - - # get all versions in list - versions = io.find({ - "type": "version", - "parent": version["parent"] - }).distinct('name') - - max_version = max(versions) - version_data = version.get("data", {}) + repr_id = representation["_id"] + colorspace = version_data.get("colorspace") + iio_colorspace = get_imageio_input_colorspace(file) + repr_cont = representation["context"] self.handle_start = version_data.get("handleStart", 0) self.handle_end = version_data.get("handleEnd", 0) - first = version_data.get("frameStart") - last = version_data.get("frameEnd") - - if first is None: - self.log.warning( - "Missing start frame for updated version" - "assuming starts at frame 0 for: " - "{} ({})".format(read_node['name'].value(), representation)) - first = 0 - + first = version_data.get("frameStart", None) + last = version_data.get("frameEnd", None) first -= self.handle_start last += self.handle_end + if not is_sequence: + duration = last - first + 1 + first = 1 + last = first + duration + elif "#" not in file: + frame = repr_cont.get("frame") + assert frame, "Representation is not sequence" + + padding = len(frame) + file = file.replace(frame, "#" * padding) + + if not file: + self.log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return + read_node["file"].setValue(file) - # set start frame depending on workfile or version - self.loader_shift( - read_node, - bool("start at" in read_node['frame_mode'].value())) + # to avoid multiple undo steps for rest of process + # we will switch off undo-ing + with viewer_update_and_undo_stop(): - read_node["origfirst"].setValue(int(first)) - read_node["first"].setValue(int(first)) - read_node["origlast"].setValue(int(last)) - read_node["last"].setValue(int(last)) + # Set colorspace defined in version data + if colorspace: + read_node["colorspace"].setValue(str(colorspace)) + elif iio_colorspace is not None: + read_node["colorspace"].setValue(iio_colorspace) - updated_dict = {} - updated_dict.update({ - "representation": str(representation["_id"]), - "frameStart": str(first), - "frameEnd": str(last), - "version": str(version.get("name")), - "colorspace": version_data.get("colorspace"), - "source": version_data.get("source"), - "handleStart": str(self.handle_start), - "handleEnd": str(self.handle_end), - "fps": str(version_data.get("fps")), - "author": version_data.get("author"), - "outputDir": version_data.get("outputDir"), - }) + self.set_range_to_node(read_node, first, last, start_at_workfile) - # change color of read_node - if version.get("name") not in [max_version]: - read_node["tile_color"].setValue(int("0xd84f20ff", 16)) - else: - read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) + updated_dict = { + "representation": str(representation["_id"]), + "frameStart": str(first), + "frameEnd": str(last), + "version": str(version.get("name")), + "colorspace": colorspace, + "source": version_data.get("source"), + "handleStart": str(self.handle_start), + "handleEnd": str(self.handle_end), + "fps": str(version_data.get("fps")), + "author": version_data.get("author"), + "outputDir": version_data.get("outputDir"), + } + + # change color of read_node + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + if version.get("name") not in [max_version]: + read_node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) + + # Update the imprinted representation + update_container( + read_node, + updated_dict + ) + self.log.info("udated to version: {}".format(version.get("name"))) if version_data.get("retime", None): - speed = version_data.get("speed", 1) - time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(speed, time_warp_nodes) + self.make_retimes(read_node, version_data) + else: + self.clear_members(read_node) - # Update the imprinted representation - update_container( - read_node, - updated_dict - ) - self.log.info("udated to version: {}".format(version.get("name"))) + self.set_as_member(read_node) + + def set_range_to_node(self, read_node, first, last, start_at_workfile): + read_node['origfirst'].setValue(int(first)) + read_node['first'].setValue(int(first)) + read_node['origlast'].setValue(int(last)) + read_node['last'].setValue(int(last)) + + # set start frame depending on workfile or version + self.loader_shift(read_node, start_at_workfile) def remove(self, container): @@ -302,36 +302,61 @@ class LoadClip(api.Loader): assert read_node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): + members = self.get_members(read_node) nuke.delete(read_node) + for member in members: + nuke.delete(member) - def make_retimes(self, speed, time_warp_nodes): + def make_retimes(self, parent_node, version_data): ''' Create all retime and timewarping nodes with coppied animation ''' - if speed != 1: - rtn = nuke.createNode( - "Retime", - "speed {}".format(speed)) - rtn["before"].setValue("continue") - rtn["after"].setValue("continue") - rtn["input.first_lock"].setValue(True) - rtn["input.first"].setValue( - self.script_start - ) + speed = version_data.get('speed', 1) + time_warp_nodes = version_data.get('timewarps', []) + last_node = None + source_id = self.get_container_id(parent_node) + self.log.info("__ source_id: {}".format(source_id)) + self.log.info("__ members: {}".format(self.get_members(parent_node))) + dependent_nodes = self.clear_members(parent_node) - if time_warp_nodes != []: - start_anim = self.script_start + (self.handle_start / speed) - for timewarp in time_warp_nodes: - twn = nuke.createNode(timewarp["Class"], - "name {}".format(timewarp["name"])) - if isinstance(timewarp["lookup"], list): - # if array for animation - twn["lookup"].setAnimated() - for i, value in enumerate(timewarp["lookup"]): - twn["lookup"].setValueAt( - (start_anim + i) + value, - (start_anim + i)) - else: - # if static value `int` - twn["lookup"].setValue(timewarp["lookup"]) + with maintained_selection(): + parent_node['selected'].setValue(True) + + if speed != 1: + rtn = nuke.createNode( + "Retime", + "speed {}".format(speed)) + + rtn["before"].setValue("continue") + rtn["after"].setValue("continue") + rtn["input.first_lock"].setValue(True) + rtn["input.first"].setValue( + self.script_start + ) + self.set_as_member(rtn) + last_node = rtn + + if time_warp_nodes != []: + start_anim = self.script_start + (self.handle_start / speed) + for timewarp in time_warp_nodes: + twn = nuke.createNode(timewarp["Class"], + "name {}".format(timewarp["name"])) + if isinstance(timewarp["lookup"], list): + # if array for animation + twn["lookup"].setAnimated() + for i, value in enumerate(timewarp["lookup"]): + twn["lookup"].setValueAt( + (start_anim + i) + value, + (start_anim + i)) + else: + # if static value `int` + twn["lookup"].setValue(timewarp["lookup"]) + + self.set_as_member(twn) + last_node = twn + + if dependent_nodes: + # connect to original inputs + for i, n in enumerate(dependent_nodes): + last_node.setInput(i, n) def loader_shift(self, read_node, workfile_start=False): """ Set start frame of read node to a workfile start From e4dc590242975ae6bca97e037db9d66697de1e74 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Oct 2021 16:26:41 +0200 Subject: [PATCH 580/716] nuke: removing obsolete loader plugins --- openpype/hosts/nuke/plugins/load/load_mov.py | 347 ------------------ .../hosts/nuke/plugins/load/load_sequence.py | 320 ---------------- 2 files changed, 667 deletions(-) delete mode 100644 openpype/hosts/nuke/plugins/load/load_mov.py delete mode 100644 openpype/hosts/nuke/plugins/load/load_sequence.py diff --git a/openpype/hosts/nuke/plugins/load/load_mov.py b/openpype/hosts/nuke/plugins/load/load_mov.py deleted file mode 100644 index f7523d0a6e..0000000000 --- a/openpype/hosts/nuke/plugins/load/load_mov.py +++ /dev/null @@ -1,347 +0,0 @@ -import nuke -from avalon.vendor import qargparse -from avalon import api, io -from openpype.api import get_current_project_settings -from openpype.hosts.nuke.api.lib import ( - get_imageio_input_colorspace -) - - -def add_review_presets_config(): - returning = { - "families": list(), - "representations": list() - } - settings = get_current_project_settings() - review_profiles = ( - settings["global"] - ["publish"] - ["ExtractReview"] - ["profiles"] - ) - - outputs = {} - for profile in review_profiles: - outputs.update(profile.get("outputs", {})) - - for output, properities in outputs.items(): - returning["representations"].append(output) - returning["families"] += properities.get("families", []) - - return returning - - -class LoadMov(api.Loader): - """Load mov file into Nuke""" - families = ["render", "source", "plate", "review"] - representations = ["mov", "review", "mp4"] - - label = "Load mov" - order = -10 - icon = "code-fork" - color = "orange" - - first_frame = nuke.root()["first_frame"].value() - - # options gui - defaults = { - "start_at_workfile": True - } - - options = [ - qargparse.Boolean( - "start_at_workfile", - help="Load at workfile start frame", - default=True - ) - ] - - node_name_template = "{class_name}_{ext}" - - def load(self, context, name, namespace, options): - from avalon.nuke import ( - containerise, - viewer_update_and_undo_stop - ) - - start_at_workfile = options.get( - "start_at_workfile", self.defaults["start_at_workfile"]) - - version = context['version'] - version_data = version.get("data", {}) - repr_id = context["representation"]["_id"] - - self.handle_start = version_data.get("handleStart", 0) - self.handle_end = version_data.get("handleEnd", 0) - - orig_first = version_data.get("frameStart") - orig_last = version_data.get("frameEnd") - diff = orig_first - 1 - - first = orig_first - diff - last = orig_last - diff - - colorspace = version_data.get("colorspace") - repr_cont = context["representation"]["context"] - - self.log.debug( - "Representation id `{}` ".format(repr_id)) - - context["representation"]["_id"] - # create handles offset (only to last, because of mov) - last += self.handle_start + self.handle_end - - # Fallback to asset name when namespace is None - if namespace is None: - namespace = context['asset']['name'] - - file = self.fname - - if not file: - self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) - return - - file = file.replace("\\", "/") - - name_data = { - "asset": repr_cont["asset"], - "subset": repr_cont["subset"], - "representation": context["representation"]["name"], - "ext": repr_cont["representation"], - "id": context["representation"]["_id"], - "class_name": self.__class__.__name__ - } - - read_name = self.node_name_template.format(**name_data) - - read_node = nuke.createNode( - "Read", - "name {}".format(read_name) - ) - - # to avoid multiple undo steps for rest of process - # we will switch off undo-ing - with viewer_update_and_undo_stop(): - read_node["file"].setValue(file) - - read_node["origfirst"].setValue(first) - read_node["first"].setValue(first) - read_node["origlast"].setValue(last) - read_node["last"].setValue(last) - read_node['frame_mode'].setValue("start at") - - if start_at_workfile: - # start at workfile start - read_node['frame'].setValue(str(self.first_frame)) - else: - # start at version frame start - read_node['frame'].setValue( - str(orig_first - self.handle_start)) - - if colorspace: - read_node["colorspace"].setValue(str(colorspace)) - - preset_clrsp = get_imageio_input_colorspace(file) - - if preset_clrsp is not None: - read_node["colorspace"].setValue(preset_clrsp) - - # add additional metadata from the version to imprint Avalon knob - add_keys = [ - "frameStart", "frameEnd", "handles", "source", "author", - "fps", "version", "handleStart", "handleEnd" - ] - - data_imprint = {} - for key in add_keys: - if key == 'version': - data_imprint.update({ - key: context["version"]['name'] - }) - else: - data_imprint.update({ - key: context["version"]['data'].get(key, str(None)) - }) - - data_imprint.update({"objectName": read_name}) - - read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) - - if version_data.get("retime", None): - speed = version_data.get("speed", 1) - time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(speed, time_warp_nodes) - - return containerise( - read_node, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__, - data=data_imprint - ) - - def switch(self, container, representation): - self.update(container, representation) - - def update(self, container, representation): - """Update the Loader's path - - Nuke automatically tries to reset some variables when changing - the loader's path to a new file. These automatic changes are to its - inputs: - - """ - - from avalon.nuke import ( - update_container - ) - - read_node = nuke.toNode(container['objectName']) - - assert read_node.Class() == "Read", "Must be Read" - - file = self.fname - - if not file: - repr_id = representation["_id"] - self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) - return - - file = file.replace("\\", "/") - - # Get start frame from version data - version = io.find_one({ - "type": "version", - "_id": representation["parent"] - }) - - # get all versions in list - versions = io.find({ - "type": "version", - "parent": version["parent"] - }).distinct('name') - - max_version = max(versions) - - version_data = version.get("data", {}) - - orig_first = version_data.get("frameStart") - orig_last = version_data.get("frameEnd") - diff = orig_first - 1 - - # set first to 1 - first = orig_first - diff - last = orig_last - diff - self.handle_start = version_data.get("handleStart", 0) - self.handle_end = version_data.get("handleEnd", 0) - colorspace = version_data.get("colorspace") - - if first is None: - self.log.warning(( - "Missing start frame for updated version" - "assuming starts at frame 0 for: " - "{} ({})").format( - read_node['name'].value(), representation)) - first = 0 - - # create handles offset (only to last, because of mov) - last += self.handle_start + self.handle_end - - read_node["file"].setValue(file) - - # Set the global in to the start frame of the sequence - read_node["origfirst"].setValue(first) - read_node["first"].setValue(first) - read_node["origlast"].setValue(last) - read_node["last"].setValue(last) - read_node['frame_mode'].setValue("start at") - - if int(float(self.first_frame)) == int( - float(read_node['frame'].value())): - # start at workfile start - read_node['frame'].setValue(str(self.first_frame)) - else: - # start at version frame start - read_node['frame'].setValue(str(orig_first - self.handle_start)) - - if colorspace: - read_node["colorspace"].setValue(str(colorspace)) - - preset_clrsp = get_imageio_input_colorspace(file) - - if preset_clrsp is not None: - read_node["colorspace"].setValue(preset_clrsp) - - updated_dict = {} - updated_dict.update({ - "representation": str(representation["_id"]), - "frameStart": str(first), - "frameEnd": str(last), - "version": str(version.get("name")), - "colorspace": version_data.get("colorspace"), - "source": version_data.get("source"), - "handleStart": str(self.handle_start), - "handleEnd": str(self.handle_end), - "fps": str(version_data.get("fps")), - "author": version_data.get("author"), - "outputDir": version_data.get("outputDir") - }) - - # change color of node - if version.get("name") not in [max_version]: - read_node["tile_color"].setValue(int("0xd84f20ff", 16)) - else: - read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) - - if version_data.get("retime", None): - speed = version_data.get("speed", 1) - time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(speed, time_warp_nodes) - - # Update the imprinted representation - update_container( - read_node, updated_dict - ) - self.log.info("udated to version: {}".format(version.get("name"))) - - def remove(self, container): - - from avalon.nuke import viewer_update_and_undo_stop - - read_node = nuke.toNode(container['objectName']) - assert read_node.Class() == "Read", "Must be Read" - - with viewer_update_and_undo_stop(): - nuke.delete(read_node) - - def make_retimes(self, speed, time_warp_nodes): - ''' Create all retime and timewarping nodes with coppied animation ''' - if speed != 1: - rtn = nuke.createNode( - "Retime", - "speed {}".format(speed)) - rtn["before"].setValue("continue") - rtn["after"].setValue("continue") - rtn["input.first_lock"].setValue(True) - rtn["input.first"].setValue( - self.first_frame - ) - - if time_warp_nodes != []: - start_anim = self.first_frame + (self.handle_start / speed) - for timewarp in time_warp_nodes: - twn = nuke.createNode(timewarp["Class"], - "name {}".format(timewarp["name"])) - if isinstance(timewarp["lookup"], list): - # if array for animation - twn["lookup"].setAnimated() - for i, value in enumerate(timewarp["lookup"]): - twn["lookup"].setValueAt( - (start_anim + i) + value, - (start_anim + i)) - else: - # if static value `int` - twn["lookup"].setValue(timewarp["lookup"]) diff --git a/openpype/hosts/nuke/plugins/load/load_sequence.py b/openpype/hosts/nuke/plugins/load/load_sequence.py deleted file mode 100644 index 003b406ee7..0000000000 --- a/openpype/hosts/nuke/plugins/load/load_sequence.py +++ /dev/null @@ -1,320 +0,0 @@ -import nuke -from avalon.vendor import qargparse -from avalon import api, io -from openpype.hosts.nuke.api.lib import ( - get_imageio_input_colorspace -) - - -class LoadSequence(api.Loader): - """Load image sequence into Nuke""" - - families = ["render", "source", "plate", "review"] - representations = ["exr", "dpx"] - - label = "Load Image Sequence" - order = -20 - icon = "file-video-o" - color = "white" - - script_start = nuke.root()["first_frame"].value() - - # option gui - defaults = { - "start_at_workfile": True - } - - options = [ - qargparse.Boolean( - "start_at_workfile", - help="Load at workfile start frame", - default=True - ) - ] - - node_name_template = "{class_name}_{ext}" - - def load(self, context, name, namespace, options): - from avalon.nuke import ( - containerise, - viewer_update_and_undo_stop - ) - - start_at_workfile = options.get( - "start_at_workfile", self.defaults["start_at_workfile"]) - - version = context['version'] - version_data = version.get("data", {}) - repr_id = context["representation"]["_id"] - - self.log.info("version_data: {}\n".format(version_data)) - self.log.debug( - "Representation id `{}` ".format(repr_id)) - - self.first_frame = int(nuke.root()["first_frame"].getValue()) - self.handle_start = version_data.get("handleStart", 0) - self.handle_end = version_data.get("handleEnd", 0) - - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - - # Fallback to asset name when namespace is None - if namespace is None: - namespace = context['asset']['name'] - - first -= self.handle_start - last += self.handle_end - - file = self.fname - - if not file: - repr_id = context["representation"]["_id"] - self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) - return - - file = file.replace("\\", "/") - - repr_cont = context["representation"]["context"] - assert repr_cont.get("frame"), "Representation is not sequence" - - if "#" not in file: - frame = repr_cont.get("frame") - if frame: - padding = len(frame) - file = file.replace(frame, "#" * padding) - - name_data = { - "asset": repr_cont["asset"], - "subset": repr_cont["subset"], - "representation": context["representation"]["name"], - "ext": repr_cont["representation"], - "id": context["representation"]["_id"], - "class_name": self.__class__.__name__ - } - - read_name = self.node_name_template.format(**name_data) - - # Create the Loader with the filename path set - read_node = nuke.createNode( - "Read", - "name {}".format(read_name)) - - # to avoid multiple undo steps for rest of process - # we will switch off undo-ing - with viewer_update_and_undo_stop(): - read_node["file"].setValue(file) - - # Set colorspace defined in version data - colorspace = context["version"]["data"].get("colorspace") - if colorspace: - read_node["colorspace"].setValue(str(colorspace)) - - preset_clrsp = get_imageio_input_colorspace(file) - - if preset_clrsp is not None: - read_node["colorspace"].setValue(preset_clrsp) - - # set start frame depending on workfile or version - self.loader_shift(read_node, start_at_workfile) - read_node["origfirst"].setValue(int(first)) - read_node["first"].setValue(int(first)) - read_node["origlast"].setValue(int(last)) - read_node["last"].setValue(int(last)) - - # add additional metadata from the version to imprint Avalon knob - add_keys = ["frameStart", "frameEnd", - "source", "colorspace", "author", "fps", "version", - "handleStart", "handleEnd"] - - data_imprint = {} - for k in add_keys: - if k == 'version': - data_imprint.update({k: context["version"]['name']}) - else: - data_imprint.update( - {k: context["version"]['data'].get(k, str(None))}) - - data_imprint.update({"objectName": read_name}) - - read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) - - if version_data.get("retime", None): - speed = version_data.get("speed", 1) - time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(speed, time_warp_nodes) - - return containerise(read_node, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__, - data=data_imprint) - - def switch(self, container, representation): - self.update(container, representation) - - def update(self, container, representation): - """Update the Loader's path - - Nuke automatically tries to reset some variables when changing - the loader's path to a new file. These automatic changes are to its - inputs: - - """ - - from avalon.nuke import ( - update_container - ) - - read_node = nuke.toNode(container['objectName']) - - assert read_node.Class() == "Read", "Must be Read" - - repr_cont = representation["context"] - assert repr_cont.get("frame"), "Representation is not sequence" - - file = api.get_representation_path(representation) - - if not file: - repr_id = representation["_id"] - self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) - return - - file = file.replace("\\", "/") - - if "#" not in file: - frame = repr_cont.get("frame") - if frame: - padding = len(frame) - file = file.replace(frame, "#" * padding) - - # Get start frame from version data - version = io.find_one({ - "type": "version", - "_id": representation["parent"] - }) - - # get all versions in list - versions = io.find({ - "type": "version", - "parent": version["parent"] - }).distinct('name') - - max_version = max(versions) - - version_data = version.get("data", {}) - - self.first_frame = int(nuke.root()["first_frame"].getValue()) - self.handle_start = version_data.get("handleStart", 0) - self.handle_end = version_data.get("handleEnd", 0) - - first = version_data.get("frameStart") - last = version_data.get("frameEnd") - - if first is None: - self.log.warning( - "Missing start frame for updated version" - "assuming starts at frame 0 for: " - "{} ({})".format(read_node['name'].value(), representation)) - first = 0 - - first -= self.handle_start - last += self.handle_end - - read_node["file"].setValue(file) - - # set start frame depending on workfile or version - self.loader_shift( - read_node, - bool("start at" in read_node['frame_mode'].value())) - - read_node["origfirst"].setValue(int(first)) - read_node["first"].setValue(int(first)) - read_node["origlast"].setValue(int(last)) - read_node["last"].setValue(int(last)) - - updated_dict = {} - updated_dict.update({ - "representation": str(representation["_id"]), - "frameStart": str(first), - "frameEnd": str(last), - "version": str(version.get("name")), - "colorspace": version_data.get("colorspace"), - "source": version_data.get("source"), - "handleStart": str(self.handle_start), - "handleEnd": str(self.handle_end), - "fps": str(version_data.get("fps")), - "author": version_data.get("author"), - "outputDir": version_data.get("outputDir"), - }) - - # change color of read_node - if version.get("name") not in [max_version]: - read_node["tile_color"].setValue(int("0xd84f20ff", 16)) - else: - read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) - - if version_data.get("retime", None): - speed = version_data.get("speed", 1) - time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(speed, time_warp_nodes) - - # Update the imprinted representation - update_container( - read_node, - updated_dict - ) - self.log.info("udated to version: {}".format(version.get("name"))) - - def remove(self, container): - - from avalon.nuke import viewer_update_and_undo_stop - - read_node = nuke.toNode(container['objectName']) - assert read_node.Class() == "Read", "Must be Read" - - with viewer_update_and_undo_stop(): - nuke.delete(read_node) - - def make_retimes(self, speed, time_warp_nodes): - ''' Create all retime and timewarping nodes with coppied animation ''' - if speed != 1: - rtn = nuke.createNode( - "Retime", - "speed {}".format(speed)) - rtn["before"].setValue("continue") - rtn["after"].setValue("continue") - rtn["input.first_lock"].setValue(True) - rtn["input.first"].setValue( - self.first_frame - ) - - if time_warp_nodes != []: - start_anim = self.first_frame + (self.handle_start / speed) - for timewarp in time_warp_nodes: - twn = nuke.createNode(timewarp["Class"], - "name {}".format(timewarp["name"])) - if isinstance(timewarp["lookup"], list): - # if array for animation - twn["lookup"].setAnimated() - for i, value in enumerate(timewarp["lookup"]): - twn["lookup"].setValueAt( - (start_anim + i) + value, - (start_anim + i)) - else: - # if static value `int` - twn["lookup"].setValue(timewarp["lookup"]) - - def loader_shift(self, read_node, workfile_start=False): - """ Set start frame of read node to a workfile start - - Args: - read_node (nuke.Node): The nuke's read node - workfile_start (bool): set workfile start frame if true - - """ - if workfile_start: - read_node['frame_mode'].setValue("start at") - read_node['frame'].setValue(str(self.script_start)) From 818ffa1ac0442966989fc085bb0c1a280488e959 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Oct 2021 16:54:33 +0200 Subject: [PATCH 581/716] global: patch discovery on pipeline too --- openpype/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/__init__.py b/openpype/__init__.py index 9d55006a67..11b563ebfe 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -69,6 +69,7 @@ def install(): """Install Pype to Avalon.""" from pyblish.lib import MessageHandler from openpype.modules import load_modules + from avalon import pipeline # Make sure modules are loaded load_modules() @@ -117,7 +118,9 @@ def install(): # apply monkey patched discover to original one log.info("Patching discovery") + avalon.discover = patched_discover + pipeline.discover = patched_discover avalon.on("taskChanged", _on_task_change) From 1b6da8f6981e2e794c6313198ac45ae76d98ae07 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Oct 2021 16:55:07 +0200 Subject: [PATCH 582/716] nuke: replacing position of add to member --- openpype/hosts/nuke/plugins/load/load_clip.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index f56120ae0a..265ab39b07 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -130,7 +130,6 @@ class LoadClip(plugin.NukeLoader): read_node = nuke.createNode( "Read", "name {}".format(read_name)) - self.set_as_member(read_node) # to avoid multiple undo steps for rest of process # we will switch off undo-ing @@ -174,6 +173,8 @@ class LoadClip(plugin.NukeLoader): if version_data.get("retime", None): self.make_retimes(read_node, version_data) + self.set_as_member(read_node) + return container def switch(self, container, representation): From 2ae4b12f218cbf82ef15753bcf3b7a388b420ff1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 14 Oct 2021 12:07:13 +0200 Subject: [PATCH 583/716] Fix - oiiotool wasn't recognized even if present This caused to DWAA support not working even if it could --- openpype/lib/plugin_tools.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 9dccadc44e..a982983805 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -377,7 +377,7 @@ def oiio_supported(): """ Checks if oiiotool is configured for this platform. - Expects full path to executable. + Triggers simple subprocess, handles exception if fails. 'should_decompress' will throw exception if configured, but not present or not working. @@ -385,7 +385,13 @@ def oiio_supported(): (bool) """ oiio_path = get_oiio_tools_path() - if not oiio_path or not os.path.exists(oiio_path): + if oiio_path: + try: + _ = run_subprocess([oiio_path, "-v"]) + except FileNotFoundError: + oiio_path = None + + if not oiio_path: log.debug("OIIOTool is not configured or not present at {}". format(oiio_path)) return False From d757793587b6f61fb6d8fd8b5a9a9f5332d9fb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 14 Oct 2021 13:10:06 +0200 Subject: [PATCH 584/716] return when not sets --- openpype/hosts/maya/plugins/publish/extract_look.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index bbf25ebdc7..e0b85907e9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -205,6 +205,9 @@ class ExtractLook(openpype.api.Extractor): lookdata = instance.data["lookData"] relationships = lookdata["relationships"] sets = relationships.keys() + if not sets: + self.log.info("No sets found") + return results = self.process_resources(instance, staging_dir=dir_path) transfers = results["fileTransfers"] From 0c410bb2b1d05e9d253bd0f867fdb5ed670b74ed Mon Sep 17 00:00:00 2001 From: karimmozlia Date: Thu, 14 Oct 2021 13:47:00 +0200 Subject: [PATCH 585/716] add UI booleans --- .../defaults/project_settings/maya.json | 15 +++++++++++ .../schemas/schema_maya_publish.json | 25 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index c592d74350..72a0ab362d 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -315,6 +315,21 @@ "optional": true, "active": true }, + "ValidateRigContents": { + "enabled": false, + "optional": true, + "active": true + }, + "ValidateJointsHidden": { + "enabled": false, + "optional": true, + "active": true + }, + "ValidateRigControllers": { + "enabled": false, + "optional": true, + "active": true + }, "ValidateCameraAttributes": { "enabled": false, "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 26ebfb2bd7..f676094e90 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 @@ -166,7 +166,6 @@ } ] }, - { "type": "collapsible-wrap", "label": "Model", @@ -329,6 +328,30 @@ } ] }, + { + "type": "collapsible-wrap", + "label": "Rig", + "children": [ + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateRigContents", + "label": "Validate Rig Contents" + }, + { + "key": "ValidateJointsHidden", + "label": "Validate Joints Hidden" + }, + { + "key": "ValidateRigControllers", + "label": "Validate Rig Controllers" + } + ] + } + ] + }, { "type": "schema_template", "name": "template_publish_plugin", From 184ae8c2685af4ca87f846c36d9a7f738dce15af Mon Sep 17 00:00:00 2001 From: karimmozlia Date: Thu, 14 Oct 2021 13:58:12 +0200 Subject: [PATCH 586/716] ValidateRigJointsHidden typo --- openpype/settings/defaults/project_settings/maya.json | 2 +- .../schemas/projects_schema/schemas/schema_maya_publish.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 72a0ab362d..f8f3432d0f 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -320,7 +320,7 @@ "optional": true, "active": true }, - "ValidateJointsHidden": { + "ValidateRigJointsHidden": { "enabled": false, "optional": true, "active": 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 f676094e90..bde8c15958 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 @@ -341,8 +341,8 @@ "label": "Validate Rig Contents" }, { - "key": "ValidateJointsHidden", - "label": "Validate Joints Hidden" + "key": "ValidateRigJointsHidden", + "label": "Validate Rig JointsHidden" }, { "key": "ValidateRigControllers", From 8d69283b8a592ebf969bf6e61ce8735d14eec4a5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 14 Oct 2021 16:27:43 +0200 Subject: [PATCH 587/716] PYPE-1343 - added project and task into context window --- openpype/hosts/maya/api/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 725a075417..13f8a4cb78 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -313,9 +313,15 @@ def on_task_changed(*args): lib.set_context_settings() lib.update_content_on_context_change() + msg = " project: {}\n asset: {}\n task:{}".format( + avalon.Session["AVALON_PROJECT"], + avalon.Session["AVALON_ASSET"], + avalon.Session["AVALON_TASK"] + ) + lib.show_message( "Context was changed", - ("Context was changed to {}".format(avalon.Session["AVALON_ASSET"])), + ("Context was changed to:\n{}".format(msg)), ) From a10963188d1aaabdb170540e6d1635409d5829ba Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 14 Oct 2021 17:32:00 +0200 Subject: [PATCH 588/716] Fix - better approach for oiio_supported --- openpype/lib/plugin_tools.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index a982983805..4eabb4d1ca 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -6,6 +6,7 @@ import logging import re import json import tempfile +import distutils from .execute import run_subprocess from .profiles_filtering import filter_profiles @@ -386,10 +387,7 @@ def oiio_supported(): """ oiio_path = get_oiio_tools_path() if oiio_path: - try: - _ = run_subprocess([oiio_path, "-v"]) - except FileNotFoundError: - oiio_path = None + oiio_path = distutils.spawn.find_executable(oiio_path) if not oiio_path: log.debug("OIIOTool is not configured or not present at {}". From df482e51b4e451ebd9ef38412b78a9c8b0a20077 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 14 Oct 2021 17:43:02 +0200 Subject: [PATCH 589/716] updated readme for entity types in settings --- openpype/settings/entities/schemas/README.md | 151 ++++++++++++------ .../schemas/system_schema/example_schema.json | 6 +- 2 files changed, 103 insertions(+), 54 deletions(-) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index c8432f0f2e..5258fef9ec 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -2,7 +2,7 @@ ## Basic rules - configurations does not define GUI, but GUI defines configurations! -- output is always json (yaml is not needed for anatomy templates anymore) +- output is always json serializable - GUI schema has multiple input types, all inputs are represented by a dictionary - each input may have "input modifiers" (keys in dictionary) that are required or optional - only required modifier for all input items is key `"type"` which says what type of item it is @@ -13,16 +13,16 @@ - `"is_group"` - define that all values under key in hierarchy will be overriden if any value is modified, this information is also stored to overrides - this keys is not allowed for all inputs as they may have not reason for that - key is validated, can be only once in hierarchy but is not required -- currently there are `system configurations` and `project configurations` +- currently there are `system settings` and `project settings` ## Inner schema - GUI schemas are huge json files, to be able to split whole configuration into multiple schema there's type `schema` -- system configuration schemas are stored in `~/tools/settings/settings/gui_schemas/system_schema/` and project configurations in `~/tools/settings/settings/gui_schemas/projects_schema/` +- system configuration schemas are stored in `~/openpype/settings/entities/schemas/system_schema/` and project configurations in `~/openpype/settings/entities/schemas/projects_schema/` - each schema name is filename of json file except extension (without ".json") - if content is dictionary content will be used as `schema` else will be used as `schema_template` ### schema -- can have only key `"children"` which is list of strings, each string should represent another schema (order matters) string represebts name of the schema +- can have only key `"children"` which is list of strings, each string should represent another schema (order matters) string represents name of the schema - will just paste schemas from other schema file in order of "children" list ``` @@ -32,8 +32,9 @@ } ``` -### schema_template +### template - allows to define schema "templates" to not duplicate same content multiple times +- legacy name is `schema_template` (still usable) ```javascript // EXAMPLE json file content (filename: example_template.json) [ @@ -59,11 +60,11 @@ // EXAMPLE usage of the template in schema { "type": "dict", - "key": "schema_template_examples", + "key": "template_examples", "label": "Schema template examples", "children": [ { - "type": "schema_template", + "type": "template", // filename of template (example_template.json) "name": "example_template", "template_data": { @@ -72,7 +73,7 @@ "multipath_executables": false } }, { - "type": "schema_template", + "type": "template", "name": "example_template", "template_data": { "host_label": "Maya 2020", @@ -98,8 +99,16 @@ ... } ``` -- Unfilled fields can be also used for non string values, in that case value must contain only one key and value for fill must contain right type. +- Unfilled fields can be also used for non string values(e.g. dictionary), in that case value must contain only one key and value for fill must contain right type. ```javascript +// Passed data +{ + "executable_multiplatform": { + "type": "schema", + "name": "my_multiplatform_schema" + } +} +// Template content { ... // Allowed @@ -121,32 +130,34 @@ "name": "project_settings/global" } ``` -- all valid `ModuleSettingsDef` classes where calling of `get_settings_schemas` +- all valid `BaseModuleSettingsDef` classes where calling of `get_settings_schemas` will return dictionary where is key "project_settings/global" with schemas will extend and replace this item -- works almost the same way as templates +- dynamic schemas work almost the same way as templates - one item can be replaced by multiple items (or by 0 items) - goal is to dynamically loaded settings of OpenPype addons without having their schemas or default values in main repository + - values of these schemas are saved using the `BaseModuleSettingsDef` methods +- easiest is to use `JsonFilesSettingsDef` which has full implementation of storing default values to json files all you have to implement is method `get_settings_root_path` which should return path to root directory where settings schema can be found and will be saved ## Basic Dictionary inputs - these inputs wraps another inputs into {key: value} relation ## dict -- this is another dictionary input wrapping more inputs but visually makes them different -- item may be used as widget (in `list` or `dict-modifiable`) +- this is dictionary type wrapping more inputs with keys defined in schema +- may be used as dynamic children (e.g. in `list` or `dict-modifiable`) - in that case the only key modifier is `children` which is list of it's keys - USAGE: e.g. List of dictionaries where each dictionary have same structure. -- item may be with or without `"label"` if is not used as widget - - required keys are `"key"` under which will be stored - - without label it is just wrap item holding `"key"` - - can't have `"is_group"` key set to True as it breaks visual override showing - - if `"label"` is entetered there which will be shown in GUI - - item with label can be collapsible - - that can be set with key `"collapsible"` as `True`/`False` (Default: `True`) - - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) - - it is possible to add darker background with `"highlight_content"` (Default: `False`) - - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color +- if is not used as dynamic children then must have defined `"key"` under which are it's values stored +- may be with or without `"label"` (only for GUI) + - `"label"` must be set to be able mark item as group with `"is_group"` key set to True +- item with label can visually wrap it's children + - this option is enabled by default to turn off set `"use_label_wrap"` to `False` + - label wrap is by default collapsible + - that can be set with key `"collapsible"` to `True`/`False` + - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) + - it is possible to add lighter background with `"highlight_content"` (Default: `False`) + - lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color - output is dictionary `{the "key": children values}` ``` # Example @@ -198,8 +209,8 @@ ``` ## dict-conditional -- is similar to `dict` but has only one child entity that will be always available -- the one entity is enumerator of possible values and based on value of the entity are defined and used other children entities +- is similar to `dict` but has always available one enum entity + - the enum entity has single selection and it's value define other children entities - each value of enumerator have defined children that will be used - there is no way how to have shared entities across multiple enum items - value from enumerator is also stored next to other values @@ -207,22 +218,27 @@ - `enum_key` must match key regex and any enum item can't have children with same key - `enum_label` is label of the entity for UI purposes - enum items are define with `enum_children` - - it's a list where each item represents enum item + - it's a list where each item represents single item for the enum - all items in `enum_children` must have at least `key` key which represents value stored under `enum_key` - - items can define `label` for UI purposes + - enum items can define `label` for UI purposes - most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`) - to set default value for `enum_key` set it with `enum_default` - entity must have defined `"label"` if is not used as widget -- is set as group if any parent is not group -- if `"label"` is entetered there which will be shown in GUI - - item with label can be collapsible - - that can be set with key `"collapsible"` as `True`/`False` (Default: `True`) - - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) - - it is possible to add darker background with `"highlight_content"` (Default: `False`) - - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color - - output is dictionary `{the "key": children values}` +- is set as group if any parent is not group (can't have children as group) +- may be with or without `"label"` (only for GUI) + - `"label"` must be set to be able mark item as group with `"is_group"` key set to True +- item with label can visually wrap it's children + - this option is enabled by default to turn off set `"use_label_wrap"` to `False` + - label wrap is by default collapsible + - that can be set with key `"collapsible"` to `True`/`False` + - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) + - it is possible to add lighter background with `"highlight_content"` (Default: `False`) + - lighter background has limits of maximum applies after 3-4 nested highlighted items there is not much difference in the color - for UI porposes was added `enum_is_horizontal` which will make combobox appear next to children inputs instead of on top of them (Default: `False`) - this has extended ability of `enum_on_right` which will move combobox to right side next to children widgets (Default: `False`) +- output is dictionary `{the "key": children values}` +- using this type as template item for list type can be used to create infinite hierarchies + ``` # Example { @@ -298,8 +314,8 @@ How output of the schema could look like on save: ``` ## Inputs for setting any kind of value (`Pure` inputs) -- all these input must have defined `"key"` under which will be stored and `"label"` which will be shown next to input - - unless they are used in different types of inputs (later) "as widgets" in that case `"key"` and `"label"` are not required as there is not place where to set them +- all inputs must have defined `"key"` if are not used as dynamic item + - they can also have defined `"label"` ### boolean - simple checkbox, nothing more to set @@ -355,21 +371,15 @@ How output of the schema could look like on save: ``` ### path-input -- enhanced text input - - does not allow to enter backslash, is auto-converted to forward slash - - may be added another validations, like do not allow end path with slash - this input is implemented to add additional features to text input -- this is meant to be used in proxy input `path-widget` +- this is meant to be used in proxy input `path` - DO NOT USE this input in schema please ### raw-json - a little bit enhanced text input for raw json +- can store dictionary (`{}`) or list (`[]`) but not both + - by default stores dictionary to change it to list set `is_list` to `True` - has validations of json format - - empty value is invalid value, always must be json serializable - - valid value types are list `[]` and dictionary `{}` -- schema also defines valid value type - - by default it is dictionary - - to be able use list it is required to define `is_list` to `true` - output can be stored as string - this is to allow any keys in dictionary - set key `store_as_string` to `true` @@ -385,7 +395,7 @@ How output of the schema could look like on save: ``` ### enum -- returns value of single on multiple items from predefined values +- enumeration of values that are predefined in schema - multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) - values are defined under value of key `"enum_items"` as list - each item in list is simple dictionary where value is label and key is value which will be stored @@ -415,6 +425,8 @@ How output of the schema could look like on save: - have only single selection mode - it is possible to define default value `default` - `"work"` is used if default value is not specified +- enum values are not updated on the fly it is required to save templates and + reset settings to recache values ``` { "key": "host", @@ -449,6 +461,42 @@ How output of the schema could look like on save: } ``` +### apps-enum +- enumeration of available application and their variants from system settings + - applications without host name are excluded +- can be used only in project settings +- has only `multiselection` +- used only in project anatomy +``` +{ + "type": "apps-enum", + "key": "applications", + "label": "Applications" +} +``` + +### tools-enum +- enumeration of available tools and their variants from system settings +- can be used only in project settings +- has only `multiselection` +- used only in project anatomy +``` +{ + "type": "tools-enum", + "key": "tools_env", + "label": "Tools" +} +``` + +### task-types-enum +- enumeration of task types from current project +- enum values are not updated on the fly and modifications of task types on project require save and reset to be propagated to this enum +- has set `multiselection` to `True` but can be changed to `False` in schema + +### deadline_url-enum +- deadline module specific enumerator using deadline system settings to fill it's values +- TODO: move this type to deadline module + ## Inputs for setting value using Pure inputs - these inputs also have required `"key"` - attribute `"label"` is required in few conditions @@ -594,7 +642,7 @@ How output of the schema could look like on save: } ``` -### path-widget +### path - input for paths, use `path-input` internally - has 2 input modifiers `"multiplatform"` and `"multipath"` - `"multiplatform"` - adds `"windows"`, `"linux"` and `"darwin"` path inputs result is dictionary @@ -685,12 +733,13 @@ How output of the schema could look like on save: } ``` -### splitter -- visual splitter of items (more divider than splitter) +### separator +- legacy name is `splitter` (still usable) +- visual separator of items (more divider than separator) ``` { - "type": "splitter" + "type": "separator" } ``` diff --git a/openpype/settings/entities/schemas/system_schema/example_schema.json b/openpype/settings/entities/schemas/system_schema/example_schema.json index af6a2d49f4..c30e1f6848 100644 --- a/openpype/settings/entities/schemas/system_schema/example_schema.json +++ b/openpype/settings/entities/schemas/system_schema/example_schema.json @@ -95,11 +95,11 @@ }, { "type": "dict", - "key": "schema_template_exaples", + "key": "template_exaples", "label": "Schema template examples", "children": [ { - "type": "schema_template", + "type": "template", "name": "example_template", "template_data": { "host_label": "Application 1", @@ -108,7 +108,7 @@ } }, { - "type": "schema_template", + "type": "template", "name": "example_template", "template_data": { "host_label": "Application 2", From 3ae3ec1185e3fd0c5c1020adedf94348f0bc8f78 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 14 Oct 2021 19:00:32 +0200 Subject: [PATCH 590/716] tools are always on top if don't have set parent --- openpype/tools/libraryloader/app.py | 5 ++++- openpype/tools/loader/app.py | 5 ++++- openpype/tools/workfiles/app.py | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 8080c547c9..3f11157418 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -38,7 +38,10 @@ class LibraryLoaderWindow(QtWidgets.QDialog): # Enable minimize and maximize for app self.setWindowTitle(self.tool_title) - self.setWindowFlags(QtCore.Qt.Window) + window_flags = QtCore.Qt.Window + if not parent: + window_flags |= QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(window_flags) self.setFocusPolicy(QtCore.Qt.StrongFocus) if icon is not None: self.setWindowIcon(icon) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index c18b6e798a..bc0eef3bca 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -51,7 +51,10 @@ class LoaderWindow(QtWidgets.QDialog): self.family_config_cache = lib.FamilyConfigCache(io) # Enable minimize and maximize for app - self.setWindowFlags(QtCore.Qt.Window) + window_flags = QtCore.Qt.Window + if not parent: + window_flags |= QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(window_flags) self.setFocusPolicy(QtCore.Qt.StrongFocus) body = QtWidgets.QWidget() diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 6fff0d0278..18e8cfc6d3 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -944,7 +944,10 @@ class Window(QtWidgets.QMainWindow): def __init__(self, parent=None): super(Window, self).__init__(parent=parent) self.setWindowTitle(self.title) - self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.WindowCloseButtonHint) + window_flags = QtCore.Qt.Window | QtCore.Qt.WindowCloseButtonHint + if not parent: + window_flags |= QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(window_flags) # Create pages widget and set it as central widget pages_widget = QtWidgets.QStackedWidget(self) From 5d426741d5553b93840912a86a991e70f3513658 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 14 Oct 2021 19:06:43 +0200 Subject: [PATCH 591/716] initial idea of caching host tools at single place --- openpype/tools/utils/host_tools.py | 205 +++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 openpype/tools/utils/host_tools.py diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py new file mode 100644 index 0000000000..203db36949 --- /dev/null +++ b/openpype/tools/utils/host_tools.py @@ -0,0 +1,205 @@ +"""Single access point to all tools usable in hosts. + +It is possible to create `HostToolsHelper` in host implementaion or +use singleton approach with global functions (using helper anyway). +""" + +from Qt import QtCore +import avalon.api + + +class HostToolsHelper: + """Create and cache tool windows in memory. + + Almost all methods expect parent widget but the parent is used only on + first tool creation. + + Class may also contain tools that are available only for one or few hosts. + """ + def __init__(self, parent=None): + self._parent = parent + self._workfiles_tool = None + self._loader_tool = None + self._creator_tool = None + self._subset_manager_tool = None + self._scene_inventory_tool = None + self._library_loader_tool = None + + def _get_workfiles_tool(self, parent): + if self._workfiles_tool is None: + from openpype.tools.workfiles.app import ( + Window, validate_host_requirements + ) + # Host validation + host = avalon.api.registered_host() + validate_host_requirements(host) + + window = Window(parent=parent) + + context = { + "asset": avalon.api.Session["AVALON_ASSET"], + "silo": avalon.api.Session["AVALON_SILO"], + "task": avalon.api.Session["AVALON_TASK"] + } + window.set_context(context) + + self._workfiles_tool = window + + return self._workfiles_tool + + def show_workfiles_tool(self, parent=None): + workfiles_tool = self._get_workfiles_tool(parent) + + workfiles_tool.refresh() + workfiles_tool.show() + # Pull window to the front. + workfiles_tool.raise_() + workfiles_tool.activateWindow() + + def _get_loader_tool(self, parent): + if self._loader_tool is None: + from openpype.tools.loader import LoaderWindow + + self._loader_tool = LoaderWindow(parent=parent or self._parent) + + return self._loader_tool + + def show_loader_tool(self, parent=None): + loader_tool = self._get_loader_tool(parent) + + context = {"asset": avalon.api.Session["AVALON_ASSET"]} + loader_tool.set_context(context, refresh=True) + + loader_tool.show() + loader_tool.raise_() + loader_tool.activateWindow() + loader_tool.refresh() + + def _get_creator_tool(self, parent): + if self._creator_tool is None: + from avalon.tools.creator.app import Window + + self._creator_tool = Window(parent=parent or self._parent) + + return self._creator_tool + + def show_creator_tool(self, parent=None): + creator_tool = self._get_creator_tool(parent) + creator_tool.refresh() + creator_tool.show() + + # Pull window to the front. + creator_tool.raise_() + creator_tool.activateWindow() + + def _get_subset_manager_tool(self, parent): + if self._subset_manager_tool is None: + from avalon.tools.subsetmanager import Window + + self._subset_manager_tool = Window(parent=parent or self._parent) + + return self._subset_manager_tool + + def show_subset_manager_tool(self, parent=None): + subset_manager_tool = self._get_subset_manager_tool(parent) + subset_manager_tool.show() + + # Pull window to the front. + subset_manager_tool.raise_() + subset_manager_tool.activateWindow() + + def _get_scene_inventory_tool(self, parent): + if self._scene_inventory_tool is None: + from avalon.tools.sceneinventory.app import Window + + self._scene_inventory_tool = Window(parent=parent or self._parent) + + return self._scene_inventory_tool + + def show_scene_inventory_tool(self, parent=None): + scene_inventory_tool = self._get_scene_inventory_tool(parent) + scene_inventory_tool.show() + scene_inventory_tool.refresh() + + # Pull window to the front. + scene_inventory_tool.raise_() + scene_inventory_tool.activateWindow() + + def _get_library_loader_tool(self, parent): + if self._library_loader_tool is None: + from openpype.tools.libraryloader import LibraryLoaderWindow + + self._library_loader_tool = LibraryLoaderWindow( + parent=parent or self._parent + ) + + return self._library_loader_tool + + def show_library_loader_tool(self, parent=None): + library_loader_tool = self._get_library_loader_tool(parent) + library_loader_tool.show() + library_loader_tool.raise_() + library_loader_tool.activateWindow() + library_loader_tool.refresh() + + def show_publish_tool(self, parent=None): + from avalon.tools import publish + + publish.show(parent) + + def show_tool_by_name(self, tool_name, parent=None): + if tool_name == "workfiles": + self.show_workfiles_tool(parent) + + elif tool_name == "loader": + self.show_loader_tool(parent) + + elif tool_name == "libraryloader": + self.show_library_loader_tool(parent) + + elif tool_name == "creator": + self.show_creator_tool(parent) + + elif tool_name == "subset_manager": + self.show_subset_manager_tool(parent) + + elif tool_name == "scene_inventory": + self.show_scene_inventory_tool(parent) + + +class _SingletonPoint: + helper = None + + @classmethod + def _create_helper(cls): + if cls.helper is None: + cls.helper = HostToolsHelper() + + @classmethod + def show_tool_by_name(cls, tool_name, parent=None): + cls._create_helper() + cls.helper.show_tool_by_name(tool_name, parent) + + +def show_workfiles_tool(parent=None): + _SingletonPoint.show_tool_by_name("workfiles", parent) + + +def show_loader_tool(parent=None): + _SingletonPoint.show_tool_by_name("loader", parent) + + +def show_library_loader_tool(parent=None): + _SingletonPoint.show_tool_by_name("libraryloader", parent) + + +def show_creator_tool(parent=None): + _SingletonPoint.show_tool_by_name("creator", parent) + + +def show_subset_manager_tool(parent=None): + _SingletonPoint.show_tool_by_name("subset_manager", parent) + + +def show_scene_inventory_tool(parent=None): + _SingletonPoint.show_tool_by_name("scene_inventory", parent) From 54c8988ce916c70634def2903fff665126b10eeb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 14 Oct 2021 19:16:55 +0200 Subject: [PATCH 592/716] enhanced passing args and kwargs --- openpype/tools/utils/host_tools.py | 42 ++++++++++++++++-------------- openpype/tools/workfiles/app.py | 5 +++- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 203db36949..aa3d1ccc94 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -34,21 +34,23 @@ class HostToolsHelper: host = avalon.api.registered_host() validate_host_requirements(host) - window = Window(parent=parent) + self._workfiles_tool = Window(parent=parent) + return self._workfiles_tool + + def show_workfiles_tool(self, parent=None, use_context=True, save=True): + workfiles_tool = self._get_workfiles_tool(parent) + + if use_context: context = { "asset": avalon.api.Session["AVALON_ASSET"], "silo": avalon.api.Session["AVALON_SILO"], "task": avalon.api.Session["AVALON_TASK"] } - window.set_context(context) + workfiles_tool.set_context(context) - self._workfiles_tool = window - - return self._workfiles_tool - - def show_workfiles_tool(self, parent=None): - workfiles_tool = self._get_workfiles_tool(parent) + if save: + workfiles_tool.set_save_enabled(save) workfiles_tool.refresh() workfiles_tool.show() @@ -147,24 +149,24 @@ class HostToolsHelper: publish.show(parent) - def show_tool_by_name(self, tool_name, parent=None): + def show_tool_by_name(self, tool_name, parent=None, *args, **kwargs): if tool_name == "workfiles": - self.show_workfiles_tool(parent) + self.show_workfiles_tool(parent, *args, **kwargs) elif tool_name == "loader": - self.show_loader_tool(parent) + self.show_loader_tool(parent, *args, **kwargs) elif tool_name == "libraryloader": - self.show_library_loader_tool(parent) + self.show_library_loader_tool(parent, *args, **kwargs) elif tool_name == "creator": - self.show_creator_tool(parent) + self.show_creator_tool(parent, *args, **kwargs) elif tool_name == "subset_manager": - self.show_subset_manager_tool(parent) + self.show_subset_manager_tool(parent, *args, **kwargs) elif tool_name == "scene_inventory": - self.show_scene_inventory_tool(parent) + self.show_scene_inventory_tool(parent, *args, **kwargs) class _SingletonPoint: @@ -176,13 +178,15 @@ class _SingletonPoint: cls.helper = HostToolsHelper() @classmethod - def show_tool_by_name(cls, tool_name, parent=None): + def show_tool_by_name(cls, tool_name, parent=None, *args, **kwargs): cls._create_helper() - cls.helper.show_tool_by_name(tool_name, parent) + cls.helper.show_tool_by_name(tool_name, parent, *args, **kwargs) -def show_workfiles_tool(parent=None): - _SingletonPoint.show_tool_by_name("workfiles", parent) +def show_workfiles_tool(parent=None, use_context=True, save=True): + _SingletonPoint.show_tool_by_name( + "workfiles", parent, use_context=use_context, save=save + ) def show_loader_tool(parent=None): diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 18e8cfc6d3..1679a18241 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -1018,6 +1018,9 @@ class Window(QtWidgets.QMainWindow): """ + def set_save_enabled(self, enabled): + self.files_widget.btn_save.setEnabled(enabled) + def on_task_changed(self): # Since we query the disk give it slightly more delay tools_lib.schedule(self._on_task_changed, 100, channel="mongo") @@ -1190,7 +1193,7 @@ def show(root=None, debug=False, parent=None, use_context=True, save=True): } window.set_context(context) - window.files_widget.btn_save.setEnabled(save) + window.set_save_enabled(save) window.show() window.setStyleSheet(style.load_stylesheet()) From 0bac8ff6de8b04ef9a8eb35d65db8a3811148dee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 10:07:20 +0200 Subject: [PATCH 593/716] removed underscores from tool names --- openpype/tools/utils/host_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index aa3d1ccc94..979341375e 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -162,10 +162,10 @@ class HostToolsHelper: elif tool_name == "creator": self.show_creator_tool(parent, *args, **kwargs) - elif tool_name == "subset_manager": + elif tool_name == "subsetmanager": self.show_subset_manager_tool(parent, *args, **kwargs) - elif tool_name == "scene_inventory": + elif tool_name == "sceneinventory": self.show_scene_inventory_tool(parent, *args, **kwargs) From 08ad338cb5d3c14fbbd781ec12873bbffda89e92 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 10:07:43 +0200 Subject: [PATCH 594/716] use_context and save don't have defaults in args definition --- openpype/tools/utils/host_tools.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 979341375e..8c9da2b4f9 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -38,9 +38,14 @@ class HostToolsHelper: return self._workfiles_tool - def show_workfiles_tool(self, parent=None, use_context=True, save=True): - workfiles_tool = self._get_workfiles_tool(parent) + def show_workfiles_tool(self, parent=None, use_context=None, save=None): + if use_context is None: + use_context = True + if save is None: + save = True + + workfiles_tool = self._get_workfiles_tool(parent) if use_context: context = { "asset": avalon.api.Session["AVALON_ASSET"], @@ -183,7 +188,7 @@ class _SingletonPoint: cls.helper.show_tool_by_name(tool_name, parent, *args, **kwargs) -def show_workfiles_tool(parent=None, use_context=True, save=True): +def show_workfiles_tool(parent=None, use_context=None, save=None): _SingletonPoint.show_tool_by_name( "workfiles", parent, use_context=use_context, save=save ) From a8e1d5164c8ca979e58f7dd8bc1f570905833539 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 10:17:20 +0200 Subject: [PATCH 595/716] loader has use_context argument --- openpype/tools/utils/host_tools.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 8c9da2b4f9..daf9356bae 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -71,11 +71,16 @@ class HostToolsHelper: return self._loader_tool - def show_loader_tool(self, parent=None): + def show_loader_tool(self, parent=None, use_context=None): + if use_context is None: + use_context = False loader_tool = self._get_loader_tool(parent) - context = {"asset": avalon.api.Session["AVALON_ASSET"]} - loader_tool.set_context(context, refresh=True) + if use_context: + context = {"asset": avalon.api.Session["AVALON_ASSET"]} + loader_tool.set_context(context, refresh=True) + else: + loader_tool.refresh() loader_tool.show() loader_tool.raise_() @@ -194,8 +199,10 @@ def show_workfiles_tool(parent=None, use_context=None, save=None): ) -def show_loader_tool(parent=None): - _SingletonPoint.show_tool_by_name("loader", parent) +def show_loader_tool(parent=None, use_context=None): + _SingletonPoint.show_tool_by_name( + "loader", parent, use_context=use_context + ) def show_library_loader_tool(parent=None): From b1cf928c3b51de5ba1b14f67a2fa92ea33e1d440 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 10:17:41 +0200 Subject: [PATCH 596/716] fixed tool names in global functions --- openpype/tools/utils/host_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index daf9356bae..492f21486d 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -214,8 +214,8 @@ def show_creator_tool(parent=None): def show_subset_manager_tool(parent=None): - _SingletonPoint.show_tool_by_name("subset_manager", parent) + _SingletonPoint.show_tool_by_name("subsetmanager", parent) def show_scene_inventory_tool(parent=None): - _SingletonPoint.show_tool_by_name("scene_inventory", parent) + _SingletonPoint.show_tool_by_name("sceneinventory", parent) From ca168580ba68c94103113a799248ddea4f1a4329 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 10:36:06 +0200 Subject: [PATCH 597/716] added few docstrings --- openpype/tools/utils/host_tools.py | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 492f21486d..7c7a566aae 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -17,7 +17,11 @@ class HostToolsHelper: Class may also contain tools that are available only for one or few hosts. """ def __init__(self, parent=None): + self._log = None + # Global parent for all tools (may and may not be set) self._parent = parent + + # Prepare attributes for all tools self._workfiles_tool = None self._loader_tool = None self._creator_tool = None @@ -25,6 +29,14 @@ class HostToolsHelper: self._scene_inventory_tool = None self._library_loader_tool = None + @property + def log(self): + if self._log is None: + from openpype.api import Logger + + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + def _get_workfiles_tool(self, parent): if self._workfiles_tool is None: from openpype.tools.workfiles.app import ( @@ -39,6 +51,7 @@ class HostToolsHelper: return self._workfiles_tool def show_workfiles_tool(self, parent=None, use_context=None, save=None): + """Workfiles tool for changing context and saving workfiles.""" if use_context is None: use_context = True @@ -72,6 +85,7 @@ class HostToolsHelper: return self._loader_tool def show_loader_tool(self, parent=None, use_context=None): + """Loader tool for loading representations.""" if use_context is None: use_context = False loader_tool = self._get_loader_tool(parent) @@ -96,6 +110,7 @@ class HostToolsHelper: return self._creator_tool def show_creator_tool(self, parent=None): + """Show tool to create new instantes for publishing.""" creator_tool = self._get_creator_tool(parent) creator_tool.refresh() creator_tool.show() @@ -113,6 +128,7 @@ class HostToolsHelper: return self._subset_manager_tool def show_subset_manager_tool(self, parent=None): + """Show tool display/remove existing created instances.""" subset_manager_tool = self._get_subset_manager_tool(parent) subset_manager_tool.show() @@ -129,6 +145,7 @@ class HostToolsHelper: return self._scene_inventory_tool def show_scene_inventory_tool(self, parent=None): + """Show tool maintain loaded containers.""" scene_inventory_tool = self._get_scene_inventory_tool(parent) scene_inventory_tool.show() scene_inventory_tool.refresh() @@ -148,6 +165,7 @@ class HostToolsHelper: return self._library_loader_tool def show_library_loader_tool(self, parent=None): + """Loader tool for loading representations from library project.""" library_loader_tool = self._get_library_loader_tool(parent) library_loader_tool.show() library_loader_tool.raise_() @@ -155,11 +173,16 @@ class HostToolsHelper: library_loader_tool.refresh() def show_publish_tool(self, parent=None): + """Publish UI.""" from avalon.tools import publish publish.show(parent) def show_tool_by_name(self, tool_name, parent=None, *args, **kwargs): + """Show tool by it's name. + + This is helper for + """ if tool_name == "workfiles": self.show_workfiles_tool(parent, *args, **kwargs) @@ -178,8 +201,18 @@ class HostToolsHelper: elif tool_name == "sceneinventory": self.show_scene_inventory_tool(parent, *args, **kwargs) + self.log.warning( + "Can't show unknown tool name: \"{}\"".format(tool_name) + ) + class _SingletonPoint: + """Singleton access to host tools. + + Some hosts don't have ability to create 'HostToolsHelper' object anc can + only register function callbacks. For those cases is created this singleton + point where 'HostToolsHelper' is created "in shared memory". + """ helper = None @classmethod @@ -193,6 +226,7 @@ class _SingletonPoint: cls.helper.show_tool_by_name(tool_name, parent, *args, **kwargs) +# Function callbacks using singleton acces point def show_workfiles_tool(parent=None, use_context=None, save=None): _SingletonPoint.show_tool_by_name( "workfiles", parent, use_context=use_context, save=save From e1281231bab2ba3db102e3d3d3aea659bef0cf0c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 10:54:06 +0200 Subject: [PATCH 598/716] added look manager --- openpype/tools/utils/host_tools.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 7c7a566aae..0cd9578f0f 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -28,6 +28,7 @@ class HostToolsHelper: self._subset_manager_tool = None self._scene_inventory_tool = None self._library_loader_tool = None + self._look_manager_tool = None @property def log(self): @@ -178,6 +179,18 @@ class HostToolsHelper: publish.show(parent) + def _get_look_manager_tool(self, parent): + if self._look_manager_tool is None: + import mayalookassigner + + self._look_manager_tool = mayalookassigner.App(parent) + return self._look_manager_tool + + def show_look_manager(self, parent=None): + """Look manager is Maya specific tool for look management.""" + look_manager_tool = self._get_look_manager_tool(parent) + look_manager_tool.show() + def show_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. @@ -201,6 +214,9 @@ class HostToolsHelper: elif tool_name == "sceneinventory": self.show_scene_inventory_tool(parent, *args, **kwargs) + elif tool_name == "lookmanager": + self.show_look_manager(parent, *args, **kwargs) + self.log.warning( "Can't show unknown tool name: \"{}\"".format(tool_name) ) @@ -253,3 +269,6 @@ def show_subset_manager_tool(parent=None): def show_scene_inventory_tool(parent=None): _SingletonPoint.show_tool_by_name("sceneinventory", parent) + +def show_look_manager(self, parent=None): + _SingletonPoint.show_tool_by_name("lookmanager", parent) From 5446ac65406e4dcb7d859b6e32facfbaefb937e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 11:03:01 +0200 Subject: [PATCH 599/716] removed _tool suffix from show methods --- openpype/tools/utils/host_tools.py | 43 +++++++++++++++++------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 0cd9578f0f..ce2a66fd4b 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -51,7 +51,7 @@ class HostToolsHelper: return self._workfiles_tool - def show_workfiles_tool(self, parent=None, use_context=None, save=None): + def show_workfiles(self, parent=None, use_context=None, save=None): """Workfiles tool for changing context and saving workfiles.""" if use_context is None: use_context = True @@ -85,7 +85,7 @@ class HostToolsHelper: return self._loader_tool - def show_loader_tool(self, parent=None, use_context=None): + def show_loader(self, parent=None, use_context=None): """Loader tool for loading representations.""" if use_context is None: use_context = False @@ -110,7 +110,7 @@ class HostToolsHelper: return self._creator_tool - def show_creator_tool(self, parent=None): + def show_creator(self, parent=None): """Show tool to create new instantes for publishing.""" creator_tool = self._get_creator_tool(parent) creator_tool.refresh() @@ -128,7 +128,7 @@ class HostToolsHelper: return self._subset_manager_tool - def show_subset_manager_tool(self, parent=None): + def show_subset_manager(self, parent=None): """Show tool display/remove existing created instances.""" subset_manager_tool = self._get_subset_manager_tool(parent) subset_manager_tool.show() @@ -145,7 +145,7 @@ class HostToolsHelper: return self._scene_inventory_tool - def show_scene_inventory_tool(self, parent=None): + def show_scene_inventory(self, parent=None): """Show tool maintain loaded containers.""" scene_inventory_tool = self._get_scene_inventory_tool(parent) scene_inventory_tool.show() @@ -165,7 +165,7 @@ class HostToolsHelper: return self._library_loader_tool - def show_library_loader_tool(self, parent=None): + def show_library_loader(self, parent=None): """Loader tool for loading representations from library project.""" library_loader_tool = self._get_library_loader_tool(parent) library_loader_tool.show() @@ -173,7 +173,7 @@ class HostToolsHelper: library_loader_tool.activateWindow() library_loader_tool.refresh() - def show_publish_tool(self, parent=None): + def show_publish(self, parent=None): """Publish UI.""" from avalon.tools import publish @@ -197,22 +197,22 @@ class HostToolsHelper: This is helper for """ if tool_name == "workfiles": - self.show_workfiles_tool(parent, *args, **kwargs) + self.show_workfiles(parent, *args, **kwargs) elif tool_name == "loader": - self.show_loader_tool(parent, *args, **kwargs) + self.show_loader(parent, *args, **kwargs) elif tool_name == "libraryloader": - self.show_library_loader_tool(parent, *args, **kwargs) + self.show_library_loader(parent, *args, **kwargs) elif tool_name == "creator": - self.show_creator_tool(parent, *args, **kwargs) + self.show_creator(parent, *args, **kwargs) elif tool_name == "subsetmanager": - self.show_subset_manager_tool(parent, *args, **kwargs) + self.show_subset_manager(parent, *args, **kwargs) elif tool_name == "sceneinventory": - self.show_scene_inventory_tool(parent, *args, **kwargs) + self.show_scene_inventory(parent, *args, **kwargs) elif tool_name == "lookmanager": self.show_look_manager(parent, *args, **kwargs) @@ -243,32 +243,37 @@ class _SingletonPoint: # Function callbacks using singleton acces point -def show_workfiles_tool(parent=None, use_context=None, save=None): +def show_tool_by_name(tool_name, parent=None, *args, **kwargs): + _SingletonPoint.show_tool_by_name(tool_name, parent, *args, **kwargs) + + +def show_workfiles(parent=None, use_context=None, save=None): _SingletonPoint.show_tool_by_name( "workfiles", parent, use_context=use_context, save=save ) -def show_loader_tool(parent=None, use_context=None): +def show_loader(parent=None, use_context=None): _SingletonPoint.show_tool_by_name( "loader", parent, use_context=use_context ) -def show_library_loader_tool(parent=None): +def show_library_loader(parent=None): _SingletonPoint.show_tool_by_name("libraryloader", parent) -def show_creator_tool(parent=None): +def show_creator(parent=None): _SingletonPoint.show_tool_by_name("creator", parent) -def show_subset_manager_tool(parent=None): +def show_subset_manager(parent=None): _SingletonPoint.show_tool_by_name("subsetmanager", parent) -def show_scene_inventory_tool(parent=None): +def show_scene_inventory(parent=None): _SingletonPoint.show_tool_by_name("sceneinventory", parent) + def show_look_manager(self, parent=None): _SingletonPoint.show_tool_by_name("lookmanager", parent) From d8064571a24e5eeb6610adfdbf964e3c5ac88cec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 11:09:10 +0200 Subject: [PATCH 600/716] fixed look manager to look assigner --- openpype/tools/utils/host_tools.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index ce2a66fd4b..96c05e4135 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -28,7 +28,7 @@ class HostToolsHelper: self._subset_manager_tool = None self._scene_inventory_tool = None self._library_loader_tool = None - self._look_manager_tool = None + self._look_assigner_tool = None @property def log(self): @@ -179,17 +179,17 @@ class HostToolsHelper: publish.show(parent) - def _get_look_manager_tool(self, parent): - if self._look_manager_tool is None: + def _get_look_assigner_tool(self, parent): + if self._look_assigner_tool is None: import mayalookassigner - self._look_manager_tool = mayalookassigner.App(parent) - return self._look_manager_tool + self._look_assigner_tool = mayalookassigner.App(parent) + return self._look_assigner_tool - def show_look_manager(self, parent=None): + def show_look_assigner(self, parent=None): """Look manager is Maya specific tool for look management.""" - look_manager_tool = self._get_look_manager_tool(parent) - look_manager_tool.show() + look_assigner_tool = self._get_look_assigner_tool(parent) + look_assigner_tool.show() def show_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. @@ -214,8 +214,8 @@ class HostToolsHelper: elif tool_name == "sceneinventory": self.show_scene_inventory(parent, *args, **kwargs) - elif tool_name == "lookmanager": - self.show_look_manager(parent, *args, **kwargs) + elif tool_name == "lookassigner": + self.show_look_assigner(parent, *args, **kwargs) self.log.warning( "Can't show unknown tool name: \"{}\"".format(tool_name) @@ -275,5 +275,5 @@ def show_scene_inventory(parent=None): _SingletonPoint.show_tool_by_name("sceneinventory", parent) -def show_look_manager(self, parent=None): - _SingletonPoint.show_tool_by_name("lookmanager", parent) +def show_look_assigner(parent=None): + _SingletonPoint.show_tool_by_name("lookassigner", parent) From 06212357a633fa575abb51be4abbf78d84b6b73c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 11:14:19 +0200 Subject: [PATCH 601/716] added publish to known tools --- openpype/tools/utils/host_tools.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 96c05e4135..09d3c808c2 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -217,9 +217,13 @@ class HostToolsHelper: elif tool_name == "lookassigner": self.show_look_assigner(parent, *args, **kwargs) - self.log.warning( - "Can't show unknown tool name: \"{}\"".format(tool_name) - ) + elif tool_name == "publish": + self.show_publish(parent, *args, **kwargs) + + else: + self.log.warning( + "Can't show unknown tool name: \"{}\"".format(tool_name) + ) class _SingletonPoint: @@ -277,3 +281,7 @@ def show_scene_inventory(parent=None): def show_look_assigner(parent=None): _SingletonPoint.show_tool_by_name("lookassigner", parent) + + +def show_publish(parent=None): + _SingletonPoint.show_tool_by_name("publish", parent) From 235bc68573417654399a5efbb9137de35daf29be Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 11:14:38 +0200 Subject: [PATCH 602/716] use host_tools in maya --- openpype/hosts/maya/api/menu.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index ad225dcd28..42cb76dca6 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -2,13 +2,15 @@ import sys import os import logging -from avalon.vendor.Qt import QtWidgets, QtGui -from avalon.maya import pipeline -from openpype.api import BuildWorkfile -import maya.cmds as cmds -from openpype.settings import get_project_settings +from Qt import QtWidgets, QtGui -self = sys.modules[__name__] +import maya.cmds as cmds + +from avalon.maya import pipeline + +from openpype.api import BuildWorkfile +from openpype.settings import get_project_settings +from openpype.tools.utils import host_tools log = logging.getLogger(__name__) @@ -26,6 +28,7 @@ def _get_menu(menu_name=None): def deferred(): + def add_build_workfiles_item(): # Add build first workfile cmds.menuItem(divider=True, parent=pipeline._menu) @@ -36,24 +39,17 @@ def deferred(): ) def add_look_assigner_item(): - import mayalookassigner cmds.menuItem( "Look assigner", parent=pipeline._menu, - command=lambda *args: mayalookassigner.show() + command=lambda *args: host_tools.show_look_assigner( + pipeline._parent + ) ) def modify_workfiles(): - from openpype.tools import workfiles - def launch_workfiles_app(*_args, **_kwargs): - workfiles.show( - os.path.join( - cmds.workspace(query=True, rootDirectory=True), - cmds.workspace(fileRuleEntry="scene") - ), - parent=pipeline._parent - ) + host_tools.show_workfiles(pipeline._parent) # Find the pipeline menu top_menu = _get_menu() From 73bb38696895bcdac33d5a85e2baac8808d665b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 11:19:43 +0200 Subject: [PATCH 603/716] added style loading --- openpype/tools/utils/host_tools.py | 35 ++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 09d3c808c2..f8a30cf8f6 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -40,6 +40,7 @@ class HostToolsHelper: def _get_workfiles_tool(self, parent): if self._workfiles_tool is None: + from avalon import style from openpype.tools.workfiles.app import ( Window, validate_host_requirements ) @@ -47,7 +48,9 @@ class HostToolsHelper: host = avalon.api.registered_host() validate_host_requirements(host) - self._workfiles_tool = Window(parent=parent) + workfiles_window = Window(parent=parent) + workfiles_window.setStyleSheet(style.load_stylesheet()) + self._workfiles_tool = workfiles_window return self._workfiles_tool @@ -79,9 +82,12 @@ class HostToolsHelper: def _get_loader_tool(self, parent): if self._loader_tool is None: + from avalon import style from openpype.tools.loader import LoaderWindow - self._loader_tool = LoaderWindow(parent=parent or self._parent) + loader_window = LoaderWindow(parent=parent or self._parent) + loader_window.setStyleSheet(style.load_stylesheet()) + self._loader_tool = loader_window return self._loader_tool @@ -104,9 +110,12 @@ class HostToolsHelper: def _get_creator_tool(self, parent): if self._creator_tool is None: + from avalon import style from avalon.tools.creator.app import Window - self._creator_tool = Window(parent=parent or self._parent) + creator_window = Window(parent=parent or self._parent) + creator_window.setStyleSheet(style.load_stylesheet()) + self._creator_tool = creator_window return self._creator_tool @@ -122,9 +131,12 @@ class HostToolsHelper: def _get_subset_manager_tool(self, parent): if self._subset_manager_tool is None: + from avalon import style from avalon.tools.subsetmanager import Window - self._subset_manager_tool = Window(parent=parent or self._parent) + subset_manager_window = Window(parent=parent or self._parent) + subset_manager_window.setStyleSheet(style.load_stylesheet()) + self._subset_manager_tool = subset_manager_window return self._subset_manager_tool @@ -139,9 +151,12 @@ class HostToolsHelper: def _get_scene_inventory_tool(self, parent): if self._scene_inventory_tool is None: + from avalon import style from avalon.tools.sceneinventory.app import Window - self._scene_inventory_tool = Window(parent=parent or self._parent) + scene_inventory_window = Window(parent=parent or self._parent) + scene_inventory_window.setStyleSheet(style.load_stylesheet()) + self._scene_inventory_tool = scene_inventory_window return self._scene_inventory_tool @@ -157,11 +172,14 @@ class HostToolsHelper: def _get_library_loader_tool(self, parent): if self._library_loader_tool is None: + from avalon import style from openpype.tools.libraryloader import LibraryLoaderWindow - self._library_loader_tool = LibraryLoaderWindow( + library_window = LibraryLoaderWindow( parent=parent or self._parent ) + library_window.setStyleSheet(style.load_stylesheet()) + self._library_loader_tool = library_window return self._library_loader_tool @@ -181,9 +199,12 @@ class HostToolsHelper: def _get_look_assigner_tool(self, parent): if self._look_assigner_tool is None: + from avalon import style import mayalookassigner - self._look_assigner_tool = mayalookassigner.App(parent) + mayalookassigner_window = mayalookassigner.App(parent) + mayalookassigner_window.setStyleSheet(style.load_stylesheet()) + self._look_assigner_tool = mayalookassigner_window return self._look_assigner_tool def show_look_assigner(self, parent=None): From ad0d1b03feca27b88e0a436f1f543207a5adab07 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 11:23:01 +0200 Subject: [PATCH 604/716] changed getters to public methods --- openpype/tools/utils/host_tools.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index f8a30cf8f6..31e305c70e 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -38,7 +38,7 @@ class HostToolsHelper: self._log = Logger.get_logger(self.__class__.__name__) return self._log - def _get_workfiles_tool(self, parent): + def get_workfiles_tool(self, parent): if self._workfiles_tool is None: from avalon import style from openpype.tools.workfiles.app import ( @@ -62,7 +62,7 @@ class HostToolsHelper: if save is None: save = True - workfiles_tool = self._get_workfiles_tool(parent) + workfiles_tool = self.get_workfiles_tool(parent) if use_context: context = { "asset": avalon.api.Session["AVALON_ASSET"], @@ -80,7 +80,7 @@ class HostToolsHelper: workfiles_tool.raise_() workfiles_tool.activateWindow() - def _get_loader_tool(self, parent): + def get_loader_tool(self, parent): if self._loader_tool is None: from avalon import style from openpype.tools.loader import LoaderWindow @@ -95,7 +95,7 @@ class HostToolsHelper: """Loader tool for loading representations.""" if use_context is None: use_context = False - loader_tool = self._get_loader_tool(parent) + loader_tool = self.get_loader_tool(parent) if use_context: context = {"asset": avalon.api.Session["AVALON_ASSET"]} @@ -108,7 +108,7 @@ class HostToolsHelper: loader_tool.activateWindow() loader_tool.refresh() - def _get_creator_tool(self, parent): + def get_creator_tool(self, parent): if self._creator_tool is None: from avalon import style from avalon.tools.creator.app import Window @@ -121,7 +121,7 @@ class HostToolsHelper: def show_creator(self, parent=None): """Show tool to create new instantes for publishing.""" - creator_tool = self._get_creator_tool(parent) + creator_tool = self.get_creator_tool(parent) creator_tool.refresh() creator_tool.show() @@ -129,7 +129,7 @@ class HostToolsHelper: creator_tool.raise_() creator_tool.activateWindow() - def _get_subset_manager_tool(self, parent): + def get_subset_manager_tool(self, parent): if self._subset_manager_tool is None: from avalon import style from avalon.tools.subsetmanager import Window @@ -142,14 +142,14 @@ class HostToolsHelper: def show_subset_manager(self, parent=None): """Show tool display/remove existing created instances.""" - subset_manager_tool = self._get_subset_manager_tool(parent) + subset_manager_tool = self.get_subset_manager_tool(parent) subset_manager_tool.show() # Pull window to the front. subset_manager_tool.raise_() subset_manager_tool.activateWindow() - def _get_scene_inventory_tool(self, parent): + def get_scene_inventory_tool(self, parent): if self._scene_inventory_tool is None: from avalon import style from avalon.tools.sceneinventory.app import Window @@ -162,7 +162,7 @@ class HostToolsHelper: def show_scene_inventory(self, parent=None): """Show tool maintain loaded containers.""" - scene_inventory_tool = self._get_scene_inventory_tool(parent) + scene_inventory_tool = self.get_scene_inventory_tool(parent) scene_inventory_tool.show() scene_inventory_tool.refresh() @@ -170,7 +170,7 @@ class HostToolsHelper: scene_inventory_tool.raise_() scene_inventory_tool.activateWindow() - def _get_library_loader_tool(self, parent): + def get_library_loader_tool(self, parent): if self._library_loader_tool is None: from avalon import style from openpype.tools.libraryloader import LibraryLoaderWindow @@ -185,7 +185,7 @@ class HostToolsHelper: def show_library_loader(self, parent=None): """Loader tool for loading representations from library project.""" - library_loader_tool = self._get_library_loader_tool(parent) + library_loader_tool = self.get_library_loader_tool(parent) library_loader_tool.show() library_loader_tool.raise_() library_loader_tool.activateWindow() @@ -197,7 +197,7 @@ class HostToolsHelper: publish.show(parent) - def _get_look_assigner_tool(self, parent): + def get_look_assigner_tool(self, parent): if self._look_assigner_tool is None: from avalon import style import mayalookassigner @@ -209,7 +209,7 @@ class HostToolsHelper: def show_look_assigner(self, parent=None): """Look manager is Maya specific tool for look management.""" - look_assigner_tool = self._get_look_assigner_tool(parent) + look_assigner_tool = self.get_look_assigner_tool(parent) look_assigner_tool.show() def show_tool_by_name(self, tool_name, parent=None, *args, **kwargs): From b2cdb17ed5efe28218b233a46df0df1d491964b7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 11:26:31 +0200 Subject: [PATCH 605/716] adde simplified docstrings to get tool methods --- openpype/tools/utils/host_tools.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 31e305c70e..6b137213f8 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -39,6 +39,7 @@ class HostToolsHelper: return self._log def get_workfiles_tool(self, parent): + """Create, cache and return workfiles tool window.""" if self._workfiles_tool is None: from avalon import style from openpype.tools.workfiles.app import ( @@ -81,6 +82,7 @@ class HostToolsHelper: workfiles_tool.activateWindow() def get_loader_tool(self, parent): + """Create, cache and return loader tool window.""" if self._loader_tool is None: from avalon import style from openpype.tools.loader import LoaderWindow @@ -109,6 +111,7 @@ class HostToolsHelper: loader_tool.refresh() def get_creator_tool(self, parent): + """Create, cache and return creator tool window.""" if self._creator_tool is None: from avalon import style from avalon.tools.creator.app import Window @@ -130,6 +133,7 @@ class HostToolsHelper: creator_tool.activateWindow() def get_subset_manager_tool(self, parent): + """Create, cache and return subset manager tool window.""" if self._subset_manager_tool is None: from avalon import style from avalon.tools.subsetmanager import Window @@ -150,6 +154,7 @@ class HostToolsHelper: subset_manager_tool.activateWindow() def get_scene_inventory_tool(self, parent): + """Create, cache and return scene inventory tool window.""" if self._scene_inventory_tool is None: from avalon import style from avalon.tools.sceneinventory.app import Window @@ -171,6 +176,7 @@ class HostToolsHelper: scene_inventory_tool.activateWindow() def get_library_loader_tool(self, parent): + """Create, cache and return library loader tool window.""" if self._library_loader_tool is None: from avalon import style from openpype.tools.libraryloader import LibraryLoaderWindow @@ -198,6 +204,7 @@ class HostToolsHelper: publish.show(parent) def get_look_assigner_tool(self, parent): + """Create, cache and return look assigner tool window.""" if self._look_assigner_tool is None: from avalon import style import mayalookassigner From 1517966a5a987803dcaf59d0ad6f8960d5c96063 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 11:42:44 +0200 Subject: [PATCH 606/716] added ability to get window by name --- openpype/tools/utils/host_tools.py | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 6b137213f8..431009ac27 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -219,6 +219,40 @@ class HostToolsHelper: look_assigner_tool = self.get_look_assigner_tool(parent) look_assigner_tool.show() + def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs): + """Show tool by it's name. + + This is helper for + """ + if tool_name == "workfiles": + return self.get_workfiles_tool(parent, *args, **kwargs) + + elif tool_name == "loader": + return self.get_loader_tool(parent, *args, **kwargs) + + elif tool_name == "libraryloader": + return self.get_library_loader_tool(parent, *args, **kwargs) + + elif tool_name == "creator": + return self.get_creator_tool(parent, *args, **kwargs) + + elif tool_name == "subsetmanager": + return self.get_subset_manager_tool(parent, *args, **kwargs) + + elif tool_name == "sceneinventory": + return self.get_scene_inventory_tool(parent, *args, **kwargs) + + elif tool_name == "lookassigner": + return self.get_look_assigner_tool(parent, *args, **kwargs) + + elif tool_name == "publish": + self.log.info("Can't return publish tool window.") + + else: + self.log.warning( + "Can't show unknown tool name: \"{}\"".format(tool_name) + ) + def show_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. @@ -273,8 +307,17 @@ class _SingletonPoint: cls._create_helper() cls.helper.show_tool_by_name(tool_name, parent, *args, **kwargs) + @classmethod + def get_tool_by_name(cls, tool_name, parent=None, *args, **kwargs): + cls._create_helper() + return cls.helper.get_tool_by_name(tool_name, parent, *args, **kwargs) + # Function callbacks using singleton acces point +def get_tool_by_name(tool_name, parent=None, *args, **kwargs): + return _SingletonPoint.get_tool_by_name(tool_name, parent, *args, **kwargs) + + def show_tool_by_name(tool_name, parent=None, *args, **kwargs): _SingletonPoint.show_tool_by_name(tool_name, parent, *args, **kwargs) From 7f22b4fc5c75cd5a237bf7aaa66b1ad4bd315b5d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 12:09:39 +0200 Subject: [PATCH 607/716] houdini use host_tools --- .../hosts/houdini/startup/MainMenuCommon.xml | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index 76585085e2..2923cb9ef5 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -7,24 +7,30 @@ @@ -32,7 +38,7 @@ cbsceneinventory.show() @@ -43,9 +49,10 @@ publish.show(parent) From dd74ab89488a9369695bb528b906b0ffb68758f0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Oct 2021 14:16:22 +0200 Subject: [PATCH 608/716] hound: suggestions --- openpype/hosts/nuke/plugins/load/load_clip.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 265ab39b07..f8fc5e3928 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -13,8 +13,6 @@ from avalon.nuke import ( ) from openpype.hosts.nuke.api import plugin -reload(plugin) - class LoadClip(plugin.NukeLoader): """Load clip into Nuke @@ -144,7 +142,6 @@ class LoadClip(plugin.NukeLoader): self.set_range_to_node(read_node, first, last, start_at_workfile) - # add additional metadata from the version to imprint Avalon knob add_keys = ["frameStart", "frameEnd", "source", "colorspace", "author", "fps", "version", @@ -338,8 +335,10 @@ class LoadClip(plugin.NukeLoader): if time_warp_nodes != []: start_anim = self.script_start + (self.handle_start / speed) for timewarp in time_warp_nodes: - twn = nuke.createNode(timewarp["Class"], - "name {}".format(timewarp["name"])) + twn = nuke.createNode( + timewarp["Class"], + "name {}".format(timewarp["name"]) + ) if isinstance(timewarp["lookup"], list): # if array for animation twn["lookup"].setAnimated() From f91760cf1267e7b054f42feeb804c7e6b89b4765 Mon Sep 17 00:00:00 2001 From: karimmozlia Date: Fri, 15 Oct 2021 14:21:26 +0200 Subject: [PATCH 609/716] ValidateRigJointsHidden typo --- .../schemas/projects_schema/schemas/schema_maya_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bde8c15958..cbacd12efa 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 @@ -342,7 +342,7 @@ }, { "key": "ValidateRigJointsHidden", - "label": "Validate Rig JointsHidden" + "label": "Validate Rig Joints Hidden" }, { "key": "ValidateRigControllers", From e39ffb35c55f974d4fad5dc231f1ae716e41e0ff Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Oct 2021 14:23:00 +0200 Subject: [PATCH 610/716] updating avalon core --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 4b80f81e66..7e5efd6885 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 4b80f81e66aca593784be8b299110a0b6541276f +Subproject commit 7e5efd6885330d84bb8495975bcab84df49bfa3d From 3810a2f8a83073959e4876c7077ab48f143d4065 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 14:36:43 +0200 Subject: [PATCH 611/716] celaction is using host_tools --- openpype/hosts/celaction/api/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/celaction/api/cli.py b/openpype/hosts/celaction/api/cli.py index acd9da8229..bc1e3eaf89 100644 --- a/openpype/hosts/celaction/api/cli.py +++ b/openpype/hosts/celaction/api/cli.py @@ -4,7 +4,6 @@ import copy import argparse from avalon import io -from avalon.tools import publish import pyblish.api import pyblish.util @@ -13,6 +12,7 @@ from openpype.api import Logger import openpype import openpype.hosts.celaction from openpype.hosts.celaction import api as celaction +from openpype.tools.utils import host_tools log = Logger().get_logger("Celaction_cli_publisher") @@ -82,7 +82,7 @@ def main(): pyblish.api.register_host(publish_host) - return publish.show() + return host_tools.show_publish() if __name__ == "__main__": From cca7b4c13d0d810f14c7c094867a95e1e5cb7909 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 14:54:54 +0200 Subject: [PATCH 612/716] use host_tools in fusion --- openpype/hosts/fusion/api/__init__.py | 7 +------ openpype/hosts/fusion/api/menu.py | 30 ++++++++------------------- openpype/hosts/fusion/api/pipeline.py | 12 ----------- 3 files changed, 10 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index 5581a0a9cb..5aee978c15 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -1,8 +1,6 @@ from .pipeline import ( install, - uninstall, - publish, - launch_workfiles_app + uninstall ) from .utils import ( @@ -22,12 +20,9 @@ __all__ = [ # pipeline "install", "uninstall", - "publish", - "launch_workfiles_app", # utils "setup", - "get_resolve_module", # lib "get_additional_data", diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 9093aa9e5e..5d2efb4911 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -3,19 +3,7 @@ import sys from Qt import QtWidgets, QtCore -from .pipeline import ( - publish, - launch_workfiles_app -) - -from avalon.tools import ( - creator, - sceneinventory, -) -from openpype.tools import ( - loader, - libraryloader -) +from openpype.tools.utils import host_tools from openpype.hosts.fusion.scripts import ( set_rendermode, @@ -36,7 +24,7 @@ def load_stylesheet(): class Spacer(QtWidgets.QWidget): def __init__(self, height, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) + super(Spacer, self).__init__(*args, **kwargs) self.setFixedHeight(height) @@ -53,7 +41,7 @@ class Spacer(QtWidgets.QWidget): class OpenPypeMenu(QtWidgets.QWidget): def __init__(self, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) + super(OpenPypeMenu, self).__init__(*args, **kwargs) self.setObjectName("OpenPypeMenu") @@ -117,27 +105,27 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_workfile_clicked(self): print("Clicked Workfile") - launch_workfiles_app() + host_tools.show_workfiles() def on_create_clicked(self): print("Clicked Create") - creator.show() + host_tools.show_creator() def on_publish_clicked(self): print("Clicked Publish") - publish(None) + host_tools.show_publish() def on_load_clicked(self): print("Clicked Load") - loader.show(use_context=True) + host_tools.show_loader(use_context=True) def on_inventory_clicked(self): print("Clicked Inventory") - sceneinventory.show() + host_tools.show_scene_inventory() def on_libload_clicked(self): print("Clicked Library") - libraryloader.show() + host_tools.show_library_loader() def on_rendernode_clicked(self): from avalon import style diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 688e75f6fe..c721146830 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -3,7 +3,6 @@ Basic avalon integration """ import os -from openpype.tools import workfiles from avalon import api as avalon from pyblish import api as pyblish from openpype.api import Logger @@ -98,14 +97,3 @@ def on_pyblish_instance_toggled(instance, new_value, old_value): current = attrs["TOOLB_PassThrough"] if current != passthrough: tool.SetAttrs({"TOOLB_PassThrough": passthrough}) - - -def launch_workfiles_app(*args): - workdir = os.environ["AVALON_WORKDIR"] - workfiles.show(workdir) - - -def publish(parent): - """Shorthand to publish from within host""" - from avalon.tools import publish - return publish.show(parent) From 6621b2c36e6d897b35421a7005857cbf51df053f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 15 Oct 2021 15:00:44 +0200 Subject: [PATCH 613/716] OP-1063 - added validator for source files for Standalone Publisher --- .../plugins/publish/validate_sources.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py new file mode 100644 index 0000000000..da424cfb45 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py @@ -0,0 +1,37 @@ +import pyblish.api +import openpype.api + +import os + + +class ValidateSources(pyblish.api.InstancePlugin): + """Validates source files. + + Loops through all 'files' in 'stagingDir' if actually exist. They might + got deleted between starting of SP and now. + + """ + + order = openpype.api.ValidateContentsOrder + label = "Check source files" + + optional = True # only for unforeseeable cases + + hosts = ["standalonepublisher"] + + def process(self, instance): + self.log.info("instance {}".format(instance.data)) + + for repr in instance.data["representations"]: + files = [] + if isinstance(repr["files"], str): + files.append(repr["files"]) + else: + files = list(repr["files"]) + + for file_name in files: + source_file = os.path.join(repr["stagingDir"], + file_name) + + if not os.path.exists(source_file): + raise ValueError("File {} not found".format(source_file)) From ad1af83d5385a362f77c84eef6c84c4facee1b50 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 15:03:40 +0200 Subject: [PATCH 614/716] maya is using host_tools --- openpype/hosts/maya/api/__init__.py | 8 +- openpype/hosts/maya/api/customize.py | 151 ++++++++++++--------------- openpype/hosts/maya/api/menu.py | 6 +- 3 files changed, 68 insertions(+), 97 deletions(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 725a075417..e4eb44fc27 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -8,7 +8,7 @@ from avalon import api as avalon from avalon import pipeline from avalon.maya import suspended_refresh from avalon.maya.pipeline import IS_HEADLESS -from openpype.tools import workfiles +from openpype.tools.utils import host_tools from pyblish import api as pyblish from openpype.lib import any_outdated import openpype.hosts.maya @@ -208,16 +208,12 @@ def on_init(_): launch_workfiles = os.environ.get("WORKFILES_STARTUP") if launch_workfiles: - safe_deferred(launch_workfiles_app) + safe_deferred(host_tools.show_workfiles) if not IS_HEADLESS: safe_deferred(override_toolbox_ui) -def launch_workfiles_app(): - workfiles.show(os.environ["AVALON_WORKDIR"]) - - def on_before_save(return_code, _): """Run validation for scene's FPS prior to saving""" return lib.validate_fps() diff --git a/openpype/hosts/maya/api/customize.py b/openpype/hosts/maya/api/customize.py index a84412963b..54c058aa75 100644 --- a/openpype/hosts/maya/api/customize.py +++ b/openpype/hosts/maya/api/customize.py @@ -1,10 +1,16 @@ """A set of commands that install overrides to Maya's UI""" +import os +import logging + +from functools import partial + import maya.cmds as mc import maya.mel as mel -from functools import partial -import os -import logging + +from avalon.maya import pipeline +from openpype.api import resources +from openpype.tools.utils import host_tools log = logging.getLogger(__name__) @@ -69,39 +75,8 @@ def override_component_mask_commands(): def override_toolbox_ui(): """Add custom buttons in Toolbox as replacement for Maya web help icon.""" - inventory = None - loader = None - launch_workfiles_app = None - mayalookassigner = None - try: - import avalon.tools.sceneinventory as inventory - except Exception: - log.warning("Could not import SceneInventory tool") - - try: - import openpype.tools.loader as loader - except Exception: - log.warning("Could not import Loader tool") - - try: - from avalon.maya.pipeline import launch_workfiles_app - except Exception: - log.warning("Could not import Workfiles tool") - - try: - from openpype.tools import mayalookassigner - except Exception: - log.warning("Could not import Maya Look assigner tool") - - from openpype.api import resources - icons = resources.get_resource("icons") - if not any(( - mayalookassigner, launch_workfiles_app, loader, inventory - )): - return - # Ensure the maya web icon on toolbox exists web_button = "ToolBox|MainToolboxLayout|mayaWebButton" if not mc.iconTextButton(web_button, query=True, exists=True): @@ -120,65 +95,69 @@ def override_toolbox_ui(): # Create our controls background_color = (0.267, 0.267, 0.267) controls = [] - if mayalookassigner: - controls.append( - mc.iconTextButton( - "pype_toolbox_lookmanager", - annotation="Look Manager", - label="Look Manager", - image=os.path.join(icons, "lookmanager.png"), - command=lambda: mayalookassigner.show(), - bgc=background_color, - width=icon_size, - height=icon_size, - parent=parent - ) + controls.append( + mc.iconTextButton( + "pype_toolbox_lookmanager", + annotation="Look Manager", + label="Look Manager", + image=os.path.join(icons, "lookmanager.png"), + command=lambda: host_tools.show_look_assigner( + parent=pipeline._parent + ), + bgc=background_color, + width=icon_size, + height=icon_size, + parent=parent ) + ) - if launch_workfiles_app: - controls.append( - mc.iconTextButton( - "pype_toolbox_workfiles", - annotation="Work Files", - label="Work Files", - image=os.path.join(icons, "workfiles.png"), - command=lambda: launch_workfiles_app(), - bgc=background_color, - width=icon_size, - height=icon_size, - parent=parent - ) + controls.append( + mc.iconTextButton( + "pype_toolbox_workfiles", + annotation="Work Files", + label="Work Files", + image=os.path.join(icons, "workfiles.png"), + command=lambda: host_tools.show_workfiles( + parent=pipeline._parent + ), + bgc=background_color, + width=icon_size, + height=icon_size, + parent=parent ) + ) - if loader: - controls.append( - mc.iconTextButton( - "pype_toolbox_loader", - annotation="Loader", - label="Loader", - image=os.path.join(icons, "loader.png"), - command=lambda: loader.show(use_context=True), - bgc=background_color, - width=icon_size, - height=icon_size, - parent=parent - ) + controls.append( + mc.iconTextButton( + "pype_toolbox_loader", + annotation="Loader", + label="Loader", + image=os.path.join(icons, "loader.png"), + command=lambda: host_tools.show_loader( + parent=pipeline._parent, use_context=True + ), + bgc=background_color, + width=icon_size, + height=icon_size, + parent=parent ) + ) - if inventory: - controls.append( - mc.iconTextButton( - "pype_toolbox_manager", - annotation="Inventory", - label="Inventory", - image=os.path.join(icons, "inventory.png"), - command=lambda: inventory.show(), - bgc=background_color, - width=icon_size, - height=icon_size, - parent=parent - ) + controls.append( + mc.iconTextButton( + "pype_toolbox_manager", + annotation="Inventory", + label="Inventory", + image=os.path.join(icons, "inventory.png"), + command=lambda: host_tools.show_scene_inventory( + parent=pipeline._parent + ), + bgc=background_color, + width=icon_size, + height=icon_size, + parent=parent ) + ) # Add the buttons on the bottom and stack # them above each other with side padding diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 42cb76dca6..4f0966abfd 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -28,7 +28,6 @@ def _get_menu(menu_name=None): def deferred(): - def add_build_workfiles_item(): # Add build first workfile cmds.menuItem(divider=True, parent=pipeline._menu) @@ -48,9 +47,6 @@ def deferred(): ) def modify_workfiles(): - def launch_workfiles_app(*_args, **_kwargs): - host_tools.show_workfiles(pipeline._parent) - # Find the pipeline menu top_menu = _get_menu() @@ -71,7 +67,7 @@ def deferred(): cmds.menuItem( "Work Files", parent=pipeline._menu, - command=launch_workfiles_app, + command=lambda *args: host_tools.show_workfiles(pipeline._parent), insertAfter=after_action ) From 3356c21db245a14bd52af50825f32f75428bba4e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 15:06:00 +0200 Subject: [PATCH 615/716] cleanup harmony imports --- openpype/hosts/harmony/api/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/harmony/api/__init__.py b/openpype/hosts/harmony/api/__init__.py index fd21725bd5..bcf7dffbe7 100644 --- a/openpype/hosts/harmony/api/__init__.py +++ b/openpype/hosts/harmony/api/__init__.py @@ -3,17 +3,14 @@ import os from pathlib import Path import logging -import re from openpype import lib -from openpype.api import (get_current_project_settings) import openpype.hosts.harmony import pyblish.api from avalon import io, harmony import avalon.api -import avalon.tools.sceneinventory log = logging.getLogger("openpype.hosts.harmony") From 296b7305072032dd20e76a41d63b67dbf52494bb Mon Sep 17 00:00:00 2001 From: karimmozlia Date: Fri, 15 Oct 2021 15:26:17 +0200 Subject: [PATCH 616/716] Root.format instead of data.width --- openpype/hosts/nuke/api/lib.py | 21 ------------------- .../plugins/create/create_write_render.py | 6 ++---- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 87bf137d93..eb4eab2b22 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1031,27 +1031,6 @@ class WorkfileSettings(object): log.error(msg) nuke.message(msg) - bbox = self._asset_entity.get('data', {}).get('reformat') - - if bbox: - try: - x, y, r, t = bbox.split(".") - data.update( - { - "x": int(x), - "y": int(y), - "r": int(r), - "t": int(t), - } - ) - except Exception as e: - bbox = None - msg = ("{}:{} \nFormat:Reformat need to be set with dots, " - "example: 0.0.1920.1080, " - "/nSetting to default").format(__name__, e) - log.error(msg) - nuke.message(msg) - existing_format = None for format in nuke.formats(): if data["name"] == format.name(): diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index acc1ee53ff..5f13fddf4e 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -112,10 +112,8 @@ class CreateWriteRender(plugin.PypeCreator): "name": "Reformat01", "class": "Reformat", "knobs": [ - ("type", 1), - ("box_fixed", 1), - ("box_width", width), - ("box_height", height) + ("resize", 0), + ("black_outside", 1), ], "dependent": None } From 835a89e82a99cb6c8a313c93ec3c403e0081bd3c Mon Sep 17 00:00:00 2001 From: karimmozlia Date: Fri, 15 Oct 2021 16:03:14 +0200 Subject: [PATCH 617/716] del bbox --- openpype/hosts/nuke/api/lib.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index eb4eab2b22..6e46747d85 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1042,12 +1042,6 @@ class WorkfileSettings(object): existing_format.setWidth(data["width"]) existing_format.setHeight(data["height"]) existing_format.setPixelAspect(data["pixel_aspect"]) - - if bbox: - existing_format.setX(data["x"]) - existing_format.setY(data["y"]) - existing_format.setR(data["r"]) - existing_format.setT(data["t"]) else: format_string = self.make_format_string(**data) log.info("Creating new format: {}".format(format_string)) From 7615d2c7af4eb1ec150f60ba6bf571ad5c3c8f06 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 16:29:49 +0200 Subject: [PATCH 618/716] hiero is using host_tools --- openpype/hosts/hiero/api/menu.py | 9 ++++----- openpype/hosts/hiero/api/pipeline.py | 9 +++------ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/hiero/api/menu.py b/openpype/hosts/hiero/api/menu.py index bcd78aa5bb..e3de220777 100644 --- a/openpype/hosts/hiero/api/menu.py +++ b/openpype/hosts/hiero/api/menu.py @@ -2,6 +2,7 @@ import os import sys import hiero.core from openpype.api import Logger +from openpype.tools.utils import host_tools from avalon.api import Session from hiero.ui import findMenuAction @@ -41,8 +42,6 @@ def menu_install(): apply_colorspace_project, apply_colorspace_clips ) # here is the best place to add menu - from avalon.tools import creator, sceneinventory - from openpype.tools import loader from avalon.vendor.Qt import QtGui menu_name = os.environ['AVALON_LABEL'] @@ -87,15 +86,15 @@ def menu_install(): creator_action = menu.addAction("Create ...") creator_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png")) - creator_action.triggered.connect(creator.show) + creator_action.triggered.connect(host_tools.show_creator) loader_action = menu.addAction("Load ...") loader_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png")) - loader_action.triggered.connect(loader.show) + loader_action.triggered.connect(host_tools.show_loader) sceneinventory_action = menu.addAction("Manage ...") sceneinventory_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png")) - sceneinventory_action.triggered.connect(sceneinventory.show) + sceneinventory_action.triggered.connect(host_tools.show_scene_inventory) menu.addSeparator() if os.getenv("OPENPYPE_DEVELOP"): diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 12f6923de7..6f6588e1be 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -4,13 +4,12 @@ Basic avalon integration import os import contextlib from collections import OrderedDict -from avalon.tools import publish as _publish -from openpype.tools import workfiles from avalon.pipeline import AVALON_CONTAINER_ID from avalon import api as avalon from avalon import schema from pyblish import api as pyblish from openpype.api import Logger +from openpype.tools.utils import host_tools from . import lib, menu, events log = Logger().get_logger(__name__) @@ -211,15 +210,13 @@ def update_container(track_item, data=None): def launch_workfiles_app(*args): ''' Wrapping function for workfiles launcher ''' - workdir = os.environ["AVALON_WORKDIR"] - # show workfile gui - workfiles.show(workdir) + host_tools.show_workfiles() def publish(parent): """Shorthand to publish from within host""" - return _publish.show(parent) + return host_tools.show_publish(parent) @contextlib.contextmanager From 72a645bd74efd26b2f7ce78ef51e50d46bd73831 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 16:47:55 +0200 Subject: [PATCH 619/716] nuke is using host_tools --- openpype/hosts/nuke/api/lib.py | 5 ++--- openpype/hosts/nuke/api/menu.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 6fe9af744b..00ed4c6e78 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -7,7 +7,6 @@ from collections import OrderedDict from avalon import api, io, lib -from openpype.tools import workfiles import avalon.nuke from avalon.nuke import lib as anlib from avalon.nuke import ( @@ -24,7 +23,7 @@ from openpype.api import ( get_current_project_settings, ApplicationManager ) - +from openpype.tools.utils import host_tools import nuke from .utils import set_context_favorites @@ -1689,7 +1688,7 @@ def launch_workfiles_app(): if not opnl.workfiles_launched: opnl.workfiles_launched = True - workfiles.show(os.environ["AVALON_WORKDIR"]) + host_tools.show_workfiles() def process_workfile_builder(): diff --git a/openpype/hosts/nuke/api/menu.py b/openpype/hosts/nuke/api/menu.py index 021ea04159..87990c5e92 100644 --- a/openpype/hosts/nuke/api/menu.py +++ b/openpype/hosts/nuke/api/menu.py @@ -4,7 +4,7 @@ from avalon.api import Session from .lib import WorkfileSettings from openpype.api import Logger, BuildWorkfile, get_current_project_settings -from openpype.tools import workfiles +from openpype.tools.utils import host_tools log = Logger().get_logger(__name__) @@ -25,7 +25,7 @@ def install(): menu.removeItem(rm_item[1].name()) menu.addCommand( name, - workfiles.show, + host_tools.show_workfiles, index=2 ) menu.addSeparator(index=3) From 60f1c3cecd0f26758d69ccfa84e6601c0000b0a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 16:51:45 +0200 Subject: [PATCH 620/716] resolve is using host_tools --- openpype/hosts/resolve/api/menu.py | 24 ++++++++---------------- openpype/hosts/resolve/api/pipeline.py | 8 +++----- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index c639fd2db8..262ce739dd 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -8,15 +8,7 @@ from .pipeline import ( launch_workfiles_app ) -from avalon.tools import ( - creator, - sceneinventory, - subsetmanager -) -from openpype.tools import ( - loader, - libraryloader, -) +from openpype.tools.utils import host_tools def load_stylesheet(): @@ -32,7 +24,7 @@ def load_stylesheet(): class Spacer(QtWidgets.QWidget): def __init__(self, height, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) + super(Spacer, self).__init__(*args, **kwargs) self.setFixedHeight(height) @@ -49,7 +41,7 @@ class Spacer(QtWidgets.QWidget): class OpenPypeMenu(QtWidgets.QWidget): def __init__(self, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) + super(OpenPypeMenu, self).__init__(*args, **kwargs) self.setObjectName("OpenPypeMenu") @@ -119,7 +111,7 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_create_clicked(self): print("Clicked Create") - creator.show() + host_tools.show_creator() def on_publish_clicked(self): print("Clicked Publish") @@ -127,19 +119,19 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_load_clicked(self): print("Clicked Load") - loader.show(use_context=True) + host_tools.show_loader(use_context=True) def on_inventory_clicked(self): print("Clicked Inventory") - sceneinventory.show() + host_tools.show_scene_inventory() def on_subsetm_clicked(self): print("Clicked Subset Manager") - subsetmanager.show() + host_tools.show_subset_manager() def on_libload_clicked(self): print("Clicked Library") - libraryloader.show() + host_tools.show_library_loader() def on_rename_clicked(self): print("Clicked Rename") diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 80249310e8..ce95cfe02a 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -4,7 +4,6 @@ Basic avalon integration import os import contextlib from collections import OrderedDict -from openpype.tools import workfiles from avalon import api as avalon from avalon import schema from avalon.pipeline import AVALON_CONTAINER_ID @@ -12,6 +11,7 @@ from pyblish import api as pyblish from openpype.api import Logger from . import lib from . import PLUGINS_DIR +from openpype.tools.utils import host_tools log = Logger().get_logger(__name__) PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") @@ -212,14 +212,12 @@ def update_container(timeline_item, data=None): def launch_workfiles_app(*args): - workdir = os.environ["AVALON_WORKDIR"] - workfiles.show(workdir) + host_tools.show_workfiles() def publish(parent): """Shorthand to publish from within host""" - from avalon.tools import publish - return publish.show(parent) + return host_tools.show_publish() @contextlib.contextmanager From 9d67911bd2f2d08ab3fb1e22221dc9bc07920b85 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 17:30:14 +0200 Subject: [PATCH 621/716] removed unused import --- openpype/tools/utils/host_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 431009ac27..ee184ccf2d 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -4,7 +4,6 @@ It is possible to create `HostToolsHelper` in host implementaion or use singleton approach with global functions (using helper anyway). """ -from Qt import QtCore import avalon.api From 97ed09c059bc269c6f4a5fd0c2e6e3b8cf5d3e99 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 18:12:17 +0200 Subject: [PATCH 622/716] maya look assigner is not added if can't be initialized --- openpype/hosts/maya/api/customize.py | 36 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/api/customize.py b/openpype/hosts/maya/api/customize.py index 54c058aa75..2e10de1351 100644 --- a/openpype/hosts/maya/api/customize.py +++ b/openpype/hosts/maya/api/customize.py @@ -95,21 +95,29 @@ def override_toolbox_ui(): # Create our controls background_color = (0.267, 0.267, 0.267) controls = [] - controls.append( - mc.iconTextButton( - "pype_toolbox_lookmanager", - annotation="Look Manager", - label="Look Manager", - image=os.path.join(icons, "lookmanager.png"), - command=lambda: host_tools.show_look_assigner( - parent=pipeline._parent - ), - bgc=background_color, - width=icon_size, - height=icon_size, - parent=parent + look_assigner = None + try: + look_assigner = host_tools.get_tool_by_name( + "lookassigner", + parent=pipeline._parent + ) + except Exception as exc: + log.warning("Couldn't create Look assigner window.") + + if look_assigner is not None: + controls.append( + mc.iconTextButton( + "pype_toolbox_lookmanager", + annotation="Look Manager", + label="Look Manager", + image=os.path.join(icons, "lookmanager.png"), + command=host_tools.show_look_assigner, + bgc=background_color, + width=icon_size, + height=icon_size, + parent=parent + ) ) - ) controls.append( mc.iconTextButton( From 447add6b4a74273e17355856778ddae6a15346dd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 18:16:19 +0200 Subject: [PATCH 623/716] log traceback of lookassigner creation --- openpype/hosts/maya/api/customize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/customize.py b/openpype/hosts/maya/api/customize.py index 2e10de1351..8474262626 100644 --- a/openpype/hosts/maya/api/customize.py +++ b/openpype/hosts/maya/api/customize.py @@ -101,8 +101,8 @@ def override_toolbox_ui(): "lookassigner", parent=pipeline._parent ) - except Exception as exc: - log.warning("Couldn't create Look assigner window.") + except Exception: + log.warning("Couldn't create Look assigner window.", exc_info=True) if look_assigner is not None: controls.append( From 9eaa0a5a380550203e55aaed36f9ed96ecf2946e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 18:58:37 +0200 Subject: [PATCH 624/716] pass parenting of widget properly in main window --- openpype/tools/loader/app.py | 96 ++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index c18b6e798a..e7a7d2c7ad 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -54,61 +54,72 @@ class LoaderWindow(QtWidgets.QDialog): self.setWindowFlags(QtCore.Qt.Window) self.setFocusPolicy(QtCore.Qt.StrongFocus) - body = QtWidgets.QWidget() - footer = QtWidgets.QWidget() - footer.setFixedHeight(20) + split = QtWidgets.QSplitter(self) - container = QtWidgets.QWidget() + asset_filter_splitter = QtWidgets.QSplitter(split) + asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) - assets = AssetWidget(io, multiselection=True, parent=self) + # --- Left part --- + # Assets widget + assets = AssetWidget( + io, multiselection=True, parent=asset_filter_splitter + ) assets.set_current_asset_btn_visibility(True) - families = FamilyListView(io, self.family_config_cache, self) - subsets = SubsetWidget( - io, - self.groups_config, - self.family_config_cache, - tool_name=self.tool_name, - parent=self + # Families widget + families = FamilyListView( + io, self.family_config_cache, asset_filter_splitter ) - version = VersionWidget(io) - thumbnail = ThumbnailWidget(io) - representations = RepresentationWidget(io, self.tool_name) - - manager = ModulesManager() - sync_server = manager.modules_by_name["sync_server"] - - thumb_ver_splitter = QtWidgets.QSplitter() - thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) - thumb_ver_splitter.addWidget(thumbnail) - thumb_ver_splitter.addWidget(version) - if sync_server.enabled: - thumb_ver_splitter.addWidget(representations) - thumb_ver_splitter.setStretchFactor(0, 30) - thumb_ver_splitter.setStretchFactor(1, 35) - - # Create splitter to show / hide family filters - asset_filter_splitter = QtWidgets.QSplitter() - asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) asset_filter_splitter.addWidget(assets) asset_filter_splitter.addWidget(families) asset_filter_splitter.setStretchFactor(0, 65) asset_filter_splitter.setStretchFactor(1, 35) - container_layout = QtWidgets.QHBoxLayout(container) - container_layout.setContentsMargins(0, 0, 0, 0) - split = QtWidgets.QSplitter() + # --- Middle part --- + # Subsets widget + subsets = SubsetWidget( + io, + self.groups_config, + self.family_config_cache, + tool_name=self.tool_name, + parent=split + ) + + # --- Right part --- + thumb_ver_splitter = QtWidgets.QSplitter(split) + thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) + version = VersionWidget(io, parent=thumb_ver_splitter) + thumbnail = ThumbnailWidget(io, parent=thumb_ver_splitter) + + manager = ModulesManager() + sync_server = manager.modules_by_name.get("sync_server") + sync_server_enabled = False + if sync_server is not None: + sync_server_enabled = sync_server.enabled + + thumb_ver_splitter.addWidget(thumbnail) + thumb_ver_splitter.addWidget(version) + + representations = None + if sync_server_enabled: + representations = RepresentationWidget( + io, self.tool_name, parent=thumb_ver_splitter + ) + thumb_ver_splitter.addWidget(representations) + + thumb_ver_splitter.setStretchFactor(0, 30) + thumb_ver_splitter.setStretchFactor(1, 35) + split.addWidget(asset_filter_splitter) split.addWidget(subsets) split.addWidget(thumb_ver_splitter) - container_layout.addWidget(split) + # TODO keep footer size by message size + footer = QtWidgets.QWidget(self) + footer.setFixedHeight(20) - body_layout = QtWidgets.QHBoxLayout(body) - body_layout.addWidget(container) - body_layout.setContentsMargins(0, 0, 0, 0) - - message = QtWidgets.QLabel() + # TODO Don't hide messsage just set label to empty string + message = QtWidgets.QLabel(footer) message.hide() footer_layout = QtWidgets.QVBoxLayout(footer) @@ -116,7 +127,7 @@ class LoaderWindow(QtWidgets.QDialog): footer_layout.setContentsMargins(0, 0, 0, 0) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(body) + layout.addWidget(split) layout.addWidget(footer) self.data = { @@ -152,6 +163,7 @@ class LoaderWindow(QtWidgets.QDialog): representations.load_started.connect(self._on_load_start) representations.load_ended.connect(self._on_load_end) + # TODO add overlay using stack widget self._overlay_frame = overlay_frame self.family_config_cache.refresh() @@ -161,7 +173,7 @@ class LoaderWindow(QtWidgets.QDialog): self._assetschanged() # Defaults - if sync_server.enabled: + if sync_server_enabled: split.setSizes([250, 1000, 550]) self.resize(1800, 900) else: From 514e49ff239927128b298e054af8ed58328e60b3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 19:07:28 +0200 Subject: [PATCH 625/716] simplified message showing without scheduler --- openpype/tools/loader/app.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index e7a7d2c7ad..a7f66bdccb 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -37,6 +37,7 @@ class LoaderWindow(QtWidgets.QDialog): """Asset loader interface""" tool_name = "loader" + message_timeout = 5000 def __init__(self, parent=None): super(LoaderWindow, self).__init__(parent) @@ -118,12 +119,10 @@ class LoaderWindow(QtWidgets.QDialog): footer = QtWidgets.QWidget(self) footer.setFixedHeight(20) - # TODO Don't hide messsage just set label to empty string - message = QtWidgets.QLabel(footer) - message.hide() + message_label = QtWidgets.QLabel(footer) footer_layout = QtWidgets.QVBoxLayout(footer) - footer_layout.addWidget(message) + footer_layout.addWidget(message_label) footer_layout.setContentsMargins(0, 0, 0, 0) layout = QtWidgets.QVBoxLayout(self) @@ -139,9 +138,6 @@ class LoaderWindow(QtWidgets.QDialog): "thumbnail": thumbnail, "representations": representations }, - "label": { - "message": message, - }, "state": { "assetIds": None } @@ -150,6 +146,12 @@ class LoaderWindow(QtWidgets.QDialog): overlay_frame = OverlayFrame("Loading...", self) overlay_frame.setVisible(False) + message_timer = QtCore.QTimer() + message_timer.setInterval(self.message_timeout) + message_timer.setSingleShot(True) + + message_timer.timeout.connect(self._on_message_timeout) + families.active_changed.connect(subsets.set_family_filters) assets.selection_changed.connect(self.on_assetschanged) assets.refresh_triggered.connect(self.on_assetschanged) @@ -164,7 +166,10 @@ class LoaderWindow(QtWidgets.QDialog): representations.load_ended.connect(self._on_load_end) # TODO add overlay using stack widget + self._message_label = message_label + self._overlay_frame = overlay_frame + self._message_timer = message_timer self.family_config_cache.refresh() self.groups_config.refresh() @@ -450,13 +455,13 @@ class LoaderWindow(QtWidgets.QDialog): asset_widget = self.data["widgets"]["assets"] asset_widget.select_assets(asset) - def echo(self, message): - widget = self.data["label"]["message"] - widget.setText(str(message)) - widget.show() - print(message) + def _on_message_timeout(self): + self._message_label.setText("") - lib.schedule(widget.hide, 5000, channel="message") + def echo(self, message): + self._message_label.setText(str(message)) + print(message) + self._message_timer.start() def closeEvent(self, event): # Kill on holding SHIFT From 893ae73437d11dbce05428a9cdf6afa7507a706c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 19:13:51 +0200 Subject: [PATCH 626/716] fix repres widget --- openpype/tools/loader/app.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index a7f66bdccb..1b316c9223 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -101,12 +101,12 @@ class LoaderWindow(QtWidgets.QDialog): thumb_ver_splitter.addWidget(thumbnail) thumb_ver_splitter.addWidget(version) - representations = None + repres_widget = None if sync_server_enabled: - representations = RepresentationWidget( + repres_widget = RepresentationWidget( io, self.tool_name, parent=thumb_ver_splitter ) - thumb_ver_splitter.addWidget(representations) + thumb_ver_splitter.addWidget(repres_widget) thumb_ver_splitter.setStretchFactor(0, 30) thumb_ver_splitter.setStretchFactor(1, 35) @@ -136,7 +136,6 @@ class LoaderWindow(QtWidgets.QDialog): "subsets": subsets, "version": version, "thumbnail": thumbnail, - "representations": representations }, "state": { "assetIds": None @@ -162,12 +161,14 @@ class LoaderWindow(QtWidgets.QDialog): subsets.load_started.connect(self._on_load_start) subsets.load_ended.connect(self._on_load_end) - representations.load_started.connect(self._on_load_start) - representations.load_ended.connect(self._on_load_end) + repres_widget.load_started.connect(self._on_load_start) + repres_widget.load_ended.connect(self._on_load_end) # TODO add overlay using stack widget self._message_label = message_label + self._repres_widget = repres_widget + self._overlay_frame = overlay_frame self._message_timer = message_timer @@ -321,9 +322,9 @@ class LoaderWindow(QtWidgets.QDialog): self.data["state"]["assetIds"] = asset_ids - representations = self.data["widgets"]["representations"] # reset repre list - representations.set_version_ids([]) + if self._repres_widget is not None: + self._repres_widget.set_version_ids([]) def _subsetschanged(self): asset_ids = self.data["state"]["assetIds"] @@ -414,12 +415,14 @@ class LoaderWindow(QtWidgets.QDialog): self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs) - representations = self.data["widgets"]["representations"] - version_ids = [doc["_id"] for doc in version_docs or []] - representations.set_version_ids(version_ids) + if self._repres_widget is not None: + version_ids = [doc["_id"] for doc in version_docs or []] + self._repres_widget.set_version_ids(version_ids) - # representations.change_visibility("subset", len(rows) > 1) - # representations.change_visibility("asset", len(asset_docs) > 1) + # self._repres_widget.change_visibility("subset", len(rows) > 1) + # self._repres_widget.change_visibility( + # "asset", len(asset_docs) > 1 + # ) def _set_context(self, context, refresh=True): """Set the selection in the interface using a context. From 6d49ebf3a85ef169bedf764e59b5915f10436ac4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 19:16:16 +0200 Subject: [PATCH 627/716] change split to main_splitter --- openpype/tools/loader/app.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 1b316c9223..ea28304134 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -55,9 +55,9 @@ class LoaderWindow(QtWidgets.QDialog): self.setWindowFlags(QtCore.Qt.Window) self.setFocusPolicy(QtCore.Qt.StrongFocus) - split = QtWidgets.QSplitter(self) + main_splitter = QtWidgets.QSplitter(self) - asset_filter_splitter = QtWidgets.QSplitter(split) + asset_filter_splitter = QtWidgets.QSplitter(main_splitter) asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) # --- Left part --- @@ -83,11 +83,11 @@ class LoaderWindow(QtWidgets.QDialog): self.groups_config, self.family_config_cache, tool_name=self.tool_name, - parent=split + parent=main_splitter ) # --- Right part --- - thumb_ver_splitter = QtWidgets.QSplitter(split) + thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) version = VersionWidget(io, parent=thumb_ver_splitter) thumbnail = ThumbnailWidget(io, parent=thumb_ver_splitter) @@ -111,9 +111,9 @@ class LoaderWindow(QtWidgets.QDialog): thumb_ver_splitter.setStretchFactor(0, 30) thumb_ver_splitter.setStretchFactor(1, 35) - split.addWidget(asset_filter_splitter) - split.addWidget(subsets) - split.addWidget(thumb_ver_splitter) + main_splitter.addWidget(asset_filter_splitter) + main_splitter.addWidget(subsets) + main_splitter.addWidget(thumb_ver_splitter) # TODO keep footer size by message size footer = QtWidgets.QWidget(self) @@ -126,7 +126,7 @@ class LoaderWindow(QtWidgets.QDialog): footer_layout.setContentsMargins(0, 0, 0, 0) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(split) + layout.addWidget(main_splitter) layout.addWidget(footer) self.data = { @@ -180,10 +180,10 @@ class LoaderWindow(QtWidgets.QDialog): # Defaults if sync_server_enabled: - split.setSizes([250, 1000, 550]) + main_splitter.setSizes([250, 1000, 550]) self.resize(1800, 900) else: - split.setSizes([250, 850, 200]) + main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) def resizeEvent(self, event): From 98a6225e5ffbba710d829b95ee2dd3cb2ca0c1e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 15 Oct 2021 19:17:04 +0200 Subject: [PATCH 628/716] renamed 'footer' to 'footer_widget' --- openpype/tools/loader/app.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index ea28304134..9569c87e35 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -116,18 +116,18 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter.addWidget(thumb_ver_splitter) # TODO keep footer size by message size - footer = QtWidgets.QWidget(self) - footer.setFixedHeight(20) + footer_widget = QtWidgets.QWidget(self) + footer_widget.setFixedHeight(20) - message_label = QtWidgets.QLabel(footer) + message_label = QtWidgets.QLabel(footer_widget) - footer_layout = QtWidgets.QVBoxLayout(footer) - footer_layout.addWidget(message_label) + footer_layout = QtWidgets.QVBoxLayout(footer_widget) footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addWidget(message_label) layout = QtWidgets.QVBoxLayout(self) layout.addWidget(main_splitter) - layout.addWidget(footer) + layout.addWidget(footer_widget) self.data = { "widgets": { From 9f02356237614f15e96d7bebd715d31863137f27 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 16 Oct 2021 03:38:58 +0000 Subject: [PATCH 629/716] [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 7f92fdc9f5..fe06b590f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.5.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.5.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...HEAD) @@ -10,12 +10,16 @@ **πŸ†• New features** +- Added project and task into context change message in Maya [\#2131](https://github.com/pypeclub/OpenPype/pull/2131) +- PYPE-1218 - changed namespace to contain subset name in Maya [\#2114](https://github.com/pypeclub/OpenPype/pull/2114) - 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) **πŸš€ Enhancements** +- Settings: Updated readme for entity types in settings [\#2132](https://github.com/pypeclub/OpenPype/pull/2132) +- Nuke: unified clip loader [\#2128](https://github.com/pypeclub/OpenPype/pull/2128) - 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) @@ -31,6 +35,7 @@ **πŸ› Bug fixes** +- Fix - oiiotool wasn't recognized even if present [\#2129](https://github.com/pypeclub/OpenPype/pull/2129) - 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) @@ -66,12 +71,11 @@ - General: Startup validations [\#2054](https://github.com/pypeclub/OpenPype/pull/2054) - Nuke: proxy mode validator [\#2052](https://github.com/pypeclub/OpenPype/pull/2052) -- Settings UI: Deffered set value on entity [\#2044](https://github.com/pypeclub/OpenPype/pull/2044) +- Ftrack: Removed ftrack interface [\#2049](https://github.com/pypeclub/OpenPype/pull/2049) - 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) @@ -102,9 +106,10 @@ **πŸš€ Enhancements** -- 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) - 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) +- Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) - 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) @@ -112,9 +117,6 @@ - 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) **πŸ› Bug fixes** @@ -127,8 +129,6 @@ - Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006) - Nuke thumbnails generated from middle of the sequence [\#1992](https://github.com/pypeclub/OpenPype/pull/1992) - Nuke: last version from path gets correct version [\#1990](https://github.com/pypeclub/OpenPype/pull/1990) -- nuke, resolve, hiero: precollector order lest then 0.5 [\#1984](https://github.com/pypeclub/OpenPype/pull/1984) -- Last workfile with multiple work templates [\#1981](https://github.com/pypeclub/OpenPype/pull/1981) ## [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 3a589bac75..3bdad62e73 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.6" +__version__ = "3.5.0-nightly.7" From 81b271dd04020a0373c69d2046c8a65553984880 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sun, 17 Oct 2021 19:03:37 +0000 Subject: [PATCH 630/716] [Automated] Bump version --- CHANGELOG.md | 13 +++++++------ openpype/version.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe06b590f5..c011c000fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.5.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.5.0-nightly.8](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...HEAD) @@ -18,6 +18,7 @@ **πŸš€ Enhancements** +- Maya: make rig validators configurable in settings [\#2137](https://github.com/pypeclub/OpenPype/pull/2137) - Settings: Updated readme for entity types in settings [\#2132](https://github.com/pypeclub/OpenPype/pull/2132) - Nuke: unified clip loader [\#2128](https://github.com/pypeclub/OpenPype/pull/2128) - Settings UI: Project model refreshing and sorting [\#2104](https://github.com/pypeclub/OpenPype/pull/2104) @@ -50,10 +51,11 @@ - 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:** +- Maya: fix model publishing [\#2130](https://github.com/pypeclub/OpenPype/pull/2130) +- Add ExtractBurnin to photoshop review [\#2124](https://github.com/pypeclub/OpenPype/pull/2124) - 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) @@ -72,16 +74,19 @@ - General: Startup validations [\#2054](https://github.com/pypeclub/OpenPype/pull/2054) - Nuke: proxy mode validator [\#2052](https://github.com/pypeclub/OpenPype/pull/2052) - Ftrack: Removed ftrack interface [\#2049](https://github.com/pypeclub/OpenPype/pull/2049) +- Settings UI: Deffered set value on entity [\#2044](https://github.com/pypeclub/OpenPype/pull/2044) - Loader: Families filtering [\#2043](https://github.com/pypeclub/OpenPype/pull/2043) - Settings UI: Project view enhancements [\#2042](https://github.com/pypeclub/OpenPype/pull/2042) - Settings for Nuke IncrementScriptVersion [\#2039](https://github.com/pypeclub/OpenPype/pull/2039) - Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038) +- Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) - 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) **πŸ› Bug fixes** +- Fix Sync Queue when project disabled [\#2063](https://github.com/pypeclub/OpenPype/pull/2063) - Timers manger: Typo fix [\#2058](https://github.com/pypeclub/OpenPype/pull/2058) - Hiero: Editorial fixes [\#2057](https://github.com/pypeclub/OpenPype/pull/2057) - Differentiate jpg sequences from thumbnail [\#2056](https://github.com/pypeclub/OpenPype/pull/2056) @@ -106,10 +111,8 @@ **πŸš€ Enhancements** -- Settings UI: Deffered set value on entity [\#2044](https://github.com/pypeclub/OpenPype/pull/2044) - 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) -- Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030) - 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) @@ -127,8 +130,6 @@ - FFmpeg: Subprocess arguments as list [\#2032](https://github.com/pypeclub/OpenPype/pull/2032) - General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016) - Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006) -- Nuke thumbnails generated from middle of the sequence [\#1992](https://github.com/pypeclub/OpenPype/pull/1992) -- Nuke: last version from path gets correct version [\#1990](https://github.com/pypeclub/OpenPype/pull/1990) ## [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 3bdad62e73..5c8555c6a7 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.7" +__version__ = "3.5.0-nightly.8" From 8be1113edcb3e38333ce14842d530a57d2fbaaf6 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sun, 17 Oct 2021 19:33:06 +0000 Subject: [PATCH 631/716] [Automated] Release --- CHANGELOG.md | 24 ++++++++++++------------ openpype/version.py | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c011c000fd..95792f8a7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.5.0-nightly.8](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...3.5.0) **Deprecated:** @@ -11,6 +11,7 @@ **πŸ†• New features** - Added project and task into context change message in Maya [\#2131](https://github.com/pypeclub/OpenPype/pull/2131) +- Add ExtractBurnin to photoshop review [\#2124](https://github.com/pypeclub/OpenPype/pull/2124) - PYPE-1218 - changed namespace to contain subset name in Maya [\#2114](https://github.com/pypeclub/OpenPype/pull/2114) - 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) @@ -36,30 +37,30 @@ **πŸ› Bug fixes** +- Maya: fix model publishing [\#2130](https://github.com/pypeclub/OpenPype/pull/2130) - Fix - oiiotool wasn't recognized even if present [\#2129](https://github.com/pypeclub/OpenPype/pull/2129) - 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) +- Blender: Fix NoneType error when animation\_data is missing for a rig [\#2101](https://github.com/pypeclub/OpenPype/pull/2101) - 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) +- Delivery Action Files Sequence fix [\#2096](https://github.com/pypeclub/OpenPype/pull/2096) - 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) - 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) +- Nuke UI scaling [\#2077](https://github.com/pypeclub/OpenPype/pull/2077) - 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:** -- Maya: fix model publishing [\#2130](https://github.com/pypeclub/OpenPype/pull/2130) -- Add ExtractBurnin to photoshop review [\#2124](https://github.com/pypeclub/OpenPype/pull/2124) -- 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) ## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23) @@ -86,7 +87,6 @@ **πŸ› Bug fixes** -- Fix Sync Queue when project disabled [\#2063](https://github.com/pypeclub/OpenPype/pull/2063) - Timers manger: Typo fix [\#2058](https://github.com/pypeclub/OpenPype/pull/2058) - Hiero: Editorial fixes [\#2057](https://github.com/pypeclub/OpenPype/pull/2057) - Differentiate jpg sequences from thumbnail [\#2056](https://github.com/pypeclub/OpenPype/pull/2056) @@ -101,10 +101,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0) -### πŸ“– Documentation - -- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) - **πŸ†• New features** - Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) @@ -131,6 +127,10 @@ - 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) +### πŸ“– Documentation + +- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) + ## [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) diff --git a/openpype/version.py b/openpype/version.py index 5c8555c6a7..d88d79b995 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.8" +__version__ = "3.5.0" From e144e3d9fd8b1cad9ac6c18ae4faf3169c0a9d58 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:03:47 +0200 Subject: [PATCH 632/716] renamed version info widget and do not store it into data --- openpype/tools/loader/app.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 9569c87e35..0ec5a50f31 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -89,7 +89,8 @@ class LoaderWindow(QtWidgets.QDialog): # --- Right part --- thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) - version = VersionWidget(io, parent=thumb_ver_splitter) + + version_info_widget = VersionWidget(io, parent=thumb_ver_splitter) thumbnail = ThumbnailWidget(io, parent=thumb_ver_splitter) manager = ModulesManager() @@ -99,7 +100,7 @@ class LoaderWindow(QtWidgets.QDialog): sync_server_enabled = sync_server.enabled thumb_ver_splitter.addWidget(thumbnail) - thumb_ver_splitter.addWidget(version) + thumb_ver_splitter.addWidget(version_info_widget) repres_widget = None if sync_server_enabled: @@ -134,8 +135,7 @@ class LoaderWindow(QtWidgets.QDialog): "families": families, "assets": assets, "subsets": subsets, - "version": version, - "thumbnail": thumbnail, + "thumbnail": thumbnail }, "state": { "assetIds": None @@ -167,6 +167,7 @@ class LoaderWindow(QtWidgets.QDialog): # TODO add overlay using stack widget self._message_label = message_label + self._version_info_widget = version_info_widget self._repres_widget = repres_widget self._overlay_frame = overlay_frame @@ -317,7 +318,7 @@ class LoaderWindow(QtWidgets.QDialog): ) # Clear the version information on asset change - self.data["widgets"]["version"].set_version(None) + self._version_info_widget.set_version(None) self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs) self.data["state"]["assetIds"] = asset_ids @@ -404,7 +405,7 @@ class LoaderWindow(QtWidgets.QDialog): else: version_docs.append(item["version_document"]) - self.data["widgets"]["version"].set_version(version_doc) + self._version_info_widget.set_version(version_doc) thumbnail_docs = version_docs assets_widget = self.data["widgets"]["assets"] From c90cf1765c04719c306ef8894eb4f545a8cb7d32 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:06:05 +0200 Subject: [PATCH 633/716] renamed thumbnail widget and do not store it into data --- openpype/tools/loader/app.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 0ec5a50f31..43e3750428 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -90,8 +90,8 @@ class LoaderWindow(QtWidgets.QDialog): thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) + thumbnail_widget = ThumbnailWidget(io, parent=thumb_ver_splitter) version_info_widget = VersionWidget(io, parent=thumb_ver_splitter) - thumbnail = ThumbnailWidget(io, parent=thumb_ver_splitter) manager = ModulesManager() sync_server = manager.modules_by_name.get("sync_server") @@ -99,7 +99,7 @@ class LoaderWindow(QtWidgets.QDialog): if sync_server is not None: sync_server_enabled = sync_server.enabled - thumb_ver_splitter.addWidget(thumbnail) + thumb_ver_splitter.addWidget(thumbnail_widget) thumb_ver_splitter.addWidget(version_info_widget) repres_widget = None @@ -134,8 +134,7 @@ class LoaderWindow(QtWidgets.QDialog): "widgets": { "families": families, "assets": assets, - "subsets": subsets, - "thumbnail": thumbnail + "subsets": subsets }, "state": { "assetIds": None @@ -168,6 +167,7 @@ class LoaderWindow(QtWidgets.QDialog): self._message_label = message_label self._version_info_widget = version_info_widget + self._thumbnail_widget = thumbnail_widget self._repres_widget = repres_widget self._overlay_frame = overlay_frame @@ -318,8 +318,8 @@ class LoaderWindow(QtWidgets.QDialog): ) # Clear the version information on asset change + self._thumbnail_widget.set_thumbnail(asset_docs) self._version_info_widget.set_version(None) - self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs) self.data["state"]["assetIds"] = asset_ids @@ -414,7 +414,7 @@ class LoaderWindow(QtWidgets.QDialog): if len(asset_docs) > 0: thumbnail_docs = asset_docs - self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs) + self._thumbnail_widget.set_thumbnail(thumbnail_docs) if self._repres_widget is not None: version_ids = [doc["_id"] for doc in version_docs or []] From a85c93cc0fa599441e8eee0e3276029469be3a31 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:12:18 +0200 Subject: [PATCH 634/716] rename families filter view and do not store it into data --- openpype/tools/loader/app.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 43e3750428..f10348eced 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -68,11 +68,11 @@ class LoaderWindow(QtWidgets.QDialog): assets.set_current_asset_btn_visibility(True) # Families widget - families = FamilyListView( + families_filter_view = FamilyListView( io, self.family_config_cache, asset_filter_splitter ) asset_filter_splitter.addWidget(assets) - asset_filter_splitter.addWidget(families) + asset_filter_splitter.addWidget(families_filter_view) asset_filter_splitter.setStretchFactor(0, 65) asset_filter_splitter.setStretchFactor(1, 35) @@ -132,7 +132,6 @@ class LoaderWindow(QtWidgets.QDialog): self.data = { "widgets": { - "families": families, "assets": assets, "subsets": subsets }, @@ -150,7 +149,7 @@ class LoaderWindow(QtWidgets.QDialog): message_timer.timeout.connect(self._on_message_timeout) - families.active_changed.connect(subsets.set_family_filters) + families_filter_view.active_changed.connect(subsets.set_family_filters) assets.selection_changed.connect(self.on_assetschanged) assets.refresh_triggered.connect(self.on_assetschanged) assets.view.clicked.connect(self.on_assetview_click) @@ -163,15 +162,17 @@ class LoaderWindow(QtWidgets.QDialog): repres_widget.load_started.connect(self._on_load_start) repres_widget.load_ended.connect(self._on_load_end) - # TODO add overlay using stack widget self._message_label = message_label + self._message_timer = message_timer + + self._families_filter_view = families_filter_view self._version_info_widget = version_info_widget self._thumbnail_widget = thumbnail_widget self._repres_widget = repres_widget + # TODO add overlay using stack widget self._overlay_frame = overlay_frame - self._message_timer = message_timer self.family_config_cache.refresh() self.groups_config.refresh() @@ -236,11 +237,10 @@ class LoaderWindow(QtWidgets.QDialog): def _on_subset_refresh(self, has_item): subsets_widget = self.data["widgets"]["subsets"] - families_view = self.data["widgets"]["families"] subsets_widget.set_loading_state(loading=False, empty=not has_item) families = subsets_widget.get_subsets_families() - families_view.set_enabled_families(families) + self._families_filter_view.set_enabled_families(families) def _on_load_end(self): # Delay hiding as click events happened during loading should be @@ -251,9 +251,8 @@ class LoaderWindow(QtWidgets.QDialog): def on_context_task_change(self, *args, **kwargs): assets_widget = self.data["widgets"]["assets"] - families_view = self.data["widgets"]["families"] # Refresh families config - families_view.refresh() + self._families_filter_view.refresh() # Change to context asset on context change assets_widget.select_assets(io.Session["AVALON_ASSET"]) @@ -268,8 +267,7 @@ class LoaderWindow(QtWidgets.QDialog): assets_widget.refresh() assets_widget.setFocus() - families_view = self.data["widgets"]["families"] - families_view.refresh() + self._families_filter_view.refresh() def clear_assets_underlines(self): """Clear colors from asset data to remove colored underlines From 6947c68bfe63014ab17a3e622e170ef27a949d81 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:13:27 +0200 Subject: [PATCH 635/716] renamed 'asset_filter_splitter' to 'left_side_splitter' --- openpype/tools/loader/app.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index f10348eced..f81f54cc23 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -57,24 +57,24 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter = QtWidgets.QSplitter(self) - asset_filter_splitter = QtWidgets.QSplitter(main_splitter) - asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) - # --- Left part --- + left_side_splitter = QtWidgets.QSplitter(main_splitter) + left_side_splitter.setOrientation(QtCore.Qt.Vertical) + # Assets widget assets = AssetWidget( - io, multiselection=True, parent=asset_filter_splitter + io, multiselection=True, parent=left_side_splitter ) assets.set_current_asset_btn_visibility(True) # Families widget families_filter_view = FamilyListView( - io, self.family_config_cache, asset_filter_splitter + io, self.family_config_cache, left_side_splitter ) - asset_filter_splitter.addWidget(assets) - asset_filter_splitter.addWidget(families_filter_view) - asset_filter_splitter.setStretchFactor(0, 65) - asset_filter_splitter.setStretchFactor(1, 35) + left_side_splitter.addWidget(assets) + left_side_splitter.addWidget(families_filter_view) + left_side_splitter.setStretchFactor(0, 65) + left_side_splitter.setStretchFactor(1, 35) # --- Middle part --- # Subsets widget @@ -112,7 +112,7 @@ class LoaderWindow(QtWidgets.QDialog): thumb_ver_splitter.setStretchFactor(0, 30) thumb_ver_splitter.setStretchFactor(1, 35) - main_splitter.addWidget(asset_filter_splitter) + main_splitter.addWidget(left_side_splitter) main_splitter.addWidget(subsets) main_splitter.addWidget(thumb_ver_splitter) From 658c4a1b6f48fdb0cc96373032815bcd559432e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:21:44 +0200 Subject: [PATCH 636/716] rename asset widget and do not store it to data --- openpype/tools/loader/app.py | 45 +++++++++++++++++------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index f81f54cc23..3f3f03867f 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -62,16 +62,16 @@ class LoaderWindow(QtWidgets.QDialog): left_side_splitter.setOrientation(QtCore.Qt.Vertical) # Assets widget - assets = AssetWidget( + assets_widget = AssetWidget( io, multiselection=True, parent=left_side_splitter ) - assets.set_current_asset_btn_visibility(True) + assets_widget.set_current_asset_btn_visibility(True) # Families widget families_filter_view = FamilyListView( io, self.family_config_cache, left_side_splitter ) - left_side_splitter.addWidget(assets) + left_side_splitter.addWidget(assets_widget) left_side_splitter.addWidget(families_filter_view) left_side_splitter.setStretchFactor(0, 65) left_side_splitter.setStretchFactor(1, 35) @@ -132,7 +132,6 @@ class LoaderWindow(QtWidgets.QDialog): self.data = { "widgets": { - "assets": assets, "subsets": subsets }, "state": { @@ -150,9 +149,9 @@ class LoaderWindow(QtWidgets.QDialog): message_timer.timeout.connect(self._on_message_timeout) families_filter_view.active_changed.connect(subsets.set_family_filters) - assets.selection_changed.connect(self.on_assetschanged) - assets.refresh_triggered.connect(self.on_assetschanged) - assets.view.clicked.connect(self.on_assetview_click) + assets_widget.selection_changed.connect(self.on_assetschanged) + assets_widget.refresh_triggered.connect(self.on_assetschanged) + assets_widget.view.clicked.connect(self.on_assetview_click) subsets.active_changed.connect(self.on_subsetschanged) subsets.version_changed.connect(self.on_versionschanged) subsets.refreshed.connect(self._on_subset_refresh) @@ -162,15 +161,16 @@ class LoaderWindow(QtWidgets.QDialog): repres_widget.load_started.connect(self._on_load_start) repres_widget.load_ended.connect(self._on_load_end) - self._message_label = message_label - self._message_timer = message_timer - + self._assets_widget = assets_widget self._families_filter_view = families_filter_view self._version_info_widget = version_info_widget self._thumbnail_widget = thumbnail_widget self._repres_widget = repres_widget + self._message_label = message_label + self._message_timer = message_timer + # TODO add overlay using stack widget self._overlay_frame = overlay_frame @@ -250,11 +250,10 @@ class LoaderWindow(QtWidgets.QDialog): # ------------------------------ def on_context_task_change(self, *args, **kwargs): - assets_widget = self.data["widgets"]["assets"] # Refresh families config self._families_filter_view.refresh() # Change to context asset on context change - assets_widget.select_assets(io.Session["AVALON_ASSET"]) + self._assets_widget.select_assets(io.Session["AVALON_ASSET"]) def _refresh(self): """Load assets from database""" @@ -263,9 +262,8 @@ class LoaderWindow(QtWidgets.QDialog): project = io.find_one({"type": "project"}, {"type": 1}) assert project, "Project was not found! This is a bug" - assets_widget = self.data["widgets"]["assets"] - assets_widget.refresh() - assets_widget.setFocus() + self._assets_widget.refresh() + self._assets_widget.setFocus() self._families_filter_view.refresh() @@ -275,11 +273,12 @@ class LoaderWindow(QtWidgets.QDialog): own selected subsets. These colors must be cleared from asset data on selection change so they match current selection. """ - last_asset_ids = self.data["state"]["assetIds"] + # TODO do not touch inner attributes of asset widget + last_asset_ids = self.data["state"]["assetIds"] or [] if not last_asset_ids: return - assets_widget = self.data["widgets"]["assets"] + assets_widget = self._assets_widget id_role = assets_widget.model.ObjectIdRole for index in lib.iter_model_rows(assets_widget.model, 0): @@ -292,7 +291,6 @@ class LoaderWindow(QtWidgets.QDialog): def _assetschanged(self): """Selected assets have changed""" - assets_widget = self.data["widgets"]["assets"] subsets_widget = self.data["widgets"]["subsets"] subsets_model = subsets_widget.model @@ -300,7 +298,7 @@ class LoaderWindow(QtWidgets.QDialog): self.clear_assets_underlines() # filter None docs they are silo - asset_docs = assets_widget.get_selected_assets() + asset_docs = self._assets_widget.get_selected_assets() asset_ids = [asset_doc["_id"] for asset_doc in asset_docs] # Start loading @@ -354,7 +352,8 @@ class LoaderWindow(QtWidgets.QDialog): self.clear_assets_underlines() - assets_widget = self.data["widgets"]["assets"] + # TODO do not use inner attributes of asset widget + assets_widget = self._assets_widget indexes = assets_widget.view.selectionModel().selectedRows() for index in indexes: @@ -406,8 +405,7 @@ class LoaderWindow(QtWidgets.QDialog): self._version_info_widget.set_version(version_doc) thumbnail_docs = version_docs - assets_widget = self.data["widgets"]["assets"] - asset_docs = assets_widget.get_selected_assets() + asset_docs = self._assets_widget.get_selected_assets() if not thumbnail_docs: if len(asset_docs) > 0: thumbnail_docs = asset_docs @@ -454,8 +452,7 @@ class LoaderWindow(QtWidgets.QDialog): # scheduled refresh and the silo tabs are not shown. self._refresh() - asset_widget = self.data["widgets"]["assets"] - asset_widget.select_assets(asset) + self._assets_widget.select_assets(asset) def _on_message_timeout(self): self._message_label.setText("") From 584fd49e7b02f5bc74c7173c9b1cce74a0f0765a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 10:34:13 +0200 Subject: [PATCH 637/716] renamed subset widget and do not store it to data --- openpype/tools/loader/app.py | 46 ++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 3f3f03867f..a4b4b5eb28 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -78,7 +78,7 @@ class LoaderWindow(QtWidgets.QDialog): # --- Middle part --- # Subsets widget - subsets = SubsetWidget( + subsets_widget = SubsetWidget( io, self.groups_config, self.family_config_cache, @@ -113,7 +113,7 @@ class LoaderWindow(QtWidgets.QDialog): thumb_ver_splitter.setStretchFactor(1, 35) main_splitter.addWidget(left_side_splitter) - main_splitter.addWidget(subsets) + main_splitter.addWidget(subsets_widget) main_splitter.addWidget(thumb_ver_splitter) # TODO keep footer size by message size @@ -148,22 +148,27 @@ class LoaderWindow(QtWidgets.QDialog): message_timer.timeout.connect(self._on_message_timeout) - families_filter_view.active_changed.connect(subsets.set_family_filters) + families_filter_view.active_changed.connect( + self._on_family_filter_change + ) assets_widget.selection_changed.connect(self.on_assetschanged) assets_widget.refresh_triggered.connect(self.on_assetschanged) + # TODO do not touch view in asset widget assets_widget.view.clicked.connect(self.on_assetview_click) - subsets.active_changed.connect(self.on_subsetschanged) - subsets.version_changed.connect(self.on_versionschanged) - subsets.refreshed.connect(self._on_subset_refresh) + subsets_widget.active_changed.connect(self.on_subsetschanged) + subsets_widget.version_changed.connect(self.on_versionschanged) + subsets_widget.refreshed.connect(self._on_subset_refresh) - subsets.load_started.connect(self._on_load_start) - subsets.load_ended.connect(self._on_load_end) + subsets_widget.load_started.connect(self._on_load_start) + subsets_widget.load_ended.connect(self._on_load_end) repres_widget.load_started.connect(self._on_load_start) repres_widget.load_ended.connect(self._on_load_end) self._assets_widget = assets_widget self._families_filter_view = families_filter_view + self._subsets_widget = subsets_widget + self._version_info_widget = version_info_widget self._thumbnail_widget = thumbnail_widget self._repres_widget = repres_widget @@ -236,10 +241,10 @@ class LoaderWindow(QtWidgets.QDialog): self._overlay_frame.setVisible(False) def _on_subset_refresh(self, has_item): - subsets_widget = self.data["widgets"]["subsets"] - - subsets_widget.set_loading_state(loading=False, empty=not has_item) - families = subsets_widget.get_subsets_families() + self._subsets_widget.set_loading_state( + loading=False, empty=not has_item + ) + families = self._subsets_widget.get_subsets_families() self._families_filter_view.set_enabled_families(families) def _on_load_end(self): @@ -248,6 +253,8 @@ class LoaderWindow(QtWidgets.QDialog): QtCore.QTimer.singleShot(100, self._hide_overlay) # ------------------------------ + def _on_family_filter_change(self, families): + self._subsets_widget.set_family_filters(families) def on_context_task_change(self, *args, **kwargs): # Refresh families config @@ -291,7 +298,8 @@ class LoaderWindow(QtWidgets.QDialog): def _assetschanged(self): """Selected assets have changed""" - subsets_widget = self.data["widgets"]["subsets"] + subsets_widget = self._subsets_widget + # TODO do not touch subset widget inner attributes subsets_model = subsets_widget.model subsets_model.clear() @@ -330,8 +338,9 @@ class LoaderWindow(QtWidgets.QDialog): self._versionschanged() return - subsets = self.data["widgets"]["subsets"] - selected_subsets = subsets.selected_subsets(_merged=True, _other=False) + selected_subsets = self._subsets_widget.selected_subsets( + _merged=True, _other=False + ) asset_models = {} asset_ids = [] @@ -370,7 +379,7 @@ class LoaderWindow(QtWidgets.QDialog): self._versionschanged() def _versionschanged(self): - subsets = self.data["widgets"]["subsets"] + subsets = self._subsets_widget selection = subsets.view.selectionModel() # Active must be in the selected rows otherwise we @@ -488,7 +497,7 @@ class LoaderWindow(QtWidgets.QDialog): event.setAccepted(True) # Avoid interfering other widgets def show_grouping_dialog(self): - subsets = self.data["widgets"]["subsets"] + subsets = self._subsets_widget if not subsets.is_groupable(): self.echo("Grouping not enabled.") return @@ -527,7 +536,8 @@ class SubsetGroupingDialog(QtWidgets.QDialog): self.items = items self.groups_config = groups_config - self.subsets = parent.data["widgets"]["subsets"] + # TODO do not touch inner attributes + self.subsets = parent._subsets_widget self.asset_ids = parent.data["state"]["assetIds"] name = QtWidgets.QLineEdit() From c2e22ba7d669421aee76e690c52a85f3e16b40cd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 13:15:38 +0200 Subject: [PATCH 638/716] Use dnxhd profile from input metadata --- openpype/scripts/otio_burnin.py | 34 ++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 184d7689e3..206abfc0b4 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -109,9 +109,7 @@ def _prores_codec_args(ffprobe_data, source_ffmpeg_cmd): def _h264_codec_args(ffprobe_data, source_ffmpeg_cmd): - output = [] - - output.extend(["-codec:v", "h264"]) + output = ["-codec:v", "h264"] # Use arguments from source if are available source arguments if source_ffmpeg_cmd: @@ -137,6 +135,32 @@ def _h264_codec_args(ffprobe_data, source_ffmpeg_cmd): return output +def _dnxhd_codec_args(ffprobe_data, source_ffmpeg_cmd): + output = ["-codec:v", "dnxhd"] + + # Use source profile (profiles in metadata are not usable in args directly) + profile = ffprobe_data.get("profile") or "" + # Lower profile and replace space with underscore + cleaned_profile = profile.lower().replace(" ", "_") + dnx_profiles = { + "dnxhd", + "dnxhr_lb", + "dnxhr_sq", + "dnxhr_hq", + "dnxhr_hqx", + "dnxhr_444" + } + if cleaned_profile in dnx_profiles: + output.extend(["-profile:v", cleaned_profile]) + + pix_fmt = ffprobe_data.get("pix_fmt") + if pix_fmt: + output.extend(["-pix_fmt", pix_fmt]) + + output.extend(["-g", "1"]) + return output + + def get_codec_args(ffprobe_data, source_ffmpeg_cmd): codec_name = ffprobe_data.get("codec_name") # Codec "prores" @@ -147,6 +171,10 @@ def get_codec_args(ffprobe_data, source_ffmpeg_cmd): if codec_name == "h264": return _h264_codec_args(ffprobe_data, source_ffmpeg_cmd) + # Coded DNxHD + if codec_name == "dnxhd": + return _dnxhd_codec_args(ffprobe_data, source_ffmpeg_cmd) + output = [] if codec_name: output.extend(["-codec:v", codec_name]) From ec6210a826e6e390a8349d1dda885e8008b0edac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 18 Oct 2021 17:42:57 +0200 Subject: [PATCH 639/716] fix publish callback code in houdini --- openpype/hosts/houdini/startup/MainMenuCommon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index 2923cb9ef5..2b556a2e75 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -40,7 +40,7 @@ host_tools.show_scene_inventory(parent) import hou from openpype.tools.utils import host_tools parent = hou.qt.mainWindow() -publish.show(parent) +host_tools.show_publish(parent) ]]> From 9ed805f1a652db2feee2554ba4d31e2f63c36695 Mon Sep 17 00:00:00 2001 From: karimmozlia Date: Tue, 19 Oct 2021 12:24:50 +0200 Subject: [PATCH 640/716] update --- openpype/hosts/nuke/api/lib.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index eb4eab2b22..6e46747d85 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1042,12 +1042,6 @@ class WorkfileSettings(object): existing_format.setWidth(data["width"]) existing_format.setHeight(data["height"]) existing_format.setPixelAspect(data["pixel_aspect"]) - - if bbox: - existing_format.setX(data["x"]) - existing_format.setY(data["y"]) - existing_format.setR(data["r"]) - existing_format.setT(data["t"]) else: format_string = self.make_format_string(**data) log.info("Creating new format: {}".format(format_string)) From 5b60f07f1373a416168dfbc9ccb3bafbb2e27f79 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 19 Oct 2021 12:35:37 +0200 Subject: [PATCH 641/716] add smooth transformation to loader thumbnail widget --- openpype/tools/loader/widgets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 6b94fc6e44..1ccbb5796d 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -786,7 +786,10 @@ class ThumbnailWidget(QtWidgets.QLabel): def scale_pixmap(self, pixmap): return pixmap.scaled( - self.width(), self.height(), QtCore.Qt.KeepAspectRatio + self.width(), + self.height(), + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation ) def set_thumbnail(self, entity=None): From 2d687e5e6a923c4f34fac241f074867f6d85580b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 19 Oct 2021 15:18:41 +0200 Subject: [PATCH 642/716] fix maya hotbox --- openpype/hosts/maya/api/menu.py | 66 ++++++++++++++++----------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index ad225dcd28..18d3e1e896 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -19,10 +19,8 @@ def _get_menu(menu_name=None): if menu_name is None: menu_name = pipeline._menu - widgets = dict(( - w.objectName(), w) for w in QtWidgets.QApplication.allWidgets()) - menu = widgets.get(menu_name) - return menu + widgets = {w.objectName(): w for w in QtWidgets.QApplication.allWidgets()} + return widgets.get(menu_name) def deferred(): @@ -43,6 +41,34 @@ def deferred(): command=lambda *args: mayalookassigner.show() ) + def add_scripts_menu(): + try: + import scriptsmenu.launchformaya as launchformaya + except ImportError: + log.warning( + "Skipping studio.menu install, because " + "'scriptsmenu' module seems unavailable." + ) + return + + # load configuration of custom menu + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + config = project_settings["maya"]["scriptsmenu"]["definition"] + _menu = project_settings["maya"]["scriptsmenu"]["name"] + + if not config: + log.warning("Skipping studio menu, no definition found.") + return + + # run the launcher for Maya menu + studio_menu = launchformaya.main( + title=_menu.title(), + objectName=_menu.title().lower().replace(" ", "_") + ) + + # apply configuration + studio_menu.build_from_configuration(studio_menu, config) + def modify_workfiles(): from openpype.tools import workfiles @@ -109,38 +135,12 @@ def deferred(): log.info("Attempting to install scripts menu ...") + # add_scripts_menu() add_build_workfiles_item() add_look_assigner_item() modify_workfiles() remove_project_manager() - - try: - import scriptsmenu.launchformaya as launchformaya - import scriptsmenu.scriptsmenu as scriptsmenu - except ImportError: - log.warning( - "Skipping studio.menu install, because " - "'scriptsmenu' module seems unavailable." - ) - return - - # load configuration of custom menu - project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) - config = project_settings["maya"]["scriptsmenu"]["definition"] - _menu = project_settings["maya"]["scriptsmenu"]["name"] - - if not config: - log.warning("Skipping studio menu, no definition found.") - return - - # run the launcher for Maya menu - studio_menu = launchformaya.main( - title=_menu.title(), - objectName=_menu.title().lower().replace(" ", "_") - ) - - # apply configuration - studio_menu.build_from_configuration(studio_menu, config) + add_scripts_menu() def uninstall(): @@ -161,7 +161,7 @@ def install(): return # Allow time for uninstallation to finish. - cmds.evalDeferred(deferred) + cmds.evalDeferred(deferred, lowestPriority=True) def popup(): From 71d79a2dc91f707bf806f864367de92e54e3cd87 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 15:25:16 +0200 Subject: [PATCH 643/716] ignore save warnings exception in prepare project --- .../event_handlers_server/action_prepare_project.py | 9 +++++++-- .../ftrack/event_handlers_user/action_prepare_project.py | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_prepare_project.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_prepare_project.py index 85317031b2..2e55be2743 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_prepare_project.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_prepare_project.py @@ -3,6 +3,7 @@ import json from avalon.api import AvalonMongoDB from openpype.api import ProjectSettings from openpype.lib import create_project +from openpype.settings import SaveWarningExc from openpype_modules.ftrack.lib import ( ServerAction, @@ -312,7 +313,6 @@ class PrepareProjectServer(ServerAction): if not in_data: return - root_values = {} root_key = "__root__" for key in tuple(in_data.keys()): @@ -392,7 +392,12 @@ class PrepareProjectServer(ServerAction): else: attributes_entity[key] = value - project_settings.save() + try: + project_settings.save() + except SaveWarningExc as exc: + self.log.info("Few warnings happened during settings save:") + for warning in exc.warnings: + self.log.info(str(warning)) # Change custom attributes on project if custom_attribute_values: diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_prepare_project.py index 87d3329179..3759bc81ac 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_prepare_project.py @@ -3,6 +3,7 @@ import json from avalon.api import AvalonMongoDB from openpype.api import ProjectSettings from openpype.lib import create_project +from openpype.settings import SaveWarningExc from openpype_modules.ftrack.lib import ( BaseAction, @@ -417,7 +418,12 @@ class PrepareProjectLocal(BaseAction): else: attributes_entity[key] = value - project_settings.save() + try: + project_settings.save() + except SaveWarningExc as exc: + self.log.info("Few warnings happened during settings save:") + for warning in exc.warnings: + self.log.info(str(warning)) # Change custom attributes on project if custom_attribute_values: From 9f35dd7763322a3c488cd827f372bae51080d800 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:31:23 +0200 Subject: [PATCH 644/716] added basic of experimental tool definitions --- openpype/tools/experimental_tools/__init__.py | 9 ++ .../tools/experimental_tools/tools_def.py | 82 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 openpype/tools/experimental_tools/__init__.py create mode 100644 openpype/tools/experimental_tools/tools_def.py diff --git a/openpype/tools/experimental_tools/__init__.py b/openpype/tools/experimental_tools/__init__.py new file mode 100644 index 0000000000..d61c560886 --- /dev/null +++ b/openpype/tools/experimental_tools/__init__.py @@ -0,0 +1,9 @@ +from .tools_def import ( + ExperimentalTools, + LOCAL_EXPERIMENTAL_KEY +) + +__all__ = ( + "ExperimentalTools", + "LOCAL_EXPERIMENTAL_KEY" +) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py new file mode 100644 index 0000000000..ec4815c741 --- /dev/null +++ b/openpype/tools/experimental_tools/tools_def.py @@ -0,0 +1,82 @@ +from openpype.settings import get_local_settings + +# Constant key under which local settings are stored +LOCAL_EXPERIMENTAL_KEY = "experimental_tools" + + +class ExperimentalTool: + """Definition of experimental tool. + + Args: + identifier (str): String identifier of tool (unique). + label (str): Label shown in UI. + callback (function): Callback for UI button. + tooltip (str): Tooltip showed on button. + hosts_filter (list): List of host names for which is tool available. + Some tools may not be available in all hosts. + """ + def __init__(self, identifier, label, callback, tooltip, hosts_filter=None): + self.identifier = identifier + self.label = label + self.callback = callback + self.tooltip = tooltip + self.hosts_filter = hosts_filter + self._enabled = True + + def is_available_for_host(self, host_name): + if self.hosts_filter: + return host_name in self.hosts_filter + return True + + @property + def enabled(self): + """Is tool enabled and button is clickable.""" + return self._enabled + + def set_enabled(self, enabled=True): + """Change if tool is enabled.""" + self._enabled = enabled + + def execute(self): + """Trigger registerd callback.""" + self.callback() + + +class ExperimentalTools: + """Wrapper around experimental tools. + + To add/remove experimental tool just add/remove tool to + `experimental_tools` variable in __init__ function. + + """ + def __init__(self, parent=None, host_name=None, filter_hosts=None): + experimental_tools = [] + if filter_hosts is None: + filter_hosts = host_name is not None + + if filter_hosts and not host_name: + filter_hosts = False + + if filter_hosts: + experimental_tools = [ + tool + for tool in experimental_tools + if tool.is_available_for_host(host_name) + ] + + self.tools_by_identifier = { + tool.identifier: tool + for tool in experimental_tools + } + self.experimental_tools = experimental_tools + self._parent_widget = parent + + def refresh_availability(self): + local_settings = get_local_settings() + experimental_settings = ( + local_settings.get(LOCAL_EXPERIMENTAL_KEY) + ) or {} + + for identifier, eperimental_tool in self.tools_by_identifier.items(): + enabled = experimental_settings.get(identifier, False) + eperimental_tool.set_enabled(enabled) From e1e2f8e9ddf60fe8b1a48a21bce67e7e2d47a3d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:33:07 +0200 Subject: [PATCH 645/716] base of dialog for experimental tools --- openpype/tools/experimental_tools/dialog.py | 146 ++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 openpype/tools/experimental_tools/dialog.py diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py new file mode 100644 index 0000000000..db868c572f --- /dev/null +++ b/openpype/tools/experimental_tools/dialog.py @@ -0,0 +1,146 @@ +from Qt import QtWidgets + +from openpype.style import ( + load_stylesheet, + app_icon_path +) + +from .tools_def import ExperimentalTools + + +class ToolButton(QtWidgets.QPushButton): + triggered = QtCore.Signal(str) + + def __init__(self, identifier, *args, **kwargs): + super(ExperimentalDialog, self).__init__(*args, **kwargs) + self._identifier = identifier + + self.clicked.connect(self._on_click) + + def _on_click(self): + self.triggered.emit(self._identifier) + + +class ExperimentalDialog(QtWidgets.QDialog): + refresh_interval = 3000 + + def __init__(self, parent=None): + super(ExperimentalDialog, self).__init__(parent) + self.setWindowTitle("OpenPype Experimental tools") + self.setWindowIcon(app_icon_path()) + + empty_label = QtWidgets.QLabel( + "There are no experimental tools available.", self + ) + content_widget = QtWidgets.QWidget(self) + content_widget.setVisible(False) + + content_layout = QtWidgets.QHBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + + experimental_tools = ExperimentalTools() + buttons_by_tool_identifier = {} + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(empty_label) + layout.addWidget(content_widget) + + refresh_timer = QtCore.QTimer() + refresh_timer.setInterval(self.refresh_interval) + refresh_timer.timeout.connect(self._on_refresh_timeout) + + self._empty_label = empty_label + self._content_widget = content_widget + self._content_layout = content_layout + + self._experimental_tools = experimental_tools + self._buttons_by_tool_identifier = buttons_by_tool_identifier + + self._is_refreshing = False + self._refresh_on_active = True + self._window_is_active = False + self._refresh_timer = refresh_timer + + def refresh(self): + if self._is_refreshing: + return + self._is_refreshing = True + + self._experimental_tools.refresh_availability() + + buttons_to_remove = set(self._buttons_by_tool_identifier.keys()) + for idx, tool in enumerate(self._experimental_tools.experimental_tools): + identifier = tool.identifier + if identifier in buttons_to_remove: + buttons_to_remove.remove(identifier) + is_new = False + button = self._buttons_by_tool_identifier[identifier] + else: + is_new = True + button = ToolButton(identifier, self) + button.triggered.connect(self._on_btn_trigger) + self._buttons_by_tool_identifier[identifier] = button + self._content_layout.insertWidget(idx, button) + + if button.text() != tool.label: + button.setText(tool.label) + + if tool.enabled: + button.setToolTip(tool.tooltip) + + elif is_new or button.isEnabled(): + button.setToolTip(( + "You can enable this tool in local settings. + "\n\nOpenPype Tray > Settings > Experimental Tools" + )) + + for identifier in buttons_to_remove: + button = self._buttons_by_tool_identifier.pop(identifier) + button.setVisible(False) + idx = self._content_layout.indexOf(button) + self._content_layout.takeAt(idx) + button.deleteLater() + + self._empty_label.setVisible(not self._buttons_by_tool_identifier) + + self._is_refreshing = False + + def _on_btn_trigger(self, identifier): + tool = self._experimental_tools.tools_by_identifier.get(identifier) + if tool is not None: + tool.execute() + + def showEvent(self, event): + super(LauncherWindow, self).showEvent(event) + + if self._refresh_on_active: + # Start/Restart timer + self._refresh_timer.start() + # Refresh + self.refresh() + + elif not self._refresh_timer.isActive(): + self._refresh_timer.start() + + def changeEvent(self, event): + if event.type() == QtCore.QEvent.ActivationChange: + self._window_is_active = self.isActiveWindow() + if self._window_is_active and self._refresh_on_active: + self._refresh_timer.start() + self.refresh() + + super(LauncherWindow, self).changeEvent(event) + + def _on_refresh_timeout(self): + # Stop timer if window is not visible + if not self.isVisible(): + self._refresh_on_active = True + self._refresh_timer.stop() + + # Skip refreshing if window is not active + elif not self._window_is_active: + self._refresh_on_active = True + + # Window is active and visible so we're refreshing buttons + else: + self.refresh() From 6a68cfd4c93f84cbd99fd1a250606debcc4e92f0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:36:46 +0200 Subject: [PATCH 646/716] added Experimental tools category to local settings --- .../local_settings/experimental_widget.py | 63 +++++++++++++++++++ .../tools/settings/local_settings/window.py | 33 ++++++++++ 2 files changed, 96 insertions(+) create mode 100644 openpype/tools/settings/local_settings/experimental_widget.py diff --git a/openpype/tools/settings/local_settings/experimental_widget.py b/openpype/tools/settings/local_settings/experimental_widget.py new file mode 100644 index 0000000000..953f8f75a9 --- /dev/null +++ b/openpype/tools/settings/local_settings/experimental_widget.py @@ -0,0 +1,63 @@ +from Qt import QtWidgets +from openpype.tools.experimental_tools import ( + ExperimentalTools, + LOCAL_EXPERIMENTAL_KEY +) + + +__all__ = ( + "LocalExperimentalToolsWidgets", + "LOCAL_EXPERIMENTAL_KEY" +) + + +class LocalExperimentalToolsWidgets(QtWidgets.QWidget): + def __init__(self, parent): + super(LocalExperimentalToolsWidgets, self).__init__(parent) + + self._loading_local_settings = False + + layout = QtWidgets.QFormLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + # Label that says there are no experimental tools available + empty_label = QtWidgets.QLabel(self) + empty_label.setText("There are no experimental tools available.") + + layout.addRow(empty_label) + + experimental_defs = ExperimentalTools() + checkboxes_by_identifier = {} + for tool in experimental_defs.experimental_tools: + checkbox = QtWidgets.QCheckBox(self) + label_widget = QtWidgets.QLabel(tool.label, self) + checkbox.setToolTip(tool.tooltip) + label_widget.setToolTip(tool.tooltip) + layout.addRow(label_widget, checkbox) + + checkboxes_by_identifier[tool.identifier] = checkbox + + empty_label.setVisible(len(checkboxes_by_identifier) == 0) + + self._empty_label = empty_label + self._checkboxes_by_identifier = checkboxes_by_identifier + self._experimental_defs = experimental_defs + + def update_local_settings(self, value): + self._loading_local_settings = True + value = value or {} + + for identifier, checkbox in self._checkboxes_by_identifier.items(): + checked = value.get(identifier, False) + checkbox.setChecked(checked) + + self._loading_local_settings = False + + def settings_value(self): + # Add changed + # If these have changed then + output = {} + for identifier, checkbox in self._checkboxes_by_identifier.items(): + if checkbox.isChecked(): + output[identifier] = True + return output diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index 9e8fd89b23..f22e397323 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -20,6 +20,10 @@ from .widgets import ( ) from .mongo_widget import OpenPypeMongoWidget from .general_widget import LocalGeneralWidgets +from .experimental_widget import ( + LocalExperimentalToolsWidgets, + LOCAL_EXPERIMENTAL_KEY +) from .apps_widget import LocalApplicationsWidgets from .projects_widget import ProjectSettingsWidget @@ -44,11 +48,13 @@ class LocalSettingsWidget(QtWidgets.QWidget): self.pype_mongo_widget = None self.general_widget = None + self.experimental_widget = None self.apps_widget = None self.projects_widget = None self._create_pype_mongo_ui() self._create_general_ui() + self._create_experimental_ui() self._create_app_ui() self._create_project_ui() @@ -85,6 +91,26 @@ class LocalSettingsWidget(QtWidgets.QWidget): self.general_widget = general_widget + def _create_experimental_ui(self): + # General + experimental_expand_widget = ExpandingWidget( + "Experimental tools", self + ) + + experimental_content = QtWidgets.QWidget(self) + experimental_layout = QtWidgets.QVBoxLayout(experimental_content) + experimental_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) + experimental_expand_widget.set_content_widget(experimental_content) + + experimental_widget = LocalExperimentalToolsWidgets( + experimental_content + ) + experimental_layout.addWidget(experimental_widget) + + self.main_layout.addWidget(experimental_expand_widget) + + self.experimental_widget = experimental_widget + def _create_app_ui(self): # Applications app_expand_widget = ExpandingWidget("Applications", self) @@ -135,6 +161,9 @@ class LocalSettingsWidget(QtWidgets.QWidget): self.projects_widget.update_local_settings( value.get(LOCAL_PROJECTS_KEY) ) + self.experimental_widget.update_local_settings( + value.get(LOCAL_EXPERIMENTAL_KEY) + ) def settings_value(self): output = {} @@ -149,6 +178,10 @@ class LocalSettingsWidget(QtWidgets.QWidget): projects_value = self.projects_widget.settings_value() if projects_value: output[LOCAL_PROJECTS_KEY] = projects_value + + experimental_value = self.experimental_widget.settings_value() + if experimental_value: + output[LOCAL_EXPERIMENTAL_KEY] = experimental_value return output From 90de4cddb582ed50eea8ac65698d3517f6dd058c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:50:10 +0200 Subject: [PATCH 647/716] store tools under 'tools' variable --- openpype/tools/experimental_tools/tools_def.py | 2 +- openpype/tools/settings/local_settings/experimental_widget.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index ec4815c741..353ec8e1d5 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -68,7 +68,7 @@ class ExperimentalTools: tool.identifier: tool for tool in experimental_tools } - self.experimental_tools = experimental_tools + self.tools = experimental_tools self._parent_widget = parent def refresh_availability(self): diff --git a/openpype/tools/settings/local_settings/experimental_widget.py b/openpype/tools/settings/local_settings/experimental_widget.py index 953f8f75a9..741c173415 100644 --- a/openpype/tools/settings/local_settings/experimental_widget.py +++ b/openpype/tools/settings/local_settings/experimental_widget.py @@ -28,7 +28,7 @@ class LocalExperimentalToolsWidgets(QtWidgets.QWidget): experimental_defs = ExperimentalTools() checkboxes_by_identifier = {} - for tool in experimental_defs.experimental_tools: + for tool in experimental_defs.tools: checkbox = QtWidgets.QCheckBox(self) label_widget = QtWidgets.QLabel(tool.label, self) checkbox.setToolTip(tool.tooltip) From 27e9f20c07cc3a9550e2fdae70d2b00560e6ede8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:58:10 +0200 Subject: [PATCH 648/716] fix dialog file --- openpype/tools/experimental_tools/dialog.py | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index db868c572f..14f3e6e48b 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -1,4 +1,4 @@ -from Qt import QtWidgets +from Qt import QtWidgets, QtCore, QtGui from openpype.style import ( load_stylesheet, @@ -12,7 +12,7 @@ class ToolButton(QtWidgets.QPushButton): triggered = QtCore.Signal(str) def __init__(self, identifier, *args, **kwargs): - super(ExperimentalDialog, self).__init__(*args, **kwargs) + super(ToolButton, self).__init__(*args, **kwargs) self._identifier = identifier self.clicked.connect(self._on_click) @@ -27,23 +27,22 @@ class ExperimentalDialog(QtWidgets.QDialog): def __init__(self, parent=None): super(ExperimentalDialog, self).__init__(parent) self.setWindowTitle("OpenPype Experimental tools") - self.setWindowIcon(app_icon_path()) + icon = QtGui.QIcon(app_icon_path()) + self.setWindowIcon(icon) empty_label = QtWidgets.QLabel( "There are no experimental tools available.", self ) content_widget = QtWidgets.QWidget(self) - content_widget.setVisible(False) content_layout = QtWidgets.QHBoxLayout(content_widget) content_layout.setContentsMargins(0, 0, 0, 0) experimental_tools = ExperimentalTools() - buttons_by_tool_identifier = {} layout = QtWidgets.QHBoxLayout(self) layout.addWidget(empty_label) - layout.addWidget(content_widget) + layout.addWidget(content_widget, 1) refresh_timer = QtCore.QTimer() refresh_timer.setInterval(self.refresh_interval) @@ -54,7 +53,7 @@ class ExperimentalDialog(QtWidgets.QDialog): self._content_layout = content_layout self._experimental_tools = experimental_tools - self._buttons_by_tool_identifier = buttons_by_tool_identifier + self._buttons_by_tool_identifier = {} self._is_refreshing = False self._refresh_on_active = True @@ -69,7 +68,7 @@ class ExperimentalDialog(QtWidgets.QDialog): self._experimental_tools.refresh_availability() buttons_to_remove = set(self._buttons_by_tool_identifier.keys()) - for idx, tool in enumerate(self._experimental_tools.experimental_tools): + for idx, tool in enumerate(self._experimental_tools.tools): identifier = tool.identifier if identifier in buttons_to_remove: buttons_to_remove.remove(identifier) @@ -90,7 +89,7 @@ class ExperimentalDialog(QtWidgets.QDialog): elif is_new or button.isEnabled(): button.setToolTip(( - "You can enable this tool in local settings. + "You can enable this tool in local settings." "\n\nOpenPype Tray > Settings > Experimental Tools" )) @@ -101,7 +100,9 @@ class ExperimentalDialog(QtWidgets.QDialog): self._content_layout.takeAt(idx) button.deleteLater() - self._empty_label.setVisible(not self._buttons_by_tool_identifier) + self._empty_label.setVisible( + len(self._buttons_by_tool_identifier) == 0 + ) self._is_refreshing = False @@ -111,7 +112,7 @@ class ExperimentalDialog(QtWidgets.QDialog): tool.execute() def showEvent(self, event): - super(LauncherWindow, self).showEvent(event) + super(ExperimentalDialog, self).showEvent(event) if self._refresh_on_active: # Start/Restart timer @@ -129,7 +130,7 @@ class ExperimentalDialog(QtWidgets.QDialog): self._refresh_timer.start() self.refresh() - super(LauncherWindow, self).changeEvent(event) + super(ExperimentalDialog, self).changeEvent(event) def _on_refresh_timeout(self): # Stop timer if window is not visible From 5026d300bbcae2eaa87dccf3ad2a0655a3ae4274 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 17:58:22 +0200 Subject: [PATCH 649/716] import ExperimentalDialog to init file --- openpype/tools/experimental_tools/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/tools/experimental_tools/__init__.py b/openpype/tools/experimental_tools/__init__.py index d61c560886..75e3210aab 100644 --- a/openpype/tools/experimental_tools/__init__.py +++ b/openpype/tools/experimental_tools/__init__.py @@ -3,7 +3,12 @@ from .tools_def import ( LOCAL_EXPERIMENTAL_KEY ) +from .dialog import ExperimentalDialog + + __all__ = ( "ExperimentalTools", - "LOCAL_EXPERIMENTAL_KEY" + "LOCAL_EXPERIMENTAL_KEY", + + "ExperimentalDialog" ) From 7d3f4d315ff71f71403dddfd3f95d1ecd57e69a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 18:20:26 +0200 Subject: [PATCH 650/716] empty dialog has Ok btn --- openpype/tools/experimental_tools/dialog.py | 65 ++++++++++++++++----- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index 14f3e6e48b..237052c055 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -30,41 +30,59 @@ class ExperimentalDialog(QtWidgets.QDialog): icon = QtGui.QIcon(app_icon_path()) self.setWindowIcon(icon) + empty_widget = QtWidgets.QWidget(self) + empty_label = QtWidgets.QLabel( - "There are no experimental tools available.", self + "There are no experimental tools available...", empty_widget ) + + empty_btns_layout = QtWidgets.QHBoxLayout() + ok_btn = QtWidgets.QPushButton("OK", empty_widget) + + empty_btns_layout.setContentsMargins(0, 0, 0, 0) + empty_btns_layout.addStretch(1) + empty_btns_layout.addWidget(ok_btn, 0) + + empty_layout = QtWidgets.QVBoxLayout(empty_widget) + empty_layout.setContentsMargins(0, 0, 0, 0) + empty_layout.addWidget(empty_label) + empty_layout.addStretch(1) + empty_layout.addLayout(empty_btns_layout) + content_widget = QtWidgets.QWidget(self) - content_layout = QtWidgets.QHBoxLayout(content_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(0, 0, 0, 0) experimental_tools = ExperimentalTools() - layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(empty_label) + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(empty_widget, 1) layout.addWidget(content_widget, 1) refresh_timer = QtCore.QTimer() refresh_timer.setInterval(self.refresh_interval) refresh_timer.timeout.connect(self._on_refresh_timeout) - self._empty_label = empty_label + ok_btn.clicked.connect(self._on_ok_click) + + self._empty_widget = empty_widget self._content_widget = content_widget self._content_layout = content_layout self._experimental_tools = experimental_tools self._buttons_by_tool_identifier = {} - self._is_refreshing = False - self._refresh_on_active = True - self._window_is_active = False self._refresh_timer = refresh_timer - def refresh(self): - if self._is_refreshing: - return - self._is_refreshing = True + # Is dialog first shown + self._first_show = True + # Trigger refresh when window get's activity + self._refresh_on_active = True + # Is window active + self._window_is_active = False + def refresh(self): self._experimental_tools.refresh_availability() buttons_to_remove = set(self._buttons_by_tool_identifier.keys()) @@ -100,11 +118,18 @@ class ExperimentalDialog(QtWidgets.QDialog): self._content_layout.takeAt(idx) button.deleteLater() - self._empty_label.setVisible( - len(self._buttons_by_tool_identifier) == 0 - ) + self._set_visibility() - self._is_refreshing = False + def _is_content_visible(self): + return len(self._buttons_by_tool_identifier) > 0 + + def _set_visibility(self): + content_visible = self._is_content_visible() + self._content_widget.setVisible(content_visible) + self._empty_widget.setVisible(not content_visible) + + def _on_ok_click(self): + self.close() def _on_btn_trigger(self, identifier): tool = self._experimental_tools.tools_by_identifier.get(identifier) @@ -123,6 +148,14 @@ class ExperimentalDialog(QtWidgets.QDialog): elif not self._refresh_timer.isActive(): self._refresh_timer.start() + if self._first_show: + self._first_show = False + # Resize dialog if there is not content + if not self._is_content_visible(): + size = self.size() + size.setWidth(size.width() + size.width() / 3) + self.resize(size) + def changeEvent(self, event): if event.type() == QtCore.QEvent.ActivationChange: self._window_is_active = self.isActiveWindow() From c7df4e9edc7185a0d155a215a8822b3bed9dcbd7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 19 Oct 2021 20:12:23 +0200 Subject: [PATCH 651/716] changed label --- openpype/tools/settings/local_settings/experimental_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/local_settings/experimental_widget.py b/openpype/tools/settings/local_settings/experimental_widget.py index 741c173415..72f999d886 100644 --- a/openpype/tools/settings/local_settings/experimental_widget.py +++ b/openpype/tools/settings/local_settings/experimental_widget.py @@ -22,7 +22,9 @@ class LocalExperimentalToolsWidgets(QtWidgets.QWidget): # Label that says there are no experimental tools available empty_label = QtWidgets.QLabel(self) - empty_label.setText("There are no experimental tools available.") + empty_label.setText( + "There are no experimental tools available..." + ) layout.addRow(empty_label) From 54a8b9d811e6d8602b2f2b729ae0db2701c678ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:28:54 +0200 Subject: [PATCH 652/716] removed "widget" key from data --- openpype/tools/loader/app.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index a4b4b5eb28..d7fa9640ac 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -131,9 +131,6 @@ class LoaderWindow(QtWidgets.QDialog): layout.addWidget(footer_widget) self.data = { - "widgets": { - "subsets": subsets - }, "state": { "assetIds": None } @@ -206,8 +203,8 @@ class LoaderWindow(QtWidgets.QDialog): # ------------------------------- def on_assetview_click(self, *args): - subsets_widget = self.data["widgets"]["subsets"] - selection_model = subsets_widget.view.selectionModel() + # TODO do not touch inner attributes of subset widget + selection_model = self._subsets_widget.view.selectionModel() if selection_model.selectedIndexes(): selection_model.clearSelection() From 3a56ac5e5e1920dcffea9d54a0f043cc92c5e628 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:05:36 +0200 Subject: [PATCH 653/716] validate tool identifier keys --- openpype/tools/experimental_tools/tools_def.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 353ec8e1d5..2fa42bcc6e 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -64,10 +64,16 @@ class ExperimentalTools: if tool.is_available_for_host(host_name) ] - self.tools_by_identifier = { - tool.identifier: tool - for tool in experimental_tools - } + # Store tools by identifier + tools_by_identifier = {} + for tool in experimental_tools: + if tool.identifier in tools_by_identifier: + raise KeyError(( + "Duplicated experimental tool identifier \"{}\"" + ).format(tool.identifier)) + tools_by_identifier[tool.identifier] = tool + + self.tools_by_identifier = tools_by_identifier self.tools = experimental_tools self._parent_widget = parent From 630299e340ab41af673dde31803288bb1587d93f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:05:46 +0200 Subject: [PATCH 654/716] fix 80char line --- openpype/tools/experimental_tools/tools_def.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 2fa42bcc6e..13283c157a 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -15,7 +15,9 @@ class ExperimentalTool: hosts_filter (list): List of host names for which is tool available. Some tools may not be available in all hosts. """ - def __init__(self, identifier, label, callback, tooltip, hosts_filter=None): + def __init__( + self, identifier, label, callback, tooltip, hosts_filter=None + ): self.identifier = identifier self.label = label self.callback = callback From b6bb7a9d09fb8e5922bfcaf40ceeaea5d5344d70 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:06:21 +0200 Subject: [PATCH 655/716] get host name from environment if not passed # Conflicts: # openpype/tools/experimental_tools/tools_def.py --- openpype/tools/experimental_tools/tools_def.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 13283c157a..0dcec7a871 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -1,3 +1,4 @@ +import os from openpype.settings import get_local_settings # Constant key under which local settings are stored @@ -53,6 +54,10 @@ class ExperimentalTools: """ def __init__(self, parent=None, host_name=None, filter_hosts=None): experimental_tools = [] + + if not host_name: + host_name = os.environ.get("AVALON_APP") + if filter_hosts is None: filter_hosts = host_name is not None From c9860711512a48d18fe9423a0b510e508c59eeb2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:06:29 +0200 Subject: [PATCH 656/716] added few docstrings # Conflicts: # openpype/tools/experimental_tools/tools_def.py --- openpype/tools/experimental_tools/tools_def.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 0dcec7a871..5dd92151ca 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -51,19 +51,30 @@ class ExperimentalTools: To add/remove experimental tool just add/remove tool to `experimental_tools` variable in __init__ function. + Args: + parent (QtWidgets.QWidget): Parent widget for tools. + host_name (str): Name of host in which context we're now. Environment + value 'AVALON_APP' is used when not passed. + filter_hosts (bool): Should filter tools. By default is set to 'True' + when 'host_name' is passed. Is always set to 'False' if 'host_name' + is not defined. """ def __init__(self, parent=None, host_name=None, filter_hosts=None): + # Definition of experimental tools experimental_tools = [] + # Try to get host name from env variable `AVALON_APP` if not host_name: host_name = os.environ.get("AVALON_APP") + # Decide if filtering by host name should happen if filter_hosts is None: filter_hosts = host_name is not None if filter_hosts and not host_name: filter_hosts = False + # Filter tools by host name if filter_hosts: experimental_tools = [ tool From dc9f901c7a83331f4495ac8230f195b371f43a24 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 11:18:13 +0200 Subject: [PATCH 657/716] use OpenPype stylesheet in experimental dialog --- openpype/tools/experimental_tools/dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index 237052c055..a611416efc 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -29,6 +29,7 @@ class ExperimentalDialog(QtWidgets.QDialog): self.setWindowTitle("OpenPype Experimental tools") icon = QtGui.QIcon(app_icon_path()) self.setWindowIcon(icon) + self.setStyleSheet(load_stylesheet()) empty_widget = QtWidgets.QWidget(self) From 661ba6090967d87edf639fa197ad14e420392196 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:21:47 +0200 Subject: [PATCH 658/716] fix parenting in subset widget --- openpype/tools/loader/widgets.py | 70 +++++++++++++++----------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 6b94fc6e44..d2b0a6b730 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -159,20 +159,25 @@ class SubsetWidget(QtWidgets.QWidget): grouping=enable_grouping ) proxy = SubsetFilterProxyModel() + proxy.setSourceModel(model) + proxy.setDynamicSortFilter(True) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + family_proxy = FamiliesFilterProxyModel() family_proxy.setSourceModel(proxy) - subset_filter = QtWidgets.QLineEdit() + subset_filter = QtWidgets.QLineEdit(self) subset_filter.setPlaceholderText("Filter subsets..") - groupable = QtWidgets.QCheckBox("Enable Grouping") - groupable.setChecked(enable_grouping) + group_checkbox = QtWidgets.QCheckBox("Enable Grouping", self) + group_checkbox.setChecked(enable_grouping) top_bar_layout = QtWidgets.QHBoxLayout() top_bar_layout.addWidget(subset_filter) - top_bar_layout.addWidget(groupable) + top_bar_layout.addWidget(group_checkbox) - view = TreeViewSpinner() + view = TreeViewSpinner(self) + view.setModel(family_proxy) view.setObjectName("SubsetView") view.setIndentation(20) view.setStyleSheet(""" @@ -192,59 +197,50 @@ class SubsetWidget(QtWidgets.QWidget): column = model.Columns.index("time") view.setItemDelegateForColumn(column, time_delegate) - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(top_bar_layout) - layout.addWidget(view) - view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) view.setSortingEnabled(True) view.sortByColumn(1, QtCore.Qt.AscendingOrder) view.setAlternatingRowColors(True) - self.data = { - "delegates": { - "version": version_delegate, - "time": time_delegate - }, - "state": { - "groupable": groupable - } - } - - self.proxy = proxy - self.model = model - self.view = view - self.filter = subset_filter - self.family_proxy = family_proxy + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(top_bar_layout) + layout.addWidget(view) # settings and connections - self.proxy.setSourceModel(self.model) - self.proxy.setDynamicSortFilter(True) - self.proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - - self.view.setModel(self.family_proxy) - self.view.customContextMenuRequested.connect(self.on_context_menu) - for column_name, width in self.default_widths: idx = model.Columns.index(column_name) view.setColumnWidth(idx, width) + self.model = model + self.view = view + actual_project = dbcon.Session["AVALON_PROJECT"] self.on_project_change(actual_project) + view.customContextMenuRequested.connect(self.on_context_menu) + selection = view.selectionModel() selection.selectionChanged.connect(self.active_changed) version_delegate.version_changed.connect(self.version_changed) - groupable.stateChanged.connect(self.set_grouping) + group_checkbox.stateChanged.connect(self.set_grouping) - self.filter.textChanged.connect(self.proxy.setFilterRegExp) - self.filter.textChanged.connect(self.view.expandAll) + subset_filter.textChanged.connect(proxy.setFilterRegExp) + subset_filter.textChanged.connect(view.expandAll) model.refreshed.connect(self.refreshed) + self.proxy = proxy + self.family_proxy = family_proxy + + self._subset_filter = subset_filter + self._group_checkbox = group_checkbox + + self._version_delegate = version_delegate + self._time_delegate = time_delegate + self.model.refresh() def get_subsets_families(self): @@ -254,7 +250,7 @@ class SubsetWidget(QtWidgets.QWidget): self.family_proxy.setFamiliesFilter(families) def is_groupable(self): - return self.data["state"]["groupable"].checkState() + return self._group_checkbox.isChecked() def set_grouping(self, state): with tools_lib.preserve_selection(tree_view=self.view, @@ -1128,7 +1124,7 @@ class RepresentationWidget(QtWidgets.QWidget): label = QtWidgets.QLabel("Representations", self) - tree_view = DeselectableTreeView() + tree_view = DeselectableTreeView(parent=self) tree_view.setModel(proxy_model) tree_view.setAllColumnsShowFocus(True) tree_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) From 6d0f9069e8f326bc8c40159e83117c5aabb42a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 20 Oct 2021 12:24:20 +0200 Subject: [PATCH 659/716] fix UNC path support --- openpype/hosts/maya/plugins/publish/collect_render.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 575cc2456b..d2f277329a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -244,17 +244,17 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # metadata file will be located in top-most common # directory. # TODO: use `os.path.commonpath()` after switch to Python 3 + publish_meta_path = os.path.normpath(publish_meta_path) 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("/"): + for part in publish_meta_path.replace( + common_publish_meta_path, "").split(os.path.sep): 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)) From c0ca4ea5893b6fa267aad72011e457d93de3722b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:29:19 +0200 Subject: [PATCH 660/716] add parenting in util widgets --- openpype/tools/utils/widgets.py | 79 ++++++++++++++++----------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index b9b542c123..878a9b7c86 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -35,28 +35,19 @@ class AssetWidget(QtWidgets.QWidget): self.dbcon = dbcon - self.setContentsMargins(0, 0, 0, 0) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(4) - # Tree View model = AssetModel(dbcon=self.dbcon, parent=self) proxy = RecursiveSortFilterProxyModel() proxy.setSourceModel(model) proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - view = AssetsView() + view = AssetsView(self) view.setModel(proxy) if multiselection: asset_delegate = AssetDelegate() view.setSelectionMode(view.ExtendedSelection) view.setItemDelegate(asset_delegate) - # Header - header = QtWidgets.QHBoxLayout() - icon = qtawesome.icon("fa.arrow-down", color=style.colors.light) set_current_asset_btn = QtWidgets.QPushButton(icon, "") set_current_asset_btn.setToolTip("Go to Asset from current Session") @@ -64,22 +55,28 @@ class AssetWidget(QtWidgets.QWidget): set_current_asset_btn.setVisible(False) icon = qtawesome.icon("fa.refresh", color=style.colors.light) - refresh = QtWidgets.QPushButton(icon, "") + refresh = QtWidgets.QPushButton(icon, "", parent=self) refresh.setToolTip("Refresh items") - filter = QtWidgets.QLineEdit() - filter.textChanged.connect(proxy.setFilterFixedString) - filter.setPlaceholderText("Filter assets..") + filter_input = QtWidgets.QLineEdit(self) + filter_input.setPlaceholderText("Filter assets..") - header.addWidget(filter) - header.addWidget(set_current_asset_btn) - header.addWidget(refresh) + # Header + header_layout = QtWidgets.QHBoxLayout() + header_layout.addWidget(filter_input) + header_layout.addWidget(set_current_asset_btn) + header_layout.addWidget(refresh) # Layout - layout.addLayout(header) + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + layout.addLayout(header_layout) layout.addWidget(view) # Signals/Slots + filter_input.textChanged.connect(proxy.setFilterFixedString) + selection = view.selectionModel() selection.selectionChanged.connect(self.selection_changed) selection.currentChanged.connect(self.current_changed) @@ -399,30 +396,30 @@ class OptionalActionWidget(QtWidgets.QWidget): def __init__(self, label, parent=None): super(OptionalActionWidget, self).__init__(parent) - body = QtWidgets.QWidget() - body.setStyleSheet("background: transparent;") + body_widget = QtWidgets.QWidget(self) + body_widget.setStyleSheet("background: transparent;") - icon = QtWidgets.QLabel() - label = QtWidgets.QLabel(label) - option = OptionBox(body) + icon = QtWidgets.QLabel(body_widget) + label = QtWidgets.QLabel(label, body_widget) + option = OptionBox(body_widget) icon.setFixedSize(24, 16) option.setFixedSize(30, 30) - layout = QtWidgets.QHBoxLayout(body) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(2) - layout.addWidget(icon) - layout.addWidget(label) - layout.addSpacing(6) + body_layout = QtWidgets.QHBoxLayout(body_widget) + body_layout.setContentsMargins(0, 0, 0, 0) + body_layout.setSpacing(2) + body_layout.addWidget(icon) + body_layout.addWidget(label) + body_layout.addSpacing(6) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(6, 1, 2, 1) layout.setSpacing(0) - layout.addWidget(body) + layout.addWidget(body_widget) layout.addWidget(option) - body.setMouseTracking(True) + body_widget.setMouseTracking(True) label.setMouseTracking(True) option.setMouseTracking(True) self.setMouseTracking(True) @@ -431,7 +428,7 @@ class OptionalActionWidget(QtWidgets.QWidget): self.icon = icon self.label = label self.option = option - self.body = body + self.body = body_widget # (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke. # See https://stackoverflow.com/q/52838690/4145300 @@ -476,20 +473,20 @@ class OptionDialog(QtWidgets.QDialog): def create(self, options): parser = qargparse.QArgumentParser(arguments=options) - decision = QtWidgets.QWidget() - accept = QtWidgets.QPushButton("Accept") - cancel = QtWidgets.QPushButton("Cancel") + decision_widget = QtWidgets.QWidget(self) + accept_btn = QtWidgets.QPushButton("Accept", decision_widget) + cancel_btn = QtWidgets.QPushButton("Cancel", decision_widget) - layout = QtWidgets.QHBoxLayout(decision) - layout.addWidget(accept) - layout.addWidget(cancel) + decision_layout = QtWidgets.QHBoxLayout(decision_widget) + decision_layout.addWidget(accept_btn) + decision_layout.addWidget(cancel_btn) layout = QtWidgets.QVBoxLayout(self) layout.addWidget(parser) - layout.addWidget(decision) + layout.addWidget(decision_widget) - accept.clicked.connect(self.accept) - cancel.clicked.connect(self.reject) + accept_btn.clicked.connect(self.accept) + cancel_btn.clicked.connect(self.reject) parser.changed.connect(self.on_changed) def on_changed(self, argument): From 95838d120a2f834fe38031b4d3fa97b2919f0f4f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:32:23 +0200 Subject: [PATCH 661/716] use openpype stylesheet --- openpype/tools/loader/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index d7fa9640ac..74e121896e 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -1,10 +1,10 @@ import sys from Qt import QtWidgets, QtCore -from avalon import api, io, style, pipeline +from avalon import api, io, pipeline +from openpype.style import load_stylesheet from openpype.tools.utils.widgets import AssetWidget - from openpype.tools.utils import lib from .widgets import ( @@ -46,6 +46,7 @@ class LoaderWindow(QtWidgets.QDialog): if project_name: title += " - {}".format(project_name) self.setWindowTitle(title) + self.setStyleSheet(load_stylesheet()) # Groups config self.groups_config = lib.GroupsConfig(io) @@ -653,7 +654,6 @@ def show(debug=False, parent=None, use_context=False): with lib.application(): window = LoaderWindow(parent) - window.setStyleSheet(style.load_stylesheet()) window.show() if use_context: From a2485eb7bf581273ec0c33b7182dd19e3a33e329 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:32:35 +0200 Subject: [PATCH 662/716] add parenting to asset view --- openpype/tools/utils/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/utils/views.py b/openpype/tools/utils/views.py index bed5655647..89e49fe142 100644 --- a/openpype/tools/utils/views.py +++ b/openpype/tools/utils/views.py @@ -68,8 +68,8 @@ class AssetsView(TreeViewSpinner, DeselectableTreeView): This implements a context menu. """ - def __init__(self): - super(AssetsView, self).__init__() + def __init__(self, parent=None): + super(AssetsView, self).__init__(parent) self.setIndentation(15) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.setHeaderHidden(True) From ce755730d2a84684cf319d203754982aae8b585a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:50:30 +0200 Subject: [PATCH 663/716] use WA_TranslucentBackground --- openpype/tools/loader/widgets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index d2b0a6b730..0946826dc4 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -37,12 +37,13 @@ class OverlayFrame(QtWidgets.QFrame): super(OverlayFrame, self).__init__(parent) label_widget = QtWidgets.QLabel(label, self) + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter) self.label_widget = label_widget - label_widget.setStyleSheet("background: transparent;") self.setStyleSheet(( "background: rgba(0, 0, 0, 127);" "font-size: 60pt;" From 4b1f739f211d550276019bbc19d53364b22ca1e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 12:50:51 +0200 Subject: [PATCH 664/716] move stylesheet of SubsetView to style.css --- openpype/style/style.css | 5 +++++ openpype/tools/loader/widgets.py | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 830ed85f9b..a7e82be567 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -629,3 +629,8 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #PythonInterpreterOutput, #PythonCodeEditor { font-family: "Roboto Mono"; } + +#SubsetView::item { + padding: 5px 1px; + border: 0px; +} diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 0946826dc4..a55cfb6e43 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -181,12 +181,6 @@ class SubsetWidget(QtWidgets.QWidget): view.setModel(family_proxy) view.setObjectName("SubsetView") view.setIndentation(20) - view.setStyleSheet(""" - QTreeView::item{ - padding: 5px 1px; - border: 0px; - } - """) view.setAllColumnsShowFocus(True) # Set view delegates From 1c179510f7d402f69949faf84b4f6abbb4192e2f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 14:36:03 +0200 Subject: [PATCH 665/716] copied color definitions from new publisher PR --- openpype/style/__init__.py | 40 ++++- openpype/style/color_defs.py | 285 +++++++++++++++++++++++++++++++++++ 2 files changed, 322 insertions(+), 3 deletions(-) create mode 100644 openpype/style/color_defs.py diff --git a/openpype/style/__init__.py b/openpype/style/__init__.py index 0d7904d133..d763bfdc3c 100644 --- a/openpype/style/__init__.py +++ b/openpype/style/__init__.py @@ -2,6 +2,8 @@ import os import json import collections from openpype import resources +import six +from .color_defs import parse_color _STYLESHEET_CACHE = None @@ -10,6 +12,40 @@ _FONT_IDS = None current_dir = os.path.dirname(os.path.abspath(__file__)) +def get_colors_data(): + data = _get_colors_raw_data() + return data.get("color") or {} + + +def _convert_color_values_to_objects(value): + if isinstance(value, dict): + output = {} + for _key, _value in value.items(): + output[_key] = _convert_color_values_to_objects(_value) + return output + + if not isinstance(value, six.string_types): + raise TypeError(( + "Unexpected type in colors data '{}'. Expected 'str' or 'dict'." + ).format(str(type(value)))) + return parse_color(value) + + +def get_objected_colors(): + colors_data = get_colors_data() + output = {} + for key, value in colors_data.items(): + output[key] = _convert_color_values_to_objects(value) + return output + + +def _get_colors_raw_data(): + data_path = os.path.join(current_dir, "data.json") + with open(data_path, "r") as data_stream: + data = json.load(data_stream) + return data + + def _load_stylesheet(): from . import qrc_resources @@ -19,9 +55,7 @@ def _load_stylesheet(): with open(style_path, "r") as style_file: stylesheet = style_file.read() - data_path = os.path.join(current_dir, "data.json") - with open(data_path, "r") as data_stream: - data = json.load(data_stream) + data = _get_colors_raw_data() data_deque = collections.deque() for item in data.items(): diff --git a/openpype/style/color_defs.py b/openpype/style/color_defs.py new file mode 100644 index 0000000000..4d726cc3f3 --- /dev/null +++ b/openpype/style/color_defs.py @@ -0,0 +1,285 @@ +import re + + +def parse_color(value): + modified_value = value.strip().lower() + if modified_value.startswith("hsla"): + return HSLAColor(value) + + if modified_value.startswith("hsl"): + return HSLColor(value) + + if modified_value.startswith("#"): + return HEXColor(value) + + if modified_value.startswith("rgba"): + return RGBAColor(value) + + if modified_value.startswith("rgb"): + return RGBColor(value) + return UnknownColor(value) + + +def create_qcolor(*args): + from Qt import QtGui + + return QtGui.QColor(*args) + + +def min_max_check(value, min_value, max_value): + if min_value is not None and value < min_value: + raise ValueError("Minimum expected value is '{}' got '{}'".format( + min_value, value + )) + + if max_value is not None and value > max_value: + raise ValueError("Maximum expected value is '{}' got '{}'".format( + min_value, value + )) + + +def int_validation(value, min_value=None, max_value=None): + if not isinstance(value, int): + raise TypeError(( + "Invalid type of hue expected 'int' got {}" + ).format(str(type(value)))) + + min_max_check(value, min_value, max_value) + + +def float_validation(value, min_value=None, max_value=None): + if not isinstance(value, float): + raise TypeError(( + "Invalid type of hue expected 'int' got {}" + ).format(str(type(value)))) + + min_max_check(value, min_value, max_value) + + +class UnknownColor: + def __init__(self, value): + self.value = value + + def get_qcolor(self): + return create_qcolor(self.value) + + +class HEXColor: + regex = re.compile(r"[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$") + + def __init__(self, color_string): + red, green, blue = self.hex_to_rgb(color_string) + + self._color_string = color_string + self._red = red + self._green = green + self._blue = blue + + @property + def red(self): + return self._red + + @property + def green(self): + return self._green + + @property + def blue(self): + return self._blue + + def to_stylesheet_str(self): + return self._color_string + + @classmethod + def hex_to_rgb(cls, value): + hex_value = value.lstrip("#") + if not cls.regex.match(hex_value): + raise ValueError("\"{}\" is not a valid HEX code.".format(value)) + + output = [] + if len(hex_value) == 3: + for char in hex_value: + output.append(int(char * 2, 16)) + else: + for idx in range(3): + start_idx = idx * 2 + output.append(int(hex_value[start_idx:start_idx + 2], 16)) + return output + + def get_qcolor(self): + return create_qcolor(self.red, self.green, self.blue) + + +class RGBColor: + def __init__(self, value): + modified_color = value.lower().strip() + content = modified_color.rstrip(")").lstrip("rgb(") + red_str, green_str, blue_str = ( + item.strip() for item in content.split(",") + ) + red = int(red_str) + green = int(green_str) + blue = int(blue_str) + + int_validation(red, 0, 255) + int_validation(green, 0, 255) + int_validation(blue, 0, 255) + + self._red = red + self._green = green + self._blue = blue + + @property + def red(self): + return self._red + + @property + def green(self): + return self._green + + @property + def blue(self): + return self._blue + + def get_qcolor(self): + return create_qcolor(self.red, self.green, self.blue) + + +class RGBAColor: + def __init__(self, value): + modified_color = value.lower().strip() + content = modified_color.rstrip(")").lstrip("rgba(") + red_str, green_str, blue_str, alpha_str = ( + item.strip() for item in content.split(",") + ) + red = int(red_str) + green = int(green_str) + blue = int(blue_str) + alpha = int(alpha_str) + + int_validation(red, 0, 255) + int_validation(green, 0, 255) + int_validation(blue, 0, 255) + int_validation(alpha, 0, 255) + + self._red = red + self._green = green + self._blue = blue + self._alpha = alpha + + @property + def red(self): + return self._red + + @property + def green(self): + return self._green + + @property + def blue(self): + return self._blue + + @property + def alpha(self): + return self._alpha + + def get_qcolor(self): + return create_qcolor(self.red, self.green, self.blue, self.alpha) + + +class HSLColor: + def __init__(self, value): + modified_color = value.lower().strip() + content = modified_color.rstrip(")").lstrip("hsl(") + hue_str, sat_str, light_str = ( + item.strip() for item in content.split(",") + ) + hue = int(hue_str) % 360 + if "%" in sat_str: + sat = float(sat_str.rstrip("%")) / 100 + else: + sat = float(sat) + + if "%" in light_str: + light = float(light_str.rstrip("%")) / 100 + else: + light = float(light_str) + + int_validation(hue, 0, 360) + float_validation(sat, 0, 1) + float_validation(light, 0, 1) + + self._hue = hue + self._saturation = sat + self._light = light + + @property + def hue(self): + return self._hue + + @property + def saturation(self): + return self._saturation + + @property + def light(self): + return self._light + + def get_qcolor(self): + color = create_qcolor() + color.setHslF(self.hue / 360, self.saturation, self.light) + return color + + +class HSLAColor: + def __init__(self, value): + modified_color = value.lower().strip() + content = modified_color.rstrip(")").lstrip("hsla(") + hue_str, sat_str, light_str, alpha_str = ( + item.strip() for item in content.split(",") + ) + hue = int(hue_str) % 360 + if "%" in sat_str: + sat = float(sat_str.rstrip("%")) / 100 + else: + sat = float(sat) + + if "%" in light_str: + light = float(light_str.rstrip("%")) / 100 + else: + light = float(light_str) + alpha = float(alpha_str) + + if isinstance(alpha, int): + alpha = float(alpha) + + int_validation(hue, 0, 360) + float_validation(sat, 0, 1) + float_validation(light, 0, 1) + float_validation(alpha, 0, 1) + + self._hue = hue + self._saturation = sat + self._light = light + self._alpha = alpha + + @property + def hue(self): + return self._hue + + @property + def saturation(self): + return self._saturation + + @property + def light(self): + return self._light + + @property + def alpha(self): + return self._alpha + + def get_qcolor(self): + color = create_qcolor() + color.setHslF(self.hue / 360, self.saturation, self.light, self.alpha) + return color From 35435fedba310293bc3d0f5c2c7979b5caccc152 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 15:23:37 +0200 Subject: [PATCH 666/716] use rgba instead of hsla --- openpype/style/data.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index a58829d946..5cac7e07db 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -28,7 +28,7 @@ "bg": "#2C313A", "bg-inputs": "#21252B", "bg-buttons": "#434a56", - "bg-button-hover": "hsla(220, 14%, 70%, .3)", + "bg-button-hover": "rgba(168, 175, 189, 0.3)", "bg-inputs-disabled": "#2C313A", "bg-buttons-disabled": "#434a56", @@ -38,15 +38,15 @@ "bg-view": "#21252B", "bg-view-header": "#373D48", - "bg-view-hover": "hsla(220, 14%, 70%, .3)", + "bg-view-hover": "rgba(168, 175, 189, .3)", "bg-view-alternate": "rgb(36, 42, 50)", "bg-view-disabled": "#434a56", "bg-view-alternate-disabled": "#2C313A", - "bg-view-selection": "hsla(200, 60%, 60%, .4)", - "bg-view-selection-hover": "hsla(200, 60%, 60%, .8)", + "bg-view-selection": "rgba(92, 173, 214, .4)", + "bg-view-selection-hover": "rgba(92, 173, 214, .8)", "border": "#373D48", - "border-hover": "hsla(220, 14%, 70%, .3)", + "border-hover": "rgba(168, 175, 189, .3)", "border-focus": "hsl(200, 60%, 60%)" } } From 66a1ba4033deb115db6627ec60fafaa0a9470bad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 15:24:03 +0200 Subject: [PATCH 667/716] added RepresentationView to stylesheet --- openpype/style/style.css | 2 +- openpype/tools/loader/widgets.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index a7e82be567..6507cbe63b 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -630,7 +630,7 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { font-family: "Roboto Mono"; } -#SubsetView::item { +#SubsetView::item, #RepresentationView:item { padding: 5px 1px; border: 0px; } diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index a55cfb6e43..f6ba200eff 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -1120,6 +1120,7 @@ class RepresentationWidget(QtWidgets.QWidget): label = QtWidgets.QLabel("Representations", self) tree_view = DeselectableTreeView(parent=self) + tree_view.setObjectName("RepresentationView") tree_view.setModel(proxy_model) tree_view.setAllColumnsShowFocus(True) tree_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) @@ -1129,12 +1130,6 @@ class RepresentationWidget(QtWidgets.QWidget): tree_view.sortByColumn(1, QtCore.Qt.AscendingOrder) tree_view.setAlternatingRowColors(True) tree_view.setIndentation(20) - tree_view.setStyleSheet(""" - QTreeView::item{ - padding: 5px 1px; - border: 0px; - } - """) tree_view.collapseAll() for column_name, width in self.default_widths: From dd0ed45d53efc57de2b7fabb3b08be495d70257f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 15:36:33 +0200 Subject: [PATCH 668/716] changed order of setting stylesheet --- openpype/tools/loader/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 74e121896e..b29f0970ca 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -46,7 +46,6 @@ class LoaderWindow(QtWidgets.QDialog): if project_name: title += " - {}".format(project_name) self.setWindowTitle(title) - self.setStyleSheet(load_stylesheet()) # Groups config self.groups_config = lib.GroupsConfig(io) @@ -191,6 +190,8 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) + self.setStyleSheet(load_stylesheet()) + def resizeEvent(self, event): super(LoaderWindow, self).resizeEvent(event) self._overlay_frame.resize(self.size()) From 740328ef851cd5cc8aa51ec752256b586e27631e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 15:37:08 +0200 Subject: [PATCH 669/716] added parents to delegates --- openpype/tools/loader/widgets.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index f6ba200eff..9a639c3b85 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -182,22 +182,21 @@ class SubsetWidget(QtWidgets.QWidget): view.setObjectName("SubsetView") view.setIndentation(20) view.setAllColumnsShowFocus(True) - - # Set view delegates - version_delegate = VersionDelegate(self.dbcon) - column = model.Columns.index("version") - view.setItemDelegateForColumn(column, version_delegate) - - time_delegate = PrettyTimeDelegate() - column = model.Columns.index("time") - view.setItemDelegateForColumn(column, time_delegate) - view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) view.setSortingEnabled(True) view.sortByColumn(1, QtCore.Qt.AscendingOrder) view.setAlternatingRowColors(True) + # Set view delegates + version_delegate = VersionDelegate(self.dbcon, view) + column = model.Columns.index("version") + view.setItemDelegateForColumn(column, version_delegate) + + time_delegate = PrettyTimeDelegate(view) + column = model.Columns.index("time") + view.setItemDelegateForColumn(column, time_delegate) + layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(top_bar_layout) From b888240bdb9b66e224e09bf445ba8b815f3e0837 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 15:37:42 +0200 Subject: [PATCH 670/716] use better margins for optional items --- openpype/tools/utils/widgets.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index 878a9b7c86..de75de705b 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -407,14 +407,13 @@ class OptionalActionWidget(QtWidgets.QWidget): option.setFixedSize(30, 30) body_layout = QtWidgets.QHBoxLayout(body_widget) - body_layout.setContentsMargins(0, 0, 0, 0) + body_layout.setContentsMargins(4, 0, 4, 0) body_layout.setSpacing(2) body_layout.addWidget(icon) body_layout.addWidget(label) - body_layout.addSpacing(6) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(6, 1, 2, 1) + layout.setContentsMargins(2, 1, 2, 1) layout.setSpacing(0) layout.addWidget(body_widget) layout.addWidget(option) From 7d70d22e2d76bdb0a7aa6c936e0c8284b7c63839 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 17:31:21 +0200 Subject: [PATCH 671/716] moved set of stylesheet after show of tool --- openpype/tools/utils/host_tools.py | 48 ++++++++++++++++++------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index ee184ccf2d..599c25d6c8 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -40,7 +40,6 @@ class HostToolsHelper: def get_workfiles_tool(self, parent): """Create, cache and return workfiles tool window.""" if self._workfiles_tool is None: - from avalon import style from openpype.tools.workfiles.app import ( Window, validate_host_requirements ) @@ -49,13 +48,14 @@ class HostToolsHelper: validate_host_requirements(host) workfiles_window = Window(parent=parent) - workfiles_window.setStyleSheet(style.load_stylesheet()) self._workfiles_tool = workfiles_window return self._workfiles_tool def show_workfiles(self, parent=None, use_context=None, save=None): """Workfiles tool for changing context and saving workfiles.""" + from avalon import style + if use_context is None: use_context = True @@ -79,24 +79,30 @@ class HostToolsHelper: # Pull window to the front. workfiles_tool.raise_() workfiles_tool.activateWindow() + workfiles_tool.setStyleSheet(style.load_stylesheet()) def get_loader_tool(self, parent): """Create, cache and return loader tool window.""" if self._loader_tool is None: - from avalon import style from openpype.tools.loader import LoaderWindow loader_window = LoaderWindow(parent=parent or self._parent) - loader_window.setStyleSheet(style.load_stylesheet()) self._loader_tool = loader_window return self._loader_tool def show_loader(self, parent=None, use_context=None): """Loader tool for loading representations.""" + from avalon import style + + loader_tool = self.get_loader_tool(parent) + + loader_tool.show() + loader_tool.raise_() + loader_tool.activateWindow() + if use_context is None: use_context = False - loader_tool = self.get_loader_tool(parent) if use_context: context = {"asset": avalon.api.Session["AVALON_ASSET"]} @@ -104,29 +110,28 @@ class HostToolsHelper: else: loader_tool.refresh() - loader_tool.show() - loader_tool.raise_() - loader_tool.activateWindow() - loader_tool.refresh() + loader_tool.setStyleSheet(style.load_stylesheet()) def get_creator_tool(self, parent): """Create, cache and return creator tool window.""" if self._creator_tool is None: - from avalon import style from avalon.tools.creator.app import Window creator_window = Window(parent=parent or self._parent) - creator_window.setStyleSheet(style.load_stylesheet()) self._creator_tool = creator_window return self._creator_tool def show_creator(self, parent=None): """Show tool to create new instantes for publishing.""" + from avalon import style + creator_tool = self.get_creator_tool(parent) creator_tool.refresh() creator_tool.show() + creator_tool.setStyleSheet(style.load_stylesheet()) + # Pull window to the front. creator_tool.raise_() creator_tool.activateWindow() @@ -134,20 +139,22 @@ class HostToolsHelper: def get_subset_manager_tool(self, parent): """Create, cache and return subset manager tool window.""" if self._subset_manager_tool is None: - from avalon import style from avalon.tools.subsetmanager import Window subset_manager_window = Window(parent=parent or self._parent) - subset_manager_window.setStyleSheet(style.load_stylesheet()) self._subset_manager_tool = subset_manager_window return self._subset_manager_tool def show_subset_manager(self, parent=None): """Show tool display/remove existing created instances.""" + from avalon import style + subset_manager_tool = self.get_subset_manager_tool(parent) subset_manager_tool.show() + subset_manager_tool.setStyleSheet(style.load_stylesheet()) + # Pull window to the front. subset_manager_tool.raise_() subset_manager_tool.activateWindow() @@ -155,20 +162,21 @@ class HostToolsHelper: def get_scene_inventory_tool(self, parent): """Create, cache and return scene inventory tool window.""" if self._scene_inventory_tool is None: - from avalon import style from avalon.tools.sceneinventory.app import Window scene_inventory_window = Window(parent=parent or self._parent) - scene_inventory_window.setStyleSheet(style.load_stylesheet()) self._scene_inventory_tool = scene_inventory_window return self._scene_inventory_tool def show_scene_inventory(self, parent=None): """Show tool maintain loaded containers.""" + from avalon import style + scene_inventory_tool = self.get_scene_inventory_tool(parent) scene_inventory_tool.show() scene_inventory_tool.refresh() + scene_inventory_tool.setStyleSheet(style.load_stylesheet()) # Pull window to the front. scene_inventory_tool.raise_() @@ -177,24 +185,25 @@ class HostToolsHelper: def get_library_loader_tool(self, parent): """Create, cache and return library loader tool window.""" if self._library_loader_tool is None: - from avalon import style from openpype.tools.libraryloader import LibraryLoaderWindow library_window = LibraryLoaderWindow( parent=parent or self._parent ) - library_window.setStyleSheet(style.load_stylesheet()) self._library_loader_tool = library_window return self._library_loader_tool def show_library_loader(self, parent=None): """Loader tool for loading representations from library project.""" + from avalon import style + library_loader_tool = self.get_library_loader_tool(parent) library_loader_tool.show() library_loader_tool.raise_() library_loader_tool.activateWindow() library_loader_tool.refresh() + library_loader_tool.setStyleSheet(style.load_stylesheet()) def show_publish(self, parent=None): """Publish UI.""" @@ -205,18 +214,19 @@ class HostToolsHelper: def get_look_assigner_tool(self, parent): """Create, cache and return look assigner tool window.""" if self._look_assigner_tool is None: - from avalon import style import mayalookassigner mayalookassigner_window = mayalookassigner.App(parent) - mayalookassigner_window.setStyleSheet(style.load_stylesheet()) self._look_assigner_tool = mayalookassigner_window return self._look_assigner_tool def show_look_assigner(self, parent=None): """Look manager is Maya specific tool for look management.""" + from avalon import style + look_assigner_tool = self.get_look_assigner_tool(parent) look_assigner_tool.show() + look_assigner_tool.setStyleSheet(style.load_stylesheet()) def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. From 961a602e1c0d2bdd33c2b7b8e3fbe2ee762db5b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 17:56:42 +0200 Subject: [PATCH 672/716] fix hovering stylesheet of optional action --- openpype/style/style.css | 8 ++++++ openpype/tools/utils/widgets.py | 47 ++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 6507cbe63b..948ee8c7b7 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -634,3 +634,11 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { padding: 5px 1px; border: 0px; } + +#OptionalActionBody, #OptionalActionOption { + background: transparent; +} + +#OptionalActionBody[state="hover"], #OptionalActionOption[state="hover"] { + background: {color:bg-view-hover}; +} diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index de75de705b..15bcbeff90 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -310,7 +310,6 @@ class OptionalMenu(QtWidgets.QMenu): actions that were instances of `QtWidgets.QWidgetAction`. """ - def mouseReleaseEvent(self, event): """Emit option clicked signal if mouse released on it""" active = self.actionAt(event.pos()) @@ -349,6 +348,7 @@ class OptionalAction(QtWidgets.QWidgetAction): self.use_option = use_option self.option_tip = "" self.optioned = False + self.widget = None def createWidget(self, parent): widget = OptionalActionWidget(self.label, parent) @@ -374,20 +374,10 @@ class OptionalAction(QtWidgets.QWidgetAction): self.optioned = True def set_highlight(self, state, global_pos=None): - body = self.widget.body - option = self.widget.option - - role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window - body.setBackgroundRole(role) - body.setAutoFillBackground(state) - - if not self.use_option: - return - - state = option.is_hovered(global_pos) - role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window - option.setBackgroundRole(role) - option.setAutoFillBackground(state) + option_state = False + if self.use_option: + option_state = self.widget.option.is_hovered(global_pos) + self.widget.set_hover_properties(state, option_state) class OptionalActionWidget(QtWidgets.QWidget): @@ -397,11 +387,15 @@ class OptionalActionWidget(QtWidgets.QWidget): super(OptionalActionWidget, self).__init__(parent) body_widget = QtWidgets.QWidget(self) - body_widget.setStyleSheet("background: transparent;") + body_widget.setObjectName("OptionalActionBody") icon = QtWidgets.QLabel(body_widget) label = QtWidgets.QLabel(label, body_widget) + # (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke. + # See https://stackoverflow.com/q/52838690/4145300 + label.setStyle(QtWidgets.QStyleFactory.create("Plastique")) option = OptionBox(body_widget) + option.setObjectName("OptionalActionOption") icon.setFixedSize(24, 16) option.setFixedSize(30, 30) @@ -429,9 +423,22 @@ class OptionalActionWidget(QtWidgets.QWidget): self.option = option self.body = body_widget - # (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke. - # See https://stackoverflow.com/q/52838690/4145300 - label.setStyle(QtWidgets.QStyleFactory.create("Plastique")) + def set_hover_properties(self, hovered, option_hovered): + body_state = "" + option_state = "" + if hovered: + body_state = "hover" + + if option_hovered: + option_state = "hover" + + if self.body.property("state") != body_state: + self.body.setProperty("state", body_state) + self.body.style().polish(self.body) + + if self.option.property("state") != option_state: + self.option.setProperty("state", option_state) + self.option.style().polish(self.option) def setIcon(self, icon): pixmap = icon.pixmap(16, 16) @@ -452,8 +459,6 @@ class OptionBox(QtWidgets.QLabel): pixmap = icon.pixmap(18, 18) self.setPixmap(pixmap) - self.setStyleSheet("background: transparent;") - def is_hovered(self, global_pos): if global_pos is None: return False From bcb27d5aebe6ccc8d5cae84dd4177f8d0c4aa7de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 18:24:18 +0200 Subject: [PATCH 673/716] changed new thumbnail --- .../tools/loader/images/default_thumbnail.png | Bin 4018 -> 5118 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openpype/tools/loader/images/default_thumbnail.png b/openpype/tools/loader/images/default_thumbnail.png index 97bd958e0daf01e740db9f426e1f9b3e22484c87..adea862e5b7169f8279bd398226a2fe6283e4925 100644 GIT binary patch literal 5118 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 aQ8TBK`_#VgnlokXr%hW!U;VAB9rQ1Cc_|D4 From acf88c7f16488da71ec5187025382a80c946919a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 18:35:09 +0200 Subject: [PATCH 674/716] alpha can have float number in rgba --- openpype/style/color_defs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/style/color_defs.py b/openpype/style/color_defs.py index 4d726cc3f3..3f504a9d3b 100644 --- a/openpype/style/color_defs.py +++ b/openpype/style/color_defs.py @@ -155,7 +155,10 @@ class RGBAColor: red = int(red_str) green = int(green_str) blue = int(blue_str) - alpha = int(alpha_str) + if "." in alpha_str: + alpha = int(float(alpha_str) * 100) + else: + alpha = int(alpha_str) int_validation(red, 0, 255) int_validation(green, 0, 255) From 0f62d59d5d89ecc19375bbc3dfa1c410c6e0efdb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 18:35:33 +0200 Subject: [PATCH 675/716] use colors in asset delegate from openpype style --- openpype/style/data.json | 10 +++++++++- openpype/tools/utils/delegates.py | 23 ++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 5cac7e07db..143c6695af 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -47,6 +47,14 @@ "border": "#373D48", "border-hover": "rgba(168, 175, 189, .3)", - "border-focus": "hsl(200, 60%, 60%)" + "border-focus": "hsl(200, 60%, 60%)", + + "loader": { + "asset-view": { + "selected": "rgba(168, 175, 189, 0.6)", + "hover": "rgba(168, 175, 189, 0.3)", + "selected-hover": "rgba(168, 175, 189, 0.7)" + } + } } } diff --git a/openpype/tools/utils/delegates.py b/openpype/tools/utils/delegates.py index 1827bc7e9b..96353c44c6 100644 --- a/openpype/tools/utils/delegates.py +++ b/openpype/tools/utils/delegates.py @@ -7,6 +7,7 @@ import Qt from Qt import QtWidgets, QtGui, QtCore from avalon.lib import HeroVersionType +from openpype.style import get_objected_colors from .models import ( AssetModel, TreeModel @@ -24,6 +25,19 @@ log = logging.getLogger(__name__) class AssetDelegate(QtWidgets.QItemDelegate): bar_height = 3 + def __init__(self, *args, **kwargs): + super(AssetDelegate, self).__init__(*args, **kwargs) + asset_view_colors = get_objected_colors()["loader"]["asset-view"] + self._selected_color = ( + asset_view_colors["selected"].get_qcolor() + ) + self._hover_color = ( + asset_view_colors["hover"].get_qcolor() + ) + self._selected_hover_color = ( + asset_view_colors["selected-hover"].get_qcolor() + ) + def sizeHint(self, option, index): result = super(AssetDelegate, self).sizeHint(option, index) height = result.height() @@ -66,17 +80,20 @@ class AssetDelegate(QtWidgets.QItemDelegate): counter += 1 # Background - bg_color = QtGui.QColor(60, 60, 60) if option.state & QtWidgets.QStyle.State_Selected: if len(subset_colors) == 0: item_rect.setTop(item_rect.top() + (self.bar_height / 2)) + if option.state & QtWidgets.QStyle.State_MouseOver: - bg_color.setRgb(70, 70, 70) + bg_color = self._selected_hover_color + else: + bg_color = self._selected_color else: item_rect.setTop(item_rect.top() + (self.bar_height / 2)) if option.state & QtWidgets.QStyle.State_MouseOver: - bg_color.setAlpha(100) + bg_color = self._hover_color else: + bg_color = QtGui.QColor() bg_color.setAlpha(0) # When not needed to do a rounded corners (easier and without From ba97477f1102551bb32cecbf0747a9d064238d5c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 20 Oct 2021 19:26:19 +0200 Subject: [PATCH 676/716] fix resources for maya --- openpype/style/pyside2_resources.py | 929 ++++++++++++++-------------- 1 file changed, 453 insertions(+), 476 deletions(-) diff --git a/openpype/style/pyside2_resources.py b/openpype/style/pyside2_resources.py index ee68a74b8e..c7328e7c91 100644 --- a/openpype/style/pyside2_resources.py +++ b/openpype/style/pyside2_resources.py @@ -1,24 +1,15 @@ -# Resource object code (Python 3) -# Created by: object code -# Created by: The Resource Compiler for Qt version 5.15.2 +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created: Wed Oct 20 19:25:24 2021 +# by: The Resource Compiler for PySide2 (Qt v5.12.5) +# # WARNING! All changes made in this file will be lost! from PySide2 import QtCore - qt_resource_data = b"\ -\x00\x00\x00\x9f\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x14\x1f\xf9\ -#\xd9\x0b\x00\x00\x00#IDAT\x08\xd7c`\xc0\ -\x0d\xe6|\x80\xb1\x18\x91\x05R\x04\xe0B\x08\x15)\x02\ -\x0c\x0c\x8c\xc8\x02\x08\x95h\x00\x00\xac\xac\x07\x90Ne\ -4\xac\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x00\xa6\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -45,31 +36,6 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ 200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ \xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ \xaeB`\x82\ -\x00\x00\x00\xa5\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ -\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ -200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ -\xaeB`\x82\ -\x00\x00\x00\xa0\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f\x0d\xfc\ -R+\x9c\x00\x00\x00$IDAT\x08\xd7c`@\ -\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\ -\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\ -\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x00\xa6\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -83,158 +49,43 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ d``b``4D\xe2 s\x19\x90\x8d@\x02\ \x00d@\x09u\x86\xb3\xad\x9c\x00\x00\x00\x00IEN\ D\xaeB`\x82\ -\x00\x00\x00\x9e\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ +\x00\x00\x00\x9f\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ -\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ -\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ -\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ -\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\x9e\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ -\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ -\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ -\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ -\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x07\x06\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ -\x00\x00\x04\xb0iTXtXML:com.\ -adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0a\x85\x9d\x9f\x08\x00\x00\x01\x83\ -iCCPsRGB IEC6196\ -6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ -\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ -\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\ -x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\ -Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\ -;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\ -\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ -\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\ -\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\ -\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\ -RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\ -?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\ -\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\ -\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\ -\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\ -\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\ -\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\ -\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\ -vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\ -\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\ -8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\ -S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ -\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ -Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00mIDAT\x18\x95u\xcf\xc1\x09\xc2P\ -\x10\x84\xe1\xd7\x85\x07\x9b\xd0C@\xd2\x82x\x14{0\ -W!\x8d\x84`?bKzH\xcc\x97\x83\xfb0\x04\ -\xdf\x9c\x86\x7fg\x99\xdd\x84\x0d\xaaT\x10jl\x13\x1e\ -\xbe\xba\xfe\x0951{\xe6\x8d\x0f&\x1c\x17\xa1S\xb0\ -\x11\x87\x0c/\x01\x07\xec\xb0\x0f?\xe1\xbc\xaei\xa3\xe6\ -\x85w\xf8[\xe9\xf0\xbb\x9f\xfa\xd2\x839\xdc\xa3[\xf3\ -\x19.\xa8\x89\xb50\xf7C\xa0\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ -\x00\x00\x00\xa6\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x14\x1f\xf9\ +#\xd9\x0b\x00\x00\x00#IDAT\x08\xd7c`\xc0\ +\x0d\xe6|\x80\xb1\x18\x91\x05R\x04\xe0B\x08\x15)\x02\ +\x0c\x0c\x8c\xc8\x02\x08\x95h\x00\x00\xac\xac\x07\x90Ne\ +4\xac\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa0\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f \xb9\ -\x8dw\xe9\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x06\xe6|```B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\ -d``b`H\x11@\xe2 s\x19\x90\x8d@\x02\ -\x00#\xed\x08\xafd\x9f\x0f\x15\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ +\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ +\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ +\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x07\xdd\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -363,6 +214,289 @@ zpp\xf0\xe3\x0e.\xa4\xd2\xae\xf0\x8a\xf7\x9a\xe3V\ q[s\x5c@H\xa5\xdda\x81\x0d\x9ek\x8e\xff\xfd\ \xcf?\xcc1\xe9\x01\x1c\x00sR-q\xe4J\x1bi\ \x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f \xb9\ +\x8dw\xe9\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x06\xe6|```B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +d``b`H\x11@\xe2 s\x19\x90\x8d@\x02\ +\x00#\xed\x08\xafd\x9f\x0f\x15\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa0\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ +\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ +\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ +\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x07\x06\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ +\x00\x00\x04\xb0iTXtXML:com.\ +adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0a\x85\x9d\x9f\x08\x00\x00\x01\x83\ +iCCPsRGB IEC6196\ +6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ +\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ +\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\ +x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\ +Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\ +;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\ +\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ +\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\ +\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\ +\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\ +RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\ +?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\ +\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\ +\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\ +\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\ +\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\ +\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\ +\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\ +vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\ +\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\ +8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\ +S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ +\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ +Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00mIDAT\x18\x95u\xcf\xc1\x09\xc2P\ +\x10\x84\xe1\xd7\x85\x07\x9b\xd0C@\xd2\x82x\x14{0\ +W!\x8d\x84`?bKzH\xcc\x97\x83\xfb0\x04\ +\xdf\x9c\x86\x7fg\x99\xdd\x84\x0d\xaaT\x10jl\x13\x1e\ +\xbe\xba\xfe\x0951{\xe6\x8d\x0f&\x1c\x17\xa1S\xb0\ +\x11\x87\x0c/\x01\x07\xec\xb0\x0f?\xe1\xbc\xaei\xa3\xe6\ +\x85w\xf8[\xe9\xf0\xbb\x9f\xfa\xd2\x839\xdc\xa3[\xf3\ +\x19.\xa8\x89\xb50\xf7C\xa0\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ +\x00\x00\x070\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ +\x00\x00\x04\xb0iTXtXML:com.\ +adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0aH\x8b[^\x00\x00\x01\x83\ +iCCPsRGB IEC6196\ +6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ +\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ +\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\ +x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\ +Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\ +;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\ +\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ +\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\ +\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\ +\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\ +RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\ +?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\ +\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\ +\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\ +\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\ +\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\ +\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\ +\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\ +vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\ +\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\ +8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\ +S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ +\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ +Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x97IDAT\x18\x95m\xcf\xb1j\x02A\ +\x14\x85\xe1o\xb7\xb6\xd0'H=Vi\x03\xb1\xb4H\ +;l\xa5\xf19\xf6Y\x02VB\xbaa\x0a\x0b;\x1b\ +\x1bkA\x18\x02)m\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\ +\x9f\xff\x5c\xee\xa9b*\x13Ls\x13nF&\xa6\xf2\ +\x82\xaeF\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a[t\ +\xd8\xc7T\xc2@\x9ac\x8f?|U=|\xc5\x09w\ +\xbc\xa1\xc2\x193,r\x13.\xd5\xe0\xc2\x12\x07\x5cQ\ +#\xe0#7\xe1\xa8O\x0e\x7f\xda`\xd7\xaf\x9f\xb9\x09\ +\xdfc\x05\xff\xe5uLe\xf5\xcc\x1f\x0d3,\x83\xb6\ +\x06D\x83\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x07\xad\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -488,6 +622,55 @@ v)`\x8b\x07>\xa8\xe6\xd1\xfe\x0b\x9d\x85\x8eW\x0d\ ^x\xa2\x9e\x0e\xa7 tG9\x1d\xf6\xe1\x95+\xd6\ \xb1D\x8e\x0e\xcbX\xf0\x0fR\x8ay\x18\xdc\xe2\x02p\ \x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa0\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f\x0d\xfc\ +R+\x9c\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\ +\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\ +\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x9e\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ +\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ +\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x9e\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ +\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ +\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ \x00\x00\x00\xa6\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -501,186 +684,6 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ d``b``4D\xe2 s\x19\x90\x8d@\x02\ \x00d@\x09u\x86\xb3\xad\x9c\x00\x00\x00\x00IEN\ D\xaeB`\x82\ -\x00\x00\x00\xa5\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ -\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ -200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ -\xaeB`\x82\ -\x00\x00\x00\xa0\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ -\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ -\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ -\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ -\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x070\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ -\x00\x00\x04\xb0iTXtXML:com.\ -adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0aH\x8b[^\x00\x00\x01\x83\ -iCCPsRGB IEC6196\ -6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ -\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ -\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\ -x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\ -Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\ -;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\ -\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ -\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\ -\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\ -\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\ -RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\ -?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\ -\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\ -\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\ -\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\ -\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\ -\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\ -\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\ -vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\ -\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\ -8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\ -S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ -\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ -Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x97IDAT\x18\x95m\xcf\xb1j\x02A\ -\x14\x85\xe1o\xb7\xb6\xd0'H=Vi\x03\xb1\xb4H\ -;l\xa5\xf19\xf6Y\x02VB\xbaa\x0a\x0b;\x1b\ -\x1bkA\x18\x02)m\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\ -\x9f\xff\x5c\xee\xa9b*\x13Ls\x13nF&\xa6\xf2\ -\x82\xaeF\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a[t\ -\xd8\xc7T\xc2@\x9ac\x8f?|U=|\xc5\x09w\ -\xbc\xa1\xc2\x193,r\x13.\xd5\xe0\xc2\x12\x07\x5cQ\ -#\xe0#7\xe1\xa8O\x0e\x7f\xda`\xd7\xaf\x9f\xb9\x09\ -\xdfc\x05\xff\xe5uLe\xf5\xcc\x1f\x0d3,\x83\xb6\ -\x06D\x83\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\xa6\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ -;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ -\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ -\x00\x00\x00\xa0\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ -\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ -\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ -\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ -\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\xa6\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ -;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ -\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ " qt_resource_name = b"\ @@ -693,61 +696,15 @@ qt_resource_name = b"\ \x00i\ \x00m\x00a\x00g\x00e\x00s\ \x00\x15\ -\x0f\xf3\xc0\x07\ -\x00u\ -\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\ -\x00.\x00p\x00n\x00g\ -\x00\x12\ -\x01.\x03'\ +\x03'rg\ \x00c\ -\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\ -\x00g\ -\x00\x0e\ -\x04\xa2\xfc\xa7\ -\x00d\ -\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\ +\x00.\x00p\x00n\x00g\ \x00\x1b\ \x03Z2'\ \x00c\ \x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\ \x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ -\x00\x18\ -\x03\x8e\xdeg\ -\x00r\ -\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\ -\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ -\x00\x11\ -\x00\xb8\x8c\x07\ -\x00l\ -\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ -\ -\x00\x0f\ -\x01s\x8b\x07\ -\x00u\ -\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ -\x00\x0c\ -\x06\xe6\xe6g\ -\x00u\ -\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ -\x00\x0f\ -\x06S%\xa7\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\ -\x00\x17\ -\x0ce\xce\x07\ -\x00l\ -\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ -\x00e\x00d\x00.\x00p\x00n\x00g\ -\x00\x14\ -\x04^-\xa7\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00_\x00o\x00n\x00.\ -\x00p\x00n\x00g\ -\x00\x11\ -\x0b\xda0\xa7\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00.\x00p\x00n\x00g\ -\ \x00\x0e\ \x0e\xde\xfa\xc7\ \x00l\ @@ -757,87 +714,107 @@ qt_resource_name = b"\ \x00d\ \x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ \ -\x00\x0f\ -\x02\x9f\x05\x87\ -\x00r\ -\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ -\x00\x12\ -\x05\x8f\x9d\x07\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00_\x00o\x00n\x00.\x00p\x00n\ -\x00g\ -\x00\x17\ -\x0c\xabQ\x07\ -\x00d\ -\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ -\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x15\ +\x0f\xf3\xc0\x07\ +\x00u\ +\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\ +\x00.\x00p\x00n\x00g\ \x00\x12\ \x03\x8d\x04G\ \x00r\ \x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\ \x00g\ -\x00\x15\ -\x03'rg\ +\x00\x14\ +\x04^-\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00_\x00o\x00n\x00.\ +\x00p\x00n\x00g\ +\x00\x17\ +\x0ce\xce\x07\ +\x00l\ +\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ +\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x02\x9f\x05\x87\ +\x00r\ +\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x06S%\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\ +\x00\x12\ +\x01.\x03'\ \x00c\ -\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\ -\x00.\x00p\x00n\x00g\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\ +\x00g\ +\x00\x0e\ +\x04\xa2\xfc\xa7\ +\x00d\ +\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x12\ +\x05\x8f\x9d\x07\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00_\x00o\x00n\x00.\x00p\x00n\ +\x00g\ +\x00\x11\ +\x0b\xda0\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00.\x00p\x00n\x00g\ +\ +\x00\x18\ +\x03\x8e\xdeg\ +\x00r\ +\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\ +\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x01s\x8b\x07\ +\x00u\ +\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ +\x00\x0c\ +\x06\xe6\xe6g\ +\x00u\ +\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x17\ +\x0c\xabQ\x07\ +\x00d\ +\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ +\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x11\ +\x00\xb8\x8c\x07\ +\x00l\ +\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ +\ " qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x00\x03C\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x00\x1d!\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x00X\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa3\ -\x00\x00\x01y\xb4r\xcc\x9c\ -\x00\x00\x01>\x00\x00\x00\x00\x00\x01\x00\x00\x03\xed\ -\x00\x00\x01vA\x9d\xa29\ -\x00\x00\x02x\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xca\ -\x00\x00\x01vA\x9d\xa27\ -\x00\x00\x03$\x00\x00\x00\x00\x00\x01\x00\x00&\xf0\ -\x00\x00\x01y\xb4r\xcc\x9c\ -\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x01\xf6\ -\x00\x00\x01y\xb4r\xcc\x9c\ -\x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00&L\ -\x00\x00\x01vA\x9d\xa27\ -\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x02\x9f\ -\x00\x00\x01vA\x9d\xa27\ -\x00\x00\x01\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xe5\ -\x00\x00\x01y\xc2\x05+`\ -\x00\x00\x00\x82\x00\x00\x00\x00\x00\x01\x00\x00\x01M\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x02\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x1en\ -\x00\x00\x01y\xc1\xfc\x16\x91\ -\x00\x00\x01\x80\x00\x00\x00\x00\x00\x01\x00\x00\x051\ -\x00\x00\x01y\xc1\xf9Kx\ -\x00\x00\x01b\x00\x00\x00\x00\x00\x01\x00\x00\x04\x8f\ -\x00\x00\x01vA\x9d\xa29\ -\x00\x00\x02\x06\x00\x00\x00\x00\x00\x01\x00\x00\x14\xc6\ -\x00\x00\x01y\xc2\x05\x91*\ -\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x0c;\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x02\xc6\x00\x00\x00\x00\x00\x01\x00\x00%\xa2\ -\x00\x00\x01vA\x9d\xa25\ -\x00\x00\x02.\x00\x00\x00\x00\x00\x01\x00\x00\x1cw\ -\x00\x00\x01vA\x9d\xa25\ +\x00\x00\x03,\x00\x00\x00\x00\x00\x01\x00\x00&\xf0\ +\x00\x00\x00\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x01\xfd\ +\x00\x00\x01\xe2\x00\x00\x00\x00\x00\x01\x00\x00\x14&\ +\x00\x00\x02\xb6\x00\x00\x00\x00\x00\x01\x00\x00%\x02\ +\x00\x00\x01\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x0cx\ \x00\x00\x00(\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01vA\x9d\xa29\ +\x00\x00\x00X\x00\x00\x00\x00\x00\x01\x00\x00\x00\xaa\ +\x00\x00\x01\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x03I\ +\x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x00$^\ +\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x00\x03\xed\ +\x00\x00\x02\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x14\xd0\ +\x00\x00\x02.\x00\x00\x00\x00\x00\x01\x00\x00\x15y\ +\x00\x00\x01\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x0d\x1c\ +\x00\x00\x02\xda\x00\x00\x00\x00\x00\x01\x00\x00%\xa4\ +\x00\x00\x02X\x00\x00\x00\x00\x00\x01\x00\x00\x1c\xad\ +\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xce\ +\x00\x00\x02\xf8\x00\x00\x00\x00\x00\x01\x00\x00&F\ +\x00\x00\x00\x94\x00\x00\x00\x00\x00\x01\x00\x00\x01S\ +\x00\x00\x00\xde\x00\x00\x00\x00\x00\x01\x00\x00\x02\xa6\ " - def qInitResources(): - QtCore.qRegisterResourceData( - 0x03, qt_resource_struct, qt_resource_name, qt_resource_data - ) - + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) def qCleanupResources(): - QtCore.qUnregisterResourceData( - 0x03, qt_resource_struct, qt_resource_name, qt_resource_data - ) + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() From 8d441da917e6601cb6a249937e3693e436a7c6b4 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 21 Oct 2021 09:59:43 +0200 Subject: [PATCH 677/716] update google analytics --- website/docusaurus.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 3ce1cde060..ddbcfd9ce7 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -116,8 +116,8 @@ module.exports = { // Optional: Algolia search parameters searchParameters: {}, }, - googleAnalytics: { - trackingID: 'G-HHJZ9VF0FG', + gtag: { + trackingID: 'G-DTKXMFENFY', // Optional fields. anonymizeIP: false, // Should IPs be anonymized? }, From 62c411f585ee59c5fc53c22f7a38bde825b56e52 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 21 Oct 2021 10:06:32 +0200 Subject: [PATCH 678/716] fix typo in ci --- tools/ci_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci_tools.py b/tools/ci_tools.py index 337b19a346..e5ca0c2c28 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -27,7 +27,7 @@ def get_release_type_github(Log, github_token): return "minor" if any(label in labels for label in patch_labels): - return "path" + return "patch" return None From 08971bb73ccd34e2f787d6c3c1042e2a9fbc9ca7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Oct 2021 08:23:02 +0000 Subject: [PATCH 679/716] Bump pillow from 8.2.0 to 8.3.2 Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.2.0 to 8.3.2. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/8.2.0...8.3.2) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 94 +++++++++++++++++++++++++++++++------------------- pyproject.toml | 2 +- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/poetry.lock b/poetry.lock index e5f5919a01..36105f4213 100644 --- a/poetry.lock +++ b/poetry.lock @@ -782,7 +782,7 @@ six = "*" [[package]] name = "pillow" -version = "8.2.0" +version = "8.3.2" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -1538,7 +1538,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "ff2bfa35a7304378917a0c25d7d7af9f81a130288d95789bdf7429f071e80b69" +content-hash = "fb6db80d126fe7ef2d1d06d0381b6d11445d6d3e54b33585f6b0a0b6b0b9d372" [metadata.files] acre = [] @@ -2058,40 +2058,59 @@ pathlib2 = [ {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, ] pillow = [ - {file = "Pillow-8.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9"}, - {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b"}, - {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b"}, - {file = "Pillow-8.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9"}, - {file = "Pillow-8.2.0-cp36-cp36m-win32.whl", hash = "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727"}, - {file = "Pillow-8.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f"}, - {file = "Pillow-8.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d"}, - {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a"}, - {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9"}, - {file = "Pillow-8.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388"}, - {file = "Pillow-8.2.0-cp37-cp37m-win32.whl", hash = "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5"}, - {file = "Pillow-8.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2"}, - {file = "Pillow-8.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4"}, - {file = "Pillow-8.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812"}, - {file = "Pillow-8.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178"}, - {file = "Pillow-8.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb"}, - {file = "Pillow-8.2.0-cp38-cp38-win32.whl", hash = "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232"}, - {file = "Pillow-8.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797"}, - {file = "Pillow-8.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5"}, - {file = "Pillow-8.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484"}, - {file = "Pillow-8.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602"}, - {file = "Pillow-8.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2"}, - {file = "Pillow-8.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef"}, - {file = "Pillow-8.2.0-cp39-cp39-win32.whl", hash = "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713"}, - {file = "Pillow-8.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c"}, - {file = "Pillow-8.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9"}, - {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9"}, - {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"}, - {file = "Pillow-8.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8b56553c0345ad6dcb2e9b433ae47d67f95fc23fe28a0bde15a120f25257e291"}, - {file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, + {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, + {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, + {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, + {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, + {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, + {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, + {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, + {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, + {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, + {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, + {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, + {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, + {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, @@ -2294,6 +2313,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-1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:ed708cc47bae8b711f81f252af09898a5f986c7a38cec5ad5623d571d328bff8"}, {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"}, @@ -2303,6 +2323,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-1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d999387927284346035cb63ebb51f86331abc41f9376f9a6970e7f18207db392"}, {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"}, @@ -2312,6 +2333,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-1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d94a3ed7051266c52392ec07d3b5adbf28d4be83341a24df0d88639344dcd84f"}, {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"}, diff --git a/pyproject.toml b/pyproject.toml index 085538d306..dade0a2f57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ jsonschema = "^3.2.0" keyring = "^22.0.1" log4mongo = "^1.7" pathlib2= "^2.3.5" # deadline submit publish job only (single place, maybe not needed?) -Pillow = "^8.1" # only used for slates prototype +Pillow = "^8.3" # only used for slates prototype pyblish-base = "^1.8.8" pynput = "^1.7.2" # idle manager in tray pymongo = "^3.11.2" From e47ed5e77713a970c3332cbc0ecf14be9e14cece Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 10:41:48 +0200 Subject: [PATCH 680/716] safer library loader action handling --- openpype/modules/default_modules/avalon_apps/avalon_app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index d21b37e520..a8e102f447 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -70,6 +70,9 @@ class AvalonModule(OpenPypeModule, ITrayModule): # Definition of Tray menu def tray_menu(self, tray_menu): + if self.libraryloader is None: + return + from Qt import QtWidgets # Actions action_library_loader = QtWidgets.QAction( @@ -87,6 +90,9 @@ class AvalonModule(OpenPypeModule, ITrayModule): return def show_library_loader(self): + if self.libraryloader is None: + return + self.libraryloader.show() # Raise and activate the window From 68ff9ea9c963df63a42594e0578c8bd7a210c88b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 10:43:02 +0200 Subject: [PATCH 681/716] converted library loader to be created same way as loader --- openpype/tools/libraryloader/app.py | 335 ++++++++++++++-------------- 1 file changed, 172 insertions(+), 163 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 3f11157418..69c5cb61e7 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -2,8 +2,8 @@ import sys from Qt import QtWidgets, QtCore, QtGui -from avalon import style from avalon.api import AvalonMongoDB +from openpype import style from openpype.tools.utils import lib as tools_lib from openpype.tools.loader.widgets import ( ThumbnailWidget, @@ -28,6 +28,8 @@ class LibraryLoaderWindow(QtWidgets.QDialog): tool_title = "Library Loader 0.5" tool_name = "library_loader" + message_timeout = 5000 + def __init__( self, parent=None, icon=None, show_projects=False, show_libraries=True ): @@ -36,6 +38,20 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self._initial_refresh = False self._ignore_project_change = False + dbcon = AvalonMongoDB() + dbcon.install() + dbcon.Session["AVALON_PROJECT"] = None + + self.dbcon = dbcon + + self.show_projects = show_projects + self.show_libraries = show_libraries + + # Groups config + self.groups_config = tools_lib.GroupsConfig(dbcon) + self.family_config_cache = tools_lib.FamilyConfigCache(dbcon) + + # UI initialization # Enable minimize and maximize for app self.setWindowTitle(self.tool_title) window_flags = QtCore.Qt.Window @@ -43,140 +59,149 @@ class LibraryLoaderWindow(QtWidgets.QDialog): window_flags |= QtCore.Qt.WindowStaysOnTopHint self.setWindowFlags(window_flags) self.setFocusPolicy(QtCore.Qt.StrongFocus) - if icon is not None: - self.setWindowIcon(icon) - # self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - body = QtWidgets.QWidget() - footer = QtWidgets.QWidget() - footer.setFixedHeight(20) + icon = QtGui.QIcon(style.app_icon_path()) + self.setWindowIcon(icon) - container = QtWidgets.QWidget() + main_splitter = QtWidgets.QSplitter(self) - self.dbcon = AvalonMongoDB() - self.dbcon.install() - self.dbcon.Session["AVALON_PROJECT"] = None + # --- Left part --- + left_side_splitter = QtWidgets.QSplitter(main_splitter) + left_side_splitter.setOrientation(QtCore.Qt.Vertical) - self.show_projects = show_projects - self.show_libraries = show_libraries + # Project combobox + projects_combobox = QtWidgets.QComboBox(left_side_splitter) + combobox_delegate = QtWidgets.QStyledItemDelegate(self) + projects_combobox.setItemDelegate(combobox_delegate) - # Groups config - self.groups_config = tools_lib.GroupsConfig(self.dbcon) - self.family_config_cache = tools_lib.FamilyConfigCache(self.dbcon) - - assets = AssetWidget( - self.dbcon, multiselection=True, parent=self + # Assets widget + assets_widget = AssetWidget( + dbcon, multiselection=True, parent=left_side_splitter ) - families = FamilyListView( - self.dbcon, self.family_config_cache, parent=self + + # Families widget + families_filter_view = FamilyListView( + dbcon, self.family_config_cache, left_side_splitter ) - subsets = LibrarySubsetWidget( - self.dbcon, + left_side_splitter.addWidget(projects_combobox) + left_side_splitter.addWidget(assets_widget) + left_side_splitter.addWidget(families_filter_view) + left_side_splitter.setStretchFactor(1, 65) + left_side_splitter.setStretchFactor(2, 35) + + # --- Middle part --- + # Subsets widget + subsets_widget = LibrarySubsetWidget( + dbcon, self.groups_config, self.family_config_cache, tool_name=self.tool_name, parent=self ) - version = VersionWidget(self.dbcon) - thumbnail = ThumbnailWidget(self.dbcon) - - # Project - self.combo_projects = QtWidgets.QComboBox() - - # Create splitter to show / hide family filters - asset_filter_splitter = QtWidgets.QSplitter() - asset_filter_splitter.setOrientation(QtCore.Qt.Vertical) - asset_filter_splitter.addWidget(self.combo_projects) - asset_filter_splitter.addWidget(assets) - asset_filter_splitter.addWidget(families) - asset_filter_splitter.setStretchFactor(1, 65) - asset_filter_splitter.setStretchFactor(2, 35) - - manager = ModulesManager() - sync_server = manager.modules_by_name["sync_server"] - - representations = RepresentationWidget(self.dbcon) - thumb_ver_splitter = QtWidgets.QSplitter() + # --- Right part --- + thumb_ver_splitter = QtWidgets.QSplitter(main_splitter) thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical) - thumb_ver_splitter.addWidget(thumbnail) - thumb_ver_splitter.addWidget(version) - if sync_server.enabled: - thumb_ver_splitter.addWidget(representations) + + thumbnail_widget = ThumbnailWidget(dbcon, parent=thumb_ver_splitter) + version_info_widget = VersionWidget(dbcon, parent=thumb_ver_splitter) + + thumb_ver_splitter.addWidget(thumbnail_widget) + thumb_ver_splitter.addWidget(version_info_widget) + thumb_ver_splitter.setStretchFactor(0, 30) thumb_ver_splitter.setStretchFactor(1, 35) - container_layout = QtWidgets.QHBoxLayout(container) - container_layout.setContentsMargins(0, 0, 0, 0) - split = QtWidgets.QSplitter() - split.addWidget(asset_filter_splitter) - split.addWidget(subsets) - split.addWidget(thumb_ver_splitter) - split.setSizes([180, 950, 200]) - container_layout.addWidget(split) + manager = ModulesManager() + sync_server = manager.modules_by_name.get("sync_server") + sync_server_enabled = False + if sync_server is not None: + sync_server_enabled = sync_server.enabled - body_layout = QtWidgets.QHBoxLayout(body) - body_layout.addWidget(container) - body_layout.setContentsMargins(0, 0, 0, 0) + repres_widget = None + if sync_server_enabled: + repres_widget = RepresentationWidget( + dbcon, self.tool_name, parent=thumb_ver_splitter + ) + thumb_ver_splitter.addWidget(repres_widget) - message = QtWidgets.QLabel() - message.hide() + main_splitter.addWidget(left_side_splitter) + main_splitter.addWidget(subsets_widget) + main_splitter.addWidget(thumb_ver_splitter) - footer_layout = QtWidgets.QVBoxLayout(footer) - footer_layout.addWidget(message) + # --- Footer --- + footer_widget = QtWidgets.QWidget(self) + footer_widget.setFixedHeight(20) + + message_label = QtWidgets.QLabel(footer_widget) + + footer_layout = QtWidgets.QVBoxLayout(footer_widget) footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addWidget(message_label) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(body) - layout.addWidget(footer) + layout.addWidget(main_splitter) + layout.addWidget(footer_widget) self.data = { - "widgets": { - "families": families, - "assets": assets, - "subsets": subsets, - "version": version, - "thumbnail": thumbnail, - "representations": representations - }, - "label": { - "message": message, - }, "state": { "assetIds": None } } - families.active_changed.connect(subsets.set_family_filters) - assets.selection_changed.connect(self.on_assetschanged) - assets.refresh_triggered.connect(self.on_assetschanged) - assets.view.clicked.connect(self.on_assetview_click) - subsets.active_changed.connect(self.on_subsetschanged) - subsets.version_changed.connect(self.on_versionschanged) - subsets.refreshed.connect(self._on_subset_refresh) - self.combo_projects.currentTextChanged.connect(self.on_project_change) + message_timer = QtCore.QTimer() + message_timer.setInterval(self.message_timeout) + message_timer.setSingleShot(True) + + message_timer.timeout.connect(self._on_message_timeout) + + families_filter_view.active_changed.connect( + self._on_family_filter_change + ) + assets_widget.selection_changed.connect(self.on_assetschanged) + assets_widget.refresh_triggered.connect(self.on_assetschanged) + assets_widget.view.clicked.connect(self.on_assetview_click) + subsets_widget.active_changed.connect(self.on_subsetschanged) + subsets_widget.version_changed.connect(self.on_versionschanged) + subsets_widget.refreshed.connect(self._on_subset_refresh) + projects_combobox.currentTextChanged.connect(self.on_project_change) self.sync_server = sync_server + self._combobox_delegate = combobox_delegate + self._projects_combobox = projects_combobox + self._assets_widget = assets_widget + self._families_filter_view = families_filter_view + + self._subsets_widget = subsets_widget + + self._version_info_widget = version_info_widget + self._thumbnail_widget = thumbnail_widget + self._repres_widget = repres_widget + + self._message_label = message_label + self._message_timer = message_timer + # Set default thumbnail on start - thumbnail.set_thumbnail(None) + thumbnail_widget.set_thumbnail(None) # Defaults - if sync_server.enabled: - split.setSizes([250, 1000, 550]) + if sync_server_enabled: + main_splitter.setSizes([250, 1000, 550]) self.resize(1800, 900) else: - split.setSizes([250, 850, 200]) + main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) + self.setStyleSheet(style.load_stylesheet()) + def showEvent(self, event): super(LibraryLoaderWindow, self).showEvent(event) if not self._initial_refresh: self.refresh() def on_assetview_click(self, *args): - subsets_widget = self.data["widgets"]["subsets"] - selection_model = subsets_widget.view.selectionModel() + selection_model = self._subsets_widget.view.selectionModel() if selection_model.selectedIndexes(): selection_model.clearSelection() @@ -187,7 +212,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self._ignore_project_change = True # Cleanup - self.combo_projects.clear() + self._projects_combobox.clear() # Fill combobox with projects select_project_item = QtGui.QStandardItem("< Select project >") @@ -202,18 +227,18 @@ class LibraryLoaderWindow(QtWidgets.QDialog): item.setData(project_name, QtCore.Qt.UserRole + 1) combobox_items.append(item) - root_item = self.combo_projects.model().invisibleRootItem() + root_item = self._projects_combobox.model().invisibleRootItem() root_item.appendRows(combobox_items) index = 0 self._ignore_project_change = False if old_project_name: - index = self.combo_projects.findText( + index = self._projects_combobox.findText( old_project_name, QtCore.Qt.MatchFixedString ) - self.combo_projects.setCurrentIndex(index) + self._projects_combobox.setCurrentIndex(index) def get_filtered_projects(self): projects = list() @@ -231,8 +256,8 @@ class LibraryLoaderWindow(QtWidgets.QDialog): if self._ignore_project_change: return - row = self.combo_projects.currentIndex() - index = self.combo_projects.model().index(row, 0) + row = self._projects_combobox.currentIndex() + index = self._projects_combobox.model().index(row, 0) project_name = index.data(QtCore.Qt.UserRole + 1) self.dbcon.Session["AVALON_PROJECT"] = project_name @@ -245,11 +270,9 @@ class LibraryLoaderWindow(QtWidgets.QDialog): "Config `%s` has no function `install`" % _config.__name__ ) - subsets = self.data["widgets"]["subsets"] - representations = self.data["widgets"]["representations"] - - subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) - representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"]) + self._subsets_widget.on_project_change(project_name) + if self._repres_widget: + self._repres_widget.on_project_change(project_name) self.family_config_cache.refresh() self.groups_config.refresh() @@ -263,13 +286,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): @property def current_project(self): - if ( - not self.dbcon.active_project() or - self.dbcon.active_project() == "" - ): - return None - - return self.dbcon.active_project() + return self.dbcon.active_project() or None # ------------------------------- # Delay calling blocking methods @@ -292,12 +309,11 @@ class LibraryLoaderWindow(QtWidgets.QDialog): tools_lib.schedule(self._versionschanged, 150, channel="mongo") def _on_subset_refresh(self, has_item): - subsets_widget = self.data["widgets"]["subsets"] - families_view = self.data["widgets"]["families"] - - subsets_widget.set_loading_state(loading=False, empty=not has_item) - families = subsets_widget.get_subsets_families() - families_view.set_enabled_families(families) + self._subsets_widget.set_loading_state( + loading=False, empty=not has_item + ) + families = self._subsets_widget.get_subsets_families() + self._families_filter_view.set_enabled_families(families) def set_context(self, context, refresh=True): self.echo("Setting context: {}".format(context)) @@ -307,6 +323,9 @@ class LibraryLoaderWindow(QtWidgets.QDialog): ) # ------------------------------ + def _on_family_filter_change(self, families): + self._subsets_widget.set_family_filters(families) + def _refresh(self): if not self._initial_refresh: self._initial_refresh = True @@ -322,74 +341,69 @@ class LibraryLoaderWindow(QtWidgets.QDialog): ) assert project_doc, "This is a bug" - assets_widget = self.data["widgets"]["assets"] - families_view = self.data["widgets"]["families"] - families_view.set_enabled_families(set()) - families_view.refresh() + self._families_filter_view.set_enabled_families(set()) + self._families_filter_view.refresh() - assets_widget.model.stop_fetch_thread() - assets_widget.refresh() - assets_widget.setFocus() + self._assets_widget.model.stop_fetch_thread() + self._assets_widget.refresh() + self._assets_widget.setFocus() def clear_assets_underlines(self): last_asset_ids = self.data["state"]["assetIds"] if not last_asset_ids: return - assets_widget = self.data["widgets"]["assets"] - id_role = assets_widget.model.ObjectIdRole + assets_model = self._assets_widget.model + id_role = assets_model.ObjectIdRole - for index in tools_lib.iter_model_rows(assets_widget.model, 0): + for index in tools_lib.iter_model_rows(assets_model, 0): if index.data(id_role) not in last_asset_ids: continue - assets_widget.model.setData( - index, [], assets_widget.model.subsetColorsRole + assets_model.setData( + index, [], assets_model.subsetColorsRole ) def _assetschanged(self): """Selected assets have changed""" - assets_widget = self.data["widgets"]["assets"] - subsets_widget = self.data["widgets"]["subsets"] - subsets_model = subsets_widget.model + subsets_model = self._subsets_widget.model subsets_model.clear() self.clear_assets_underlines() if not self.dbcon.Session.get("AVALON_PROJECT"): - subsets_widget.set_loading_state( + self._subsets_widget.set_loading_state( loading=False, empty=True ) return # filter None docs they are silo - asset_docs = assets_widget.get_selected_assets() + asset_docs = self._assets_widget.get_selected_assets() if len(asset_docs) == 0: return asset_ids = [asset_doc["_id"] for asset_doc in asset_docs] # Start loading - subsets_widget.set_loading_state( + self._subsets_widget.set_loading_state( loading=bool(asset_ids), empty=True ) subsets_model.set_assets(asset_ids) - subsets_widget.view.setColumnHidden( + self._subsets_widget.view.setColumnHidden( subsets_model.Columns.index("asset"), len(asset_ids) < 2 ) # Clear the version information on asset change - self.data["widgets"]["version"].set_version(None) - self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs) + self._version_info_widget.set_version(None) + self._thumbnail_widget.set_thumbnail(asset_docs) self.data["state"]["assetIds"] = asset_ids - representations = self.data["widgets"]["representations"] # reset repre list - representations.set_version_ids([]) + self._repres_widget.set_version_ids([]) def _subsetschanged(self): asset_ids = self.data["state"]["assetIds"] @@ -398,8 +412,9 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self._versionschanged() return - subsets = self.data["widgets"]["subsets"] - selected_subsets = subsets.selected_subsets(_merged=True, _other=False) + selected_subsets = self._subsets_widget.selected_subsets( + _merged=True, _other=False + ) asset_models = {} asset_ids = [] @@ -420,26 +435,24 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self.clear_assets_underlines() - assets_widget = self.data["widgets"]["assets"] - indexes = assets_widget.view.selectionModel().selectedRows() + indexes = self._assets_widget.view.selectionModel().selectedRows() + assets_model = self._assets_widget.model for index in indexes: - id = index.data(assets_widget.model.ObjectIdRole) + id = index.data(assets_model.ObjectIdRole) if id not in asset_models: continue - assets_widget.model.setData( - index, asset_models[id], assets_widget.model.subsetColorsRole + assets_model.setData( + index, asset_models[id], assets_model.subsetColorsRole ) # Trigger repaint - assets_widget.view.updateGeometries() + self._assets_widget.view.updateGeometries() # Set version in Version Widget self._versionschanged() def _versionschanged(self): - - subsets = self.data["widgets"]["subsets"] - selection = subsets.view.selectionModel() + selection = self._subsets_widget.view.selectionModel() # Active must be in the selected rows otherwise we # assume it's not actually an "active" current index. @@ -448,7 +461,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): active = selection.currentIndex() rows = selection.selectedRows(column=active.column()) if active and active in rows: - item = active.data(subsets.model.ItemRole) + item = active.data(self._subsets_widget.model.ItemRole) if ( item is not None and not (item.get("isGroup") or item.get("isMerged")) @@ -460,7 +473,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): for index in rows: if not index or not index.isValid(): continue - item = index.data(subsets.model.ItemRole) + item = index.data(self._subsets_widget.model.ItemRole) if ( item is None or item.get("isGroup") @@ -469,20 +482,18 @@ class LibraryLoaderWindow(QtWidgets.QDialog): continue version_docs.append(item["version_document"]) - self.data["widgets"]["version"].set_version(version_doc) + self._version_info_widget.set_version(version_doc) thumbnail_docs = version_docs if not thumbnail_docs: - assets_widget = self.data["widgets"]["assets"] - asset_docs = assets_widget.get_selected_assets() + asset_docs = self._assets_widget.get_selected_assets() if len(asset_docs) > 0: thumbnail_docs = asset_docs - self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs) + self._thumbnail_widget.set_thumbnail(thumbnail_docs) - representations = self.data["widgets"]["representations"] version_ids = [doc["_id"] for doc in version_docs or []] - representations.set_version_ids(version_ids) + self._repres_widget.set_version_ids(version_ids) def _set_context(self, context, refresh=True): """Set the selection in the interface using a context. @@ -510,16 +521,15 @@ class LibraryLoaderWindow(QtWidgets.QDialog): # scheduled refresh and the silo tabs are not shown. self._refresh_assets() - asset_widget = self.data["widgets"]["assets"] - asset_widget.select_assets(asset) + self._assets_widget.select_assets(asset) + + def _on_message_timeout(self): + self._message_label.setText("") def echo(self, message): - widget = self.data["label"]["message"] - widget.setText(str(message)) - widget.show() + self._message_label.setText(str(message)) print(message) - - tools_lib.schedule(widget.hide, 5000, channel="message") + self._message_timer.start() def closeEvent(self, event): # Kill on holding SHIFT @@ -576,7 +586,6 @@ def show( window = LibraryLoaderWindow( parent, icon, show_projects, show_libraries ) - window.setStyleSheet(style.load_stylesheet()) window.show() module.window = window From b642d770e58643af253e96e739aabe7b8a9d230b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 10:44:28 +0200 Subject: [PATCH 682/716] reorganize initialization --- openpype/tools/loader/app.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 955242e551..54eafd8f6d 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -3,7 +3,7 @@ import sys from Qt import QtWidgets, QtCore from avalon import api, io, pipeline -from openpype.style import load_stylesheet +from openpype import style from openpype.tools.utils.widgets import AssetWidget from openpype.tools.utils import lib @@ -96,15 +96,18 @@ class LoaderWindow(QtWidgets.QDialog): thumbnail_widget = ThumbnailWidget(io, parent=thumb_ver_splitter) version_info_widget = VersionWidget(io, parent=thumb_ver_splitter) + thumb_ver_splitter.addWidget(thumbnail_widget) + thumb_ver_splitter.addWidget(version_info_widget) + + thumb_ver_splitter.setStretchFactor(0, 30) + thumb_ver_splitter.setStretchFactor(1, 35) + manager = ModulesManager() sync_server = manager.modules_by_name.get("sync_server") sync_server_enabled = False if sync_server is not None: sync_server_enabled = sync_server.enabled - thumb_ver_splitter.addWidget(thumbnail_widget) - thumb_ver_splitter.addWidget(version_info_widget) - repres_widget = None if sync_server_enabled: repres_widget = RepresentationWidget( @@ -112,9 +115,6 @@ class LoaderWindow(QtWidgets.QDialog): ) thumb_ver_splitter.addWidget(repres_widget) - thumb_ver_splitter.setStretchFactor(0, 30) - thumb_ver_splitter.setStretchFactor(1, 35) - main_splitter.addWidget(left_side_splitter) main_splitter.addWidget(subsets_widget) main_splitter.addWidget(thumb_ver_splitter) @@ -193,7 +193,7 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) - self.setStyleSheet(load_stylesheet()) + self.setStyleSheet(style.load_stylesheet()) def resizeEvent(self, event): super(LoaderWindow, self).resizeEvent(event) From 7cedb5af316288abbb9481a13f6e0951170a11e1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 10:45:32 +0200 Subject: [PATCH 683/716] do not set stylesheets of library loader --- openpype/modules/default_modules/avalon_apps/avalon_app.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index a8e102f447..9e650a097e 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -1,6 +1,5 @@ import os import openpype -from openpype import resources from openpype.modules import OpenPypeModule from openpype_interfaces import ITrayModule @@ -52,16 +51,12 @@ class AvalonModule(OpenPypeModule, ITrayModule): def tray_init(self): # Add library tool try: - from Qt import QtGui - from avalon import style from openpype.tools.libraryloader import LibraryLoaderWindow self.libraryloader = LibraryLoaderWindow( - icon=QtGui.QIcon(resources.get_openpype_icon_filepath()), show_projects=True, show_libraries=True ) - self.libraryloader.setStyleSheet(style.load_stylesheet()) except Exception: self.log.warning( "Couldn't load Library loader tool for tray.", From 80b0b590b8c824123b687702c5de5180f038de1b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 21 Oct 2021 11:27:35 +0100 Subject: [PATCH 684/716] Implemented deselect function to handle objects not in 'object mode' --- openpype/hosts/blender/api/plugin.py | 24 +++++++++++++++++++ .../hosts/blender/plugins/load/load_abc.py | 4 ++-- .../hosts/blender/plugins/load/load_fbx.py | 4 ++-- .../blender/plugins/load/load_layout_blend.py | 2 +- .../blender/plugins/load/load_layout_json.py | 2 +- .../hosts/blender/plugins/load/load_model.py | 6 ++--- .../hosts/blender/plugins/load/load_rig.py | 8 +++---- .../blender/plugins/publish/extract_abc.py | 4 ++-- .../blender/plugins/publish/extract_fbx.py | 6 +++-- 9 files changed, 43 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 50b73ade2b..181e5972a8 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -95,6 +95,30 @@ def get_local_collection_with_name(name): return None +def deselect_all(): + """Deselect all objects in the scene. + + Blender gives context error if trying to deselect object that it isn't + in object mode. + """ + modes = [] + active = bpy.context.view_layer.objects.active + + for obj in bpy.data.objects: + if obj.mode != 'OBJECT': + modes.append((obj, obj.mode)) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode = 'OBJECT') + + bpy.ops.object.select_all(action='DESELECT') + + for p in modes: + bpy.context.view_layer.objects.active = p[0] + bpy.ops.object.mode_set(mode = p[1]) + + bpy.context.view_layer.objects.active = active + + class Creator(PypeCreatorMixin, blender.Creator): pass diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 92656fac9e..5969432c36 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -47,7 +47,7 @@ class CacheModelLoader(plugin.AssetLoader): bpy.data.objects.remove(empty) def _process(self, libpath, asset_group, group_name): - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() collection = bpy.context.view_layer.active_layer_collection.collection @@ -109,7 +109,7 @@ class CacheModelLoader(plugin.AssetLoader): avalon_info = obj[AVALON_PROPERTY] avalon_info.update({"container_name": group_name}) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() return objects diff --git a/openpype/hosts/blender/plugins/load/load_fbx.py b/openpype/hosts/blender/plugins/load/load_fbx.py index b80dc69adc..5f69aecb1a 100644 --- a/openpype/hosts/blender/plugins/load/load_fbx.py +++ b/openpype/hosts/blender/plugins/load/load_fbx.py @@ -46,7 +46,7 @@ class FbxModelLoader(plugin.AssetLoader): bpy.data.objects.remove(obj) def _process(self, libpath, asset_group, group_name, action): - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() collection = bpy.context.view_layer.active_layer_collection.collection @@ -112,7 +112,7 @@ class FbxModelLoader(plugin.AssetLoader): avalon_info = obj[AVALON_PROPERTY] avalon_info.update({"container_name": group_name}) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() return objects diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py index 85cb4dfbd3..4c1f751a77 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ b/openpype/hosts/blender/plugins/load/load_layout_blend.py @@ -150,7 +150,7 @@ class BlendLayoutLoader(plugin.AssetLoader): bpy.data.orphans_purge(do_local_ids=False) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() return objects diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index 1a4dbbb5cb..38718fd9b2 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -59,7 +59,7 @@ class JsonLayoutLoader(plugin.AssetLoader): return None def _process(self, libpath, asset, asset_group, actions): - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() with open(libpath, "r") as fp: data = json.load(fp) diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index af5591c299..c33c656dec 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -93,7 +93,7 @@ class BlendModelLoader(plugin.AssetLoader): bpy.data.orphans_purge(do_local_ids=False) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() return objects @@ -126,7 +126,7 @@ class BlendModelLoader(plugin.AssetLoader): asset_group.empty_display_type = 'SINGLE_ARROW' avalon_container.objects.link(asset_group) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() if options is not None: parent = options.get('parent') @@ -158,7 +158,7 @@ class BlendModelLoader(plugin.AssetLoader): bpy.ops.object.parent_set(keep_transform=True) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() objects = self._process(libpath, asset_group, group_name) diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index 6062c293df..e80da8af45 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -156,7 +156,7 @@ class BlendRigLoader(plugin.AssetLoader): while bpy.data.orphans_purge(do_local_ids=False): pass - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() return objects @@ -191,7 +191,7 @@ class BlendRigLoader(plugin.AssetLoader): action = None - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() create_animation = False @@ -227,7 +227,7 @@ class BlendRigLoader(plugin.AssetLoader): bpy.ops.object.parent_set(keep_transform=True) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() objects = self._process(libpath, asset_group, group_name, action) @@ -250,7 +250,7 @@ class BlendRigLoader(plugin.AssetLoader): data={"dependencies": str(context["representation"]["_id"])} ) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() bpy.context.scene.collection.objects.link(asset_group) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 4696da3db4..b75bec4e28 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -28,7 +28,7 @@ class ExtractABC(api.Extractor): # Perform extraction self.log.info("Performing extraction..") - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() selected = [] asset_group = None @@ -50,7 +50,7 @@ class ExtractABC(api.Extractor): flatten=False ) - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index b91f2a75ef..31d37da8e0 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -24,7 +24,7 @@ class ExtractFBX(api.Extractor): # Perform extraction self.log.info("Performing extraction..") - bpy.ops.object.select_all(action='DESELECT') + plugin.deselect_all() selected = [] asset_group = None @@ -60,7 +60,9 @@ class ExtractFBX(api.Extractor): add_leaf_bones=False ) - bpy.ops.object.select_all(action='DESELECT') + bpy.context.scene.unit_settings.scale_length = scale_length + + plugin.deselect_all() for mat in new_materials: bpy.data.materials.remove(mat) From a2397f48f032389184f8cee414c674cd8ae4d6c8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 12:40:33 +0200 Subject: [PATCH 685/716] set stylesheet on first show --- openpype/tools/libraryloader/app.py | 30 ++++++++++++++++------------- openpype/tools/loader/app.py | 8 +++++++- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 69c5cb61e7..700d3c05bd 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -35,6 +35,18 @@ class LibraryLoaderWindow(QtWidgets.QDialog): ): super(LibraryLoaderWindow, self).__init__(parent) + # Window modifications + self.setWindowTitle(self.tool_title) + window_flags = QtCore.Qt.Window + if not parent: + window_flags |= QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(window_flags) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + icon = QtGui.QIcon(style.app_icon_path()) + self.setWindowIcon(icon) + + self._first_show = True self._initial_refresh = False self._ignore_project_change = False @@ -52,17 +64,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self.family_config_cache = tools_lib.FamilyConfigCache(dbcon) # UI initialization - # Enable minimize and maximize for app - self.setWindowTitle(self.tool_title) - window_flags = QtCore.Qt.Window - if not parent: - window_flags |= QtCore.Qt.WindowStaysOnTopHint - self.setWindowFlags(window_flags) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - icon = QtGui.QIcon(style.app_icon_path()) - self.setWindowIcon(icon) - main_splitter = QtWidgets.QSplitter(self) # --- Left part --- @@ -193,11 +194,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog): main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) - self.setStyleSheet(style.load_stylesheet()) - def showEvent(self, event): super(LibraryLoaderWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self.setStyleSheet(style.load_stylesheet()) + if not self._initial_refresh: + self._initial_refresh = True self.refresh() def on_assetview_click(self, *args): diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 54eafd8f6d..a98c7e2f2f 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -193,7 +193,7 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter.setSizes([250, 850, 200]) self.resize(1300, 700) - self.setStyleSheet(style.load_stylesheet()) + self._first_show = True def resizeEvent(self, event): super(LoaderWindow, self).resizeEvent(event) @@ -203,6 +203,12 @@ class LoaderWindow(QtWidgets.QDialog): super(LoaderWindow, self).moveEvent(event) self._overlay_frame.move(0, 0) + def showEvent(self, event): + super(LoaderWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self.setStyleSheet(style.load_stylesheet()) + # ------------------------------- # Delay calling blocking methods # ------------------------------- From e545d431da9a8283b5a957c048fa298bd4ca848b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 12:40:42 +0200 Subject: [PATCH 686/716] modified splitter style --- openpype/style/style.css | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 948ee8c7b7..8013f38bea 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -200,12 +200,44 @@ QComboBox::down-arrow, QComboBox::down-arrow:on, QComboBox::down-arrow:hover, QC } /* Splitter */ -QSplitter { - border: none; +QSplitter::handle { + border: 3px solid transparent; } -QSplitter::handle { - border: 1px dotted {color:bg-menu-separator}; +QSplitter::handle:horizontal { + background: qlineargradient( + x1:0, y1:0, x2:1, y2:0, + stop:0.3 rgba(0, 0, 0, 0), + stop:0.5 {color:bg-buttons}, + stop:0.7 rgba(0, 0, 0, 0) + ); +} + +QSplitter::handle:vertical { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0.3 rgba(0, 0, 0, 0), + stop:0.5 {color:bg-buttons}, + stop:0.7 rgba(0, 0, 0, 0) + ); +} + +QSplitter::handle:horizontal:hover { + background: qlineargradient( + x1:0, y1:0, x2:1, y2:0, + stop:0.3 rgba(0, 0, 0, 0), + stop:0.5 {color:bg-button-hover}, + stop:0.7 rgba(0, 0, 0, 0) + ); +} + +QSplitter::handle:vertical:hover { + background: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0.3 rgba(0, 0, 0, 0), + stop:0.5 {color:bg-button-hover}, + stop:0.7 rgba(0, 0, 0, 0) + ); } /* SLider */ From 52f474d62d1bfb22b0ada0908a768d90728841b7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 12:42:01 +0200 Subject: [PATCH 687/716] splitter has it's own key in data --- openpype/style/data.json | 3 +++ openpype/style/style.css | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 143c6695af..c33c2eaa5e 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -32,6 +32,9 @@ "bg-inputs-disabled": "#2C313A", "bg-buttons-disabled": "#434a56", + "bg-splitter": "#434a56", + "bg-splitter-hover": "rgba(168, 175, 189, 0.3)", + "bg-menu-separator": "rgba(75, 83, 98, 127)", "bg-scroll-handle": "#4B5362", diff --git a/openpype/style/style.css b/openpype/style/style.css index 8013f38bea..3f006fb845 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -208,7 +208,7 @@ QSplitter::handle:horizontal { background: qlineargradient( x1:0, y1:0, x2:1, y2:0, stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-buttons}, + stop:0.5 {color:bg-splitter}, stop:0.7 rgba(0, 0, 0, 0) ); } @@ -217,7 +217,7 @@ QSplitter::handle:vertical { background: qlineargradient( x1:0, y1:0, x2:0, y2:1, stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-buttons}, + stop:0.5 {color:bg-splitter}, stop:0.7 rgba(0, 0, 0, 0) ); } @@ -226,7 +226,7 @@ QSplitter::handle:horizontal:hover { background: qlineargradient( x1:0, y1:0, x2:1, y2:0, stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-button-hover}, + stop:0.5 {color:bg-splitter-hover}, stop:0.7 rgba(0, 0, 0, 0) ); } @@ -235,7 +235,7 @@ QSplitter::handle:vertical:hover { background: qlineargradient( x1:0, y1:0, x2:0, y2:1, stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-button-hover}, + stop:0.5 {color:bg-splitter-hover}, stop:0.7 rgba(0, 0, 0, 0) ); } From 36f49122416af0138f8acae4fd72792f6a1c673f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 21 Oct 2021 11:51:12 +0100 Subject: [PATCH 688/716] Hound fixes --- openpype/hosts/blender/api/plugin.py | 4 ++-- openpype/hosts/blender/plugins/publish/extract_fbx.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 181e5972a8..6d437059b8 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -108,13 +108,13 @@ def deselect_all(): if obj.mode != 'OBJECT': modes.append((obj, obj.mode)) bpy.context.view_layer.objects.active = obj - bpy.ops.object.mode_set(mode = 'OBJECT') + bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') for p in modes: bpy.context.view_layer.objects.active = p[0] - bpy.ops.object.mode_set(mode = p[1]) + bpy.ops.object.mode_set(mode=p[1]) bpy.context.view_layer.objects.active = active diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index 31d37da8e0..f9ffdea1d1 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -60,8 +60,6 @@ class ExtractFBX(api.Extractor): add_leaf_bones=False ) - bpy.context.scene.unit_settings.scale_length = scale_length - plugin.deselect_all() for mat in new_materials: From a1262df627bba883379425c5c231fbe6a254baac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 14:44:39 +0200 Subject: [PATCH 689/716] added few docstrings --- openpype/style/__init__.py | 47 ++++++++++++--- openpype/style/color_defs.py | 109 ++++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 10 deletions(-) diff --git a/openpype/style/__init__.py b/openpype/style/__init__.py index d763bfdc3c..fd39e93b5d 100644 --- a/openpype/style/__init__.py +++ b/openpype/style/__init__.py @@ -12,12 +12,36 @@ _FONT_IDS = None current_dir = os.path.dirname(os.path.abspath(__file__)) +def _get_colors_raw_data(): + """Read data file with stylesheet fill values. + + Returns: + dict: Loaded data for stylesheet. + """ + data_path = os.path.join(current_dir, "data.json") + with open(data_path, "r") as data_stream: + data = json.load(data_stream) + return data + + def get_colors_data(): + """Only color data from stylesheet data.""" data = _get_colors_raw_data() return data.get("color") or {} def _convert_color_values_to_objects(value): + """Parse all string values in dictionary to Color definitions. + + Recursive function calling itself if value is dictionary. + + Args: + value (dict, str): String is parsed into color definition object and + dictionary is passed into this function. + + Raises: + TypeError: If value in color data do not contain string of dictionary. + """ if isinstance(value, dict): output = {} for _key, _value in value.items(): @@ -32,6 +56,11 @@ def _convert_color_values_to_objects(value): def get_objected_colors(): + """Colors parsed from stylesheet data into color definitions. + + Returns: + dict: Parsed color objects by keys in data. + """ colors_data = get_colors_data() output = {} for key, value in colors_data.items(): @@ -39,14 +68,15 @@ def get_objected_colors(): return output -def _get_colors_raw_data(): - data_path = os.path.join(current_dir, "data.json") - with open(data_path, "r") as data_stream: - data = json.load(data_stream) - return data - - def _load_stylesheet(): + """Load strylesheet and trigger all related callbacks. + + Style require more than a stylesheet string. Stylesheet string + contains paths to resources which must be registered into Qt application + and load fonts used in stylesheets. + + Also replace values from stylesheet data into stylesheet text. + """ from . import qrc_resources qrc_resources.qInitResources() @@ -78,6 +108,7 @@ def _load_stylesheet(): def _load_font(): + """Load and register fonts into Qt application.""" from Qt import QtGui global _FONT_IDS @@ -117,6 +148,7 @@ def _load_font(): def load_stylesheet(): + """Load and return OpenPype Qt stylesheet.""" global _STYLESHEET_CACHE if _STYLESHEET_CACHE is None: _STYLESHEET_CACHE = _load_stylesheet() @@ -125,4 +157,5 @@ def load_stylesheet(): def app_icon_path(): + """Path to OpenPype icon.""" return resources.get_openpype_icon_filepath() diff --git a/openpype/style/color_defs.py b/openpype/style/color_defs.py index 3f504a9d3b..0f4e145ca0 100644 --- a/openpype/style/color_defs.py +++ b/openpype/style/color_defs.py @@ -1,7 +1,27 @@ +"""Color definitions that can be used to parse strings for stylesheet. + +Each definition must have available method `get_qcolor` which should return +`QtGui.QColor` representation of the color. + +# TODO create abstract class to force this method implementation + +Usage: Some colors may be not be used only in stylesheet but is required to +use them in code too. To not hardcode these color values into code it is better +to use same colors that are available fro stylesheets. + +It is possible that some colors may not be used in stylesheet at all and thei +definition is used only in code. +""" + import re def parse_color(value): + """Parse string value of color to one of objected representation. + + Args: + value(str): Color definition usable in stylesheet. + """ modified_value = value.strip().lower() if modified_value.startswith("hsla"): return HSLAColor(value) @@ -21,12 +41,30 @@ def parse_color(value): def create_qcolor(*args): + """Create QtGui.QColor object. + + Args: + *args (tuple): It is possible to pass initialization arguments for + Qcolor. + """ from Qt import QtGui return QtGui.QColor(*args) def min_max_check(value, min_value, max_value): + """Validate number value if is in passed range. + + Args: + value (int, float): Value which is validated. + min_value (int, float): Minimum possible value. Validation is skipped + if passed value is None. + max_value (int, float): Maximum possible value. Validation is skipped + if passed value is None. + + Raises: + ValueError: When 'value' is out of specified range. + """ if min_value is not None and value < min_value: raise ValueError("Minimum expected value is '{}' got '{}'".format( min_value, value @@ -39,6 +77,16 @@ def min_max_check(value, min_value, max_value): def int_validation(value, min_value=None, max_value=None): + """Validation of integer value within range. + + Args: + value (int): Validated value. + min_value (int): Minimum possible value. + max_value (int): Maximum possible value. + + Raises: + TypeError: If 'value' is not 'int' type. + """ if not isinstance(value, int): raise TypeError(( "Invalid type of hue expected 'int' got {}" @@ -48,6 +96,16 @@ def int_validation(value, min_value=None, max_value=None): def float_validation(value, min_value=None, max_value=None): + """Validation of float value within range. + + Args: + value (float): Validated value. + min_value (float): Minimum possible value. + max_value (float): Maximum possible value. + + Raises: + TypeError: If 'value' is not 'float' type. + """ if not isinstance(value, float): raise TypeError(( "Invalid type of hue expected 'int' got {}" @@ -57,6 +115,11 @@ def float_validation(value, min_value=None, max_value=None): class UnknownColor: + """Color from stylesheet data without known color definition. + + This is backup for unknown color definitions which may be for example + constants or definition not yet defined by class. + """ def __init__(self, value): self.value = value @@ -65,6 +128,14 @@ class UnknownColor: class HEXColor: + """Hex color definition. + + Hex color is defined by '#' and 3 or 6 hex values (0-F). + + Examples: + "#fff" + "#f3f3f3" + """ regex = re.compile(r"[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$") def __init__(self, color_string): @@ -92,6 +163,7 @@ class HEXColor: @classmethod def hex_to_rgb(cls, value): + """Convert hex value to rgb.""" hex_value = value.lstrip("#") if not cls.regex.match(hex_value): raise ValueError("\"{}\" is not a valid HEX code.".format(value)) @@ -111,6 +183,13 @@ class HEXColor: class RGBColor: + """Color defined by red green and blue values. + + Each color has possible integer range 0-255. + + Examples: + "rgb(255, 127, 0)" + """ def __init__(self, value): modified_color = value.lower().strip() content = modified_color.rstrip(")").lstrip("rgb(") @@ -146,6 +225,13 @@ class RGBColor: class RGBAColor: + """Color defined by red green, blue and alpha values. + + Each color has possible integer range 0-255. + + Examples: + "rgba(255, 127, 0, 127)" + """ def __init__(self, value): modified_color = value.lower().strip() content = modified_color.rstrip(")").lstrip("rgba(") @@ -191,6 +277,15 @@ class RGBAColor: class HSLColor: + """Color defined by hue, saturation and light values. + + Hue is defined as integer in rage 0-360. Saturation and light can be + defined as float or percent value. + + Examples: + "hsl(27, 0.7, 0.3)" + "hsl(27, 70%, 30%)" + """ def __init__(self, value): modified_color = value.lower().strip() content = modified_color.rstrip(")").lstrip("hsl(") @@ -235,6 +330,16 @@ class HSLColor: class HSLAColor: + """Color defined by hue, saturation, light and alpha values. + + Hue is defined as integer in rage 0-360. Saturation and light can be + defined as float (0-1 range) or percent value(0-100%). And alpha + as float (0-1 range). + + Examples: + "hsl(27, 0.7, 0.3)" + "hsl(27, 70%, 30%)" + """ def __init__(self, value): modified_color = value.lower().strip() content = modified_color.rstrip(")").lstrip("hsla(") @@ -251,10 +356,8 @@ class HSLAColor: light = float(light_str.rstrip("%")) / 100 else: light = float(light_str) - alpha = float(alpha_str) - if isinstance(alpha, int): - alpha = float(alpha) + alpha = float(alpha_str) int_validation(hue, 0, 360) float_validation(sat, 0, 1) From 426c996b71e673ee39edbc84c86170241b9f38b3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 14:50:06 +0200 Subject: [PATCH 690/716] hound fixes in pyside2 resources --- openpype/style/pyside2_resources.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/style/pyside2_resources.py b/openpype/style/pyside2_resources.py index c7328e7c91..80f9b904fd 100644 --- a/openpype/style/pyside2_resources.py +++ b/openpype/style/pyside2_resources.py @@ -811,10 +811,14 @@ qt_resource_struct = b"\ \x00\x00\x00\xde\x00\x00\x00\x00\x00\x01\x00\x00\x02\xa6\ " + def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData( + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data + ) + def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() + QtCore.qUnregisterResourceData( + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data + ) From 610477961025d7dd7b149668f084baee857aa903 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:00:05 +0200 Subject: [PATCH 691/716] renamed 'ExperimentalDialog' to 'ExperimentalToolsDialog' --- openpype/tools/experimental_tools/__init__.py | 4 ++-- openpype/tools/experimental_tools/dialog.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/experimental_tools/__init__.py b/openpype/tools/experimental_tools/__init__.py index 75e3210aab..d6315e4655 100644 --- a/openpype/tools/experimental_tools/__init__.py +++ b/openpype/tools/experimental_tools/__init__.py @@ -3,12 +3,12 @@ from .tools_def import ( LOCAL_EXPERIMENTAL_KEY ) -from .dialog import ExperimentalDialog +from .dialog import ExperimentalToolsDialog __all__ = ( "ExperimentalTools", "LOCAL_EXPERIMENTAL_KEY", - "ExperimentalDialog" + "ExperimentalToolsDialog" ) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index a611416efc..6173deb693 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -21,11 +21,11 @@ class ToolButton(QtWidgets.QPushButton): self.triggered.emit(self._identifier) -class ExperimentalDialog(QtWidgets.QDialog): +class ExperimentalToolsDialog(QtWidgets.QDialog): refresh_interval = 3000 def __init__(self, parent=None): - super(ExperimentalDialog, self).__init__(parent) + super(ExperimentalToolsDialog, self).__init__(parent) self.setWindowTitle("OpenPype Experimental tools") icon = QtGui.QIcon(app_icon_path()) self.setWindowIcon(icon) @@ -138,7 +138,7 @@ class ExperimentalDialog(QtWidgets.QDialog): tool.execute() def showEvent(self, event): - super(ExperimentalDialog, self).showEvent(event) + super(ExperimentalToolsDialog, self).showEvent(event) if self._refresh_on_active: # Start/Restart timer @@ -164,7 +164,7 @@ class ExperimentalDialog(QtWidgets.QDialog): self._refresh_timer.start() self.refresh() - super(ExperimentalDialog, self).changeEvent(event) + super(ExperimentalToolsDialog, self).changeEvent(event) def _on_refresh_timeout(self): # Stop timer if window is not visible From 0c0809469cddf15bc5412856ed6f478f001cc716 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:00:16 +0200 Subject: [PATCH 692/716] added experimental dialog to host tools --- openpype/tools/utils/host_tools.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index ee184ccf2d..c0e6d71b73 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -28,6 +28,7 @@ class HostToolsHelper: self._scene_inventory_tool = None self._library_loader_tool = None self._look_assigner_tool = None + self._experimental_tools_dialog = None @property def log(self): @@ -218,6 +219,22 @@ class HostToolsHelper: look_assigner_tool = self.get_look_assigner_tool(parent) look_assigner_tool.show() + def get_experimental_tools_dialog(self, parent=None): + if self._experimental_tools_dialog is None: + from openpype.tools.experimental_tools import ( + ExperimentalToolsDialog + ) + + self._experimental_tools_dialog = ExperimentalToolsDialog(parent) + return self._experimental_tools_dialog + + def show_experimental_tools_dialog(self, parent=None): + dialog = self.get_experimental_tools_dialog(parent) + + dialog.show() + dialog.raise_() + dialog.activateWindow() + def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. @@ -247,6 +264,9 @@ class HostToolsHelper: elif tool_name == "publish": self.log.info("Can't return publish tool window.") + elif tool_name == "experimental_tools": + return self.get_experimental_tools_dialog(parent, *args, **kwargs) + else: self.log.warning( "Can't show unknown tool name: \"{}\"".format(tool_name) @@ -281,6 +301,9 @@ class HostToolsHelper: elif tool_name == "publish": self.show_publish(parent, *args, **kwargs) + elif tool_name == "experimental_tools": + self.show_experimental_tools_dialog(parent, *args, **kwargs) + else: self.log.warning( "Can't show unknown tool name: \"{}\"".format(tool_name) @@ -355,3 +378,7 @@ def show_look_assigner(parent=None): def show_publish(parent=None): _SingletonPoint.show_tool_by_name("publish", parent) + + +def show_experimental_tools_dialog(parent=None): + _SingletonPoint.show_tool_by_name("experimental_tools", parent) From d4729ab0695ac59673583f02efb9bb9fdfb136ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:05:11 +0200 Subject: [PATCH 693/716] disable filtering by host name when used in local settings --- openpype/tools/settings/local_settings/experimental_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/local_settings/experimental_widget.py b/openpype/tools/settings/local_settings/experimental_widget.py index 72f999d886..e863d9afb0 100644 --- a/openpype/tools/settings/local_settings/experimental_widget.py +++ b/openpype/tools/settings/local_settings/experimental_widget.py @@ -28,7 +28,7 @@ class LocalExperimentalToolsWidgets(QtWidgets.QWidget): layout.addRow(empty_label) - experimental_defs = ExperimentalTools() + experimental_defs = ExperimentalTools(filter_hosts=False) checkboxes_by_identifier = {} for tool in experimental_defs.tools: checkbox = QtWidgets.QCheckBox(self) From 7a50a106e333ed472dcb8fa6dfd7acc7e71d5efb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:05:20 +0200 Subject: [PATCH 694/716] added few docstrings --- .../tools/experimental_tools/tools_def.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 5dd92151ca..6ae4637039 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -8,6 +8,8 @@ LOCAL_EXPERIMENTAL_KEY = "experimental_tools" class ExperimentalTool: """Definition of experimental tool. + Definition is used in local settings and in experimental tools dialog. + Args: identifier (str): String identifier of tool (unique). label (str): Label shown in UI. @@ -91,11 +93,32 @@ class ExperimentalTools: ).format(tool.identifier)) tools_by_identifier[tool.identifier] = tool - self.tools_by_identifier = tools_by_identifier - self.tools = experimental_tools + self._tools_by_identifier = tools_by_identifier + self._tools = experimental_tools self._parent_widget = parent + @property + def tools(self): + """Tools in list. + + Returns: + list: Tools filtered by host name if filtering was enabled + on initialization. + """ + return self._tools + + @property + def tools_by_identifier(self): + """Tools by their identifier. + + Returns: + dict: Tools by identifier filtered by host name if filtering + was enabled on initialization. + """ + return self._tools_by_identifier + def refresh_availability(self): + """Reload local settings and check if any tool changed ability.""" local_settings = get_local_settings() experimental_settings = ( local_settings.get(LOCAL_EXPERIMENTAL_KEY) From 33764f6e16c3cc0e46bc75d7945db592acf0623f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:08:39 +0200 Subject: [PATCH 695/716] added docstrings to host tools --- openpype/tools/utils/host_tools.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index c0e6d71b73..2ac9d0c48b 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -220,6 +220,16 @@ class HostToolsHelper: look_assigner_tool.show() def get_experimental_tools_dialog(self, parent=None): + """Dialog of experimental tools. + + For some hosts it is not easy to modify menu of tools. For + those cases was addded experimental tools dialog which is Qt based + and can dynamically filled by experimental tools so + host need only single "Experimental tools" button to see them. + + Dialog can be also empty with a message that there are not available + experimental tools. + """ if self._experimental_tools_dialog is None: from openpype.tools.experimental_tools import ( ExperimentalToolsDialog @@ -229,6 +239,7 @@ class HostToolsHelper: return self._experimental_tools_dialog def show_experimental_tools_dialog(self, parent=None): + """Show dialog with experimental tools.""" dialog = self.get_experimental_tools_dialog(parent) dialog.show() From 29d1c47a58751ba16f9a5f71ff5d35cf7839ac61 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:12:12 +0200 Subject: [PATCH 696/716] disable buttons of tools that are not turned on --- openpype/tools/experimental_tools/dialog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index 6173deb693..4923759249 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -112,6 +112,9 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): "\n\nOpenPype Tray > Settings > Experimental Tools" )) + if tool.enabled != button.isEnabled(): + button.setEnabled(tool.enabled) + for identifier in buttons_to_remove: button = self._buttons_by_tool_identifier.pop(identifier) button.setVisible(False) From de9f0f7fa46fa757f1b6ce6d6b985360de98410d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:28:25 +0200 Subject: [PATCH 697/716] adde commented example tool --- openpype/tools/experimental_tools/tools_def.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 6ae4637039..3657c2385b 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -65,6 +65,19 @@ class ExperimentalTools: # Definition of experimental tools experimental_tools = [] + # --- Example tool (callback will just print on click) --- + # def example_callback(*args): + # print("Triggered tool") + # + # experimental_tools = [ + # ExperimentalTool( + # "example", + # "Exmaple experimental tool", + # example_callback, + # "Example tool tooltip." + # ) + # ] + # Try to get host name from env variable `AVALON_APP` if not host_name: host_name = os.environ.get("AVALON_APP") From ee443cb735637b56be5367164309481f7defb35a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:29:28 +0200 Subject: [PATCH 698/716] added example tool into experimental tools --- openpype/hosts/maya/api/menu.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 4f0966abfd..5eb8882030 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -46,6 +46,15 @@ def deferred(): ) ) + def add_experimental_item(): + cmds.menuItem( + "Experimental tools...", + parent=pipeline._menu, + command=lambda *args: host_tools.show_experimental_tools_dialog( + pipeline._parent + ) + ) + def modify_workfiles(): # Find the pipeline menu top_menu = _get_menu() @@ -103,6 +112,7 @@ def deferred(): add_build_workfiles_item() add_look_assigner_item() + add_experimental_item() modify_workfiles() remove_project_manager() From 501f0ed550e76ebbff0340b6bdf2bb14a75e06ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 15:50:10 +0200 Subject: [PATCH 699/716] added experimental tools into nuke menu --- openpype/hosts/nuke/api/menu.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/nuke/api/menu.py b/openpype/hosts/nuke/api/menu.py index 87990c5e92..3e74893589 100644 --- a/openpype/hosts/nuke/api/menu.py +++ b/openpype/hosts/nuke/api/menu.py @@ -84,6 +84,12 @@ def install(): ) log.debug("Adding menu item: {}".format(name)) + # Add experimental tools action + menu.addSeparator() + menu.addCommand( + "Experimental tools...", + host_tools.show_experimental_tools_dialog + ) # adding shortcuts add_shortcuts_from_presets() From 6da7c65e295be27cab7fd1c594f78945247ec989 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 16:19:43 +0200 Subject: [PATCH 700/716] set stylesheet after show --- openpype/tools/experimental_tools/dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index 4923759249..c7c8ce83fc 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -29,7 +29,6 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): self.setWindowTitle("OpenPype Experimental tools") icon = QtGui.QIcon(app_icon_path()) self.setWindowIcon(icon) - self.setStyleSheet(load_stylesheet()) empty_widget = QtWidgets.QWidget(self) @@ -154,6 +153,8 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): if self._first_show: self._first_show = False + # Set stylesheet + self.setStyleSheet(load_stylesheet()) # Resize dialog if there is not content if not self._is_content_visible(): size = self.size() From 28bf0a23996dd5538b9843dfaa271904b41a416e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 17:14:17 +0200 Subject: [PATCH 701/716] resize after showing --- openpype/tools/libraryloader/app.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 700d3c05bd..3e4c5d5850 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -129,6 +129,10 @@ class LibraryLoaderWindow(QtWidgets.QDialog): main_splitter.addWidget(left_side_splitter) main_splitter.addWidget(subsets_widget) main_splitter.addWidget(thumb_ver_splitter) + if sync_server_enabled: + main_splitter.setSizes([250, 1000, 550]) + else: + main_splitter.setSizes([250, 850, 200]) # --- Footer --- footer_widget = QtWidgets.QWidget(self) @@ -168,6 +172,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): projects_combobox.currentTextChanged.connect(self.on_project_change) self.sync_server = sync_server + self._sync_server_enabled = sync_server_enabled self._combobox_delegate = combobox_delegate self._projects_combobox = projects_combobox @@ -186,19 +191,15 @@ class LibraryLoaderWindow(QtWidgets.QDialog): # Set default thumbnail on start thumbnail_widget.set_thumbnail(None) - # Defaults - if sync_server_enabled: - main_splitter.setSizes([250, 1000, 550]) - self.resize(1800, 900) - else: - main_splitter.setSizes([250, 850, 200]) - self.resize(1300, 700) - def showEvent(self, event): super(LibraryLoaderWindow, self).showEvent(event) if self._first_show: self._first_show = False self.setStyleSheet(style.load_stylesheet()) + if self._sync_server_enabled: + self.resize(1800, 900) + else: + self.resize(1300, 700) if not self._initial_refresh: self._initial_refresh = True From 2b2f27b74e557a1b34e15bc30ffb8fc9ca23d489 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 17:15:40 +0200 Subject: [PATCH 702/716] set default thumbnail on initialization of ThumbnailWidget --- openpype/tools/libraryloader/app.py | 3 --- openpype/tools/loader/widgets.py | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 3e4c5d5850..d7c6c162e6 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -188,9 +188,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self._message_label = message_label self._message_timer = message_timer - # Set default thumbnail on start - thumbnail_widget.set_thumbnail(None) - def showEvent(self, event): super(LibraryLoaderWindow, self).showEvent(event) if self._first_show: diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index b068dd95d1..4c075382ac 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -745,6 +745,7 @@ class ThumbnailWidget(QtWidgets.QLabel): "default_thumbnail.png" ) self.default_pix = QtGui.QPixmap(default_pix_path) + self.set_pixmap() def height(self): width = self.width() From 1b10ef39fd43e65a8ddcfcabb185066d88a578b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 21 Oct 2021 17:17:24 +0200 Subject: [PATCH 703/716] resize loader after showing --- openpype/tools/loader/app.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index a98c7e2f2f..bbf6719af5 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -119,6 +119,11 @@ class LoaderWindow(QtWidgets.QDialog): main_splitter.addWidget(subsets_widget) main_splitter.addWidget(thumb_ver_splitter) + if sync_server_enabled: + main_splitter.setSizes([250, 1000, 550]) + else: + main_splitter.setSizes([250, 850, 200]) + # TODO keep footer size by message size footer_widget = QtWidgets.QWidget(self) footer_widget.setFixedHeight(20) @@ -164,6 +169,8 @@ class LoaderWindow(QtWidgets.QDialog): repres_widget.load_started.connect(self._on_load_start) repres_widget.load_ended.connect(self._on_load_end) + self._sync_server_enabled = sync_server_enabled + self._assets_widget = assets_widget self._families_filter_view = families_filter_view @@ -185,14 +192,6 @@ class LoaderWindow(QtWidgets.QDialog): self._refresh() self._assetschanged() - # Defaults - if sync_server_enabled: - main_splitter.setSizes([250, 1000, 550]) - self.resize(1800, 900) - else: - main_splitter.setSizes([250, 850, 200]) - self.resize(1300, 700) - self._first_show = True def resizeEvent(self, event): @@ -208,6 +207,10 @@ class LoaderWindow(QtWidgets.QDialog): if self._first_show: self._first_show = False self.setStyleSheet(style.load_stylesheet()) + if self._sync_server_enabled: + self.resize(1800, 900) + else: + self.resize(1300, 700) # ------------------------------- # Delay calling blocking methods From 5b95e21c6f4da5683043895ae97222c10fafcae6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 10:53:03 +0200 Subject: [PATCH 704/716] do not apply avalon style on loader and library loader --- openpype/tools/utils/host_tools.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 599c25d6c8..2a64e23883 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -93,8 +93,6 @@ class HostToolsHelper: def show_loader(self, parent=None, use_context=None): """Loader tool for loading representations.""" - from avalon import style - loader_tool = self.get_loader_tool(parent) loader_tool.show() @@ -110,8 +108,6 @@ class HostToolsHelper: else: loader_tool.refresh() - loader_tool.setStyleSheet(style.load_stylesheet()) - def get_creator_tool(self, parent): """Create, cache and return creator tool window.""" if self._creator_tool is None: @@ -196,14 +192,11 @@ class HostToolsHelper: def show_library_loader(self, parent=None): """Loader tool for loading representations from library project.""" - from avalon import style - library_loader_tool = self.get_library_loader_tool(parent) library_loader_tool.show() library_loader_tool.raise_() library_loader_tool.activateWindow() library_loader_tool.refresh() - library_loader_tool.setStyleSheet(style.load_stylesheet()) def show_publish(self, parent=None): """Publish UI.""" From 600c94f464d93edded5927873b86379838708a12 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 11:49:15 +0200 Subject: [PATCH 705/716] fix double slashes --- openpype/style/style.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 3f006fb845..6921a786f3 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -425,20 +425,20 @@ QAbstractItemView::branch:open:has-children:has-siblings { QAbstractItemView::branch:open:has-children:!has-siblings:hover, QAbstractItemView::branch:open:has-children:has-siblings:hover { border-image: none; - image: url(:/openpype/images//branch_open_on.png); + image: url(:/openpype/images/branch_open_on.png); background: transparent; } QAbstractItemView::branch:has-children:!has-siblings:closed, QAbstractItemView::branch:closed:has-children:has-siblings { border-image: none; - image: url(:/openpype/images//branch_closed.png); + image: url(:/openpype/images/branch_closed.png); background: transparent; } QAbstractItemView::branch:has-children:!has-siblings:closed:hover, QAbstractItemView::branch:closed:has-children:has-siblings:hover { border-image: none; - image: url(:/openpype/images//branch_closed_on.png); + image: url(:/openpype/images/branch_closed_on.png); background: transparent; } From c85fb71103c22a2be372a1e6ea1c55ed4522c1d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:26:12 +0200 Subject: [PATCH 706/716] qlineargradient are single line --- openpype/style/style.css | 69 +++++++++++----------------------------- 1 file changed, 19 insertions(+), 50 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 6921a786f3..8e9827084e 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -205,39 +205,23 @@ QSplitter::handle { } QSplitter::handle:horizontal { - background: qlineargradient( - x1:0, y1:0, x2:1, y2:0, - stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-splitter}, - stop:0.7 rgba(0, 0, 0, 0) - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1:0, y1:0, x2:1, y2:0,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter},stop:0.7 rgba(0, 0, 0, 0)); } QSplitter::handle:vertical { - background: qlineargradient( - x1:0, y1:0, x2:0, y2:1, - stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-splitter}, - stop:0.7 rgba(0, 0, 0, 0) - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter},stop:0.7 rgba(0, 0, 0, 0)); } QSplitter::handle:horizontal:hover { - background: qlineargradient( - x1:0, y1:0, x2:1, y2:0, - stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-splitter-hover}, - stop:0.7 rgba(0, 0, 0, 0) - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1:0, y1:0, x2:1, y2:0,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter-hover},stop:0.7 rgba(0, 0, 0, 0)); } QSplitter::handle:vertical:hover { - background: qlineargradient( - x1:0, y1:0, x2:0, y2:1, - stop:0.3 rgba(0, 0, 0, 0), - stop:0.5 {color:bg-splitter-hover}, - stop:0.7 rgba(0, 0, 0, 0) - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter-hover},stop:0.7 rgba(0, 0, 0, 0)); } /* SLider */ @@ -264,18 +248,15 @@ QSlider::groove:focus { border-color: {color:border-focus}; } QSlider::handle { - background: qlineargradient( - x1: 0, y1: 0.5, - x2: 1, y2: 0.5, - stop: 0 {palette:blue-base}, - stop: 1 {palette:green-base} - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5,stop: 0 {palette:blue-base},stop: 1 {palette:green-base}); border: 1px solid #5c5c5c; width: 10px; height: 10px; border-radius: 5px; } + QSlider::handle:horizontal { margin: -2px 0; } @@ -284,12 +265,8 @@ QSlider::handle:vertical { } QSlider::handle:disabled { - background: qlineargradient( - x1:0, y1:0, - x2:1, y2:1, - stop:0 {color:bg-buttons}, - stop:1 {color:bg-buttons-disabled} - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1:0, y1:0,x2:1, y2:1,stop:0 {color:bg-buttons},stop:1 {color:bg-buttons-disabled}); } /* Tab widget*/ @@ -307,19 +284,15 @@ QTabBar::tab { border-left: 3px solid transparent; border-top: 1px solid {color:border}; border-right: 1px solid {color:border}; - background: qlineargradient( - x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0.5 {color:bg}, stop: 1.0 {color:bg-inputs} - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0,stop: 0.5 {color:bg}, stop: 1.0 {color:bg-inputs}); } QTabBar::tab:selected { background: {color:grey-lighter}; border-left: 3px solid {color:border-focus}; - background: qlineargradient( - x1: 0, y1: 1, x2: 0, y2: 0, - stop: 0.5 {color:bg}, stop: 1.0 {color:border} - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0,stop: 0.5 {color:bg}, stop: 1.0 {color:border}); } QTabBar::tab:!selected { @@ -457,12 +430,8 @@ QProgressBar:vertical { } QProgressBar::chunk { - background: qlineargradient( - x1: 0, y1: 0.5, - x2: 1, y2: 0.5, - stop: 0 {palette:blue-base}, - stop: 1 {palette:green-base} - ); + /* must be single like because of Nuke*/ + background: qlineargradient(x1: 0, y1: 0.5,x2: 1, y2: 0.5,stop: 0 {palette:blue-base},stop: 1 {palette:green-base}); } /* Scroll bars */ From 3158710d43f06fd5e2793d84483ba7770c2a2753 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:28:58 +0200 Subject: [PATCH 707/716] removed fixed height of footer widget --- openpype/tools/loader/app.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index bbf6719af5..dac5e11d4c 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -124,19 +124,17 @@ class LoaderWindow(QtWidgets.QDialog): else: main_splitter.setSizes([250, 850, 200]) - # TODO keep footer size by message size footer_widget = QtWidgets.QWidget(self) - footer_widget.setFixedHeight(20) message_label = QtWidgets.QLabel(footer_widget) - footer_layout = QtWidgets.QVBoxLayout(footer_widget) + footer_layout = QtWidgets.QHBoxLayout(footer_widget) footer_layout.setContentsMargins(0, 0, 0, 0) - footer_layout.addWidget(message_label) + footer_layout.addWidget(message_label, 1) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(main_splitter) - layout.addWidget(footer_widget) + layout.addWidget(main_splitter, 1) + layout.addWidget(footer_widget, 0) self.data = { "state": { From 01d059a993af1e486bedb8ae051299f1cbcab07c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:31:52 +0200 Subject: [PATCH 708/716] added transparent image to resources --- openpype/style/images/transparent.png | Bin 0 -> 69 bytes openpype/style/pyqt5_resources.py | 745 +++++++++++++------------- openpype/style/pyside2_resources.py | 602 +++++++++++---------- openpype/style/resources.qrc | 1 + 4 files changed, 679 insertions(+), 669 deletions(-) create mode 100644 openpype/style/images/transparent.png diff --git a/openpype/style/images/transparent.png b/openpype/style/images/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..0f2e143b39a2e37e52841ff55d410a2000125eca GIT binary patch literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&AsjQ46A}`DJQfDV#ayC~ Pfh-13S3j3^P6 \x0b\xa4\x08020 \x0b\xa6\ -\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ -\x00\x00\x00\xa5\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ -\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ -200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ -\xaeB`\x82\ -\x00\x00\x00\xa6\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ @@ -62,18 +36,136 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ 200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ \xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ \xaeB`\x82\ -\x00\x00\x00\x9f\ +\x00\x00\x00\xa6\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x14\x1f\xf9\ -#\xd9\x0b\x00\x00\x00#IDAT\x08\xd7c`\xc0\ -\x0d\xe6|\x80\xb1\x18\x91\x05R\x04\xe0B\x08\x15)\x02\ -\x0c\x0c\x8c\xc8\x02\x08\x95h\x00\x00\xac\xac\x07\x90Ne\ -4\xac\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x070\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ +\x00\x00\x04\xb0iTXtXML:com.\ +adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0aH\x8b[^\x00\x00\x01\x83\ +iCCPsRGB IEC6196\ +6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ +\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ +\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\ +x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\ +Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\ +;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\ +\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ +\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\ +\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\ +\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\ +RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\ +?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\ +\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\ +\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\ +\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\ +\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\ +\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\ +\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\ +vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\ +\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\ +8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\ +S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ +\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ +Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x97IDAT\x18\x95m\xcf\xb1j\x02A\ +\x14\x85\xe1o\xb7\xb6\xd0'H=Vi\x03\xb1\xb4H\ +;l\xa5\xf19\xf6Y\x02VB\xbaa\x0a\x0b;\x1b\ +\x1bkA\x18\x02)m\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\ +\x9f\xff\x5c\xee\xa9b*\x13Ls\x13nF&\xa6\xf2\ +\x82\xaeF\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a[t\ +\xd8\xc7T\xc2@\x9ac\x8f?|U=|\xc5\x09w\ +\xbc\xa1\xc2\x193,r\x13.\xd5\xe0\xc2\x12\x07\x5cQ\ +#\xe0#7\xe1\xa8O\x0e\x7f\xda`\xd7\xaf\x9f\xb9\x09\ +\xdfc\x05\xff\xe5uLe\xf5\xcc\x1f\x0d3,\x83\xb6\ +\x06D\x83\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x00\xa0\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -86,6 +178,31 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ \x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ \xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ \x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x9e\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ +\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ +\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ \x00\x00\x07\xdd\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -214,19 +331,57 @@ zpp\xf0\xe3\x0e.\xa4\xd2\xae\xf0\x8a\xf7\x9a\xe3V\ q[s\x5c@H\xa5\xdda\x81\x0d\x9ek\x8e\xff\xfd\ \xcf?\xcc1\xe9\x01\x1c\x00sR-q\xe4J\x1bi\ \x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\xa6\ +\x00\x00\x00\x9e\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f \xb9\ -\x8dw\xe9\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x06\xe6|```B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\ -d``b`H\x11@\xe2 s\x19\x90\x8d@\x02\ -\x00#\xed\x08\xafd\x9f\x0f\x15\x00\x00\x00\x00IEN\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ +\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ +\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ +\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ +\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ D\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ \x00\x00\x00\xa0\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -234,11 +389,23 @@ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ -\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ -\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ -\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ -\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f\x0d\xfc\ +R+\x9c\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\ +\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\ +\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x9f\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x14\x1f\xf9\ +#\xd9\x0b\x00\x00\x00#IDAT\x08\xd7c`\xc0\ +\x0d\xe6|\x80\xb1\x18\x91\x05R\x04\xe0B\x08\x15)\x02\ +\x0c\x0c\x8c\xc8\x02\x08\x95h\x00\x00\xac\xac\x07\x90Ne\ +4\xac\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x07\x06\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -357,146 +524,28 @@ D\xaeB`\x82\ \x00\x00\x00\xa6\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ \x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ -;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ -\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f \xb9\ +\x8dw\xe9\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x06\xe6|```B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\ +d``b`H\x11@\xe2 s\x19\x90\x8d@\x02\ +\x00#\xed\x08\xafd\x9f\x0f\x15\x00\x00\x00\x00IEN\ D\xaeB`\x82\ -\x00\x00\x00\xa5\ +\x00\x00\x00\xa0\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ -\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ -200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ -\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ -\xaeB`\x82\ -\x00\x00\x070\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\ -\x00\x00\x04\xb0iTXtXML:com.\ -adobe.xmp\x00\x00\x00\x00\x00\x0a\x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a \x0a\x0aH\x8b[^\x00\x00\x01\x83\ -iCCPsRGB IEC6196\ -6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\ -\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\ -\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\ -x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\ -Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\ -;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\ -\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\ -\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\ -\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\ -\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\ -RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\ -?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\ -\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\ -\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\ -\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\ -\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\ -\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\ -\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\ -vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\ -\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\ -8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\ -S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\ -\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\ -Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x97IDAT\x18\x95m\xcf\xb1j\x02A\ -\x14\x85\xe1o\xb7\xb6\xd0'H=Vi\x03\xb1\xb4H\ -;l\xa5\xf19\xf6Y\x02VB\xbaa\x0a\x0b;\x1b\ -\x1bkA\x18\x02)m\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\ -\x9f\xff\x5c\xee\xa9b*\x13Ls\x13nF&\xa6\xf2\ -\x82\xaeF\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a[t\ -\xd8\xc7T\xc2@\x9ac\x8f?|U=|\xc5\x09w\ -\xbc\xa1\xc2\x193,r\x13.\xd5\xe0\xc2\x12\x07\x5cQ\ -#\xe0#7\xe1\xa8O\x0e\x7f\xda`\xd7\xaf\x9f\xb9\x09\ -\xdfc\x05\xff\xe5uLe\xf5\xcc\x1f\x0d3,\x83\xb6\ -\x06D\x83\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\ +\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\ +\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\ +\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\ +\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x07\xad\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -622,55 +671,6 @@ v)`\x8b\x07>\xa8\xe6\xd1\xfe\x0b\x9d\x85\x8eW\x0d\ ^x\xa2\x9e\x0e\xa7 tG9\x1d\xf6\xe1\x95+\xd6\ \xb1D\x8e\x0e\xcbX\xf0\x0fR\x8ay\x18\xdc\xe2\x02p\ \x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\xa0\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f\x0d\xfc\ -R+\x9c\x00\x00\x00$IDAT\x08\xd7c`@\ -\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\ -\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\ -\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\x9e\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ -\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ -\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ -\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ -\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\x9e\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\ -\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\ -\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\ -\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\ -\xc5\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00\x00\xa6\ -\x89\ -PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ -HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ -\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ -;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ -\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ -\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ -\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ -D\xaeB`\x82\ \x00\x00\x00\xa6\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -695,16 +695,6 @@ qt_resource_name = b"\ \x07\x03}\xc3\ \x00i\ \x00m\x00a\x00g\x00e\x00s\ -\x00\x15\ -\x03'rg\ -\x00c\ -\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\ -\x00.\x00p\x00n\x00g\ -\x00\x1b\ -\x03Z2'\ -\x00c\ -\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\ -\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ \x00\x0e\ \x0e\xde\xfa\xc7\ \x00l\ @@ -714,62 +704,35 @@ qt_resource_name = b"\ \x00d\ \x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ \ -\x00\x15\ -\x0f\xf3\xc0\x07\ -\x00u\ -\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\ -\x00.\x00p\x00n\x00g\ -\x00\x12\ -\x03\x8d\x04G\ -\x00r\ -\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\ -\x00g\ -\x00\x14\ -\x04^-\xa7\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00_\x00o\x00n\x00.\ -\x00p\x00n\x00g\ -\x00\x17\ -\x0ce\xce\x07\ -\x00l\ -\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ -\x00e\x00d\x00.\x00p\x00n\x00g\ -\x00\x0f\ -\x02\x9f\x05\x87\ -\x00r\ -\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ -\x00\x0f\ -\x06S%\xa7\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\ \x00\x12\ \x01.\x03'\ \x00c\ \x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\ \x00g\ -\x00\x0e\ -\x04\xa2\xfc\xa7\ -\x00d\ -\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ \x00\x12\ \x05\x8f\x9d\x07\ \x00b\ \x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00_\x00o\x00n\x00.\x00p\x00n\ \x00g\ -\x00\x11\ -\x0b\xda0\xa7\ -\x00b\ -\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00.\x00p\x00n\x00g\ -\ -\x00\x18\ -\x03\x8e\xdeg\ +\x00\x12\ +\x03\x8d\x04G\ \x00r\ -\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\ -\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\ +\x00g\ \x00\x0f\ \x01s\x8b\x07\ \x00u\ \x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\ +\x00\x1b\ +\x03Z2'\ +\x00c\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\ +\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x14\ +\x04^-\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00_\x00o\x00n\x00.\ +\x00p\x00n\x00g\ \x00\x0c\ \x06\xe6\xe6g\ \x00u\ @@ -779,6 +742,43 @@ qt_resource_name = b"\ \x00d\ \x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ \x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x15\ +\x03'rg\ +\x00c\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\ +\x00.\x00p\x00n\x00g\ +\x00\x0e\ +\x04\xa2\xfc\xa7\ +\x00d\ +\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x18\ +\x03\x8e\xdeg\ +\x00r\ +\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\ +\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x15\ +\x0f\xf3\xc0\x07\ +\x00u\ +\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\ +\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x06S%\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\ +\x00\x17\ +\x0ce\xce\x07\ +\x00l\ +\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\ +\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x02\x9f\x05\x87\ +\x00r\ +\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\ +\x00\x11\ +\x0b\xda0\xa7\ +\x00b\ +\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00.\x00p\x00n\x00g\ +\ \x00\x11\ \x00\xb8\x8c\x07\ \x00l\ @@ -791,34 +791,30 @@ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\ \x00\x00\x03,\x00\x00\x00\x00\x00\x01\x00\x00&\xf0\ -\x00\x00\x00\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x01\xfd\ -\x00\x00\x01\xe2\x00\x00\x00\x00\x00\x01\x00\x00\x14&\ -\x00\x00\x02\xb6\x00\x00\x00\x00\x00\x01\x00\x00%\x02\ -\x00\x00\x01\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x0cx\ +\x00\x00\x00J\x00\x00\x00\x00\x00\x01\x00\x00\x00\xaa\ +\x00\x00\x00r\x00\x00\x00\x00\x00\x01\x00\x00\x01S\ +\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x01\x00\x00\x09\xd5\ +\x00\x00\x02\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x9b\ +\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x14M\ +\x00\x00\x01\x14\x00\x00\x00\x00\x00\x01\x00\x00\x0aw\ +\x00\x00\x00\xc6\x00\x00\x00\x00\x00\x01\x00\x00\x091\ +\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x00\x15\xa0\ +\x00\x00\x01P\x00\x00\x00\x00\x00\x01\x00\x00\x0b \ +\x00\x00\x02\x00\x00\x00\x00\x00\x00\x01\x00\x00\x14\xf7\ +\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x01\xfd\ +\x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x16\xe7\ +\x00\x00\x01~\x00\x00\x00\x00\x00\x01\x00\x00\x13\x01\ +\x00\x00\x03\x04\x00\x00\x00\x00\x00\x01\x00\x00\x1f?\ +\x00\x00\x02\xac\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf1\ +\x00\x00\x01\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x13\xa3\ \x00\x00\x00(\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00X\x00\x00\x00\x00\x00\x01\x00\x00\x00\xaa\ -\x00\x00\x01\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x03I\ -\x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x00$^\ -\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x00\x03\xed\ -\x00\x00\x02\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x14\xd0\ -\x00\x00\x02.\x00\x00\x00\x00\x00\x01\x00\x00\x15y\ -\x00\x00\x01\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x0d\x1c\ -\x00\x00\x02\xda\x00\x00\x00\x00\x00\x01\x00\x00%\xa4\ -\x00\x00\x02X\x00\x00\x00\x00\x00\x01\x00\x00\x1c\xad\ -\x00\x00\x01f\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xce\ -\x00\x00\x02\xf8\x00\x00\x00\x00\x00\x01\x00\x00&F\ -\x00\x00\x00\x94\x00\x00\x00\x00\x00\x01\x00\x00\x01S\ -\x00\x00\x00\xde\x00\x00\x00\x00\x00\x01\x00\x00\x02\xa6\ +\x00\x00\x02X\x00\x00\x00\x00\x00\x01\x00\x00\x16D\ " - def qInitResources(): - QtCore.qRegisterResourceData( - 0x01, qt_resource_struct, qt_resource_name, qt_resource_data - ) - + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) def qCleanupResources(): - QtCore.qUnregisterResourceData( - 0x01, qt_resource_struct, qt_resource_name, qt_resource_data - ) + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/openpype/style/resources.qrc b/openpype/style/resources.qrc index a583d9458e..e2e69711f4 100644 --- a/openpype/style/resources.qrc +++ b/openpype/style/resources.qrc @@ -19,5 +19,6 @@ images/up_arrow.png images/up_arrow_disabled.png images/up_arrow_on.png + images/transparent.png From 536c6383715627a7778927f58840db3f71d84d69 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:32:09 +0200 Subject: [PATCH 709/716] use transparent image to hide branches in tree view --- openpype/style/style.css | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index 8e9827084e..f8a61cbbd3 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -415,6 +415,25 @@ QAbstractItemView::branch:closed:has-children:has-siblings:hover { background: transparent; } +QAbstractItemView::branch:has-siblings:!adjoins-item { + border-image: none; + image: url(:/openpype/images/transparent.png); + background: transparent; +} + +QAbstractItemView::branch:has-siblings:adjoins-item { + border-image: none; + image: url(:/openpype/images/transparent.png); + background: transparent; +} + +QAbstractItemView::branch:!has-children:!has-siblings:adjoins-item { + border-image: none; + image: url(:/openpype/images/transparent.png); + background: transparent; +} + + /* Progress bar */ QProgressBar { border: 1px solid {color:border}; From 140c290c607ad0dd87cce1e5f44afce38091c0ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:32:19 +0200 Subject: [PATCH 710/716] set down/up arrow for header --- openpype/style/style.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index f8a61cbbd3..d6f2460a27 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -340,6 +340,15 @@ QHeaderView::section:first { QHeaderView::section:last { border-right: none; } + +QHeaderView::down-arrow { + image: url(:/openpype/images/down_arrow.png); +} + +QHeaderView::up-arrow { + image: url(:/openpype/images/up_arrow.png); +} + /* Views QListView QTreeView QTableView */ QAbstractItemView { border: 0px solid {color:border}; From d4b4d2e8417e267a6381cf0fd883910b3b4bf5ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:40:12 +0200 Subject: [PATCH 711/716] fix formatting --- openpype/style/pyside2_resources.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/style/pyside2_resources.py b/openpype/style/pyside2_resources.py index e4bbc50533..97ee781c5d 100644 --- a/openpype/style/pyside2_resources.py +++ b/openpype/style/pyside2_resources.py @@ -812,9 +812,13 @@ qt_resource_struct = b"\ " def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData( + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data + ) def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qUnregisterResourceData( + 0x01, qt_resource_struct, qt_resource_name, qt_resource_data + ) qInitResources() From c39c3ecb045629160d809fd3e25bdb3bad173ea1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 12:40:38 +0200 Subject: [PATCH 712/716] do not call pyside initialization on import --- openpype/style/pyside2_resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/style/pyside2_resources.py b/openpype/style/pyside2_resources.py index 97ee781c5d..dff01eec49 100644 --- a/openpype/style/pyside2_resources.py +++ b/openpype/style/pyside2_resources.py @@ -811,14 +811,14 @@ qt_resource_struct = b"\ \x00\x00\x02X\x00\x00\x00\x00\x00\x01\x00\x00\x16D\ " + def qInitResources(): QtCore.qRegisterResourceData( 0x01, qt_resource_struct, qt_resource_name, qt_resource_data ) + def qCleanupResources(): QtCore.qUnregisterResourceData( 0x01, qt_resource_struct, qt_resource_name, qt_resource_data ) - -qInitResources() From 765fcf701434a19c3ff4e0401a0bb904e6e8f1f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 14:24:25 +0200 Subject: [PATCH 713/716] fix typo in label --- openpype/tools/experimental_tools/tools_def.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index 3657c2385b..254f542c4d 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -72,7 +72,7 @@ class ExperimentalTools: # experimental_tools = [ # ExperimentalTool( # "example", - # "Exmaple experimental tool", + # "Example experimental tool", # example_callback, # "Example tool tooltip." # ) From dc5f2221b9c6d838ef383c648bea66e8fd0e2b69 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 22 Oct 2021 14:34:43 +0200 Subject: [PATCH 714/716] added label describing how to turn onoff experimental tools --- openpype/tools/experimental_tools/dialog.py | 39 +++++++++++++++++---- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/openpype/tools/experimental_tools/dialog.py b/openpype/tools/experimental_tools/dialog.py index c7c8ce83fc..0fd170b31e 100644 --- a/openpype/tools/experimental_tools/dialog.py +++ b/openpype/tools/experimental_tools/dialog.py @@ -30,6 +30,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): icon = QtGui.QIcon(app_icon_path()) self.setWindowIcon(icon) + # Widgets for cases there are not available experimental tools empty_widget = QtWidgets.QWidget(self) empty_label = QtWidgets.QLabel( @@ -49,16 +50,42 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): empty_layout.addStretch(1) empty_layout.addLayout(empty_btns_layout) - content_widget = QtWidgets.QWidget(self) + # Content of Experimental tools - content_layout = QtWidgets.QVBoxLayout(content_widget) + # Layout where buttons are added + content_layout = QtWidgets.QVBoxLayout() content_layout.setContentsMargins(0, 0, 0, 0) + # Separator line + separator_widget = QtWidgets.QWidget(self) + separator_widget.setObjectName("Separator") + separator_widget.setMinimumHeight(2) + separator_widget.setMaximumHeight(2) + + # Label describing how to turn off tools + tool_btns_widget = QtWidgets.QWidget(self) + tool_btns_label = QtWidgets.QLabel( + ( + "You can enable these features in" + "
    OpenPype tray -> Settings -> Experimental tools" + ), + tool_btns_widget + ) + tool_btns_label.setAlignment(QtCore.Qt.AlignCenter) + + tool_btns_layout = QtWidgets.QVBoxLayout(tool_btns_widget) + tool_btns_layout.setContentsMargins(0, 0, 0, 0) + tool_btns_layout.addLayout(content_layout) + tool_btns_layout.addStretch(1) + tool_btns_layout.addWidget(separator_widget, 0) + tool_btns_layout.addWidget(tool_btns_label, 0) + experimental_tools = ExperimentalTools() + # Main layout layout = QtWidgets.QVBoxLayout(self) layout.addWidget(empty_widget, 1) - layout.addWidget(content_widget, 1) + layout.addWidget(tool_btns_widget, 1) refresh_timer = QtCore.QTimer() refresh_timer.setInterval(self.refresh_interval) @@ -67,7 +94,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): ok_btn.clicked.connect(self._on_ok_click) self._empty_widget = empty_widget - self._content_widget = content_widget + self._tool_btns_widget = tool_btns_widget self._content_layout = content_layout self._experimental_tools = experimental_tools @@ -94,7 +121,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): button = self._buttons_by_tool_identifier[identifier] else: is_new = True - button = ToolButton(identifier, self) + button = ToolButton(identifier, self._tool_btns_widget) button.triggered.connect(self._on_btn_trigger) self._buttons_by_tool_identifier[identifier] = button self._content_layout.insertWidget(idx, button) @@ -128,7 +155,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog): def _set_visibility(self): content_visible = self._is_content_visible() - self._content_widget.setVisible(content_visible) + self._tool_btns_widget.setVisible(content_visible) self._empty_widget.setVisible(not content_visible) def _on_ok_click(self): From e320065986f25b5b8a814b59a56363edef1548b2 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 23 Oct 2021 03:40:10 +0000 Subject: [PATCH 715/716] [Automated] Bump version --- CHANGELOG.md | 51 +++++++++++++++++++++++++-------------------- openpype/version.py | 2 +- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95792f8a7a..eca2a8b423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,34 @@ # Changelog +## [3.5.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...HEAD) + +**πŸš€ Enhancements** + +- Tools: Experimental tools [\#2167](https://github.com/pypeclub/OpenPype/pull/2167) +- Loader: Refactor and use OpenPype stylesheets [\#2166](https://github.com/pypeclub/OpenPype/pull/2166) +- Burnins: DNxHD profiles handling [\#2142](https://github.com/pypeclub/OpenPype/pull/2142) +- Tools: Single access point for host tools [\#2139](https://github.com/pypeclub/OpenPype/pull/2139) + +**πŸ› Bug fixes** + +- Blender: Fix 'Deselect All' with object not in 'Object Mode' [\#2163](https://github.com/pypeclub/OpenPype/pull/2163) +- Tools: Stylesheets are applied after tool show [\#2161](https://github.com/pypeclub/OpenPype/pull/2161) +- Maya: Collect render - fix UNC path support πŸ› [\#2158](https://github.com/pypeclub/OpenPype/pull/2158) +- Maya: Fix hotbox broken by scriptsmenu [\#2151](https://github.com/pypeclub/OpenPype/pull/2151) +- Ftrack: Ignore save warnings exception in Prepare project action [\#2150](https://github.com/pypeclub/OpenPype/pull/2150) +- Loader thumbnails with smooth edges [\#2147](https://github.com/pypeclub/OpenPype/pull/2147) +- Added validator for source files for Standalone Publisher [\#2138](https://github.com/pypeclub/OpenPype/pull/2138) + +**Merged pull requests:** + +- Bump pillow from 8.2.0 to 8.3.2 [\#2162](https://github.com/pypeclub/OpenPype/pull/2162) +- Bump axios from 0.21.1 to 0.21.4 in /website [\#2059](https://github.com/pypeclub/OpenPype/pull/2059) + ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...3.5.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0) **Deprecated:** @@ -66,10 +92,6 @@ [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** - General: Startup validations [\#2054](https://github.com/pypeclub/OpenPype/pull/2054) @@ -81,9 +103,7 @@ - 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) **πŸ› Bug fixes** @@ -101,21 +121,12 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0) -**πŸ†• New features** - -- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003) - **πŸš€ 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) **πŸ› Bug fixes** @@ -124,12 +135,6 @@ - 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) - -### πŸ“– Documentation - -- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014) ## [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 d88d79b995..49b61c755e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.5.0" +__version__ = "3.5.1-nightly.1" From db7c8b69909472e2a8de435c2518731e9a53cc25 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 25 Oct 2021 09:22:38 +0200 Subject: [PATCH 716/716] fix Ci expected format in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dade0a2f57..c49ecabdab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.0.0" +version = "3.5.1-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License"