From 946c93ad5d5bac0f80972c3b779a4ab5c20e0555 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Mar 2021 13:13:42 +0100 Subject: [PATCH 01/68] igniter UI improvements simplifying installation --- igniter/__init__.py | 1 - igniter/install_dialog.py | 170 ++++++++++++++++++++++++++++---------- igniter/tools.py | 47 ++++++++++- 3 files changed, 171 insertions(+), 47 deletions(-) diff --git a/igniter/__init__.py b/igniter/__init__.py index ffac2b023f..6fce75e95a 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -12,7 +12,6 @@ def run(): """Show Igniter dialog.""" app = QtWidgets.QApplication(sys.argv) d = InstallDialog() - d.exec_() d.show() sys.exit(app.exec_()) diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index b4fa68d89a..f54acf49f2 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -3,23 +3,46 @@ import os import sys -from Qt import QtCore, QtGui, QtWidgets -from Qt.QtGui import QValidator +from Qt import QtCore, QtGui, QtWidgets # noqa +from Qt.QtGui import QValidator # noqa from .install_thread import InstallThread -from .tools import validate_path_string, validate_mongo_connection +from .tools import ( + validate_path_string, + validate_mongo_connection, + get_pype_path_from_db +) +from .user_settings import PypeSettingsRegistry + + +class FocusHandlingLineEdit(QtWidgets.QLineEdit): + """Handling focus in/out on QLineEdit.""" + focusIn = QtCore.Signal() + focusOut = QtCore.Signal() + + def focusOutEvent(self, event): # noqa + """For emitting signal on focus out.""" + self.focusOut.emit() + super().focusOutEvent(event) + + def focusInEvent(self, event): # noqa + """For emitting signal on focus in.""" + self.focusIn.emit() + super().focusInEvent(event) class InstallDialog(QtWidgets.QDialog): + """Main Igniter dialog window.""" _size_w = 400 - _size_h = 400 - _path = None + _size_h = 600 + path = None _controls_disabled = False def __init__(self, parent=None): super(InstallDialog, self).__init__(parent) + self.registry = PypeSettingsRegistry() - self._mongo_url = os.getenv("PYPE_MONGO", "") + self._mongo_url = os.getenv("PYPE_MONGO", "") or self.registry.get_secure_item("pypeMongo") or "" # noqa: E501 self.setWindowTitle("Pype - Configure Pype repository path") self._icon_path = os.path.join( @@ -34,7 +57,7 @@ class InstallDialog(QtWidgets.QDialog): self.setMinimumSize( QtCore.QSize(self._size_w, self._size_h)) self.setMaximumSize( - QtCore.QSize(self._size_w + 100, self._size_h + 100)) + QtCore.QSize(self._size_w + 100, self._size_h + 500)) # style for normal console text self.default_console_style = QtGui.QTextCharFormat() @@ -98,7 +121,7 @@ class InstallDialog(QtWidgets.QDialog): input_layout = QtWidgets.QHBoxLayout() input_layout.setContentsMargins(0, 10, 0, 10) - self.user_input = QtWidgets.QLineEdit() + self.user_input = FocusHandlingLineEdit() self.user_input.setPlaceholderText("Pype repository path or url") self.user_input.textChanged.connect(self._path_changed) @@ -130,14 +153,20 @@ class InstallDialog(QtWidgets.QDialog): # -------------------------------------------------------------------- class MongoWidget(QtWidgets.QWidget): + """Widget to input mongodb URL.""" + def __init__(self, parent=None): self._btn_mongo = None super(MongoWidget, self).__init__(parent) mongo_layout = QtWidgets.QHBoxLayout() mongo_layout.setContentsMargins(0, 0, 0, 0) - self._mongo_input = QtWidgets.QLineEdit() + self._mongo_input = FocusHandlingLineEdit() self._mongo_input.setPlaceholderText("Mongo URL") self._mongo_input.textChanged.connect(self._mongo_changed) + self._mongo_input.focusIn.connect(self._focus_in) + self._mongo_input.focusOut.connect(self._focus_out) + # self._mongo_input.setValidator( + # PathValidator(self._mongo_input)) self._mongo_input.setStyleSheet( ("color: rgb(233, 233, 233);" "background-color: rgb(64, 64, 64);" @@ -148,16 +177,37 @@ class InstallDialog(QtWidgets.QDialog): mongo_layout.addWidget(self._mongo_input) self.setLayout(mongo_layout) + def _focus_out(self): + self.validate_url() + + def _focus_in(self): + self._mongo_input.setStyleSheet( + """ + background-color: rgb(32, 32, 19); + color: rgb(255, 190, 15); + padding: 0.5em; + border: 1px solid rgb(64, 64, 32); + """ + ) + def _mongo_changed(self, mongo: str): self.parent().mongo_url = mongo - def get_mongo_url(self): + def get_mongo_url(self) -> str: + """Helper to get url from parent.""" return self.parent().mongo_url def set_mongo_url(self, mongo: str): + """Helper to set url to parent. + + Args: + mongo (str): mongodb url string. + + """ self._mongo_input.setText(mongo) def set_valid(self): + """Set valid state on mongo url input.""" self._mongo_input.setStyleSheet( """ background-color: rgb(19, 19, 19); @@ -166,16 +216,44 @@ class InstallDialog(QtWidgets.QDialog): border: 1px solid rgb(32, 64, 32); """ ) + self.parent().ok_button.setEnabled(True) + if self.parent().path != "": + path = get_pype_path_from_db(self.parent().mongo_url) + self.parent().path = path + self.parent().user_input.setText(path) def set_invalid(self): + """Set invalid state on mongo url input.""" self._mongo_input.setStyleSheet( """ background-color: rgb(32, 19, 19); color: rgb(255, 69, 0); padding: 0.5em; - border: 1px solid rgb(32, 64, 32); + border: 1px solid rgb(64, 32, 32); """ ) + self.parent().ok_button.setEnabled(False) + + def validate_url(self) -> bool: + """Validate if entered url is ok. + + Returns: + True if url is valid monogo string. + + """ + if self.parent().mongo_url == "": + return False + + is_valid, reason_str = validate_mongo_connection( + self.parent().mongo_url + ) + if not is_valid: + self.set_invalid() + self.parent().update_console(f"!!! {reason_str}", True) + return False + else: + self.set_valid() + return True self._mongo = MongoWidget(self) if self._mongo_url: @@ -193,15 +271,15 @@ class InstallDialog(QtWidgets.QDialog): pype_logo_label.setPixmap(pype_logo) pype_logo_label.setContentsMargins(10, 0, 0, 10) - self._ok_button = QtWidgets.QPushButton("OK") - self._ok_button.setStyleSheet( + self.ok_button = QtWidgets.QPushButton("OK") + self.ok_button.setStyleSheet( ("color: rgb(64, 64, 64);" "background-color: rgb(72, 200, 150);" "padding: 0.5em;") ) - self._ok_button.setMinimumSize(64, 24) - self._ok_button.setToolTip("Save and continue") - self._ok_button.clicked.connect(self._on_ok_clicked) + self.ok_button.setMinimumSize(64, 24) + self.ok_button.setToolTip("Save and continue") + self.ok_button.clicked.connect(self._on_ok_clicked) self._exit_button = QtWidgets.QPushButton("Exit") self._exit_button.setStyleSheet( @@ -216,7 +294,7 @@ class InstallDialog(QtWidgets.QDialog): bottom_layout.setContentsMargins(0, 10, 0, 0) bottom_layout.addWidget(pype_logo_label) bottom_layout.addStretch(1) - bottom_layout.addWidget(self._ok_button) + bottom_layout.addWidget(self.ok_button) bottom_layout.addWidget(self._exit_button) bottom_widget.setLayout(bottom_layout) @@ -301,6 +379,10 @@ class InstallDialog(QtWidgets.QDialog): main.addWidget(self._progress_bar) main.addWidget(bottom_widget) self.setLayout(main) + if not self._mongo_url: + self._mongo.setVisible(False) + else: + self._mongo.validate_url() def _on_select_clicked(self): """Show directory dialog.""" @@ -317,8 +399,7 @@ class InstallDialog(QtWidgets.QDialog): if not result: return - filename = result[0] - filename = QtCore.QDir.toNativeSeparators(filename) + filename = QtCore.QDir.toNativeSeparators(result) if os.path.isdir(filename): self.user_input.setText(filename) @@ -329,7 +410,7 @@ class InstallDialog(QtWidgets.QDialog): This will once again validate entered path and if ok, start working thread that will do actual job. """ - valid, reason = validate_path_string(self._path) + valid, reason = validate_path_string(self.path) if not valid: self.user_input.setStyleSheet( """ @@ -339,7 +420,7 @@ class InstallDialog(QtWidgets.QDialog): border: 1px solid rgb(32, 64, 32); """ ) - self._update_console(reason, True) + self.update_console(reason, True) return else: self.user_input.setStyleSheet( @@ -350,23 +431,23 @@ class InstallDialog(QtWidgets.QDialog): border: 1px solid rgb(32, 64, 32); """ ) - if not self._path or not self._path.startswith("mongodb"): + if not self.path or not self.path.startswith("mongodb"): valid, reason = validate_mongo_connection( self._mongo.get_mongo_url() ) if not valid: self._mongo.set_invalid() - self._update_console(f"!!! {reason}", True) + self.update_console(f"!!! {reason}", True) return else: self._mongo.set_valid() self._disable_buttons() self._install_thread = InstallThread(self) - self._install_thread.message.connect(self._update_console) + self._install_thread.message.connect(self.update_console) self._install_thread.progress.connect(self._update_progress) self._install_thread.finished.connect(self._enable_buttons) - self._install_thread.set_path(self._path) + self._install_thread.set_path(self.path) self._install_thread.set_mongo(self._mongo.get_mongo_url()) self._install_thread.start() @@ -378,17 +459,17 @@ class InstallDialog(QtWidgets.QDialog): def _path_changed(self, path: str) -> str: """Set path.""" - self._path = path - if not self._path.startswith("mongodb"): + self.path = path + if not self.path.startswith("mongodb"): self._mongo.setVisible(True) else: self._mongo.setVisible(False) - if len(self._path) < 1: + if len(self.path) < 1: self._mongo.setVisible(False) return path - def _update_console(self, msg: str, error: bool = False) -> None: + def update_console(self, msg: str, error: bool = False) -> None: """Display message in console. Args: @@ -405,17 +486,17 @@ class InstallDialog(QtWidgets.QDialog): """Disable buttons so user interaction doesn't interfere.""" self._btn_select.setEnabled(False) self._exit_button.setEnabled(False) - self._ok_button.setEnabled(False) + self.ok_button.setEnabled(False) self._controls_disabled = True def _enable_buttons(self): """Enable buttons after operation is complete.""" self._btn_select.setEnabled(True) self._exit_button.setEnabled(True) - self._ok_button.setEnabled(True) + self.ok_button.setEnabled(True) self._controls_disabled = False - def closeEvent(self, event): + def closeEvent(self, event): # noqa """Prevent closing if window when controls are disabled.""" if self._controls_disabled: return event.ignore() @@ -423,13 +504,15 @@ class InstallDialog(QtWidgets.QDialog): class PathValidator(QValidator): + """Validate mongodb url for Qt widgets.""" - def __init__(self, parent=None): + def __init__(self, parent=None, intermediate=False): self.parent = parent + self.intermediate = intermediate super(PathValidator, self).__init__(parent) def _return_state( - self, state: QValidator.State, reason: str, path: str, pos: int): + self, state: QValidator.State, reason: str, path: str): """Set stylesheets and actions on parent based on state. Warning: @@ -448,7 +531,7 @@ class PathValidator(QValidator): border: 1px solid rgb(32, 64, 32); """ ) - elif state == QValidator.State.Intermediate: + elif state == QValidator.State.Intermediate and self.intermediate: self.parent.setToolTip(reason) self.parent.setStyleSheet( """ @@ -489,22 +572,23 @@ class PathValidator(QValidator): if path.startswith("mongodb"): pos = len(path) return self._return_state( - QValidator.State.Intermediate, "", path, pos) + QValidator.State.Intermediate, "", path) if len(path) < 6: return self._return_state( - QValidator.State.Intermediate, "", path, pos) + QValidator.State.Intermediate, "", path) valid, reason = validate_path_string(path) if not valid: return self._return_state( - QValidator.State.Invalid, reason, path, pos) + QValidator.State.Invalid, reason, path) else: return self._return_state( - QValidator.State.Acceptable, reason, path, pos) + QValidator.State.Acceptable, reason, path) class CollapsibleWidget(QtWidgets.QWidget): + """Collapsible widget to hide mongo url in necessary.""" def __init__(self, parent=None, title: str = "", animation: int = 300): self._mainLayout = QtWidgets.QGridLayout(parent) @@ -515,9 +599,9 @@ class CollapsibleWidget(QtWidgets.QWidget): self._animation = animation self._title = title super(CollapsibleWidget, self).__init__(parent) - self._initUi() + self._init_ui() - def _initUi(self): + def _init_ui(self): self._toggleButton.setStyleSheet( """QToolButton { border: none; @@ -576,13 +660,13 @@ class CollapsibleWidget(QtWidgets.QWidget): self._toggleAnimation.setDirection(direction) self._toggleAnimation.start() - def setContentLayout(self, content_layout: QtWidgets.QLayout): + def setContentLayout(self, content_layout: QtWidgets.QLayout): # noqa self._contentArea.setLayout(content_layout) collapsed_height = \ self.sizeHint().height() - self._contentArea.maximumHeight() content_height = self._contentArea.sizeHint().height() - for i in range(0, self._toggleAnimation.animationCount() - 1): + for i in range(self._toggleAnimation.animationCount() - 1): sec_anim = self._toggleAnimation.animationAt(i) sec_anim.setDuration(self._animation) sec_anim.setStartValue(collapsed_height) diff --git a/igniter/tools.py b/igniter/tools.py index d9a315834a..43e34fced2 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -9,8 +9,9 @@ version is decided. import os import uuid -from typing import Dict +from typing import Dict, Union from urllib.parse import urlparse, parse_qs +import platform from pymongo import MongoClient from pymongo.errors import ServerSelectionTimeoutError, InvalidURI @@ -115,10 +116,14 @@ def validate_mongo_connection(cnx: str) -> (bool, str): if parsed.scheme not in ["mongodb", "mongodb+srv"]: return False, "Not mongodb schema" # we have mongo connection string. Let's try if we can connect. - components = decompose_url(cnx) + try: + components = decompose_url(cnx) + except RuntimeError: + return False, f"Invalid port specified." + mongo_args = { "host": compose_url(**components), - "serverSelectionTimeoutMS": 1000 + "serverSelectionTimeoutMS": 2000 } port = components.get("port") if port is not None: @@ -200,3 +205,39 @@ def load_environments(sections: list = None) -> dict: merged_env = acre.append(merged_env, parsed_env) return acre.compute(merged_env, cleanup=True) + + +def get_pype_path_from_db(url: str) -> Union[str, None]: + """Get Pype path from database. + + Args: + url (str): mongodb url. + + Returns: + path to Pype or None if not found + + """ + try: + components = decompose_url(url) + except RuntimeError: + return None + mongo_args = { + "host": compose_url(**components), + "serverSelectionTimeoutMS": 2000 + } + port = components.get("port") + if port is not None: + mongo_args["port"] = int(port) + + try: + client = MongoClient(**mongo_args) + except Exception: + return None + + db = client.pype + col = db.settings + + result = col.find_one({"type": "global_settings"}, {"value": 1}) + global_settings = result.get("value") + + return global_settings.get("pype_path", {}).get(platform.system().lower()) From 09a0388622dd14d4e72b37fec26efabce0fa3879 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Mar 2021 21:31:04 +0100 Subject: [PATCH 02/68] updated pytest to 6.1 --- poetry.lock | 45 ++++++++++++++++++++++----------------------- pyproject.toml | 2 +- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/poetry.lock b/poetry.lock index c670dcd6af..3c15a41ea1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -451,6 +451,14 @@ zipp = ">=0.5" docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "isort" version = "5.7.0" @@ -585,14 +593,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "more-itertools" -version = "8.6.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.5" - [[package]] name = "multidict" version = "5.1.0" @@ -892,25 +892,24 @@ python-versions = ">=3.5" [[package]] name = "pytest" -version = "5.4.3" +version = "6.2.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=17.4.0" +attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -more-itertools = ">=4.0.0" +iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" [package.extras] -checkqa-mypy = ["mypy (==v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] @@ -1354,7 +1353,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "923189591ed78935c05c922eda38f0b993e3d310aa13e53a438b2c27fa4fc9f7" +content-hash = "7f432949d1985c76be0307c20508c5934aa826f43117e394de9661c71737e098" [metadata.files] acre = [] @@ -1643,6 +1642,10 @@ importlib-metadata = [ {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, ] +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.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"}, {file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"}, @@ -1755,10 +1758,6 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] -more-itertools = [ - {file = "more-itertools-8.6.0.tar.gz", hash = "sha256:b3a9005928e5bed54076e6e549c792b306fddfe72b2d1d22dd63d42d5d3899cf"}, - {file = "more_itertools-8.6.0-py3-none-any.whl", hash = "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330"}, -] multidict = [ {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, @@ -2068,8 +2067,8 @@ pyrsistent = [ {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, ] pytest = [ - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, + {file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"}, + {file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"}, ] pytest-cov = [ {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"}, diff --git a/pyproject.toml b/pyproject.toml index dc2a956cb5..74d8771174 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ Jinja2 = "^2.11" pycodestyle = "^2.5.0" pydocstyle = "^3.0.0" pylint = "^2.4.4" -pytest = "^5.3.2" +pytest = "^6.1" pytest-cov = "*" pytest-print = "*" Sphinx = "*" From 4d24a5d1f0cf87456e1fbd8fc6457864a78484b4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Mar 2021 21:32:43 +0100 Subject: [PATCH 03/68] stop loading environments before version is determined --- igniter/bootstrap_repos.py | 645 +++++++++++++++----------- igniter/install_thread.py | 49 +- igniter/tools.py | 7 +- start.py | 54 ++- tests/igniter/test_bootstrap_repos.py | 7 +- 5 files changed, 439 insertions(+), 323 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 38de3007b4..bf121ba97c 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -15,7 +15,12 @@ from appdirs import user_data_dir from speedcopy import copyfile from .user_settings import PypeSettingsRegistry -from .tools import load_environments +from .tools import get_pype_path_from_db + + +LOG_INFO = 0 +LOG_WARNING = 1 +LOG_ERROR = 3 @functools.total_ordering @@ -285,6 +290,9 @@ class BootstrapRepos: def get_version(repo_dir: Path) -> Union[str, None]: """Get version of Pype in given directory. + Note: in frozen Pype installed in user data dir, this must point + one level deeper as it is `pype-version-v3.0.0/pype/pype/version.py` + Args: repo_dir (Path): Path to Pype repo. @@ -304,7 +312,8 @@ class BootstrapRepos: return version['__version__'] - def install_live_repos(self, repo_dir: Path = None) -> Union[Path, None]: + def create_version_from_live_code( + self, repo_dir: Path = None) -> Union[PypeVersion, None]: """Copy zip created from Pype repositories to user data dir. This detect Pype version either in local "live" Pype repository @@ -336,30 +345,123 @@ class BootstrapRepos: with tempfile.TemporaryDirectory() as temp_dir: temp_zip = \ Path(temp_dir) / f"pype-v{version}.zip" - self._log.info(f"creating zip: {temp_zip}") + self._print(f"creating zip: {temp_zip}") self._create_pype_zip(temp_zip, repo_dir) if not os.path.exists(temp_zip): - self._log.error("make archive failed.") + self._print("make archive failed.", LOG_ERROR) return None - destination = self.data_dir / temp_zip.name + destination = self._move_zip_to_data_dir(temp_zip) - if destination.exists(): - self._log.warning( - f"Destination file {destination} exists, removing.") - try: - destination.unlink() - except Exception as e: - self._log.error(e) - return None + return PypeVersion(version=version, path=destination) + + def _move_zip_to_data_dir(self, zip_file) -> Union[None, Path]: + """Move zip with Pype version to user data directory. + + Args: + zip_file (Path): Path to zip file. + + Returns: + None if move fails. + Path to moved zip on success. + + """ + destination = self.data_dir / zip_file.name + + if destination.exists(): + self._print( + f"Destination file {destination} exists, removing.", + LOG_WARNING) try: - shutil.move(temp_zip.as_posix(), self.data_dir.as_posix()) - except shutil.Error as e: - self._log.error(e) + destination.unlink() + except Exception as e: + self._print(str(e), LOG_ERROR, exc_info=True) return None + try: + shutil.move(zip_file.as_posix(), self.data_dir.as_posix()) + except shutil.Error as e: + self._print(str(e), LOG_ERROR, exc_info=True) + return None + return destination + def _filter_dir(self, path: Path, path_filter: List) -> List[Path]: + """Recursively crawl over path and filter.""" + result = [] + for item in path.iterdir(): + if item.name in path_filter: + continue + if item.name.startswith('.'): + continue + if item.is_dir(): + result.extend(self._filter_dir(item, path_filter)) + else: + result.append(item) + return result + + def create_version_from_frozen_code(self) -> Union[None, PypeVersion]: + """Create Pype version from *frozen* code distributed by installer. + + This should be real edge case for those wanting to try out Pype + without setting up whole infrastructure but is strongly discouraged + in studio setup as this use local version independent of others + that can be out of date. + + Returns: + :class:`PypeVersion` zip file to be installed. + + """ + frozen_root = Path(sys.executable).parent + repo_dir = frozen_root / "repos" + repo_list = self._filter_dir( + repo_dir, self.zip_filter) + + # from frozen code we need igniter, pype, schema vendor + pype_list = self._filter_dir( + frozen_root / "pype", self.zip_filter) + pype_list += self._filter_dir( + frozen_root / "igniter", self.zip_filter) + pype_list += self._filter_dir( + frozen_root / "schema", self.zip_filter) + pype_list += self._filter_dir( + frozen_root / "vendor", self.zip_filter) + pype_list.append(frozen_root / "README.md") + pype_list.append(frozen_root / "LICENSE") + + version = self.get_version(frozen_root) + + # create zip inside temporary directory. + with tempfile.TemporaryDirectory() as temp_dir: + temp_zip = \ + Path(temp_dir) / f"pype-v{version}.zip" + self._print(f"creating zip: {temp_zip}") + + with ZipFile(temp_zip, "w") as zip_file: + progress = 0 + repo_inc = 48.0 / float(len(repo_list)) + file: Path + for file in repo_list: + progress += repo_inc + self._progress_callback(int(progress)) + + # archive name is relative to repos dir + arc_name = file.relative_to(repo_dir) + zip_file.write(file, arc_name) + + pype_inc = 48.0 / float(len(pype_list)) + file: Path + for file in pype_list: + progress += pype_inc + self._progress_callback(int(progress)) + + arc_name = file.relative_to(frozen_root.parent) + zip_file.write(file, arc_name) + + destination = self._move_zip_to_data_dir(temp_zip) + + return PypeVersion(version=version, path=destination) + def _create_pype_zip( self, zip_path: Path, include_dir: Path, @@ -379,23 +481,9 @@ class BootstrapRepos: """ include_dir = include_dir.resolve() - def _filter_dir(path: Path, path_filter: List) -> List[Path]: - """Recursively crawl over path and filter.""" - result = [] - for item in path.iterdir(): - if item.name in path_filter: - continue - if item.name.startswith('.'): - continue - if item.is_dir(): - result.extend(_filter_dir(item, path_filter)) - else: - result.append(item) - return result - pype_list = [] # get filtered list of files in repositories (repos directory) - repo_list = _filter_dir(include_dir, self.zip_filter) + repo_list = self._filter_dir(include_dir, self.zip_filter) # count them repo_files = len(repo_list) @@ -404,15 +492,15 @@ class BootstrapRepos: pype_inc = 0 if include_pype: # get filtered list of file in Pype repository - pype_list = _filter_dir(include_dir.parent, self.zip_filter) + pype_list = self._filter_dir(include_dir.parent, self.zip_filter) pype_files = len(pype_list) repo_inc = 48.0 / float(repo_files) pype_inc = 48.0 / float(pype_files) else: repo_inc = 98.0 / float(repo_files) - progress = 0 with ZipFile(zip_path, "w") as zip_file: + progress = 0 file: Path for file in repo_list: progress += repo_inc @@ -446,8 +534,7 @@ class BootstrapRepos: continue processed_path = file - self._log.debug(f"processing {processed_path}") - self._print(f"- processing {processed_path}", False) + self._print(f"- processing {processed_path}") zip_file.write(file, "pype" / file.relative_to(pype_root)) @@ -468,6 +555,9 @@ class BootstrapRepos: Args: archive (Path): path to archive. + .. deprecated:: 3.0 + we don't use zip archives directly + """ if not archive.is_file() and not archive.exists(): raise ValueError("Archive is not file.") @@ -520,7 +610,7 @@ class BootstrapRepos: def find_pype( self, - pype_path: Path = None, + pype_path: Union[Path, str] = None, staging: bool = False, include_zips: bool = False) -> Union[List[PypeVersion], None]: """Get ordered dict of detected Pype version. @@ -532,7 +622,8 @@ class BootstrapRepos: 3) We use user data directory Args: - pype_path (Path, optional): Try to find Pype on the given path. + pype_path (Path or str, optional): Try to find Pype on the given + path or url. staging (bool, optional): Filter only staging version, skip them otherwise. include_zips (bool, optional): If set True it will try to find @@ -544,7 +635,17 @@ class BootstrapRepos: None: if Pype is not found. + Todo: + implement git/url support as Pype location, so it would be + possible to enter git url, Pype would check it out and if it is + ok install it as normal version. + """ + if pype_path and not isinstance(pype_path, Path): + raise NotImplementedError( + ("Finding Pype in non-filesystem locations is" + " not implemented yet.")) + dir_to_search = self.data_dir # if we have pype_path specified, search only there. @@ -565,116 +666,15 @@ class BootstrapRepos: # nothing found in registry, we'll use data dir pass - # pype installation dir doesn't exists - if not dir_to_search.exists(): - return None + pype_versions = self.get_pype_versions(dir_to_search, staging) - _pype_versions = [] - # iterate over directory in first level and find all that might - # contain Pype. - for file in dir_to_search.iterdir(): + # remove zip file version if needed. + if not include_zips: + pype_versions = [ + v for v in pype_versions if v.path.suffix != ".zip" + ] - # if file, strip extension, in case of dir not. - name = file.name if file.is_dir() else file.stem - result = PypeVersion.version_in_str(name) - - if result[0]: - detected_version: PypeVersion - detected_version = result[1] - - if file.is_dir(): - # if item is directory that might (based on it's name) - # contain Pype version, check if it really does contain - # Pype and that their versions matches. - try: - # add one 'pype' level as inside dir there should - # be many other repositories. - version_str = BootstrapRepos.get_version( - file / "pype") - version_check = PypeVersion(version=version_str) - except ValueError: - self._log.error( - f"cannot determine version from {file}") - continue - - version_main = version_check.get_main_version() - detected_main = detected_version.get_main_version() - if version_main != detected_main: - self._log.error( - (f"dir version ({detected_version}) and " - f"its content version ({version_check}) " - "doesn't match. Skipping.")) - continue - - if file.is_file(): - - if not include_zips: - continue - - # skip non-zip files - if file.suffix.lower() != ".zip": - continue - - # open zip file, look inside and parse version from Pype - # inside it. If there is none, or it is different from - # version specified in file name, skip it. - try: - with ZipFile(file, "r") as zip_file: - with zip_file.open( - "pype/pype/version.py") as version_file: - zip_version = {} - exec(version_file.read(), zip_version) - version_check = PypeVersion( - version=zip_version["__version__"]) - - version_main = version_check.get_main_version() # noqa: E501 - detected_main = detected_version.get_main_version() # noqa: E501 - - if version_main != detected_main: - self._log.error( - (f"zip version ({detected_version}) " - f"and its content version " - f"({version_check}) " - "doesn't match. Skipping.")) - continue - except BadZipFile: - self._log.error(f"{file} is not zip file") - continue - except KeyError: - self._log.error("Zip not containing Pype") - continue - - detected_version.path = file - if staging and detected_version.is_staging(): - _pype_versions.append(detected_version) - - if not staging and not detected_version.is_staging(): - _pype_versions.append(detected_version) - - return sorted(_pype_versions) - - @staticmethod - def _get_pype_from_mongo(mongo_url: str) -> Union[Path, None]: - """Get path from Mongo database. - - This sets environment variable ``PYPE_MONGO`` for - :mod:`pype.settings` to be able to read data from database. - It will then retrieve environment variables and among them - must be ``PYPE_PATH``. - - Args: - mongo_url (str): mongodb connection url - - Returns: - Path: if path from ``PYPE_PATH`` is found. - None: if not. - - """ - os.environ["PYPE_MONGO"] = mongo_url - env = load_environments() - if not env.get("PYPE_PATH"): - return None - return Path(env.get("PYPE_PATH")) + return pype_versions def process_entered_location(self, location: str) -> Union[Path, None]: """Process user entered location string. @@ -683,7 +683,7 @@ class BootstrapRepos: If it is mongodb url, it will connect and load ``PYPE_PATH`` from there and use it as path to Pype. In it is _not_ mongodb url, it is assumed we have a path, this is tested and zip file is - produced and installed using :meth:`install_live_repos`. + produced and installed using :meth:`create_version_from_live_code`. Args: location (str): User entered location. @@ -696,9 +696,9 @@ class BootstrapRepos: pype_path = None # try to get pype path from mongo. if location.startswith("mongodb"): - pype_path = self._get_pype_from_mongo(location) + pype_path = get_pype_path_from_db(location) if not pype_path: - self._log.error("cannot find PYPE_PATH in settings.") + self._print("cannot find PYPE_PATH in settings.") return None # if not successful, consider location to be fs path. @@ -707,12 +707,12 @@ class BootstrapRepos: # test if this path does exist. if not pype_path.exists(): - self._log.error(f"{pype_path} doesn't exists.") + self._print(f"{pype_path} doesn't exists.") return None # test if entered path isn't user data dir if self.data_dir == pype_path: - self._log.error("cannot point to user data dir") + self._print("cannot point to user data dir", LOG_ERROR) return None # find pype zip files in location. There can be @@ -721,94 +721,45 @@ class BootstrapRepos: # files and directories and tries to parse `version.py` file. versions = self.find_pype(pype_path) if versions: - self._log.info(f"found Pype in [ {pype_path} ]") - self._log.info(f"latest version found is [ {versions[-1]} ]") + self._print(f"found Pype in [ {pype_path} ]") + self._print(f"latest version found is [ {versions[-1]} ]") - destination = self.data_dir / versions[-1].path.name - - # test if destination file already exist, if so lets delete it. - # we consider path on location as authoritative place. - if destination.exists(): - try: - destination.unlink() - except OSError: - self._log.error( - f"cannot remove already existing {destination}", - exc_info=True) - return None - - # create destination parent directories even if they don't exist. - if not destination.parent.exists(): - destination.parent.mkdir(parents=True) - - # latest version found is directory - if versions[-1].path.is_dir(): - # zip it, copy it and extract it - # create zip inside temporary directory. - self._log.info("Creating zip from directory ...") - with tempfile.TemporaryDirectory() as temp_dir: - temp_zip = \ - Path(temp_dir) / f"pype-v{versions[-1]}.zip" - self._log.info(f"creating zip: {temp_zip}") - - self._create_pype_zip(temp_zip, versions[-1].path) - if not os.path.exists(temp_zip): - self._log.error("make archive failed.") - return None - - destination = self.data_dir / temp_zip.name - - elif versions[-1].path.is_file(): - # in this place, it must be zip file as `find_pype()` is - # checking just that. - assert versions[-1].path.suffix.lower() == ".zip", ( - "Invalid file format" - ) - try: - self._log.info("Copying zip to destination ...") - copyfile(versions[-1].path.as_posix(), destination.as_posix()) - except OSError: - self._log.error( - "cannot copy detected version to user data directory", - exc_info=True) - return None - - # extract zip there - self._log.info("extracting zip to destination ...") - with ZipFile(versions[-1].path, "r") as zip_ref: - zip_ref.extractall(destination) - - return destination + return self.install_version(versions[-1]) # if we got here, it means that location is "live" Pype repository. # we'll create zip from it and move it to user data dir. - repo_file = self.install_live_repos(pype_path) - if not repo_file.exists(): - self._log.error(f"installing zip {repo_file} failed.") + live_pype = self.create_version_from_live_code(pype_path) + if not live_pype.path.exists(): + self._print(f"installing zip {live_pype} failed.", LOG_ERROR) return None + # install it + return self.install_version(live_pype) - destination = self.data_dir / repo_file.stem - if destination.exists(): - try: - destination.unlink() - except OSError: - self._log.error( - f"cannot remove already existing {destination}", - exc_info=True) - return None + def _print(self, + message: str, + level: int = LOG_INFO, + exc_info: bool = False): + """Helper function passing logs to UI and to logger. - destination.mkdir(parents=True) + Supporting 3 levels of logs defined with `LOG_INFO`, `LOG_WARNING` and + `LOG_ERROR` constants. - # extract zip there - self._log.info("extracting zip to destination ...") - with ZipFile(versions[-1].path, "r") as zip_ref: - zip_ref.extractall(destination) + Args: + message (str): Message to log. + level (int, optional): Log level to use. + exc_info (bool, optional): Exception info object to pass to logger. - return destination - - def _print(self, message, error=False): + """ if self._message: - self._message.emit(message, error) + self._message.emit(message, level == LOG_ERROR) + + if level == LOG_WARNING: + self._log.warning(message, exc_info=exc_info) + return + if level == LOG_ERROR: + self._log.error(message, exc_info=exc_info) + return + self._log.info(message, exc_info=exc_info) def extract_pype(self, version: PypeVersion) -> Union[Path, None]: """Extract zipped Pype version to user data directory. @@ -829,12 +780,9 @@ class BootstrapRepos: if destination.exists(): try: destination.unlink() - except OSError as e: + except OSError: msg = f"!!! Cannot remove already existing {destination}" - self._log.error(msg) - self._log.error(e.strerror) - self._print(msg, True) - self._print(e.strerror, True) + self._print(msg, LOG_ERROR, exc_info=True) return None destination.mkdir(parents=True) @@ -848,7 +796,29 @@ class BootstrapRepos: return destination - def install_version(self, pype_version: PypeVersion, force: bool = False): + def is_inside_user_data(self, path: Path) -> bool: + """Test if version is located in user data dir. + + Args: + path (Path) Path to test. + + Returns: + True if path is inside user data dir. + + """ + is_inside = False + try: + is_inside = path.resolve().relative_to( + self.data_dir) + except ValueError: + # if relative path cannot be calculated, Pype version is not + # inside user data dir + pass + return is_inside + + def install_version(self, + pype_version: PypeVersion, + force: bool = False) -> Path: """Install Pype version to user data directory. Args: @@ -866,52 +836,46 @@ class BootstrapRepos: """ - # test if version is located (in user data dir) - is_inside = False - try: - is_inside = pype_version.path.resolve().relative_to( - self.data_dir) - except ValueError: - # if relative path cannot be calculated, Pype version is not - # inside user data dir - pass - - if is_inside: + if self.is_inside_user_data(pype_version.path) and not pype_version.path.is_file(): # noqa raise PypeVersionExists("Pype already inside user data dir") # determine destination directory name - # for zip file strip suffix - destination = self.data_dir / pype_version.path.stem + # for zip file strip suffix, in case of dir use whole dir name + if pype_version.path.is_dir(): + dir_name = pype_version.path.name + else: + dir_name = pype_version.path.stem - # test if destination file already exist, if so lets delete it. - # we consider path on location as authoritative place. + destination = self.data_dir / dir_name + + # test if destination directory already exist, if so lets delete it. if destination.exists() and force: try: - destination.unlink() - except OSError: - self._log.error( + shutil.rmtree(destination) + except OSError as e: + self._print( f"cannot remove already existing {destination}", - exc_info=True) - return None - else: + LOG_ERROR, exc_info=True) + raise PypeVersionIOError( + f"cannot remove existing {destination}") from e + elif destination.exists() and not force: raise PypeVersionExists(f"{destination} already exist.") - - # create destination parent directories even if they don't exist. - if not destination.exists(): + else: + # create destination parent directories even if they don't exist. destination.mkdir(parents=True) # version is directory if pype_version.path.is_dir(): # create zip inside temporary directory. - self._log.info("Creating zip from directory ...") + self._print("Creating zip from directory ...") with tempfile.TemporaryDirectory() as temp_dir: temp_zip = \ Path(temp_dir) / f"pype-v{pype_version}.zip" - self._log.info(f"creating zip: {temp_zip}") + self._print(f"creating zip: {temp_zip}") self._create_pype_zip(temp_zip, pype_version.path) if not os.path.exists(temp_zip): - self._log.error("make archive failed.") + self._print("make archive failed.", LOG_ERROR) raise PypeVersionIOError("Zip creation failed.") # set zip as version source @@ -922,24 +886,161 @@ class BootstrapRepos: if pype_version.path.suffix.lower() != ".zip": raise PypeVersionInvalid("Invalid file format") - try: - # copy file to destination - self._log.info("Copying zip to destination ...") - copyfile(pype_version.path.as_posix(), destination.as_posix()) - except OSError as e: - self._log.error( - "cannot copy version to user data directory", - exc_info=True) - raise PypeVersionIOError( - "can't copy version to destination") from e + if not self.is_inside_user_data(pype_version.path): + try: + # copy file to destination + self._print("Copying zip to destination ...") + copyfile( + pype_version.path.as_posix(), destination.parent.as_posix()) + except OSError as e: + self._print( + "cannot copy version to user data directory", LOG_ERROR, + exc_info=True) + raise PypeVersionIOError( + "can't copy version to destination") from e # extract zip there - self._log.info("extracting zip to destination ...") + self._print("extracting zip to destination ...") with ZipFile(pype_version.path, "r") as zip_ref: zip_ref.extractall(destination) return destination + def _is_pype_in_dir(self, + dir_item: Path, + detected_version: PypeVersion) -> bool: + """Test if path item is Pype version matching detected version. + + If item is directory that might (based on it's name) + contain Pype version, check if it really does contain + Pype and that their versions matches. + + Args: + dir_item (Path): Directory to test. + detected_version (PypeVersion): Pype version detected from name. + + Returns: + True if it is valid Pype version, False otherwise. + + """ + try: + # add one 'pype' level as inside dir there should + # be many other repositories. + version_str = BootstrapRepos.get_version( + dir_item / "pype") + version_check = PypeVersion(version=version_str) + except ValueError: + self._print( + f"cannot determine version from {dir_item}", True) + return False + + version_main = version_check.get_main_version() + detected_main = detected_version.get_main_version() + if version_main != detected_main: + self._print( + (f"dir version ({detected_version}) and " + f"its content version ({version_check}) " + "doesn't match. Skipping.")) + return False + return True + + def _is_pype_in_zip(self, + zip_item: Path, + detected_version: PypeVersion) -> bool: + """Test if zip path is Pype version matching detected version. + + Open zip file, look inside and parse version from Pype + inside it. If there is none, or it is different from + version specified in file name, skip it. + + Args: + zip_item (Path): Zip file to test. + detected_version (PypeVersion): Pype version detected from name. + + Returns: + True if it is valid Pype version, False otherwise. + + """ + # skip non-zip files + if zip_item.suffix.lower() != ".zip": + return False + + try: + with ZipFile(zip_item, "r") as zip_file: + with zip_file.open( + "pype/pype/version.py") as version_file: + zip_version = {} + exec(version_file.read(), zip_version) + version_check = PypeVersion( + version=zip_version["__version__"]) + + version_main = version_check.get_main_version() # noqa: E501 + detected_main = detected_version.get_main_version() # noqa: E501 + + if version_main != detected_main: + self._print( + (f"zip version ({detected_version}) " + f"and its content version " + f"({version_check}) " + "doesn't match. Skipping."), True) + return False + except BadZipFile: + self._print(f"{zip_item} is not a zip file", True) + return False + except KeyError: + self._print("Zip does not contain Pype", True) + return False + return True + + def get_pype_versions(self, pype_dir: Path, staging: bool = False) -> list: + """Get all detected Pype versions in directory. + + Args: + pype_dir (Path): Directory to scan. + staging (bool, optional): Find staging versions if True. + + Returns: + list of PypeVersion + + Throws: + ValueError: if invalid path is specified. + + """ + if not pype_dir.exists() and not pype_dir.is_dir(): + raise ValueError("specified directory is invalid") + + _pype_versions = [] + # iterate over directory in first level and find all that might + # contain Pype. + for item in pype_dir.iterdir(): + + # if file, strip extension, in case of dir not. + name = item.name if item.is_dir() else item.stem + result = PypeVersion.version_in_str(name) + + if result[0]: + detected_version: PypeVersion + detected_version = result[1] + + if item.is_dir() and not self._is_pype_in_dir( + item, detected_version + ): + continue + + if item.is_file() and not self._is_pype_in_zip( + item, detected_version + ): + continue + + detected_version.path = item + if staging and detected_version.is_staging(): + _pype_versions.append(detected_version) + + if not staging and not detected_version.is_staging(): + _pype_versions.append(detected_version) + + return sorted(_pype_versions) + class PypeVersionExists(Exception): """Exception for handling existing Pype version.""" diff --git a/igniter/install_thread.py b/igniter/install_thread.py index ad24913ed7..945049d1d7 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -2,9 +2,9 @@ """Working thread for installer.""" import os import sys -from zipfile import ZipFile +from pathlib import Path -from Qt.QtCore import QThread, Signal +from Qt.QtCore import QThread, Signal # noqa from .bootstrap_repos import BootstrapRepos from .bootstrap_repos import PypeVersion @@ -21,18 +21,14 @@ class InstallThread(QThread): If path contains plain repositories, they are zipped and installed to user data dir. - Attributes: - progress (Signal): signal reporting progress back o UI. - message (Signal): message displaying in UI console. - """ - progress = Signal(int) message = Signal((str, bool)) def __init__(self, parent=None): self._mongo = None self._path = None + QThread.__init__(self, parent) def run(self): @@ -77,7 +73,7 @@ class InstallThread(QThread): detected = bs.find_pype(include_zips=True) if detected: - if PypeVersion(version=local_version) < detected[-1]: + if PypeVersion(version=local_version, path=Path()) < detected[-1]: self.message.emit(( f"Latest installed version {detected[-1]} is newer " f"then currently running {local_version}" @@ -87,7 +83,7 @@ class InstallThread(QThread): bs.extract_pype(detected[-1]) return - if PypeVersion(version=local_version) == detected[-1]: + if PypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa self.message.emit(( f"Latest installed version is the same as " f"currently running {local_version}" @@ -101,42 +97,33 @@ class InstallThread(QThread): ), False) else: # we cannot build install package from frozen code. + # todo: we can if getattr(sys, 'frozen', False): self.message.emit("None detected.", True) self.message.emit(("Please set path to Pype sources to " "build installation."), False) - return + pype_version = bs.create_version_from_frozen_code() + if not pype_version: + self.message.emit( + f"!!! Install failed - {pype_version}", True) + return + bs.install_version(pype_version) + self.message.emit(f"Installed as {pype_version}", False) else: self.message.emit("None detected.", False) self.message.emit( f"We will use local Pype version {local_version}", False) - repo_file = bs.install_live_repos() - if not repo_file: + local_pype = bs.create_version_from_live_code() + if not local_pype: self.message.emit( - f"!!! Install failed - {repo_file}", True) + f"!!! Install failed - {local_pype}", True) return - destination = bs.data_dir / repo_file.stem - if destination.exists(): - try: - destination.unlink() - except OSError as e: - self.message.emit( - f"!!! Cannot remove already existing {destination}", - True) - self.message.emit(e.strerror, True) - return + bs.install_version(local_pype) - destination.mkdir(parents=True) - - # extract zip there - self.message.emit("Extracting zip to destination ...", False) - with ZipFile(repo_file, "r") as zip_ref: - zip_ref.extractall(destination) - - self.message.emit(f"Installed as {repo_file}", False) + self.message.emit(f"Installed as {local_pype}", False) else: # if we have mongo connection string, validate it, set it to # user settings and get PYPE_PATH from there. diff --git a/igniter/tools.py b/igniter/tools.py index 43e34fced2..ae2ab4c586 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -210,6 +210,9 @@ def load_environments(sections: list = None) -> dict: def get_pype_path_from_db(url: str) -> Union[str, None]: """Get Pype path from database. + We are loading data from database `pype` and collection `settings`. + There we expect document type `global_settings`. + Args: url (str): mongodb url. @@ -237,7 +240,7 @@ def get_pype_path_from_db(url: str) -> Union[str, None]: db = client.pype col = db.settings - result = col.find_one({"type": "global_settings"}, {"value": 1}) - global_settings = result.get("value") + global_settings = col.find_one( + {"type": "global_settings"}, {"data": 1}).get("data") return global_settings.get("pype_path", {}).get(platform.system().lower()) diff --git a/start.py b/start.py index 5a34bbc11a..1af1abb075 100644 --- a/start.py +++ b/start.py @@ -112,7 +112,7 @@ if getattr(sys, 'frozen', False): os.environ["PYTHONPATH"] = os.pathsep.join(paths) from igniter import BootstrapRepos # noqa: E402 -from igniter.tools import load_environments # noqa: E402 +from igniter.tools import load_environments, get_pype_path_from_db # noqa from igniter.bootstrap_repos import PypeVersion # noqa: E402 bootstrap = BootstrapRepos() @@ -122,6 +122,9 @@ silent_commands = ["run", "igniter", "standalonepublisher"] def set_environments() -> None: """Set loaded environments. + .. deprecated:: 3.0 + no environment loading from settings until Pype version is established + .. todo: better handling of environments @@ -134,14 +137,21 @@ def set_environments() -> None: os.path.dirname(sys.executable), "dependencies" )) - import acre + try: + import acre + except ImportError as e: + # giving up + print("!!! cannot import acre") + print(f"{e}") + sys.exit(1) try: env = load_environments(["global"]) except OSError as e: print(f"!!! {e}") sys.exit(1) - env = acre.merge(env, dict(os.environ)) + # acre must be available here + env = acre.merge(env, dict(os.environ)) # noqa os.environ.clear() os.environ.update(env) @@ -264,7 +274,9 @@ def _determine_mongodb() -> str: except ValueError: print("*** No DB connection string specified.") print("--- launching setup UI ...") - run(["igniter"]) + return_code = run(["igniter"]) + if return_code != 0: + raise RuntimeError("mongodb is not set") try: pype_mongo = bootstrap.registry.get_secure_item("pypeMongo") except ValueError: @@ -337,7 +349,9 @@ def _find_frozen_pype(use_version: str = None, # no pype version found, run Igniter and ask for them. print('*** No Pype versions found.') print("--- launching setup UI ...") - run(["igniter"]) + return_code = run(["igniter"]) + if return_code != 0: + raise RuntimeError("igniter crashed.") pype_versions = bootstrap.find_pype() if not pype_versions: @@ -463,7 +477,6 @@ def _bootstrap_from_code(use_version): def boot(): """Bootstrap Pype.""" - version_path = None # ------------------------------------------------------------------------ # Play animation @@ -495,7 +508,7 @@ def boot(): os.environ["PYPE_MONGO"] = pype_mongo # ------------------------------------------------------------------------ - # Load environments from database + # Set environments - load Pype path from database (if set) # ------------------------------------------------------------------------ # set PYPE_ROOT to running location until proper version can be # determined. @@ -503,7 +516,15 @@ def boot(): os.environ["PYPE_ROOT"] = os.path.dirname(sys.executable) else: os.environ["PYPE_ROOT"] = os.path.dirname(__file__) - set_environments() + + # No environment loading from settings until Pype version is established. + # set_environments() + + # Get Pype path from database and set it to environment so Pype can + # find its versions there and bootstrap them. + pype_path = get_pype_path_from_db(pype_mongo) + if not os.getenv("PYPE_PATH") and pype_path: + os.environ["PYPE_PATH"] = pype_path # ------------------------------------------------------------------------ # Find Pype versions @@ -532,10 +553,12 @@ def boot(): # delete Pype module and it's submodules from cache so it is used from # specific version - modules_to_del = [] - for module_name in tuple(sys.modules): - if module_name == "pype" or module_name.startswith("pype."): - modules_to_del.append(sys.modules.pop(module_name)) + modules_to_del = [ + sys.modules.pop(module_name) + for module_name in tuple(sys.modules) + if module_name == "pype" or module_name.startswith("pype.") + ] + try: for module_name in modules_to_del: del sys.modules[module_name] @@ -557,10 +580,7 @@ def boot(): t_width = 20 try: t_width = os.get_terminal_size().columns - 2 - except ValueError: - # running without terminal - pass - except OSError: + except (ValueError, OSError): # running without terminal pass @@ -574,7 +594,7 @@ def boot(): try: cli.main(obj={}, prog_name="pype") - except Exception: + except Exception: # noqa exc_info = sys.exc_info() print("!!! Pype crashed:") traceback.print_exception(*exc_info) diff --git a/tests/igniter/test_bootstrap_repos.py b/tests/igniter/test_bootstrap_repos.py index 59469b0687..70edc5b89c 100644 --- a/tests/igniter/test_bootstrap_repos.py +++ b/tests/igniter/test_bootstrap_repos.py @@ -28,6 +28,7 @@ def test_pype_version(): v2 = PypeVersion(1, 2, 3, client="x") assert str(v2) == "1.2.3-x" + assert v1 < v2 v3 = PypeVersion(1, 2, 3, variant="staging") assert str(v3) == "1.2.3-staging" @@ -35,6 +36,7 @@ def test_pype_version(): v4 = PypeVersion(1, 2, 3, variant="staging", client="client") assert str(v4) == "1.2.3-client-staging" assert v3 < v4 + assert v1 < v4 v5 = PypeVersion(1, 2, 3, variant="foo", client="x") assert str(v5) == "1.2.3-x" @@ -55,6 +57,9 @@ def test_pype_version(): v10 = PypeVersion(1, 2, 2) assert v10 < v1 + v11 = PypeVersion(1, 2, 3, path=Path("/foo/bar")) + assert v10 < v11 + assert v5 == v2 sort_versions = [ @@ -141,7 +146,7 @@ def test_search_string_for_pype_version(printer): def test_install_live_repos(fix_bootstrap, printer): - rf = fix_bootstrap.install_live_repos() + rf = fix_bootstrap.create_version_from_live_code() sep = os.path.sep expected_paths = [ f"{rf}{sep}avalon-core", From 74e2b580c941f6770973b3d3aa0e4e666159be91 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Mar 2021 21:33:23 +0100 Subject: [PATCH 04/68] igniter dialog improvements --- igniter/install_dialog.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index f54acf49f2..561b17408f 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -42,7 +42,7 @@ class InstallDialog(QtWidgets.QDialog): super(InstallDialog, self).__init__(parent) self.registry = PypeSettingsRegistry() - self._mongo_url = os.getenv("PYPE_MONGO", "") or self.registry.get_secure_item("pypeMongo") or "" # noqa: E501 + self.mongo_url = os.getenv("PYPE_MONGO", "") or self.registry.get_secure_item("pypeMongo") or "" # noqa: E501 self.setWindowTitle("Pype - Configure Pype repository path") self._icon_path = os.path.join( @@ -234,6 +234,10 @@ class InstallDialog(QtWidgets.QDialog): ) self.parent().ok_button.setEnabled(False) + def set_read_only(self, state: bool): + """Set input read-only.""" + self._mongo_input.setReadOnly(state) + def validate_url(self) -> bool: """Validate if entered url is ok. @@ -256,8 +260,8 @@ class InstallDialog(QtWidgets.QDialog): return True self._mongo = MongoWidget(self) - if self._mongo_url: - self._mongo.set_mongo_url(self._mongo_url) + if self.mongo_url: + self._mongo.set_mongo_url(self.mongo_url) # Bottom button bar # -------------------------------------------------------------------- @@ -379,10 +383,12 @@ class InstallDialog(QtWidgets.QDialog): main.addWidget(self._progress_bar) main.addWidget(bottom_widget) self.setLayout(main) - if not self._mongo_url: + if not self.mongo_url: self._mongo.setVisible(False) else: - self._mongo.validate_url() + if self._mongo.validate_url() and len(self.path) == 0: + self._mongo.setVisible(True) + self._mongo.setReadonly(True) def _on_select_clicked(self): """Show directory dialog.""" @@ -465,8 +471,14 @@ class InstallDialog(QtWidgets.QDialog): else: self._mongo.setVisible(False) - if len(self.path) < 1: + if len(self.path) < 1 and not self.mongo_url: self._mongo.setVisible(False) + + if len(self.path) == 0 and self._mongo.validate_url(): + self._mongo.setVisible(True) + self._mongo.set_read_only(True) + else: + self._mongo.set_read_only(False) return path def update_console(self, msg: str, error: bool = False) -> None: @@ -554,7 +566,7 @@ class PathValidator(QValidator): return QValidator.State.Acceptable, path, len(path) - def validate(self, path: str, pos: int) -> (QValidator.State, str, int): + def validate(self, path: str, pos: int) -> (QValidator.State, str, int): # noqa """Validate entered path. It can be regular path - in that case we test if it does exist. @@ -570,7 +582,6 @@ class PathValidator(QValidator): """ if path.startswith("mongodb"): - pos = len(path) return self._return_state( QValidator.State.Intermediate, "", path) From 9d8cf26087070228984fcacbaafc74f0787ae769 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Mar 2021 21:33:41 +0100 Subject: [PATCH 05/68] commands cleanup --- pype/pype_commands.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pype/pype_commands.py b/pype/pype_commands.py index 2f81e6a405..455cd4bffb 100644 --- a/pype/pype_commands.py +++ b/pype/pype_commands.py @@ -63,15 +63,6 @@ class PypeCommands: def texture_copy(self, project, asset, path): pass - def run_pype_tests(self, keyword, id): - pass - - def make_docs(self): - pass - - def pype_setup_coverage(self): - pass - def run_application(self, app, project, asset, task, tools, arguments): pass @@ -95,7 +86,7 @@ class PypeCommands: bs.data_dir = out_path.parent print(f">>> Creating zip in {bs.data_dir} ...") - repo_file = bs.install_live_repos() + repo_file = bs.create_version_from_live_code() if not repo_file: print("!!! Error while creating zip file.") exit(1) From 30537adba6712bddb139594c7370db56fa924432 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Mar 2021 21:39:09 +0100 Subject: [PATCH 06/68] hound fixes --- igniter/bootstrap_repos.py | 3 ++- igniter/install_thread.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index bf121ba97c..da796b3e74 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -891,7 +891,8 @@ class BootstrapRepos: # copy file to destination self._print("Copying zip to destination ...") copyfile( - pype_version.path.as_posix(), destination.parent.as_posix()) + pype_version.path.as_posix(), + destination.parent.as_posix()) except OSError as e: self._print( "cannot copy version to user data directory", LOG_ERROR, diff --git a/igniter/install_thread.py b/igniter/install_thread.py index 945049d1d7..29adc36ddc 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -73,7 +73,8 @@ class InstallThread(QThread): detected = bs.find_pype(include_zips=True) if detected: - if PypeVersion(version=local_version, path=Path()) < detected[-1]: + if PypeVersion( + version=local_version, path=Path()) < detected[-1]: self.message.emit(( f"Latest installed version {detected[-1]} is newer " f"then currently running {local_version}" From 765b8a5213d54def16266a1744d31ecdaf494987 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 09:57:44 +0100 Subject: [PATCH 07/68] move maya capture preset to plugin --- .../maya/plugins/publish/extract_playblast.py | 6 +- .../maya/plugins/publish/extract_thumbnail.py | 2 +- .../defaults/project_settings/maya.json | 230 +++---- .../schemas/schema_maya_capture.json | 581 ----------------- .../schemas/template_maya_capture.json | 588 ++++++++++++++++++ 5 files changed, 713 insertions(+), 694 deletions(-) delete mode 100644 pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json create mode 100644 pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json diff --git a/pype/hosts/maya/plugins/publish/extract_playblast.py b/pype/hosts/maya/plugins/publish/extract_playblast.py index 770b077e41..da1428400e 100644 --- a/pype/hosts/maya/plugins/publish/extract_playblast.py +++ b/pype/hosts/maya/plugins/publish/extract_playblast.py @@ -23,6 +23,7 @@ class ExtractPlayblast(pype.api.Extractor): hosts = ["maya"] families = ["review"] optional = True + capture_preset = {} def process(self, instance): self.log.info("Extracting capture..") @@ -43,12 +44,9 @@ class ExtractPlayblast(pype.api.Extractor): # get cameras camera = instance.data['review_camera'] - capture_preset = ( - instance.context.data['project_settings']['maya']['capture'] - ) try: - preset = lib.load_capture_preset(data=capture_preset) + preset = lib.load_capture_preset(data=self.capture_preset) except Exception: preset = {} self.log.info('using viewport preset: {}'.format(preset)) diff --git a/pype/hosts/maya/plugins/publish/extract_thumbnail.py b/pype/hosts/maya/plugins/publish/extract_thumbnail.py index 49511f6af6..9f6d16275f 100644 --- a/pype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/pype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -34,7 +34,7 @@ class ExtractThumbnail(pype.api.Extractor): capture_preset = "" capture_preset = ( - instance.context.data["project_settings"]['maya']['capture'] + instance.context.data["project_settings"]['maya']['publish']['ExtractPlayblast'] ) try: diff --git a/pype/settings/defaults/project_settings/maya.json b/pype/settings/defaults/project_settings/maya.json index 03955732d2..c1f43ff81e 100644 --- a/pype/settings/defaults/project_settings/maya.json +++ b/pype/settings/defaults/project_settings/maya.json @@ -1,111 +1,11 @@ { - "capture": { - "Codec": { - "compression": "jpg", - "format": "image", - "quality": 95 - }, - "Display Options": { - "background": [ - 0.7, - 0.7, - 0.7 - ], - "backgroundBottom": [ - 0.7, - 0.7, - 0.7 - ], - "backgroundTop": [ - 0.7, - 0.7, - 0.7 - ], - "override_display": true - }, - "Generic": { - "isolate_view": true, - "off_screen": true - }, - "IO": { - "name": "", - "open_finished": true, - "raw_frame_numbers": true, - "recent_playblasts": [], - "save_file": true - }, - "PanZoom": { - "pan_zoom": true - }, - "Renderer": { - "rendererName": "vp2Renderer" - }, - "Resolution": { - "width": 1080, - "height": 1920, - "percent": 1.0, - "mode": "Custom" - }, - "Time Range": { - "start_frame": 0, - "end_frame": 0, - "frame": "", - "time": "Time Slider" - }, - "Viewport Options": { - "cameras": false, - "clipGhosts": false, - "controlVertices": false, - "deformers": false, - "dimensions": false, - "displayLights": 0, - "dynamicConstraints": false, - "dynamics": false, - "fluids": false, - "follicles": false, - "gpuCacheDisplayFilter": false, - "greasePencils": false, - "grid": false, - "hairSystems": true, - "handles": false, - "high_quality": true, - "hud": false, - "hulls": false, - "ikHandles": false, - "imagePlane": true, - "joints": false, - "lights": false, - "locators": false, - "manipulators": false, - "motionTrails": false, - "nCloths": false, - "nParticles": false, - "nRigids": false, - "nurbsCurves": false, - "nurbsSurfaces": false, - "override_viewport_options": true, - "particleInstancers": false, - "pivots": false, - "planes": false, - "pluginShapes": false, - "polymeshes": true, - "shadows": true, - "strokes": false, - "subdivSurfaces": false, - "textures": false, - "twoSidedLighting": true - }, - "Camera Options": { - "displayGateMask": false, - "displayResolution": false, - "displayFilmGate": false, - "displayFieldChart": false, - "displaySafeAction": false, - "displaySafeTitle": false, - "displayFilmPivot": false, - "displayFilmOrigin": false, - "overscan": 1.0 - } + "ext_mapping": { + "model": "ma", + "mayaAscii": "ma", + "camera": "ma", + "rig": "ma", + "workfile": "ma", + "yetiRig": "ma" }, "create": { "CreateAnimation": { @@ -299,6 +199,10 @@ "enabled": false, "optional": true }, + "ValidateMeshNormalsUnlocked": { + "enabled": false, + "optional": true + }, "ValidateMeshUVSetMap1": { "enabled": false, "optional": true @@ -336,7 +240,7 @@ "optional": true }, "ValidateTransformZero": { - "enabled": true, + "enabled": false, "optional": true }, "ValidateCameraAttributes": { @@ -351,6 +255,116 @@ "enabled": true, "optional": true }, + "ExtractPlayblast": { + "capture_preset": { + "Codec": { + "compression": "jpg", + "format": "image", + "quality": 95 + }, + "Display Options": { + "background": [ + 0.7, + 0.7, + 0.7 + ], + "backgroundBottom": [ + 0.7, + 0.7, + 0.7 + ], + "backgroundTop": [ + 0.7, + 0.7, + 0.7 + ], + "override_display": true + }, + "Generic": { + "isolate_view": true, + "off_screen": true + }, + "IO": { + "name": "", + "open_finished": true, + "raw_frame_numbers": true, + "recent_playblasts": [], + "save_file": true + }, + "PanZoom": { + "pan_zoom": true + }, + "Renderer": { + "rendererName": "vp2Renderer" + }, + "Resolution": { + "width": 1080, + "height": 1920, + "percent": 1.0, + "mode": "Custom" + }, + "Time Range": { + "start_frame": 0, + "end_frame": 0, + "frame": "", + "time": "Time Slider" + }, + "Viewport Options": { + "cameras": false, + "clipGhosts": false, + "controlVertices": false, + "deformers": false, + "dimensions": false, + "displayLights": 0, + "dynamicConstraints": false, + "dynamics": false, + "fluids": false, + "follicles": false, + "gpuCacheDisplayFilter": false, + "greasePencils": false, + "grid": false, + "hairSystems": true, + "handles": false, + "high_quality": true, + "hud": false, + "hulls": false, + "ikHandles": false, + "imagePlane": true, + "joints": false, + "lights": false, + "locators": false, + "manipulators": false, + "motionTrails": false, + "nCloths": false, + "nParticles": false, + "nRigids": false, + "nurbsCurves": false, + "nurbsSurfaces": false, + "override_viewport_options": true, + "particleInstancers": false, + "pivots": false, + "planes": false, + "pluginShapes": false, + "polymeshes": true, + "shadows": true, + "strokes": false, + "subdivSurfaces": false, + "textures": false, + "twoSidedLighting": true + }, + "Camera Options": { + "displayGateMask": false, + "displayResolution": false, + "displayFilmGate": false, + "displayFieldChart": false, + "displaySafeAction": false, + "displaySafeTitle": false, + "displayFilmPivot": false, + "displayFilmOrigin": false, + "overscan": 1.0 + } + } + }, "ExtractCameraAlembic": { "enabled": true, "optional": true, diff --git a/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json deleted file mode 100644 index 4745a19075..0000000000 --- a/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ /dev/null @@ -1,581 +0,0 @@ -{ - "type": "dict", - "collapsible": true, - "key": "capture", - "label": "Maya Playblast settings", - "is_file": true, - "children": [ - { - "type": "dict", - "key": "Codec", - "children": [ - { - "type": "label", - "label": "Codec" - }, - { - "type": "text", - "key": "compression", - "label": "Compression type" - }, - { - "type": "text", - "key": "format", - "label": "Data format" - }, - { - "type": "number", - "key": "quality", - "label": "Quality", - "decimal": 0, - "minimum": 0, - "maximum": 100 - }, - - { - "type": "splitter" - } - ] - }, - { - "type": "dict", - "key": "Display Options", - "children": [ - { - "type": "label", - "label": "Display Options" - }, - { - "type": "list-strict", - "key": "background", - "label": "Background Color: ", - "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 - } - ] - }, - { - "type": "list-strict", - "key": "backgroundBottom", - "label": "Background Bottom: ", - "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 - } - ] - }, - { - "type": "list-strict", - "key": "backgroundTop", - "label": "Background Top: ", - "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 - } - ] - }, - { - "type": "boolean", - "key": "override_display", - "label": "Override display options" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Generic", - "children": [ - { - "type": "label", - "label": "Generic" - }, - { - "type": "boolean", - "key": "isolate_view", - "label": " Isolate view" - }, - { - "type": "boolean", - "key": "off_screen", - "label": " Off Screen" - } - ] - }, - { - "type": "dict", - "key": "IO", - "children": [ - { - "type": "label", - "label": "IO" - }, - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "boolean", - "key": "open_finished", - "label": "Open finished" - }, - { - "type": "boolean", - "key": "raw_frame_numbers", - "label": "Raw frame numbers" - }, - { - "type": "list", - "key": "recent_playblasts", - "label": "Recent Playblasts", - "object_type": "text" - }, - { - "type": "boolean", - "key": "save_file", - "label": "Save file" - } - ] - }, - { - "type": "dict", - "key": "PanZoom", - "children": [ - { - "type": "boolean", - "key": "pan_zoom", - "label": " Pan Zoom" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Renderer", - "children": [ - { - "type": "label", - "label": "Renderer" - }, - { - "type": "text", - "key": "rendererName", - "label": " Renderer name" - } - ] - }, - { - "type": "dict", - "key": "Resolution", - "children": [ - { - "type": "splitter" - }, - { - "type": "label", - "label": "Resolution" - }, - { - "type": "number", - "key": "width", - "label": " Width", - "decimal": 0, - "minimum": 0, - "maximum": 99999 - }, - { - "type": "number", - "key": "height", - "label": "Height", - "decimal": 0, - "minimum": 0, - "maximum": 99999 - }, - { - "type": "number", - "key": "percent", - "label": "percent", - "decimal": 1, - "minimum": 0, - "maximum": 200 - }, - { - "type": "text", - "key": "mode", - "label": "Mode" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Time Range", - "children": [ - { - "type": "label", - "label": "Time Range" - }, - { - "type": "number", - "key": "start_frame", - "label": " Start frame", - "decimal": 0, - "minimum": 0, - "maximum": 999999 - }, - { - "type": "number", - "key": "end_frame", - "label": "End frame", - "decimal": 0, - "minimum": 0, - "maximum": 999999 - }, - { - "type": "text", - "key": "frame", - "label": "Frame" - }, - { - "type": "text", - "key": "time", - "label": "Time" - } - ] - }, - { - "type": "dict", - "collapsible": true, - "key": "Viewport Options", - "label": "Viewport Options", - "children": [ - { - "type": "boolean", - "key": "cameras", - "label": "cameras" - }, - { - "type": "boolean", - "key": "clipGhosts", - "label": "clipGhosts" - }, - { - "type": "boolean", - "key": "controlVertices", - "label": "controlVertices" - }, - { - "type": "boolean", - "key": "deformers", - "label": "deformers" - }, - { - "type": "boolean", - "key": "dimensions", - "label": "dimensions" - }, - { - "type": "number", - "key": "displayLights", - "label": "displayLights", - "decimal": 0, - "minimum": 0, - "maximum": 10 - }, - { - "type": "boolean", - "key": "dynamicConstraints", - "label": "dynamicConstraints" - }, - { - "type": "boolean", - "key": "dynamics", - "label": "dynamics" - }, - { - "type": "boolean", - "key": "fluids", - "label": "fluids" - }, - { - "type": "boolean", - "key": "follicles", - "label": "follicles" - }, - { - "type": "boolean", - "key": "gpuCacheDisplayFilter", - "label": "gpuCacheDisplayFilter" - }, - { - "type": "boolean", - "key": "greasePencils", - "label": "greasePencils" - }, - { - "type": "boolean", - "key": "grid", - "label": "grid" - }, - { - "type": "boolean", - "key": "hairSystems", - "label": "hairSystems" - }, - { - "type": "boolean", - "key": "handles", - "label": "handles" - }, - { - "type": "boolean", - "key": "high_quality", - "label": "high_quality" - }, - { - "type": "boolean", - "key": "hud", - "label": "hud" - }, - { - "type": "boolean", - "key": "hulls", - "label": "hulls" - }, - { - "type": "boolean", - "key": "ikHandles", - "label": "ikHandles" - }, - { - "type": "boolean", - "key": "imagePlane", - "label": "imagePlane" - }, - { - "type": "boolean", - "key": "joints", - "label": "joints" - }, - { - "type": "boolean", - "key": "lights", - "label": "lights" - }, - { - "type": "boolean", - "key": "locators", - "label": "locators" - }, - { - "type": "boolean", - "key": "manipulators", - "label": "manipulators" - }, - { - "type": "boolean", - "key": "motionTrails", - "label": "motionTrails" - }, - { - "type": "boolean", - "key": "nCloths", - "label": "nCloths" - }, - { - "type": "boolean", - "key": "nParticles", - "label": "nParticles" - }, - { - "type": "boolean", - "key": "nRigids", - "label": "nRigids" - }, - { - "type": "boolean", - "key": "nurbsCurves", - "label": "nurbsCurves" - }, - { - "type": "boolean", - "key": "nurbsSurfaces", - "label": "nurbsSurfaces" - }, - { - "type": "boolean", - "key": "override_viewport_options", - "label": "override_viewport_options" - }, - { - "type": "boolean", - "key": "particleInstancers", - "label": "particleInstancers" - }, - { - "type": "boolean", - "key": "pivots", - "label": "pivots" - }, - { - "type": "boolean", - "key": "planes", - "label": "planes" - }, - { - "type": "boolean", - "key": "pluginShapes", - "label": "pluginShapes" - }, - { - "type": "boolean", - "key": "polymeshes", - "label": "polymeshes" - }, - { - "type": "boolean", - "key": "shadows", - "label": "shadows" - }, - { - "type": "boolean", - "key": "strokes", - "label": "strokes" - }, - { - "type": "boolean", - "key": "subdivSurfaces", - "label": "subdivSurfaces" - }, - { - "type": "boolean", - "key": "textures", - "label": "textures" - }, - { - "type": "boolean", - "key": "twoSidedLighting", - "label": "twoSidedLighting" - } - ] - }, - { - "type": "dict", - "collapsible": true, - "key": "Camera Options", - "label": "Camera Options", - "children": [ - { - "type": "boolean", - "key": "displayGateMask", - "label": "displayGateMask" - }, - { - "type": "boolean", - "key": "displayResolution", - "label": "displayResolution" - }, - { - "type": "boolean", - "key": "displayFilmGate", - "label": "displayFilmGate" - }, - { - "type": "boolean", - "key": "displayFieldChart", - "label": "displayFieldChart" - }, - { - "type": "boolean", - "key": "displaySafeAction", - "label": "displaySafeAction" - }, - { - "type": "boolean", - "key": "displaySafeTitle", - "label": "displaySafeTitle" - }, - { - "type": "boolean", - "key": "displayFilmPivot", - "label": "displayFilmPivot" - }, - { - "type": "boolean", - "key": "displayFilmOrigin", - "label": "displayFilmOrigin" - }, - { - "type": "number", - "key": "overscan", - "label": "overscan", - "decimal": 1, - "minimum": 0, - "maximum": 10 - } - ] - } - ] -} diff --git a/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json b/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json new file mode 100644 index 0000000000..f6fcb3b998 --- /dev/null +++ b/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json @@ -0,0 +1,588 @@ +[ + { + "type": "dict", + "collapsible": true, + "key": "ExtractPlayblast", + "label": "Extract Playblast settings", + "children": [ + { + "type": "dict", + "key": "capture_preset", + "children": [ + { + "type": "dict", + "key": "Codec", + "children": [ + { + "type": "label", + "label": "Codec" + }, + { + "type": "text", + "key": "compression", + "label": "Compression type" + }, + { + "type": "text", + "key": "format", + "label": "Data format" + }, + { + "type": "number", + "key": "quality", + "label": "Quality", + "decimal": 0, + "minimum": 0, + "maximum": 100 + }, + + { + "type": "splitter" + } + ] + }, + { + "type": "dict", + "key": "Display Options", + "children": [ + { + "type": "label", + "label": "Display Options" + }, + { + "type": "list-strict", + "key": "background", + "label": "Background Color: ", + "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 + } + ] + }, + { + "type": "list-strict", + "key": "backgroundBottom", + "label": "Background Bottom: ", + "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 + } + ] + }, + { + "type": "list-strict", + "key": "backgroundTop", + "label": "Background Top: ", + "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 + } + ] + }, + { + "type": "boolean", + "key": "override_display", + "label": "Override display options" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Generic", + "children": [ + { + "type": "label", + "label": "Generic" + }, + { + "type": "boolean", + "key": "isolate_view", + "label": " Isolate view" + }, + { + "type": "boolean", + "key": "off_screen", + "label": " Off Screen" + } + ] + }, + { + "type": "dict", + "key": "IO", + "children": [ + { + "type": "label", + "label": "IO" + }, + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "boolean", + "key": "open_finished", + "label": "Open finished" + }, + { + "type": "boolean", + "key": "raw_frame_numbers", + "label": "Raw frame numbers" + }, + { + "type": "list", + "key": "recent_playblasts", + "label": "Recent Playblasts", + "object_type": "text" + }, + { + "type": "boolean", + "key": "save_file", + "label": "Save file" + } + ] + }, + { + "type": "dict", + "key": "PanZoom", + "children": [ + { + "type": "boolean", + "key": "pan_zoom", + "label": " Pan Zoom" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Renderer", + "children": [ + { + "type": "label", + "label": "Renderer" + }, + { + "type": "text", + "key": "rendererName", + "label": " Renderer name" + } + ] + }, + { + "type": "dict", + "key": "Resolution", + "children": [ + { + "type": "splitter" + }, + { + "type": "label", + "label": "Resolution" + }, + { + "type": "number", + "key": "width", + "label": " Width", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + }, + { + "type": "number", + "key": "height", + "label": "Height", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + }, + { + "type": "number", + "key": "percent", + "label": "percent", + "decimal": 1, + "minimum": 0, + "maximum": 200 + }, + { + "type": "text", + "key": "mode", + "label": "Mode" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Time Range", + "children": [ + { + "type": "label", + "label": "Time Range" + }, + { + "type": "number", + "key": "start_frame", + "label": " Start frame", + "decimal": 0, + "minimum": 0, + "maximum": 999999 + }, + { + "type": "number", + "key": "end_frame", + "label": "End frame", + "decimal": 0, + "minimum": 0, + "maximum": 999999 + }, + { + "type": "text", + "key": "frame", + "label": "Frame" + }, + { + "type": "text", + "key": "time", + "label": "Time" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "Viewport Options", + "label": "Viewport Options", + "children": [ + { + "type": "boolean", + "key": "cameras", + "label": "cameras" + }, + { + "type": "boolean", + "key": "clipGhosts", + "label": "clipGhosts" + }, + { + "type": "boolean", + "key": "controlVertices", + "label": "controlVertices" + }, + { + "type": "boolean", + "key": "deformers", + "label": "deformers" + }, + { + "type": "boolean", + "key": "dimensions", + "label": "dimensions" + }, + { + "type": "number", + "key": "displayLights", + "label": "displayLights", + "decimal": 0, + "minimum": 0, + "maximum": 10 + }, + { + "type": "boolean", + "key": "dynamicConstraints", + "label": "dynamicConstraints" + }, + { + "type": "boolean", + "key": "dynamics", + "label": "dynamics" + }, + { + "type": "boolean", + "key": "fluids", + "label": "fluids" + }, + { + "type": "boolean", + "key": "follicles", + "label": "follicles" + }, + { + "type": "boolean", + "key": "gpuCacheDisplayFilter", + "label": "gpuCacheDisplayFilter" + }, + { + "type": "boolean", + "key": "greasePencils", + "label": "greasePencils" + }, + { + "type": "boolean", + "key": "grid", + "label": "grid" + }, + { + "type": "boolean", + "key": "hairSystems", + "label": "hairSystems" + }, + { + "type": "boolean", + "key": "handles", + "label": "handles" + }, + { + "type": "boolean", + "key": "high_quality", + "label": "high_quality" + }, + { + "type": "boolean", + "key": "hud", + "label": "hud" + }, + { + "type": "boolean", + "key": "hulls", + "label": "hulls" + }, + { + "type": "boolean", + "key": "ikHandles", + "label": "ikHandles" + }, + { + "type": "boolean", + "key": "imagePlane", + "label": "imagePlane" + }, + { + "type": "boolean", + "key": "joints", + "label": "joints" + }, + { + "type": "boolean", + "key": "lights", + "label": "lights" + }, + { + "type": "boolean", + "key": "locators", + "label": "locators" + }, + { + "type": "boolean", + "key": "manipulators", + "label": "manipulators" + }, + { + "type": "boolean", + "key": "motionTrails", + "label": "motionTrails" + }, + { + "type": "boolean", + "key": "nCloths", + "label": "nCloths" + }, + { + "type": "boolean", + "key": "nParticles", + "label": "nParticles" + }, + { + "type": "boolean", + "key": "nRigids", + "label": "nRigids" + }, + { + "type": "boolean", + "key": "nurbsCurves", + "label": "nurbsCurves" + }, + { + "type": "boolean", + "key": "nurbsSurfaces", + "label": "nurbsSurfaces" + }, + { + "type": "boolean", + "key": "override_viewport_options", + "label": "override_viewport_options" + }, + { + "type": "boolean", + "key": "particleInstancers", + "label": "particleInstancers" + }, + { + "type": "boolean", + "key": "pivots", + "label": "pivots" + }, + { + "type": "boolean", + "key": "planes", + "label": "planes" + }, + { + "type": "boolean", + "key": "pluginShapes", + "label": "pluginShapes" + }, + { + "type": "boolean", + "key": "polymeshes", + "label": "polymeshes" + }, + { + "type": "boolean", + "key": "shadows", + "label": "shadows" + }, + { + "type": "boolean", + "key": "strokes", + "label": "strokes" + }, + { + "type": "boolean", + "key": "subdivSurfaces", + "label": "subdivSurfaces" + }, + { + "type": "boolean", + "key": "textures", + "label": "textures" + }, + { + "type": "boolean", + "key": "twoSidedLighting", + "label": "twoSidedLighting" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "Camera Options", + "label": "Camera Options", + "children": [ + { + "type": "boolean", + "key": "displayGateMask", + "label": "displayGateMask" + }, + { + "type": "boolean", + "key": "displayResolution", + "label": "displayResolution" + }, + { + "type": "boolean", + "key": "displayFilmGate", + "label": "displayFilmGate" + }, + { + "type": "boolean", + "key": "displayFieldChart", + "label": "displayFieldChart" + }, + { + "type": "boolean", + "key": "displaySafeAction", + "label": "displaySafeAction" + }, + { + "type": "boolean", + "key": "displaySafeTitle", + "label": "displaySafeTitle" + }, + { + "type": "boolean", + "key": "displayFilmPivot", + "label": "displayFilmPivot" + }, + { + "type": "boolean", + "key": "displayFilmOrigin", + "label": "displayFilmOrigin" + }, + { + "type": "number", + "key": "overscan", + "label": "overscan", + "decimal": 1, + "minimum": 0, + "maximum": 10 + } + ] + } + ] + } + ] + } +] From d1aebe70bc611def48a606f785d62348a0390900 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 09:57:58 +0100 Subject: [PATCH 08/68] return extension mapping to maya settings --- .../schemas/projects_schema/schema_project_maya.json | 9 +++++++-- .../projects_schema/schemas/schema_maya_publish.json | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pype/settings/entities/schemas/projects_schema/schema_project_maya.json b/pype/settings/entities/schemas/projects_schema/schema_project_maya.json index 7a270b0046..0a59cab510 100644 --- a/pype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -6,8 +6,13 @@ "is_file": true, "children": [ { - "type": "schema", - "name": "schema_maya_capture" + "type": "dict-modifiable", + "key": "ext_mapping", + "label": "Extension Mapping", + "use_label_wrap": true, + "object_type": { + "type": "text" + } }, { "type": "schema", diff --git a/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 58a21c185a..6ecda224ea 100644 --- a/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -170,6 +170,10 @@ "key": "ValidateMeshNonManifold", "label": "ValidateMeshNonManifold" }, + { + "key": "ValidateMeshNormalsUnlocked", + "label": "ValidateMeshNormalsUnlocked" + }, { "key": "ValidateMeshUVSetMap1", "label": "ValidateMeshUVSetMap1", @@ -242,6 +246,10 @@ "type": "label", "label": "Extractors" }, + { + "type": "schema_template", + "name": "template_maya_capture" + }, { "type": "dict", "collapsible": true, From a378bb4bd02c8e9099213cdc9dd543b300b1f3da Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 09:58:09 +0100 Subject: [PATCH 09/68] maya wrong api import --- pype/hosts/maya/api/action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/maya/api/action.py b/pype/hosts/maya/api/action.py index 35a57a4445..f623298451 100644 --- a/pype/hosts/maya/api/action.py +++ b/pype/hosts/maya/api/action.py @@ -72,7 +72,7 @@ class GenerateUUIDsOnInvalidAction(pyblish.api.Action): nodes (list): all nodes to regenerate ids on """ - from pype.hosts.maya import lib + from pype.hosts.maya.api import lib import avalon.io as io asset = instance.data['asset'] From 97324bc5e306559e6d4f737ac81edee67f6c1da8 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 09:58:23 +0100 Subject: [PATCH 10/68] add nuke validator defaults --- .../defaults/project_settings/nuke.json | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/pype/settings/defaults/project_settings/nuke.json b/pype/settings/defaults/project_settings/nuke.json index 517065f79a..392f189bd7 100644 --- a/pype/settings/defaults/project_settings/nuke.json +++ b/pype/settings/defaults/project_settings/nuke.json @@ -11,6 +11,30 @@ "PreCollectNukeInstances": { "sync_workfile_version": true }, + "ValidateKnobs": { + "enabled": false, + "knobs": { + "render": { + "review": true + } + } + }, + "ValidateOutputResolution": { + "enabled": true, + "optional": true + }, + "ValidateGizmo": { + "enabled": true, + "optional": true + }, + "ValidateScript": { + "enabled": true, + "optional": true + }, + "ValidateNukeWriteBoundingBox": { + "enabled": true, + "optional": true + }, "ExtractThumbnail": { "enabled": true, "nodes": { @@ -38,14 +62,6 @@ ] } }, - "ValidateKnobs": { - "enabled": false, - "knobs": { - "render": { - "review": true - } - } - }, "ExtractReviewDataLut": { "enabled": false }, @@ -61,22 +77,6 @@ "deadline_pool": "", "deadline_pool_secondary": "", "deadline_chunk_size": 1 - }, - "ValidateOutputResolution": { - "enabled": true, - "optional": true - }, - "ValidateGizmo": { - "enabled": true, - "optional": true - }, - "ValidateScript": { - "enabled": true, - "optional": true - }, - "ValidateNukeWriteBoundingBox": { - "enabled": true, - "optional": true } }, "workfile_build": { From 4ca92280a78bc5bc98d79d0affbf6d20988566e6 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 09:58:38 +0100 Subject: [PATCH 11/68] remove standalone publisher thumbnail default --- .../project_settings/standalonepublisher.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pype/settings/defaults/project_settings/standalonepublisher.json b/pype/settings/defaults/project_settings/standalonepublisher.json index ad1b5e82b2..08895bcba9 100644 --- a/pype/settings/defaults/project_settings/standalonepublisher.json +++ b/pype/settings/defaults/project_settings/standalonepublisher.json @@ -1,14 +1,4 @@ { - "publish": { - "ExtractThumbnailSP": { - "ffmpeg_args": { - "input": [ - "gamma 2.2" - ], - "output": [] - } - } - }, "create": { "create_workfile": { "name": "workfile", @@ -121,5 +111,15 @@ "create_image": "Image", "create_matchmove": "Matchmove" } + }, + "publish": { + "ExtractThumbnailSP": { + "ffmpeg_args": { + "input": [ + "gamma 2.2" + ], + "output": [] + } + } } } \ No newline at end of file From f166dee723b9acdef67c3857128239cdd0a07000 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Mar 2021 15:26:35 +0100 Subject: [PATCH 12/68] fix maya playblast settings --- pype/hosts/maya/api/lib.py | 49 +++-- .../maya/plugins/publish/extract_playblast.py | 10 +- .../defaults/project_settings/maya.json | 29 +-- .../schemas/template_maya_capture.json | 167 +++++++----------- 4 files changed, 94 insertions(+), 161 deletions(-) diff --git a/pype/hosts/maya/api/lib.py b/pype/hosts/maya/api/lib.py index dc802b6a37..56621949b3 100644 --- a/pype/hosts/maya/api/lib.py +++ b/pype/hosts/maya/api/lib.py @@ -2127,15 +2127,9 @@ def bake_to_world_space(nodes, def load_capture_preset(path=None, data=None): - import capture_gui import capture - if data: - preset = data - else: - path = path - preset = capture_gui.lib.load_json(path) - print(preset) + preset = data options = dict() @@ -2177,29 +2171,27 @@ def load_capture_preset(path=None, data=None): temp_options2 = {} id = 'Viewport Options' - light_options = { - 0: "default", - 1: 'all', - 2: 'selected', - 3: 'flat', - 4: 'nolights'} for key in preset[id]: - if key == 'high_quality': - if preset[id][key] == True: - temp_options2['multiSampleEnable'] = True - temp_options2['multiSampleCount'] = 4 - temp_options2['textureMaxResolution'] = 1024 + if key == 'textureMaxResolution': + if preset[id][key] > 0: + temp_options2['textureMaxResolution'] = preset[id][key] temp_options2['enableTextureMaxRes'] = True temp_options2['textureMaxResMode'] = 1 else: - temp_options2['multiSampleEnable'] = False - temp_options2['multiSampleCount'] = 4 - temp_options2['textureMaxResolution'] = 512 - temp_options2['enableTextureMaxRes'] = True + temp_options2['textureMaxResolution'] = preset[id][key] + temp_options2['enableTextureMaxRes'] = False temp_options2['textureMaxResMode'] = 0 + if key == 'multiSample': + if preset[id][key] > 0: + temp_options2['multiSampleEnable'] = True + temp_options2['multiSampleCount'] = preset[id][key] + else: + temp_options2['multiSampleEnable'] = False + temp_options2['multiSampleCount'] = preset[id][key] + if key == 'ssaoEnable': - if preset[id][key] == True: + if preset[id][key] is True: temp_options2['ssaoEnable'] = True else: temp_options2['ssaoEnable'] = False @@ -2211,18 +2203,17 @@ def load_capture_preset(path=None, data=None): if key == 'headsUpDisplay': temp_options['headsUpDisplay'] = True - if key == 'displayLights': - temp_options[str(key)] = light_options[preset[id][key]] else: temp_options[str(key)] = preset[id][key] for key in ['override_viewport_options', 'high_quality', 'alphaCut', - 'gpuCacheDisplayFilter']: - temp_options.pop(key, None) - - for key in ['ssaoEnable']: + 'gpuCacheDisplayFilter', + 'multiSample', + 'ssaoEnable', + 'textureMaxResolution' + ]: temp_options.pop(key, None) options['viewport_options'] = temp_options diff --git a/pype/hosts/maya/plugins/publish/extract_playblast.py b/pype/hosts/maya/plugins/publish/extract_playblast.py index da1428400e..99411e7f53 100644 --- a/pype/hosts/maya/plugins/publish/extract_playblast.py +++ b/pype/hosts/maya/plugins/publish/extract_playblast.py @@ -45,11 +45,8 @@ class ExtractPlayblast(pype.api.Extractor): # get cameras camera = instance.data['review_camera'] - try: - preset = lib.load_capture_preset(data=self.capture_preset) - except Exception: - preset = {} - self.log.info('using viewport preset: {}'.format(preset)) + preset = lib.load_capture_preset(data=self.capture_preset) + preset['camera'] = camera preset['format'] = "image" @@ -99,6 +96,9 @@ class ExtractPlayblast(pype.api.Extractor): # Remove panel key since it's internal value to capture_gui preset.pop("panel", None) + + self.log.info('using viewport preset: {}'.format(preset)) + path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) diff --git a/pype/settings/defaults/project_settings/maya.json b/pype/settings/defaults/project_settings/maya.json index c1f43ff81e..75aa32fb36 100644 --- a/pype/settings/defaults/project_settings/maya.json +++ b/pype/settings/defaults/project_settings/maya.json @@ -284,13 +284,6 @@ "isolate_view": true, "off_screen": true }, - "IO": { - "name": "", - "open_finished": true, - "raw_frame_numbers": true, - "recent_playblasts": [], - "save_file": true - }, "PanZoom": { "pan_zoom": true }, @@ -303,19 +296,20 @@ "percent": 1.0, "mode": "Custom" }, - "Time Range": { - "start_frame": 0, - "end_frame": 0, - "frame": "", - "time": "Time Slider" - }, "Viewport Options": { + "override_viewport_options": true, + "displayLights": "0", + "textureMaxResolution": 1024, + "multiSample": 4, + "shadows": true, + "textures": true, + "twoSidedLighting": true, + "ssaoEnable": true, "cameras": false, "clipGhosts": false, "controlVertices": false, "deformers": false, "dimensions": false, - "displayLights": 0, "dynamicConstraints": false, "dynamics": false, "fluids": false, @@ -325,7 +319,6 @@ "grid": false, "hairSystems": true, "handles": false, - "high_quality": true, "hud": false, "hulls": false, "ikHandles": false, @@ -340,17 +333,13 @@ "nRigids": false, "nurbsCurves": false, "nurbsSurfaces": false, - "override_viewport_options": true, "particleInstancers": false, "pivots": false, "planes": false, "pluginShapes": false, "polymeshes": true, - "shadows": true, "strokes": false, - "subdivSurfaces": false, - "textures": false, - "twoSidedLighting": true + "subdivSurfaces": false }, "Camera Options": { "displayGateMask": false, diff --git a/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json b/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json index f6fcb3b998..e4e0b034dd 100644 --- a/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/template_maya_capture.json @@ -163,42 +163,7 @@ } ] }, - { - "type": "dict", - "key": "IO", - "children": [ - { - "type": "label", - "label": "IO" - }, - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "boolean", - "key": "open_finished", - "label": "Open finished" - }, - { - "type": "boolean", - "key": "raw_frame_numbers", - "label": "Raw frame numbers" - }, - { - "type": "list", - "key": "recent_playblasts", - "label": "Recent Playblasts", - "object_type": "text" - }, - { - "type": "boolean", - "key": "save_file", - "label": "Save file" - } - ] - }, + { "type": "dict", "key": "PanZoom", @@ -222,9 +187,12 @@ "label": "Renderer" }, { - "type": "text", + "type": "enum", "key": "rendererName", - "label": " Renderer name" + "label": "Renderer name", + "enum_items": [ + { "vp2Renderer": "Viewport 2.0" } + ] } ] }, @@ -273,48 +241,66 @@ { "type": "splitter" }, - { - "type": "dict", - "key": "Time Range", - "children": [ - { - "type": "label", - "label": "Time Range" - }, - { - "type": "number", - "key": "start_frame", - "label": " Start frame", - "decimal": 0, - "minimum": 0, - "maximum": 999999 - }, - { - "type": "number", - "key": "end_frame", - "label": "End frame", - "decimal": 0, - "minimum": 0, - "maximum": 999999 - }, - { - "type": "text", - "key": "frame", - "label": "Frame" - }, - { - "type": "text", - "key": "time", - "label": "Time" - } - ] - }, { "type": "dict", "collapsible": true, "key": "Viewport Options", "label": "Viewport Options", "children": [ + { + "type": "boolean", + "key": "override_viewport_options", + "label": "override_viewport_options" + }, + { + "type": "enum", + "key": "displayLights", + "label": "Display Lights", + "enum_items": [ + { "default": "Default Lighting"}, + { "all": "All Lights"}, + { "selected": "Selected Lights"}, + { "flat": "Flat Lighting"}, + { "nolights": "No Lights"} + ] + }, + { + "type": "number", + "key": "textureMaxResolution", + "label": "Texture Clamp Resolution", + "decimal": 0 + }, + { + "type": "number", + "key": "multiSample", + "label": "Anti Aliasing Samples", + "decimal": 0, + "minimum": 0, + "maximum": 32 + }, + { + "type": "boolean", + "key": "shadows", + "label": "Display Shadows" + }, + { + "type": "boolean", + "key": "textures", + "label": "Display Textures" + }, + { + "type": "boolean", + "key": "twoSidedLighting", + "label": "Two Sided Lighting" + }, + { + "type": "boolean", + "key": "ssaoEnable", + "label": "Screen Space Ambient Occlusion" + }, + { + "type": "splitter" + }, { "type": "boolean", "key": "cameras", @@ -340,14 +326,6 @@ "key": "dimensions", "label": "dimensions" }, - { - "type": "number", - "key": "displayLights", - "label": "displayLights", - "decimal": 0, - "minimum": 0, - "maximum": 10 - }, { "type": "boolean", "key": "dynamicConstraints", @@ -393,11 +371,6 @@ "key": "handles", "label": "handles" }, - { - "type": "boolean", - "key": "high_quality", - "label": "high_quality" - }, { "type": "boolean", "key": "hud", @@ -468,11 +441,6 @@ "key": "nurbsSurfaces", "label": "nurbsSurfaces" }, - { - "type": "boolean", - "key": "override_viewport_options", - "label": "override_viewport_options" - }, { "type": "boolean", "key": "particleInstancers", @@ -498,11 +466,6 @@ "key": "polymeshes", "label": "polymeshes" }, - { - "type": "boolean", - "key": "shadows", - "label": "shadows" - }, { "type": "boolean", "key": "strokes", @@ -512,16 +475,6 @@ "type": "boolean", "key": "subdivSurfaces", "label": "subdivSurfaces" - }, - { - "type": "boolean", - "key": "textures", - "label": "textures" - }, - { - "type": "boolean", - "key": "twoSidedLighting", - "label": "twoSidedLighting" } ] }, From 5c930f7f3912cfe694b90b21a3c17ded71089ee1 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 4 Mar 2021 12:48:06 +0100 Subject: [PATCH 13/68] nuke loaders --- pype/hosts/nuke/plugins/load/load_mov.py | 1 + .../defaults/project_settings/nuke.json | 19 ++++++++++++++ .../projects_schema/schema_project_nuke.json | 5 ++++ .../schemas/schema_nuke_load.json | 26 +++++++++++++++++++ 4 files changed, 51 insertions(+) create mode 100644 pype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json diff --git a/pype/hosts/nuke/plugins/load/load_mov.py b/pype/hosts/nuke/plugins/load/load_mov.py index 435f26ad98..830359ccf9 100644 --- a/pype/hosts/nuke/plugins/load/load_mov.py +++ b/pype/hosts/nuke/plugins/load/load_mov.py @@ -95,6 +95,7 @@ class LoadMov(api.Loader): containerise, viewer_update_and_undo_stop ) + version = context['version'] version_data = version.get("data", {}) repr_id = context["representation"]["_id"] diff --git a/pype/settings/defaults/project_settings/nuke.json b/pype/settings/defaults/project_settings/nuke.json index 392f189bd7..5821584932 100644 --- a/pype/settings/defaults/project_settings/nuke.json +++ b/pype/settings/defaults/project_settings/nuke.json @@ -79,6 +79,25 @@ "deadline_chunk_size": 1 } }, + "load": { + "LoadImage": { + "enabled": true, + "representations": [] + }, + "LoadMov": { + "enabled": true, + "representations": [] + }, + "LoadSequence": { + "enabled": true, + "representations": [ + "png", + "jpg", + "exr", + "" + ] + } + }, "workfile_build": { "profiles": [ { diff --git a/pype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/pype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 0548bd3544..220d56a306 100644 --- a/pype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/pype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -45,6 +45,11 @@ "type": "schema", "name": "schema_nuke_publish", "template_data": [] + }, + { + "type": "schema", + "name": "schema_nuke_load", + "template_data": [] }, { "type": "schema", diff --git a/pype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json new file mode 100644 index 0000000000..9d132e33b4 --- /dev/null +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_nuke_load.json @@ -0,0 +1,26 @@ +{ + "type": "dict", + "collapsible": true, + "key": "load", + "label": "Loader plugins", + "children": [ + { + "type": "schema_template", + "name": "template_loader_plugin", + "template_data": [ + { + "key": "LoadImage", + "label": "Image Loader" + }, + { + "key": "LoadMov", + "label": "Movie Loader" + }, + { + "key": "LoadSequence", + "label": "Image Sequence Loader" + } + ] + } + ] +} From e27e431742419e31d557419cdda4df3286bb4118 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 4 Mar 2021 12:49:23 +0100 Subject: [PATCH 14/68] remove look assigner folder --- repos/maya-look-assigner | 1 - 1 file changed, 1 deletion(-) delete mode 160000 repos/maya-look-assigner diff --git a/repos/maya-look-assigner b/repos/maya-look-assigner deleted file mode 160000 index 7adabe8f0e..0000000000 --- a/repos/maya-look-assigner +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7adabe8f0e6858bfe5b6bf0b39bd428ed72d0452 From 80d7bb8067e037ee180598ac0b23028504dd3c9e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 3 Mar 2021 19:01:44 +0100 Subject: [PATCH 15/68] PS - added Subset Manager into menu Implemented list_instances and remove_instance methods --- .../websocket_server/hosts/photoshop.py | 3 +++ .../stubs/photoshop_server_stub.py | 22 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pype/modules/websocket_server/hosts/photoshop.py b/pype/modules/websocket_server/hosts/photoshop.py index cdfb9413a0..b75874aa6c 100644 --- a/pype/modules/websocket_server/hosts/photoshop.py +++ b/pype/modules/websocket_server/hosts/photoshop.py @@ -54,6 +54,9 @@ class Photoshop(WebSocketRoute): async def projectmanager_route(self): self._tool_route("projectmanager") + async def subsetmanager_route(self): + self._tool_route("subsetmanager") + def _tool_route(self, tool_name): """The address accessed when clicking on the buttons.""" partial_method = functools.partial(photoshop.show, tool_name) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index d223153797..9677fa61a8 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -238,7 +238,14 @@ class PhotoshopServerStub(): """ Reads layers metadata from Headline from active document in PS. (Headline accessible by File > File Info) - Returns(string): - json documents + + Returns: + (string): - json documents + example: + {"8":{"active":true,"subset":"imageBG", + "family":"image","id":"pyblish.avalon.instance", + "asset":"Town"}} + 8 is layer(group) id - used for deletion, update etc. """ layers_data = {} res = self.websocketserver.call(self.client.call('Photoshop.read')) @@ -288,6 +295,19 @@ class PhotoshopServerStub(): ('Photoshop.delete_layer', layer_id=layer_id)) + def remove_instance(self, instance_id): + cleaned_data = {} + + for key, instance in self.get_layers_metadata().items(): + if key != instance_id: + cleaned_data[key] = instance + + payload = json.dumps(cleaned_data, indent=4) + + self.websocketserver.call(self.client.call + ('Photoshop.imprint', payload=payload) + ) + def close(self): self.client.close() From e19b960096e0a0b7879346678cdde883b771a70f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Mar 2021 15:02:23 +0100 Subject: [PATCH 16/68] PS - use subset name as instance name Warn if duplicate subsets found --- .../hosts/photoshop/plugins/publish/collect_instances.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/hosts/photoshop/plugins/publish/collect_instances.py b/pype/hosts/photoshop/plugins/publish/collect_instances.py index 14803cceee..5390df768b 100644 --- a/pype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/pype/hosts/photoshop/plugins/publish/collect_instances.py @@ -24,6 +24,7 @@ class CollectInstances(pyblish.api.ContextPlugin): stub = photoshop.stub() layers = stub.get_layers() layers_meta = stub.get_layers_metadata() + instance_names = [] for layer in layers: layer_data = stub.read(layer, layers_meta) @@ -41,14 +42,20 @@ class CollectInstances(pyblish.api.ContextPlugin): # self.log.info("%s skipped, it was empty." % layer.Name) # continue - instance = context.create_instance(layer.name) + instance = context.create_instance(layer_data["subset"]) instance.append(layer) instance.data.update(layer_data) instance.data["families"] = self.families_mapping[ layer_data["family"] ] instance.data["publish"] = layer.visible + instance_names.append(layer_data["subset"]) # Produce diagnostic message for any graphical # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) + self.log.info("instance: {} ".format(instance.data)) + + if len(instance_names) != len(set(instance_names)): + self.log.warning("Duplicate instances found. " + + "Remove unwanted via SubsetManager") From 514dbfc2af08d2c2635bf3c1632021726312031a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Mar 2021 15:54:29 +0100 Subject: [PATCH 17/68] PS - added validator for unique subsets --- .../publish/validate_unique_subsets.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 pype/hosts/photoshop/plugins/publish/validate_unique_subsets.py diff --git a/pype/hosts/photoshop/plugins/publish/validate_unique_subsets.py b/pype/hosts/photoshop/plugins/publish/validate_unique_subsets.py new file mode 100644 index 0000000000..5871f79668 --- /dev/null +++ b/pype/hosts/photoshop/plugins/publish/validate_unique_subsets.py @@ -0,0 +1,26 @@ +import pyblish.api +import pype.api + + +class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): + """ + Validate that all subset's names are unique. + """ + + label = "Validate Subset Uniqueness" + hosts = ["photoshop"] + order = pype.api.ValidateContentsOrder + families = ["image"] + + def process(self, context): + subset_names = [] + + for instance in context: + if instance.data.get('publish'): + subset_names.append(instance.data.get('subset')) + + msg = ( + "Instance subset names are not unique. " + + "Remove duplicates via SubsetManager." + ) + assert len(subset_names) == len(set(subset_names)), msg From e414ac8a2db4bb094b938d12d6877312bae5a025 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 Mar 2021 16:24:24 +0100 Subject: [PATCH 18/68] PS - fixed validate_naming repair --- pype/hosts/photoshop/plugins/publish/validate_naming.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/hosts/photoshop/plugins/publish/validate_naming.py b/pype/hosts/photoshop/plugins/publish/validate_naming.py index 2483adcb5e..02f6fdcacb 100644 --- a/pype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/pype/hosts/photoshop/plugins/publish/validate_naming.py @@ -30,6 +30,10 @@ class ValidateNamingRepair(pyblish.api.Action): data["subset"] = "image" + name stub.imprint(instance[0], data) + name = name.replace(instance.data["family"], '') + name = stub.PUBLISH_ICON + name + stub.rename_layer(instance.data["uuid"], name) + return True From ec06cc9c4625b3666fa617d516d69dbfa4107257 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 11:41:34 +0100 Subject: [PATCH 19/68] PS - added highlight with icon for publishable instances --- pype/hosts/photoshop/plugins/create/create_image.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pype/hosts/photoshop/plugins/create/create_image.py b/pype/hosts/photoshop/plugins/create/create_image.py index 54b6efad29..03250acd48 100644 --- a/pype/hosts/photoshop/plugins/create/create_image.py +++ b/pype/hosts/photoshop/plugins/create/create_image.py @@ -73,5 +73,17 @@ class CreateImage(pype.api.Creator): groups.append(group) for group in groups: + long_names = [] + if group.long_name: + for directory in group.long_name[::-1]: + name = directory.replace(stub.PUBLISH_ICON, '').\ + replace(stub.LOADED_ICON, '') + long_names.append(name) + self.data.update({"subset": "image" + group.name}) + self.data.update({"uuid": str(group.id)}) + self.data.update({"long_name": "_".join(long_names)}) stub.imprint(group, self.data) + # reusing existing group, need to rename afterwards + if not create_group: + stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name) From 065266cf40bb541b05a8e0caa71baa6f2bcc45a9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 12:03:58 +0100 Subject: [PATCH 20/68] PS - added highlight with icon for publishable instances Changed structure of metadata from {} to [] Added rename_layer method Switched to attr instead of namedtuple (same as in AE) --- .../stubs/photoshop_server_stub.py | 163 +++++++++++++++--- 1 file changed, 135 insertions(+), 28 deletions(-) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index 9677fa61a8..79a486a20c 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -4,16 +4,37 @@ from pype.modules.websocket_server import WebSocketServer Used anywhere solution is calling client methods. """ import json -from collections import namedtuple +import attr -class PhotoshopServerStub(): +@attr.s +class PSItem(object): + """ + Object denoting layer or group item in PS. Each item is created in + PS by any Loader, but contains same fields, which are being used + in later processing. + """ + # metadata + id = attr.ib() # id created by AE, could be used for querying + name = attr.ib() # name of item + group = attr.ib(default=None) # item type (footage, folder, comp) + parents = attr.ib(factory=list) + visible = attr.ib(default=True) + type = attr.ib(default=None) + # all imported elements, single for + members = attr.ib(factory=list) + long_name = attr.ib(default=None) + + +class PhotoshopServerStub: """ Stub for calling function on client (Photoshop js) side. Expects that client is already connected (started when avalon menu is opened). 'self.websocketserver.call' is used as async wrapper """ + PUBLISH_ICON = '\u2117 ' + LOADED_ICON = '\u25bc' def __init__(self): self.websocketserver = WebSocketServer.get_instance() @@ -34,7 +55,7 @@ class PhotoshopServerStub(): """ Parses layer metadata from Headline field of active document Args: - layer: + Returns: Format of tuple: { 'id':'123', 'name': 'My Layer 1', 'type': 'GUIDE'|'FG'|'BG'|'OBJ' @@ -100,12 +145,26 @@ class PhotoshopServerStub(): return self._to_records(res) + def get_layer(self, layer_id): + """ + Returns PSItem for specific 'layer_id' or None if not found + Args: + layer_id (string): unique layer id, stored in 'uuid' field + + Returns: + (PSItem) or None + """ + layers = self.get_layers() + for layer in layers: + if str(layer.id) == str(layer_id): + return layer + def get_layers_in_layers(self, layers): """ Return all layers that belong to layers (might be groups). Args: - layers : - Returns: + layers : + Returns: """ all_layers = self.get_layers() ret = [] @@ -123,28 +182,30 @@ class PhotoshopServerStub(): def create_group(self, name): """ Create new group (eg. LayerSet) - Returns: + Returns: """ + enhanced_name = self.PUBLISH_ICON + name ret = self.websocketserver.call(self.client.call ('Photoshop.create_group', - name=name)) + name=enhanced_name)) # create group on PS is asynchronous, returns only id - layer = {"id": ret, "name": name, "group": True} - return namedtuple('Layer', layer.keys())(*layer.values()) + return PSItem(id=ret, name=name, group=True) def group_selected_layers(self, name): """ Group selected layers into new LayerSet (eg. group) Returns: (Layer) """ + enhanced_name = self.PUBLISH_ICON + name res = self.websocketserver.call(self.client.call ('Photoshop.group_selected_layers', - name=name) + name=enhanced_name) ) res = self._to_records(res) - if res: - return res.pop() + rec = res.pop() + rec.name = rec.name.replace(self.PUBLISH_ICON, '') + return rec raise ValueError("No group record returned") def get_selected_layers(self): @@ -253,6 +314,23 @@ class PhotoshopServerStub(): layers_data = json.loads(res) except json.decoder.JSONDecodeError: pass + # format of metadata changed from {} to [] because of standardization + # keep current implementation logic as its working + if not isinstance(layers_data, dict): + temp_layers_meta = {} + for layer_meta in layers_data: + layer_id = layer_meta.get("uuid") or \ + (layer_meta.get("members")[0]) + temp_layers_meta[layer_id] = layer_meta + layers_data = temp_layers_meta + else: + # legacy version of metadata + for layer_id, layer_meta in layers_data.items(): + if layer_meta.get("schema") != "avalon-core:container-2.0": + layer_meta["uuid"] = str(layer_id) + else: + layer_meta["members"] = [str(layer_id)] + return layers_data def import_smart_object(self, path, layer_name): @@ -264,11 +342,14 @@ class PhotoshopServerStub(): layer_name (str): Unique layer name to differentiate how many times same smart object was loaded """ + enhanced_name = self.LOADED_ICON + layer_name res = self.websocketserver.call(self.client.call ('Photoshop.import_smart_object', - path=path, name=layer_name)) - - return self._to_records(res).pop() + path=path, name=enhanced_name)) + rec = self._to_records(res).pop() + if rec: + rec.name = rec.name.replace(self.LOADED_ICON, '') + return rec def replace_smart_object(self, layer, path, layer_name): """ @@ -277,13 +358,14 @@ class PhotoshopServerStub(): same smart object was loaded Args: - layer (namedTuple): Layer("id":XX, "name":"YY"..). + layer (PSItem): path (str): File to import. """ + enhanced_name = self.LOADED_ICON + layer_name self.websocketserver.call(self.client.call ('Photoshop.replace_smart_object', layer_id=layer.id, - path=path, name=layer_name)) + path=path, name=enhanced_name)) def delete_layer(self, layer_id): """ @@ -295,6 +377,18 @@ class PhotoshopServerStub(): ('Photoshop.delete_layer', layer_id=layer_id)) + def rename_layer(self, layer_id, name): + """ + Renames specific layer by it's id. + Args: + layer_id (int): id of layer to delete + name (str): new name + """ + self.websocketserver.call(self.client.call + ('Photoshop.rename_layer', + layer_id=layer_id, + name=name)) + def remove_instance(self, instance_id): cleaned_data = {} @@ -313,19 +407,32 @@ class PhotoshopServerStub(): def _to_records(self, res): """ - Converts string json representation into list of named tuples for + Converts string json representation into list of PSItem for dot notation access to work. - Returns: - res(string): - json representation + Args: + res (string): valid json + Returns: + """ try: layers_data = json.loads(res) except json.decoder.JSONDecodeError: raise ValueError("Received broken JSON {}".format(res)) ret = [] - # convert to namedtuple to use dot donation - if isinstance(layers_data, dict): # TODO refactore + + # convert to AEItem to use dot donation + if isinstance(layers_data, dict): layers_data = [layers_data] for d in layers_data: - ret.append(namedtuple('Layer', d.keys())(*d.values())) + # currently implemented and expected fields + item = PSItem(d.get('id'), + d.get('name'), + d.get('group'), + d.get('parents'), + d.get('visible'), + d.get('type'), + d.get('members'), + d.get('long_name')) + + ret.append(item) return ret From 628b34a3cd374b632a196630beb8d30df38cf2c9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 13:52:37 +0100 Subject: [PATCH 21/68] PS - fixed validate_naming --- pype/hosts/photoshop/plugins/publish/validate_naming.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pype/hosts/photoshop/plugins/publish/validate_naming.py b/pype/hosts/photoshop/plugins/publish/validate_naming.py index 02f6fdcacb..6130e58375 100644 --- a/pype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/pype/hosts/photoshop/plugins/publish/validate_naming.py @@ -50,8 +50,11 @@ class ValidateNaming(pyblish.api.InstancePlugin): actions = [ValidateNamingRepair] def process(self, instance): - msg = "Name \"{}\" is not allowed.".format(instance.data["name"]) + help_msg = ' Use Repair action (A) in Pyblish to fix it.' + msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"], + help_msg) assert " " not in instance.data["name"], msg - msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"]) + msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"], + help_msg) assert " " not in instance.data["subset"], msg From 2f8ecc6d82e9d0e0827e2bf8da7aed58265a6a34 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 14:10:14 +0100 Subject: [PATCH 22/68] PS - fix repair validator - limit duplication of family --- pype/hosts/photoshop/plugins/publish/validate_naming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/photoshop/plugins/publish/validate_naming.py b/pype/hosts/photoshop/plugins/publish/validate_naming.py index 6130e58375..48f5901233 100644 --- a/pype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/pype/hosts/photoshop/plugins/publish/validate_naming.py @@ -25,12 +25,12 @@ class ValidateNamingRepair(pyblish.api.Action): for instance in instances: self.log.info("validate_naming instance {}".format(instance)) name = instance.data["name"].replace(" ", "_") + name = name.replace(instance.data["family"], '') instance[0].Name = name data = stub.read(instance[0]) data["subset"] = "image" + name stub.imprint(instance[0], data) - name = name.replace(instance.data["family"], '') name = stub.PUBLISH_ICON + name stub.rename_layer(instance.data["uuid"], name) From 9e7f39957bb243502a28780d28e68ffd9761a59f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:41:02 +0100 Subject: [PATCH 23/68] removed unused import --- pype/tools/tray/pype_tray.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index c27df16276..b5fbd2598a 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -8,10 +8,6 @@ from pype.api import Logger, resources from pype.modules import TrayModulesManager, ITrayService from pype.settings.lib import get_system_settings import pype.version -try: - import configparser -except Exception: - import ConfigParser as configparser class TrayManager: From 81b081c24f78f129d7cb5f52155fabfdf8399f7f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:41:59 +0100 Subject: [PATCH 24/68] removed unused working widget --- pype/tools/tray/pype_tray.py | 55 ++---------------------------------- 1 file changed, 3 insertions(+), 52 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index b5fbd2598a..27f00c8065 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -124,63 +124,14 @@ class TrayMainWindow(QtWidgets.QMainWindow): Every widget should have set this window as parent because QSystemTrayIcon widget is not allowed to be a parent of any widget. - - :param app: Qt application manages application's control flow - :type app: QtWidgets.QApplication - - .. note:: - *TrayMainWindow* has ability to show **working** widget. - Calling methods: - - ``show_working()`` - - ``hide_working()`` - .. todo:: Hide working widget if idle is too long """ def __init__(self, app): - super().__init__() + super(TrayMainWindow, self).__init__() self.app = app - self.set_working_widget() - - self.trayIcon = SystemTrayIcon(self) - self.trayIcon.show() - - def set_working_widget(self): - image_file = resources.get_resource("icons", "working.svg") - img_pix = QtGui.QPixmap(image_file) - if image_file.endswith('.svg'): - widget = QtSvg.QSvgWidget(image_file) - else: - widget = QtWidgets.QLabel() - widget.setPixmap(img_pix) - - # Set widget properties - widget.setGeometry(img_pix.rect()) - widget.setMask(img_pix.mask()) - widget.setWindowFlags( - QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint - ) - widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) - - self.center_widget(widget) - self._working_widget = widget - self.helper = DragAndDropHelper(self._working_widget) - - def center_widget(self, widget): - frame_geo = widget.frameGeometry() - screen = self.app.desktop().cursor().pos() - center_point = self.app.desktop().screenGeometry( - self.app.desktop().screenNumber(screen) - ).center() - frame_geo.moveCenter(center_point) - widget.move(frame_geo.topLeft()) - - def show_working(self): - self._working_widget.show() - - def hide_working(self): - self.center_widget(self._working_widget) - self._working_widget.hide() + self.tray_widget = SystemTrayIcon(self) + self.tray_widget.show() class DragAndDropHelper: From 70fedd42bc08666f670a4e1ea11fb556c270f0b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:42:31 +0100 Subject: [PATCH 25/68] PypeTrayApplication does not change application name on windows --- pype/tools/tray/pype_tray.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 27f00c8065..48bc9fcb0c 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -177,17 +177,10 @@ class PypeTrayApplication(QtWidgets.QApplication): """Qt application manages application's control flow.""" def __init__(self): - super(self.__class__, self).__init__(sys.argv) + super(PypeTrayApplication, self).__init__(sys.argv) # Allows to close widgets without exiting app self.setQuitOnLastWindowClosed(False) - # Allow show icon istead of python icon in task bar (Windows) - if os.name == "nt": - import ctypes - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( - u"pype_tray" - ) - # Sets up splash splash_widget = self.set_splash() From 27c860ffe1a4819b7c305806c7594a122bb95214 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:42:50 +0100 Subject: [PATCH 26/68] removed drag and drop helper --- pype/tools/tray/pype_tray.py | 39 ------------------------------------ 1 file changed, 39 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 48bc9fcb0c..8e0757ec9d 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -134,45 +134,6 @@ class TrayMainWindow(QtWidgets.QMainWindow): self.tray_widget.show() -class DragAndDropHelper: - """ Helper adds to widget drag and drop ability - - :param widget: Qt Widget where drag and drop ability will be added - """ - - def __init__(self, widget): - self.widget = widget - self.widget.mousePressEvent = self.mousePressEvent - self.widget.mouseMoveEvent = self.mouseMoveEvent - self.widget.mouseReleaseEvent = self.mouseReleaseEvent - - def mousePressEvent(self, event): - self.__mousePressPos = None - self.__mouseMovePos = None - if event.button() == QtCore.Qt.LeftButton: - self.__mousePressPos = event.globalPos() - self.__mouseMovePos = event.globalPos() - - def mouseMoveEvent(self, event): - if event.buttons() == QtCore.Qt.LeftButton: - # adjust offset from clicked point to origin of widget - currPos = self.widget.mapToGlobal( - self.widget.pos() - ) - globalPos = event.globalPos() - diff = globalPos - self.__mouseMovePos - newPos = self.widget.mapFromGlobal(currPos + diff) - self.widget.move(newPos) - self.__mouseMovePos = globalPos - - def mouseReleaseEvent(self, event): - if self.__mousePressPos is not None: - moved = event.globalPos() - self.__mousePressPos - if moved.manhattanLength() > 3: - event.ignore() - return - - class PypeTrayApplication(QtWidgets.QApplication): """Qt application manages application's control flow.""" From 29f6c1e88a7e6bdf939b1f97b0091bf369882b0b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:44:10 +0100 Subject: [PATCH 27/68] removed working svg --- pype/resources/icons/working.svg | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 pype/resources/icons/working.svg diff --git a/pype/resources/icons/working.svg b/pype/resources/icons/working.svg deleted file mode 100644 index fe73f15a31..0000000000 --- a/pype/resources/icons/working.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Working... - From eeb4529b8835735d1d659ff6a752c585c0d7e094 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:44:42 +0100 Subject: [PATCH 28/68] tray manager sends it's pointer to modules manager --- pype/modules/base.py | 4 +++- pype/tools/tray/pype_tray.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/modules/base.py b/pype/modules/base.py index 7efd00e39e..b295746b9b 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -638,8 +638,10 @@ class TrayModulesManager(ModulesManager): self.modules_by_id = {} self.modules_by_name = {} self._report = {} + self.tray_manager = None - def initialize(self, tray_menu): + def initialize(self, tray_manager, tray_menu): + self.tray_manager = tray_manager self.initialize_modules() self.tray_init() self.connect_modules() diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 8e0757ec9d..7413fe399b 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -32,7 +32,7 @@ class TrayManager: def initialize_modules(self): """Add modules to tray.""" - self.modules_manager.initialize(self.tray_widget.menu) + self.modules_manager.initialize(self, self.tray_widget.menu) # Add services if they are services_submenu = ITrayService.services_submenu(self.tray_widget.menu) From 6695a452f6496a076497353974667ca13ef7f80e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:45:05 +0100 Subject: [PATCH 29/68] tray manger has method to show tray message --- pype/tools/tray/pype_tray.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 7413fe399b..062d825c09 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -54,6 +54,26 @@ class TrayManager: # Print time report self.modules_manager.print_report() + def show_tray_message(self, title, message, icon=None, msecs=None): + """Show tray message. + + Args: + title (str): Title of message. + message (str): Content of message. + icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is + Information icon, may differ by Qt version. + msecs (int): Duration of message visibility in miliseconds. + Default is 10000 msecs, may differ by Qt version. + """ + args = [title, message] + kwargs = {} + if icon: + kwargs["icon"] = icon + if msecs: + kwargs["msecs"] = msecs + + self.tray_widget.showMessage(*args, **kwargs) + def _add_version_item(self): subversion = os.environ.get("PYPE_SUBVERSION") client_name = os.environ.get("PYPE_CLIENT") From e49749d307fd6b5b999bba76a04e176a28c024e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:45:21 +0100 Subject: [PATCH 30/68] tray modules can show tray message --- pype/modules/base.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pype/modules/base.py b/pype/modules/base.py index b295746b9b..03a5965841 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -108,6 +108,7 @@ class ITrayModule: would do nothing. """ tray_initialized = False + _tray_manager = None @abstractmethod def tray_init(self): @@ -138,6 +139,20 @@ class ITrayModule: """ pass + def show_tray_message(self, title, message, icon=None, msecs=None): + """Show tray message. + + Args: + title (str): Title of message. + message (str): Content of message. + icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is + Information icon, may differ by Qt version. + msecs (int): Duration of message visibility in miliseconds. + Default is 10000 msecs, may differ by Qt version. + """ + if self._tray_manager: + self._tray_manager.show_tray_message(title, message, icon, msecs) + class ITrayAction(ITrayModule): """Implementation of Tray action. @@ -660,6 +675,7 @@ class TrayModulesManager(ModulesManager): prev_start_time = time_start for module in self.get_enabled_tray_modules(): try: + module._tray_manager = self.tray_manager module.tray_init() module.tray_initialized = True except Exception: From cfa0f4add963a04f16f1dc7d9cc5956705cdbe10 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 14:50:22 +0100 Subject: [PATCH 31/68] small cleanup of tray file --- pype/tools/tray/pype_tray.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 062d825c09..202b801343 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -15,13 +15,12 @@ class TrayManager: Load submenus, actions, separators and modules into tray's context. """ - available_sourcetypes = ["python", "file"] def __init__(self, tray_widget, main_window): self.tray_widget = tray_widget self.main_window = main_window - self.log = Logger().get_logger(self.__class__.__name__) + self.log = Logger.get_logger(self.__class__.__name__) self.module_settings = get_system_settings()["modules"] @@ -101,9 +100,9 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """ def __init__(self, parent): - self.icon = QtGui.QIcon(resources.pype_icon_filepath()) + icon = QtGui.QIcon(resources.pype_icon_filepath()) - QtWidgets.QSystemTrayIcon.__init__(self, self.icon, parent) + super(SystemTrayIcon, self).__init__(icon, parent) # Store parent - QtWidgets.QMainWindow() self.parent = parent @@ -116,15 +115,15 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): self.tray_man = TrayManager(self, self.parent) self.tray_man.initialize_modules() - # Catch activate event - self.activated.connect(self.on_systray_activated) + # Catch activate event for left click if not on MacOS + # - MacOS has this ability by design so menu would be doubled + if platform.system().lower() != "darwin": + self.activated.connect(self.on_systray_activated) # Add menu to Context of SystemTrayIcon self.setContextMenu(self.menu) def on_systray_activated(self, reason): # show contextMenu if left click - if platform.system().lower() == "darwin": - return if reason == QtWidgets.QSystemTrayIcon.Trigger: position = QtGui.QCursor().pos() self.contextMenu().popup(position) From 5e7d057da13a2c38b2f7fa09381c20d0d2b2fa21 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 15:35:47 +0100 Subject: [PATCH 32/68] tray shows small widget with basic information on version click --- pype/tools/tray/pype_tray.py | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 202b801343..5870db9c49 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -10,6 +10,51 @@ from pype.settings.lib import get_system_settings import pype.version +class PypeInfoWidget(QtWidgets.QWidget): + not_allowed = "N/A" + + def __init__(self, parent=None): + super(PypeInfoWidget, self).__init__(parent) + + self.setStyleSheet(style.load_stylesheet()) + + main_layout = QtWidgets.QFormLayout(self) + + if getattr(sys, "frozen", False): + version_end = "build" + else: + version_end = "code" + version_value = "{} ({})".format( + pype.version.__version__, version_end + ) + + lable_value = [ + # Pype version + ("Pype version:", version_value), + ("Pype location:", os.environ.get("PYPE_ROOT")), + + # Mongo URL + ("Pype Mongo URL:", os.environ.get("PYPE_MONGO")) + ] + + for label, value in lable_value: + main_layout.addRow( + label, + QtWidgets.QLabel(value or self.not_allowed) + ) + + def showEvent(self, event): + result = super(PypeInfoWidget, self).showEvent(event) + screen_center = ( + QtWidgets.QApplication.desktop().availableGeometry(self).center() + ) + self.move( + screen_center.x() - (self.width() / 2), + screen_center.y() - (self.height() / 2) + ) + return result + + class TrayManager: """Cares about context of application. @@ -20,6 +65,8 @@ class TrayManager: self.tray_widget = tray_widget self.main_window = main_window + self.pype_info_widget = None + self.log = Logger.get_logger(self.__class__.__name__) self.module_settings = get_system_settings()["modules"] @@ -85,12 +132,21 @@ class TrayManager: version_string += ", {}".format(client_name) version_action = QtWidgets.QAction(version_string, self.tray_widget) + version_action.triggered.connect(self._on_version_action) self.tray_widget.menu.addAction(version_action) self.tray_widget.menu.addSeparator() def on_exit(self): self.modules_manager.on_exit() + def _on_version_action(self): + if self.pype_info_widget is None: + self.pype_info_widget = PypeInfoWidget() + + self.pype_info_widget.show() + self.pype_info_widget.raise_() + self.pype_info_widget.activateWindow() + class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """Tray widget. From 7c9df14fc33f4957e7ddac91f643e88cb5eb8953 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 16:06:40 +0100 Subject: [PATCH 33/68] changed window icon and title --- pype/tools/tray/pype_tray.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 5870db9c49..8e6a351efd 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -18,6 +18,10 @@ class PypeInfoWidget(QtWidgets.QWidget): self.setStyleSheet(style.load_stylesheet()) + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("Pype info") + main_layout = QtWidgets.QFormLayout(self) if getattr(sys, "frozen", False): From 409504927211bafa179de6140a23e922a673d4fa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 16:13:57 +0100 Subject: [PATCH 34/68] change label alignment --- pype/tools/tray/pype_tray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 8e6a351efd..b6ee033484 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -23,7 +23,7 @@ class PypeInfoWidget(QtWidgets.QWidget): self.setWindowTitle("Pype info") main_layout = QtWidgets.QFormLayout(self) - + main_layout.setLabelAlignment(QtCore.Qt.AlignRight) if getattr(sys, "frozen", False): version_end = "build" else: From 4fa47cbe019e220a31076c258b8f6fb5f8218230 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 5 Mar 2021 17:06:39 +0100 Subject: [PATCH 35/68] streamlined UI --- igniter/__init__.py | 16 ++- igniter/__main__.py | 19 ++- igniter/bootstrap_repos.py | 14 +- igniter/install_dialog.py | 263 +++++++++++++++++++++---------------- igniter/install_thread.py | 75 ++++++++--- igniter/tools.py | 58 ++++---- start.py | 18 ++- 7 files changed, 300 insertions(+), 163 deletions(-) diff --git a/igniter/__init__.py b/igniter/__init__.py index 6fce75e95a..12f3b49457 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -2,18 +2,30 @@ """Open install dialog.""" import sys -from Qt import QtWidgets +from Qt import QtWidgets # noqa +from Qt.QtCore import Signal # noqa from .install_dialog import InstallDialog from .bootstrap_repos import BootstrapRepos +RESULT = 0 + + +def get_result(res: int): + """Sets result returned from dialog.""" + global RESULT + RESULT = res + + def run(): """Show Igniter dialog.""" app = QtWidgets.QApplication(sys.argv) d = InstallDialog() + d.finished.connect(get_result) d.show() - sys.exit(app.exec_()) + app.exec_() + return RESULT __all__ = [ diff --git a/igniter/__main__.py b/igniter/__main__.py index adabb02357..d56cc893a0 100644 --- a/igniter/__main__.py +++ b/igniter/__main__.py @@ -2,11 +2,26 @@ """Open install dialog.""" import sys -from Qt import QtWidgets +from Qt import QtWidgets # noqa +from Qt.QtCore import Signal # noqa from .install_dialog import InstallDialog + +RESULT = 0 + + +def get_result(res: int): + """Sets result returned from dialog.""" + global RESULT + RESULT = res + + app = QtWidgets.QApplication(sys.argv) + d = InstallDialog() +d.finished.connect(get_result) d.show() -sys.exit(app.exec_()) +app.exec_() +print(RESULT) +sys.exit(RESULT) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index da796b3e74..ccf40bfafe 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -456,6 +456,10 @@ class BootstrapRepos: self._progress_callback(int(progress)) arc_name = file.relative_to(frozen_root.parent) + # we need to replace first part of path which starts with + # something like `exe.win/linux....` with `pype` as this + # is expected by Pype in zip archive. + arc_name = Path("pype").joinpath(*arc_name.parts[1:]) zip_file.write(file, arc_name) destination = self._move_zip_to_data_dir(temp_zip) @@ -719,7 +723,7 @@ class BootstrapRepos: # either "live" Pype repository, or multiple zip files or even # multiple pype version directories. This process looks into zip # files and directories and tries to parse `version.py` file. - versions = self.find_pype(pype_path) + versions = self.find_pype(pype_path, include_zips=True) if versions: self._print(f"found Pype in [ {pype_path} ]") self._print(f"latest version found is [ {versions[-1]} ]") @@ -890,15 +894,17 @@ class BootstrapRepos: try: # copy file to destination self._print("Copying zip to destination ...") + _destination_zip = destination.parent / pype_version.path.name copyfile( pype_version.path.as_posix(), - destination.parent.as_posix()) + _destination_zip.as_posix()) except OSError as e: self._print( "cannot copy version to user data directory", LOG_ERROR, exc_info=True) - raise PypeVersionIOError( - "can't copy version to destination") from e + raise PypeVersionIOError(( + f"can't copy version {pype_version.path.as_posix()} " + f"to destination {destination.parent.as_posix()}")) from e # extract zip there self._print("extracting zip to destination ...") diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index 561b17408f..0431a857c0 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -5,6 +5,7 @@ import sys from Qt import QtCore, QtGui, QtWidgets # noqa from Qt.QtGui import QValidator # noqa +from Qt.QtCore import QTimer # noqa from .install_thread import InstallThread from .tools import ( @@ -35,7 +36,7 @@ class InstallDialog(QtWidgets.QDialog): """Main Igniter dialog window.""" _size_w = 400 _size_h = 600 - path = None + path = "" _controls_disabled = False def __init__(self, parent=None): @@ -75,6 +76,7 @@ class InstallDialog(QtWidgets.QDialog): os.path.join( os.path.dirname(__file__), 'RobotoMono-Regular.ttf') ) + self._pype_run_ready = False self._init_ui() @@ -93,7 +95,7 @@ class InstallDialog(QtWidgets.QDialog): """Welcome to Pype

We've detected Pype is not configured yet. But don't worry, - this is as easy as setting one thing. + this is as easy as setting one or two things.

""") self.main_label.setWordWrap(True) @@ -103,8 +105,9 @@ class InstallDialog(QtWidgets.QDialog): # -------------------------------------------------------------------- self.pype_path_label = QtWidgets.QLabel( - """This can be either Path to studio location - or Database connection string or Pype Token. + """This is Path to studio location where Pype versions + are stored. It will be pre-filled if your mongoDB connection is + already set and your studio defined this location.

Leave it empty if you want to use Pype version that come with this installation. @@ -123,7 +126,7 @@ class InstallDialog(QtWidgets.QDialog): input_layout.setContentsMargins(0, 10, 0, 10) self.user_input = FocusHandlingLineEdit() - self.user_input.setPlaceholderText("Pype repository path or url") + self.user_input.setPlaceholderText("Path to Pype versions") self.user_input.textChanged.connect(self._path_changed) self.user_input.setStyleSheet( ("color: rgb(233, 233, 233);" @@ -152,6 +155,13 @@ class InstallDialog(QtWidgets.QDialog): # Mongo box | OK button # -------------------------------------------------------------------- + self.mongo_label = QtWidgets.QLabel( + """Enter URL for running MongoDB instance:""" + ) + + self.mongo_label.setWordWrap(True) + self.mongo_label.setStyleSheet("color: rgb(150, 150, 150);") + class MongoWidget(QtWidgets.QWidget): """Widget to input mongodb URL.""" @@ -165,8 +175,8 @@ class InstallDialog(QtWidgets.QDialog): self._mongo_input.textChanged.connect(self._mongo_changed) self._mongo_input.focusIn.connect(self._focus_in) self._mongo_input.focusOut.connect(self._focus_out) - # self._mongo_input.setValidator( - # PathValidator(self._mongo_input)) + self._mongo_input.setValidator( + MongoValidator(self._mongo_input)) self._mongo_input.setStyleSheet( ("color: rgb(233, 233, 233);" "background-color: rgb(64, 64, 64);" @@ -216,11 +226,7 @@ class InstallDialog(QtWidgets.QDialog): border: 1px solid rgb(32, 64, 32); """ ) - self.parent().ok_button.setEnabled(True) - if self.parent().path != "": - path = get_pype_path_from_db(self.parent().mongo_url) - self.parent().path = path - self.parent().user_input.setText(path) + self.parent().install_button.setEnabled(True) def set_invalid(self): """Set invalid state on mongo url input.""" @@ -232,7 +238,7 @@ class InstallDialog(QtWidgets.QDialog): border: 1px solid rgb(64, 32, 32); """ ) - self.parent().ok_button.setEnabled(False) + self.parent().install_button.setEnabled(False) def set_read_only(self, state: bool): """Set input read-only.""" @@ -275,16 +281,29 @@ class InstallDialog(QtWidgets.QDialog): pype_logo_label.setPixmap(pype_logo) pype_logo_label.setContentsMargins(10, 0, 0, 10) - self.ok_button = QtWidgets.QPushButton("OK") - self.ok_button.setStyleSheet( + # install button - - - - - - - - - - - - - - - - - - - - - - - - - - - + self.install_button = QtWidgets.QPushButton("Install") + self.install_button.setStyleSheet( ("color: rgb(64, 64, 64);" "background-color: rgb(72, 200, 150);" "padding: 0.5em;") ) - self.ok_button.setMinimumSize(64, 24) - self.ok_button.setToolTip("Save and continue") - self.ok_button.clicked.connect(self._on_ok_clicked) + self.install_button.setMinimumSize(64, 24) + self.install_button.setToolTip("Install Pype") + self.install_button.clicked.connect(self._on_ok_clicked) + # run from current button - - - - - - - - - - - - - - - - - - - - - - + self.run_button = QtWidgets.QPushButton("Run without installation") + self.run_button.setStyleSheet( + ("color: rgb(64, 64, 64);" + "background-color: rgb(200, 164, 64);" + "padding: 0.5em;") + ) + self.run_button.setMinimumSize(64, 24) + self.run_button.setToolTip("Run without installing Pype") + self.run_button.clicked.connect(self._on_run_clicked) + + # install button - - - - - - - - - - - - - - - - - - - - - - - - - - - self._exit_button = QtWidgets.QPushButton("Exit") self._exit_button.setStyleSheet( ("color: rgb(64, 64, 64);" @@ -292,14 +311,16 @@ class InstallDialog(QtWidgets.QDialog): "padding: 0.5em;") ) self._exit_button.setMinimumSize(64, 24) - self._exit_button.setToolTip("Exit without saving") + self._exit_button.setToolTip("Exit") self._exit_button.clicked.connect(self._on_exit_clicked) - bottom_layout.setContentsMargins(0, 10, 0, 0) - bottom_layout.addWidget(pype_logo_label) + bottom_layout.setContentsMargins(0, 10, 10, 0) + bottom_layout.setAlignment(QtCore.Qt.AlignVCenter) + bottom_layout.addWidget(pype_logo_label, 0, QtCore.Qt.AlignVCenter) bottom_layout.addStretch(1) - bottom_layout.addWidget(self.ok_button) - bottom_layout.addWidget(self._exit_button) + bottom_layout.addWidget(self.install_button, 0, QtCore.Qt.AlignVCenter) + bottom_layout.addWidget(self.run_button, 0, QtCore.Qt.AlignVCenter) + bottom_layout.addWidget(self._exit_button, 0, QtCore.Qt.AlignVCenter) bottom_widget.setLayout(bottom_layout) bottom_widget.setStyleSheet("background-color: rgb(32, 32, 32);") @@ -321,6 +342,7 @@ class InstallDialog(QtWidgets.QDialog): color: rgb(72, 200, 150); font-family: "Roboto Mono"; font-size: 0.5em; + border: 1px solid rgb(48, 48, 48); } QScrollBar:vertical { border: 1px solid rgb(61, 115, 97); @@ -373,22 +395,24 @@ class InstallDialog(QtWidgets.QDialog): """ ) # add all to main - main.addWidget(self.main_label) - main.addWidget(self.pype_path_label) - main.addLayout(input_layout) - main.addWidget(self._mongo) - main.addStretch(1) - main.addWidget(self._status_label) - main.addWidget(self._status_box) - main.addWidget(self._progress_bar) - main.addWidget(bottom_widget) + main.addWidget(self.main_label, 0) + main.addWidget(self.pype_path_label, 0) + main.addLayout(input_layout, 0) + main.addWidget(self.mongo_label, 0) + main.addWidget(self._mongo, 0) + + main.addWidget(self._status_label, 0) + main.addWidget(self._status_box, 1) + + main.addWidget(self._progress_bar, 0) + main.addWidget(bottom_widget, 0) + self.setLayout(main) - if not self.mongo_url: - self._mongo.setVisible(False) - else: - if self._mongo.validate_url() and len(self.path) == 0: - self._mongo.setVisible(True) - self._mongo.setReadonly(True) + + # if mongo url is ok, try to get pype path from there + if self._mongo.validate_url() and len(self.path) == 0: + self.path = get_pype_path_from_db(self.mongo_url) + self.user_input.setText(self.path) def _on_select_clicked(self): """Show directory dialog.""" @@ -408,48 +432,41 @@ class InstallDialog(QtWidgets.QDialog): filename = QtCore.QDir.toNativeSeparators(result) if os.path.isdir(filename): + self.path = filename self.user_input.setText(filename) + def _on_run_clicked(self): + valid, reason = validate_mongo_connection( + self._mongo.get_mongo_url() + ) + if not valid: + self._mongo.set_invalid() + self.update_console(f"!!! {reason}", True) + return + else: + self._mongo.set_valid() + + self.done(2) + def _on_ok_clicked(self): """Start install process. - This will once again validate entered path and if ok, start + This will once again validate entered path and mongo if ok, start working thread that will do actual job. """ - valid, reason = validate_path_string(self.path) + valid, reason = validate_mongo_connection( + self._mongo.get_mongo_url() + ) if not valid: - self.user_input.setStyleSheet( - """ - background-color: rgb(32, 19, 19); - color: rgb(255, 69, 0); - padding: 0.5em; - border: 1px solid rgb(32, 64, 32); - """ - ) - self.update_console(reason, True) + self._mongo.set_invalid() + self.update_console(f"!!! {reason}", True) return else: - self.user_input.setStyleSheet( - """ - background-color: rgb(19, 19, 19); - color: rgb(64, 230, 132); - padding: 0.5em; - border: 1px solid rgb(32, 64, 32); - """ - ) - if not self.path or not self.path.startswith("mongodb"): - valid, reason = validate_mongo_connection( - self._mongo.get_mongo_url() - ) - if not valid: - self._mongo.set_invalid() - self.update_console(f"!!! {reason}", True) - return - else: - self._mongo.set_valid() + self._mongo.set_valid() self._disable_buttons() - self._install_thread = InstallThread(self) + self._install_thread = InstallThread( + self.install_result_callback_handler, self) self._install_thread.message.connect(self.update_console) self._install_thread.progress.connect(self._update_progress) self._install_thread.finished.connect(self._enable_buttons) @@ -457,28 +474,22 @@ class InstallDialog(QtWidgets.QDialog): self._install_thread.set_mongo(self._mongo.get_mongo_url()) self._install_thread.start() + def install_result_callback_handler(self, status): + """Change button behaviour based on installation outcome.""" + self.update_console(f"--- {status}") + if status >= 0: + self.install_button.setText("Run installed Pype") + self._pype_run_ready = True + def _update_progress(self, progress: int): self._progress_bar.setValue(progress) def _on_exit_clicked(self): - self.close() + self.reject() def _path_changed(self, path: str) -> str: """Set path.""" self.path = path - if not self.path.startswith("mongodb"): - self._mongo.setVisible(True) - else: - self._mongo.setVisible(False) - - if len(self.path) < 1 and not self.mongo_url: - self._mongo.setVisible(False) - - if len(self.path) == 0 and self._mongo.validate_url(): - self._mongo.setVisible(True) - self._mongo.set_read_only(True) - else: - self._mongo.set_read_only(False) return path def update_console(self, msg: str, error: bool = False) -> None: @@ -497,15 +508,17 @@ class InstallDialog(QtWidgets.QDialog): def _disable_buttons(self): """Disable buttons so user interaction doesn't interfere.""" self._btn_select.setEnabled(False) + self.run_button.setEnabled(False) self._exit_button.setEnabled(False) - self.ok_button.setEnabled(False) + self.install_button.setEnabled(False) self._controls_disabled = True def _enable_buttons(self): """Enable buttons after operation is complete.""" self._btn_select.setEnabled(True) + self.run_button.setEnabled(True) self._exit_button.setEnabled(True) - self.ok_button.setEnabled(True) + self.install_button.setEnabled(True) self._controls_disabled = False def closeEvent(self, event): # noqa @@ -515,20 +528,26 @@ class InstallDialog(QtWidgets.QDialog): return super(InstallDialog, self).closeEvent(event) -class PathValidator(QValidator): +class MongoValidator(QValidator): """Validate mongodb url for Qt widgets.""" def __init__(self, parent=None, intermediate=False): self.parent = parent self.intermediate = intermediate - super(PathValidator, self).__init__(parent) + self._validate_lock = False + self.timer = QTimer() + self.timer.timeout.connect(self._unlock_validator) + super().__init__(parent) + + def _unlock_validator(self): + self._validate_lock = False def _return_state( - self, state: QValidator.State, reason: str, path: str): + self, state: QValidator.State, reason: str, mongo: str): """Set stylesheets and actions on parent based on state. Warning: - This will always return `QFileDialog.State.Acceptable` as + This will always return `QValidator.State.Acceptable` as anything different will stop input to `QLineEdit` """ @@ -540,7 +559,7 @@ class PathValidator(QValidator): background-color: rgb(32, 19, 19); color: rgb(255, 69, 0); padding: 0.5em; - border: 1px solid rgb(32, 64, 32); + border: 1px solid rgb(64, 32, 32); """ ) elif state == QValidator.State.Intermediate and self.intermediate: @@ -564,38 +583,62 @@ class PathValidator(QValidator): """ ) - return QValidator.State.Acceptable, path, len(path) + return QValidator.State.Acceptable, mongo, len(mongo) - def validate(self, path: str, pos: int) -> (QValidator.State, str, int): # noqa - """Validate entered path. + def validate(self, mongo: str, pos: int) -> (QValidator.State, str, int): # noqa + """Validate entered mongodb connection string. - It can be regular path - in that case we test if it does exist. - It can also be mongodb connection string. In that case we parse it - as url (it should start with `mongodb://` url schema. + As url (it should start with `mongodb://` or + `mongodb+srv:// url schema. Args: - path (str): path, connection string url or pype token. + mongo (str): connection string url. pos (int): current position. - Todo: - It can also be Pype token, binding it to Pype user account. + Returns: + (QValidator.State.Acceptable, str, int): + Indicate input state with color and always return + Acceptable state as we need to be able to edit input further. """ - if path.startswith("mongodb"): + if not mongo.startswith("mongodb"): return self._return_state( - QValidator.State.Intermediate, "", path) + QValidator.State.Invalid, "need mongodb schema", mongo) - if len(path) < 6: - return self._return_state( - QValidator.State.Intermediate, "", path) + return self._return_state( + QValidator.State.Intermediate, "", mongo) - valid, reason = validate_path_string(path) - if not valid: + +class PathValidator(MongoValidator): + """Validate mongodb url for Qt widgets.""" + + def validate(self, path: str, pos: int) -> (QValidator.State, str, int): # noqa + """Validate path to be accepted by Igniter. + + Args: + path (str): path to Pype. + pos (int): current position. + + Returns: + (QValidator.State.Acceptable, str, int): + Indicate input state with color and always return + Acceptable state as we need to be able to edit input further. + + """ + # allow empty path as that will use current version coming with + # Pype Igniter + if len(path) == 0: return self._return_state( - QValidator.State.Invalid, reason, path) - else: - return self._return_state( - QValidator.State.Acceptable, reason, path) + QValidator.State.Acceptable, "Use version with Igniter", path) + + if len(path) > 3: + valid, reason = validate_path_string(path) + if not valid: + return self._return_state( + QValidator.State.Invalid, reason, path) + else: + return self._return_state( + QValidator.State.Acceptable, reason, path) class CollapsibleWidget(QtWidgets.QWidget): @@ -688,7 +731,7 @@ class CollapsibleWidget(QtWidgets.QWidget): con_anim.setDuration(self._animation) con_anim.setStartValue(0) - con_anim.setEndValue(32) + con_anim.setEndValue(collapsed_height + content_height) if __name__ == "__main__": diff --git a/igniter/install_thread.py b/igniter/install_thread.py index 29adc36ddc..51bdc31a2d 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -4,13 +4,25 @@ import os import sys from pathlib import Path -from Qt.QtCore import QThread, Signal # noqa +from Qt.QtCore import QThread, Signal, QObject # noqa + +from .bootstrap_repos import ( + BootstrapRepos, + PypeVersionInvalid, + PypeVersionIOError, + PypeVersionExists, + PypeVersion +) -from .bootstrap_repos import BootstrapRepos -from .bootstrap_repos import PypeVersion from .tools import validate_mongo_connection +class InstallResult(QObject): + """Used to pass results back.""" + def __init__(self, value): + self.status = value + + class InstallThread(QThread): """Install Worker thread. @@ -24,12 +36,15 @@ class InstallThread(QThread): """ progress = Signal(int) message = Signal((str, bool)) + finished = Signal(object) - def __init__(self, parent=None): + def __init__(self, callback, parent=None,): self._mongo = None self._path = None + self.result_callback = callback QThread.__init__(self, parent) + self.finished.connect(callback) def run(self): """Thread entry point. @@ -60,6 +75,7 @@ class InstallThread(QThread): except ValueError: self.message.emit( "!!! We need MongoDB URL to proceed.", True) + self.finished.emit(InstallResult(-1)) return else: self._mongo = os.getenv("PYPE_MONGO") @@ -82,7 +98,7 @@ class InstallThread(QThread): self.message.emit("Skipping Pype install ...", False) if detected[-1].path.suffix.lower() == ".zip": bs.extract_pype(detected[-1]) - return + self.finished.emit(InstallResult(0)) if PypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa self.message.emit(( @@ -90,26 +106,26 @@ class InstallThread(QThread): f"currently running {local_version}" ), False) self.message.emit("Skipping Pype install ...", False) - return + self.finished.emit(InstallResult(0)) self.message.emit(( "All installed versions are older then " f"currently running one {local_version}" ), False) else: - # we cannot build install package from frozen code. - # todo: we can if getattr(sys, 'frozen', False): self.message.emit("None detected.", True) - self.message.emit(("Please set path to Pype sources to " - "build installation."), False) + self.message.emit(("We will use Pype coming with " + "installer."), False) pype_version = bs.create_version_from_frozen_code() if not pype_version: self.message.emit( f"!!! Install failed - {pype_version}", True) - return + self.finished.emit(InstallResult(-1)) + self.message.emit(f"Using: {pype_version}", False) bs.install_version(pype_version) self.message.emit(f"Installed as {pype_version}", False) + self.finished.emit(InstallResult(1)) else: self.message.emit("None detected.", False) @@ -120,9 +136,15 @@ class InstallThread(QThread): if not local_pype: self.message.emit( f"!!! Install failed - {local_pype}", True) - return + self.finished.emit(InstallResult(-1)) - bs.install_version(local_pype) + try: + bs.install_version(local_pype) + except (PypeVersionExists, + PypeVersionInvalid, + PypeVersionIOError) as e: + self.message.emit(f"Installed failed", True) + self.finished.emit(InstallResult(-1)) self.message.emit(f"Installed as {local_pype}", False) else: @@ -132,25 +154,44 @@ class InstallThread(QThread): if not validate_mongo_connection(self._mongo): self.message.emit( f"!!! invalid mongo url {self._mongo}", True) - return + self.finished.emit(InstallResult(-1)) bs.registry.set_secure_item("pypeMongo", self._mongo) os.environ["PYPE_MONGO"] = self._mongo - if os.getenv("PYPE_PATH") == self._path: - ... - self.message.emit(f"processing {self._path}", True) repo_file = bs.process_entered_location(self._path) if not repo_file: self.message.emit("!!! Cannot install", True) + self.finished.emit(InstallResult(-1)) return + self.finished.emit(InstallResult(1)) + return + def set_path(self, path: str) -> None: + """Helper to set path. + + Args: + path (str): Path to set. + + """ self._path = path def set_mongo(self, mongo: str) -> None: + """Helper to set mongo url. + + Args: + mongo (str): Mongodb url. + + """ self._mongo = mongo def set_progress(self, progress: int) -> None: + """Helper to set progress bar. + + Args: + progress (int): Progress in percents. + + """ self.progress.emit(progress) diff --git a/igniter/tools.py b/igniter/tools.py index ae2ab4c586..bd9b4577a0 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -6,11 +6,9 @@ Functions ``compose_url()`` and ``decompose_url()`` are the same as in version is decided. """ - -import os -import uuid from typing import Dict, Union from urllib.parse import urlparse, parse_qs +from pathlib import Path import platform from pymongo import MongoClient @@ -142,13 +140,32 @@ def validate_mongo_connection(cnx: str) -> (bool, str): return True, "Connection is successful" -def validate_path_string(path: str) -> (bool, str): - """Validate string if it is acceptable by **Igniter**. - - `path` string can be either regular path, or mongodb url or Pype token. +def validate_mongo_string(mongo: str) -> (bool, str): + """Validate string if it is mongo url acceptable by **Igniter**.. Args: - path (str): String to validate. + mongo (str): String to validate. + + Returns: + (bool, str): + True if valid, False if not and in second part of tuple + the reason why it failed. + + """ + 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" + + +def validate_path_string(path: str) -> (bool, str): + """Validate string if it is path to Pype repository. + + Args: + path (str): Path to validate. + Returns: (bool, str): @@ -157,22 +174,15 @@ def validate_path_string(path: str) -> (bool, str): """ if not path: - return True, "Empty string" - parsed = urlparse(path) - if parsed.scheme == "mongodb": - return validate_mongo_connection(path) - # test for uuid - try: - uuid.UUID(path) - except (ValueError, TypeError): - # not uuid - if not os.path.exists(path): - return False, "Path doesn't exist or invalid token" - return True, "Path exists" - else: - # we have pype token - # todo: implement - return False, "Not implemented yet" + return False, "empty string" + + if Path(path).exists(): + return False, "path doesn't exists" + + if not Path(path).is_dir(): + return False, "path is not directory" + + return True, "valid path" def load_environments(sections: list = None) -> dict: diff --git a/start.py b/start.py index 13514e853f..0bcbaff9e1 100644 --- a/start.py +++ b/start.py @@ -122,9 +122,6 @@ silent_commands = ["run", "igniter", "standalonepublisher"] def set_environments() -> None: """Set loaded environments. - .. deprecated:: 3.0 - no environment loading from settings until Pype version is established - .. todo: better handling of environments @@ -281,6 +278,7 @@ def _process_arguments() -> tuple: import igniter igniter.run() + return use_version, use_staging @@ -384,11 +382,19 @@ def _find_frozen_pype(use_version: str = None, return_code = run(["igniter"]) if return_code != 0: raise RuntimeError("igniter crashed.") - pype_versions = bootstrap.find_pype() + print('>>> Finding Pype again ...') + pype_versions = bootstrap.find_pype(staging=use_staging) + try: + pype_version = pype_versions[-1] + except IndexError: + print("!!! Something is wrong and we didn't found it again.") + pype_versions = None if not pype_versions: # no Pype versions found anyway, lets use then the one # shipped with frozen Pype + print("*** Still no luck finding Pype.") + print("*** We'll try to use the one coming with Pype installation.") version_path = _bootstrap_from_code(use_version) pype_version = PypeVersion( version=BootstrapRepos.get_version(version_path), @@ -452,6 +458,8 @@ def _bootstrap_from_code(use_version): pype_root = os.path.normpath( os.path.dirname(sys.executable)) local_version = bootstrap.get_version(Path(pype_root)) + print(f" - running version: {local_version}") + assert local_version else: pype_root = os.path.normpath( os.path.dirname(os.path.realpath(__file__))) @@ -604,7 +612,9 @@ def boot(): from pype.version import __version__ print(">>> loading environments ...") # Must happen before `set_modules_environments` + print(" - for Avalon ...") set_avalon_environments() + print(" - for modules ...") set_modules_environments() assert version_path, "Version path not defined." From 8db42c93c3b843b699b08de4fac076ac35a04e7b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 5 Mar 2021 17:14:53 +0100 Subject: [PATCH 36/68] hound fixes and version for igniter --- igniter/install_dialog.py | 5 +++-- igniter/install_thread.py | 3 ++- igniter/version.py | 4 ++++ 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 igniter/version.py diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index 0431a857c0..f7bbc90fa3 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -14,6 +14,7 @@ from .tools import ( get_pype_path_from_db ) from .user_settings import PypeSettingsRegistry +from .version import __version__ class FocusHandlingLineEdit(QtWidgets.QLineEdit): @@ -45,7 +46,7 @@ class InstallDialog(QtWidgets.QDialog): self.mongo_url = os.getenv("PYPE_MONGO", "") or self.registry.get_secure_item("pypeMongo") or "" # noqa: E501 - self.setWindowTitle("Pype - Configure Pype repository path") + self.setWindowTitle(f"Pype Igniter {__version__} - Pype installation") self._icon_path = os.path.join( os.path.dirname(__file__), 'pype_icon.png') icon = QtGui.QIcon(self._icon_path) @@ -342,7 +343,7 @@ class InstallDialog(QtWidgets.QDialog): color: rgb(72, 200, 150); font-family: "Roboto Mono"; font-size: 0.5em; - border: 1px solid rgb(48, 48, 48); + border: 1px solid rgb(48, 48, 48); } QScrollBar:vertical { border: 1px solid rgb(61, 115, 97); diff --git a/igniter/install_thread.py b/igniter/install_thread.py index 51bdc31a2d..e3c26459a0 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -143,7 +143,8 @@ class InstallThread(QThread): except (PypeVersionExists, PypeVersionInvalid, PypeVersionIOError) as e: - self.message.emit(f"Installed failed", True) + self.message.emit(f"Installed failed: ", True) + self.message.emit(e, True) self.finished.emit(InstallResult(-1)) self.message.emit(f"Installed as {local_pype}", False) diff --git a/igniter/version.py b/igniter/version.py new file mode 100644 index 0000000000..3c627aaa1a --- /dev/null +++ b/igniter/version.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +"""Definition of Igniter version.""" + +__version__ = "1.0.0" From a39f9d74c3909ec1f784fb86bd6fd767943b3827 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 18:12:41 +0100 Subject: [PATCH 37/68] moved pype info widget to separate file --- pype/tools/tray/pype_info_widget.py | 77 +++++++++++++++++++++++++++++ pype/tools/tray/pype_tray.py | 52 +------------------ 2 files changed, 79 insertions(+), 50 deletions(-) create mode 100644 pype/tools/tray/pype_info_widget.py diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py new file mode 100644 index 0000000000..7c961687f1 --- /dev/null +++ b/pype/tools/tray/pype_info_widget.py @@ -0,0 +1,77 @@ +import os +import sys + +from avalon import style +from Qt import QtCore, QtGui, QtWidgets +from pype.api import Logger, resources +import pype.version + + +class PypeInfoWidget(QtWidgets.QWidget): + not_allowed = "N/A" + + def __init__(self, parent=None): + super(PypeInfoWidget, self).__init__(parent) + + self.setStyleSheet(style.load_stylesheet()) + + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("Pype info") + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(self._create_pype_info_widget()) + + def _create_pype_info_widget(self): + """Create widget with information about pype application.""" + pype_root = os.environ["PYPE_ROOT"] + if getattr(sys, "frozen", False): + version_end = "build" + executable_path = sys.executable + else: + version_end = "code" + executable_path = os.path.join(pype_root, "start.py") + version_value = "{} ({})".format( + pype.version.__version__, version_end + ) + + lable_value = [ + # Pype version + ("Pype version:", version_value), + ("Pype executable:", executable_path), + ("Pype location:", pype_root), + + # Mongo URL + ("Pype Mongo URL:", os.environ.get("PYPE_MONGO")) + ] + + info_widget = QtWidgets.QWidget(self) + info_layout = QtWidgets.QGridLayout(info_widget) + + title_label = QtWidgets.QLabel(info_widget) + title_label.setText("Application information") + title_label.setStyleSheet("font-weight: bold;") + info_layout.addWidget(title_label, 0, 0, 1, 2) + + for label, value in lable_value: + row = info_layout.rowCount() + info_layout.addWidget( + QtWidgets.QLabel(label), row, 0, 1, 1 + ) + value_label = QtWidgets.QLabel(value or self.not_allowed) + info_layout.addWidget( + value_label, row, 1, 1, 1 + ) + return info_widget + + def showEvent(self, event): + """Center widget to center of desktop on show.""" + result = super(PypeInfoWidget, self).showEvent(event) + screen_center = ( + QtWidgets.QApplication.desktop().availableGeometry(self).center() + ) + self.move( + screen_center.x() - (self.width() / 2), + screen_center.y() - (self.height() / 2) + ) + return result diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index b6ee033484..2d37c04136 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -3,60 +3,12 @@ import sys import platform from avalon import style -from Qt import QtCore, QtGui, QtWidgets, QtSvg +from Qt import QtCore, QtGui, QtWidgets from pype.api import Logger, resources from pype.modules import TrayModulesManager, ITrayService from pype.settings.lib import get_system_settings import pype.version - - -class PypeInfoWidget(QtWidgets.QWidget): - not_allowed = "N/A" - - def __init__(self, parent=None): - super(PypeInfoWidget, self).__init__(parent) - - self.setStyleSheet(style.load_stylesheet()) - - icon = QtGui.QIcon(resources.pype_icon_filepath()) - self.setWindowIcon(icon) - self.setWindowTitle("Pype info") - - main_layout = QtWidgets.QFormLayout(self) - main_layout.setLabelAlignment(QtCore.Qt.AlignRight) - if getattr(sys, "frozen", False): - version_end = "build" - else: - version_end = "code" - version_value = "{} ({})".format( - pype.version.__version__, version_end - ) - - lable_value = [ - # Pype version - ("Pype version:", version_value), - ("Pype location:", os.environ.get("PYPE_ROOT")), - - # Mongo URL - ("Pype Mongo URL:", os.environ.get("PYPE_MONGO")) - ] - - for label, value in lable_value: - main_layout.addRow( - label, - QtWidgets.QLabel(value or self.not_allowed) - ) - - def showEvent(self, event): - result = super(PypeInfoWidget, self).showEvent(event) - screen_center = ( - QtWidgets.QApplication.desktop().availableGeometry(self).center() - ) - self.move( - screen_center.x() - (self.width() / 2), - screen_center.y() - (self.height() / 2) - ) - return result +from .pype_info_widget import PypeInfoWidget class TrayManager: From bc4f46cf0d8496774bc7dd5b5dbe2356d6775a54 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 19:11:57 +0100 Subject: [PATCH 38/68] added environments view --- pype/tools/tray/pype_info_widget.py | 85 ++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 7c961687f1..d48dcb0f9c 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -1,12 +1,61 @@ import os import sys +import json +import collections from avalon import style from Qt import QtCore, QtGui, QtWidgets -from pype.api import Logger, resources +from pype.api import resources import pype.version +class EnvironmentsView(QtWidgets.QTreeView): + def __init__(self, model, parent=None): + super(EnvironmentsView, self).__init__(parent) + self.setModel(model) + self.setIndentation(0) + self.header().setSectionResizeMode( + 0, QtWidgets.QHeaderView.ResizeToContents + ) + self.setSelectionMode(QtWidgets.QTreeView.MultiSelection) + + def get_all_as_dict(self): + pass + + def get_selection_as_dict(self): + indexes = self.selectionModel().selectedIndexes() + mapping = collections.defaultdict(dict) + for index in indexes: + row = index.row() + value = index.data(QtCore.Qt.DisplayRole) + if index.column() == 0: + key = "key" + else: + key = "value" + mapping[row][key] = value + result = {} + for item in mapping.values(): + result[item["key"]] = item["value"] + return result + + def keyPressEvent(self, event): + if ( + event.type() == QtGui.QKeyEvent.KeyPress + and event.matches(QtGui.QKeySequence.Copy) + ): + selected_dict = self.get_selection_as_dict() + selected_str = json.dumps(selected_dict, indent=4) + + mime_data = QtCore.QMimeData() + mime_data.setText(selected_str) + QtWidgets.QApplication.instance().clipboard().setMimeData( + mime_data + ) + event.accept() + else: + return super(EnvironmentsView, self).keyPressEvent(event) + + class PypeInfoWidget(QtWidgets.QWidget): not_allowed = "N/A" @@ -21,6 +70,34 @@ class PypeInfoWidget(QtWidgets.QWidget): main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(self._create_pype_info_widget()) + main_layout.addWidget(self._create_environ_widget()) + + def _create_environ_widget(self): + env_widget = QtWidgets.QWidget(self) + + env_label_widget = QtWidgets.QLabel("Environments", env_widget) + env_label_widget.setStyleSheet("font-weight: bold;") + + env_model = QtGui.QStandardItemModel() + + env = os.environ.copy() + keys = [] + values = [] + for key in sorted(env.keys()): + keys.append(QtGui.QStandardItem(key)) + values.append(QtGui.QStandardItem(env[key])) + + env_model.appendColumn(keys) + env_model.appendColumn(values) + env_model.setHorizontalHeaderLabels(["Key", "Value"]) + + env_view = EnvironmentsView(env_model, env_widget) + + env_layout = QtWidgets.QVBoxLayout(env_widget) + env_layout.addWidget(env_label_widget) + env_layout.addWidget(env_view) + + return env_widget def _create_pype_info_widget(self): """Create widget with information about pype application.""" @@ -47,6 +124,9 @@ class PypeInfoWidget(QtWidgets.QWidget): info_widget = QtWidgets.QWidget(self) info_layout = QtWidgets.QGridLayout(info_widget) + # Add spacer to 3rd column + info_layout.addWidget(QtWidgets.QWidget(info_widget), 0, 2) + info_layout.setColumnStretch(2, 1) title_label = QtWidgets.QLabel(info_widget) title_label.setText("Application information") @@ -59,6 +139,9 @@ class PypeInfoWidget(QtWidgets.QWidget): QtWidgets.QLabel(label), row, 0, 1, 1 ) value_label = QtWidgets.QLabel(value or self.not_allowed) + value_label.setTextInteractionFlags( + QtCore.Qt.TextSelectableByMouse + ) info_layout.addWidget( value_label, row, 1, 1, 1 ) From c7ffc7ee4d8b3098cf6e7e16013aba3cb7f48679 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 5 Mar 2021 20:05:34 +0100 Subject: [PATCH 39/68] updated poetry --- poetry.lock | 305 +++++++++++++++++++++++++------------------------ pyproject.toml | 1 + 2 files changed, 159 insertions(+), 147 deletions(-) diff --git a/poetry.lock b/poetry.lock index a3e90eea38..913e1189dd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -80,7 +80,7 @@ python-dateutil = ">=2.7.0" [[package]] name = "astroid" -version = "2.5" +version = "2.5.1" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -234,7 +234,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" -version = "5.4" +version = "5.5" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -273,6 +273,21 @@ python-versions = ">=3.6" [package.dependencies] importlib-metadata = ">=3.1.1" +[[package]] +name = "dnspython" +version = "2.1.0" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dnssec = ["cryptography (>=2.6)"] +doh = ["requests", "requests-toolbelt"] +idna = ["idna (>=2.1)"] +curio = ["curio (>=1.2)", "sniffio (>=1.1)"] +trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] + [[package]] name = "docutils" version = "0.16" @@ -331,7 +346,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "google-api-core" -version = "1.26.0" +version = "1.26.1" description = "Google API client core library" category = "main" optional = false @@ -369,7 +384,7 @@ uritemplate = ">=3.0.0,<4dev" [[package]] name = "google-auth" -version = "1.27.0" +version = "1.27.1" description = "Google Authentication Library" category = "main" optional = false @@ -387,7 +402,7 @@ pyopenssl = ["pyopenssl (>=20.0.0)"] [[package]] name = "google-auth-httplib2" -version = "0.0.4" +version = "0.1.0" description = "Google Authentication Library: httplib2 transport" category = "main" optional = false @@ -395,7 +410,7 @@ python-versions = "*" [package.dependencies] google-auth = "*" -httplib2 = ">=0.9.1" +httplib2 = ">=0.15.0" six = "*" [[package]] @@ -546,7 +561,7 @@ format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator [[package]] name = "keyring" -version = "22.2.0" +version = "22.3.0" description = "Store and access your passwords safely." category = "main" optional = false @@ -597,14 +612,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "more-itertools" -version = "8.7.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.5" - [[package]] name = "multidict" version = "5.1.0" @@ -669,7 +676,7 @@ six = "*" [[package]] name = "pillow" -version = "8.1.0" +version = "8.1.1" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -691,7 +698,7 @@ dev = ["pre-commit", "tox"] [[package]] name = "protobuf" -version = "3.15.3" +version = "3.15.5" description = "Protocol Buffers" category = "main" optional = false @@ -789,21 +796,21 @@ python-versions = ">=3.5" [[package]] name = "pylint" -version = "2.7.1" +version = "2.7.2" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] -astroid = "2.5.0" +astroid = ">=2.5.1,<2.6" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" toml = ">=0.7.1" [package.extras] -docs = ["sphinx (>=3.2,<4.0)", "python-docs-theme"] +docs = ["sphinx (==3.5.1)", "python-docs-theme (==2020.12)"] [[package]] name = "pymongo" @@ -1321,8 +1328,8 @@ python-versions = "*" [[package]] name = "websocket-client" -version = "0.57.0" -description = "WebSocket client for Python. hybi13 is supported." +version = "0.58.0" +description = "WebSocket client for Python with low level API options" category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -1370,20 +1377,20 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.4.0" +version = "3.4.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.6" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +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"] [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "22fb546eefa7ced2769c1dcb3220fd2ef4b22f0dc8436e023774e50e2ee6bde1" +content-hash = "4905515073ad2bf2a8517d513d68e48669b6a829f24e540b2dd60bc70cbea26b" [metadata.files] acre = [] @@ -1447,8 +1454,8 @@ arrow = [ {file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"}, ] astroid = [ - {file = "astroid-2.5-py3-none-any.whl", hash = "sha256:87ae7f2398b8a0ae5638ddecf9987f081b756e0e9fc071aeebdca525671fc4dc"}, - {file = "astroid-2.5.tar.gz", hash = "sha256:b31c92f545517dcc452f284bc9c044050862fbe6d93d2b3de4a215a6b384bf0d"}, + {file = "astroid-2.5.1-py3-none-any.whl", hash = "sha256:21d735aab248253531bb0f1e1e6d068f0ee23533e18ae8a6171ff892b98297cf"}, + {file = "astroid-2.5.1.tar.gz", hash = "sha256:cfc35498ee64017be059ceffab0a25bedf7548ab76f2bea691c5565896e7128d"}, ] async-timeout = [ {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, @@ -1542,55 +1549,58 @@ commonmark = [ {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] coverage = [ - {file = "coverage-5.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135"}, - {file = "coverage-5.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c"}, - {file = "coverage-5.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44"}, - {file = "coverage-5.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3"}, - {file = "coverage-5.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9"}, - {file = "coverage-5.4-cp27-cp27m-win32.whl", hash = "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1"}, - {file = "coverage-5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370"}, - {file = "coverage-5.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0"}, - {file = "coverage-5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8"}, - {file = "coverage-5.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19"}, - {file = "coverage-5.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247"}, - {file = "coverage-5.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339"}, - {file = "coverage-5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337"}, - {file = "coverage-5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3"}, - {file = "coverage-5.4-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4"}, - {file = "coverage-5.4-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c"}, - {file = "coverage-5.4-cp35-cp35m-win32.whl", hash = "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f"}, - {file = "coverage-5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66"}, - {file = "coverage-5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d"}, - {file = "coverage-5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b"}, - {file = "coverage-5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9"}, - {file = "coverage-5.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af"}, - {file = "coverage-5.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5"}, - {file = "coverage-5.4-cp36-cp36m-win32.whl", hash = "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec"}, - {file = "coverage-5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9"}, - {file = "coverage-5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90"}, - {file = "coverage-5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc"}, - {file = "coverage-5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37"}, - {file = "coverage-5.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409"}, - {file = "coverage-5.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb"}, - {file = "coverage-5.4-cp37-cp37m-win32.whl", hash = "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a"}, - {file = "coverage-5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22"}, - {file = "coverage-5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f"}, - {file = "coverage-5.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3"}, - {file = "coverage-5.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786"}, - {file = "coverage-5.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c"}, - {file = "coverage-5.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994"}, - {file = "coverage-5.4-cp38-cp38-win32.whl", hash = "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39"}, - {file = "coverage-5.4-cp38-cp38-win_amd64.whl", hash = "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7"}, - {file = "coverage-5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c"}, - {file = "coverage-5.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3"}, - {file = "coverage-5.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde"}, - {file = "coverage-5.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f"}, - {file = "coverage-5.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f"}, - {file = "coverage-5.4-cp39-cp39-win32.whl", hash = "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880"}, - {file = "coverage-5.4-cp39-cp39-win_amd64.whl", hash = "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345"}, - {file = "coverage-5.4-pp36-none-any.whl", hash = "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f"}, - {file = "coverage-5.4-pp37-none-any.whl", hash = "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b"}, - {file = "coverage-5.4.tar.gz", hash = "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca"}, + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] cryptography = [ {file = "cryptography-3.4.6-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799"}, @@ -1617,6 +1627,10 @@ cx-freeze = [ {file = "cx_Freeze-6.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:507bbaace2fd27edb0e6b024898ab2e4831d45d7238264f578a5e4fa70f065e5"}, {file = "cx_Freeze-6.5.3.tar.gz", hash = "sha256:e0d03cabcdf9b9c21354807ed9f06fa9481a8fd5a0838968a830f01a70820ff1"}, ] +dnspython = [ + {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, + {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, +] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, @@ -1636,20 +1650,20 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] google-api-core = [ - {file = "google-api-core-1.26.0.tar.gz", hash = "sha256:4230ec764d48ca934fe69b85cc217e31e844e176f68df93e252acd55350e730b"}, - {file = "google_api_core-1.26.0-py2.py3-none-any.whl", hash = "sha256:002e44c533299aecd9dd265d200f9eacd9957cddd2c72e2cd1cb5cea127e972d"}, + {file = "google-api-core-1.26.1.tar.gz", hash = "sha256:23b0df512c4cc8729793f8992edb350e3211f5fd0ec007afb1599864b421beef"}, + {file = "google_api_core-1.26.1-py2.py3-none-any.whl", hash = "sha256:c383206f0f87545d3e658c4f8dc3b18a8457610fdbd791a15757c5b42d1e0e7f"}, ] 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.27.0.tar.gz", hash = "sha256:da5218cbf33b8461d7661d6b4ad91c12c0107e2767904d5e3ae6408031d5463e"}, - {file = "google_auth-1.27.0-py2.py3-none-any.whl", hash = "sha256:d3640ea61ee025d5af00e3ffd82ba0a06dd99724adaf50bdd52f49daf29f3f65"}, + {file = "google-auth-1.27.1.tar.gz", hash = "sha256:d8958af6968e4ecd599f82357ebcfeb126f826ed0656126ad68416f810f7531e"}, + {file = "google_auth-1.27.1-py2.py3-none-any.whl", hash = "sha256:63a5636d7eacfe6ef5b7e36e112b3149fa1c5b5ad77dd6df54910459bcd6b89f"}, ] google-auth-httplib2 = [ - {file = "google-auth-httplib2-0.0.4.tar.gz", hash = "sha256:8d092cc60fb16517b12057ec0bba9185a96e3b7169d86ae12eae98e645b7bc39"}, - {file = "google_auth_httplib2-0.0.4-py2.py3-none-any.whl", hash = "sha256:aeaff501738b289717fac1980db9711d77908a6c227f60e4aa1923410b43e2ee"}, + {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, + {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"}, ] googleapis-common-protos = [ {file = "googleapis-common-protos-1.53.0.tar.gz", hash = "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4"}, @@ -1700,8 +1714,8 @@ jsonschema = [ {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, ] keyring = [ - {file = "keyring-22.2.0-py3-none-any.whl", hash = "sha256:8ae8e53d744e3e395e7402fd04c7474d46c8ad2d65e095bcde8a622dc643f7cd"}, - {file = "keyring-22.2.0.tar.gz", hash = "sha256:c73c66c4ca89bee6a233b1638e1d2f5bcba4da35f8713ad4f98decc46e64cccd"}, + {file = "keyring-22.3.0-py3-none-any.whl", hash = "sha256:2bc8363ebdd63886126a012057a85c8cb6e143877afa02619ac7dbc9f38a207b"}, + {file = "keyring-22.3.0.tar.gz", hash = "sha256:16927a444b2c73f983520a48dec79ddab49fe76429ea05b8d528d778c8339522"}, ] lazy-object-proxy = [ {file = "lazy-object-proxy-1.5.2.tar.gz", hash = "sha256:5944a9b95e97de1980c65f03b79b356f30a43de48682b8bdd90aa5089f0ec1f4"}, @@ -1790,10 +1804,6 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] -more-itertools = [ - {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"}, - {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"}, -] multidict = [ {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, @@ -1847,64 +1857,65 @@ pathlib2 = [ {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, ] pillow = [ - {file = "Pillow-8.1.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a"}, - {file = "Pillow-8.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2"}, - {file = "Pillow-8.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174"}, - {file = "Pillow-8.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded"}, - {file = "Pillow-8.1.0-cp36-cp36m-win32.whl", hash = "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d"}, - {file = "Pillow-8.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d"}, - {file = "Pillow-8.1.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234"}, - {file = "Pillow-8.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8"}, - {file = "Pillow-8.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17"}, - {file = "Pillow-8.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7"}, - {file = "Pillow-8.1.0-cp37-cp37m-win32.whl", hash = "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e"}, - {file = "Pillow-8.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b"}, - {file = "Pillow-8.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0"}, - {file = "Pillow-8.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a"}, - {file = "Pillow-8.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d"}, - {file = "Pillow-8.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae"}, - {file = "Pillow-8.1.0-cp38-cp38-win32.whl", hash = "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59"}, - {file = "Pillow-8.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c"}, - {file = "Pillow-8.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6"}, - {file = "Pillow-8.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378"}, - {file = "Pillow-8.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7"}, - {file = "Pillow-8.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0"}, - {file = "Pillow-8.1.0-cp39-cp39-win32.whl", hash = "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b"}, - {file = "Pillow-8.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865"}, - {file = "Pillow-8.1.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9"}, - {file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913"}, - {file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206"}, - {file = "Pillow-8.1.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9"}, - {file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032"}, - {file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820"}, - {file = "Pillow-8.1.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5"}, - {file = "Pillow-8.1.0.tar.gz", hash = "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba"}, + {file = "Pillow-8.1.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:14415e9e28410232370615dbde0cf0a00e526f522f665460344a5b96973a3086"}, + {file = "Pillow-8.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:924fc33cb4acaf6267b8ca3b8f1922620d57a28470d5e4f49672cea9a841eb08"}, + {file = "Pillow-8.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:df534e64d4f3e84e8f1e1a37da3f541555d947c1c1c09b32178537f0f243f69d"}, + {file = "Pillow-8.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4fe74636ee71c57a7f65d7b21a9f127d842b4fb75511e5d256ace258826eb352"}, + {file = "Pillow-8.1.1-cp36-cp36m-win32.whl", hash = "sha256:3e759bcc03d6f39bc751e56d86bc87252b9a21c689a27c5ed753717a87d53a5b"}, + {file = "Pillow-8.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:292f2aa1ae5c5c1451cb4b558addb88c257411d3fd71c6cf45562911baffc979"}, + {file = "Pillow-8.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8211cac9bf10461f9e33fe9a3af6c5131f3fdd0d10672afc2abb2c70cf95c5ca"}, + {file = "Pillow-8.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:d30f30c044bdc0ab8f3924e1eeaac87e0ff8a27e87369c5cac4064b6ec78fd83"}, + {file = "Pillow-8.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7094bbdecb95ebe53166e4c12cf5e28310c2b550b08c07c5dc15433898e2238e"}, + {file = "Pillow-8.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:1022f8f6dc3c5b0dcf928f1c49ba2ac73051f576af100d57776e2b65c1f76a8d"}, + {file = "Pillow-8.1.1-cp37-cp37m-win32.whl", hash = "sha256:a7d690b2c5f7e4a932374615fedceb1e305d2dd5363c1de15961725fe10e7d16"}, + {file = "Pillow-8.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:436b0a2dd9fe3f7aa6a444af6bdf53c1eb8f5ced9ea3ef104daa83f0ea18e7bc"}, + {file = "Pillow-8.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:c448d2b335e21951416a30cd48d35588d122a912d5fe9e41900afacecc7d21a1"}, + {file = "Pillow-8.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:bb18422ad00c1fecc731d06592e99c3be2c634da19e26942ba2f13d805005cf2"}, + {file = "Pillow-8.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3ec87bd1248b23a2e4e19e774367fbe30fddc73913edc5f9b37470624f55dc1f"}, + {file = "Pillow-8.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99ce3333b40b7a4435e0a18baad468d44ab118a4b1da0af0a888893d03253f1d"}, + {file = "Pillow-8.1.1-cp38-cp38-win32.whl", hash = "sha256:2f0d7034d5faae9a8d1019d152ede924f653df2ce77d3bba4ce62cd21b5f94ae"}, + {file = "Pillow-8.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:07872f1d8421db5a3fe770f7480835e5e90fddb58f36c216d4a2ac0d594de474"}, + {file = "Pillow-8.1.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:69da5b1d7102a61ce9b45deb2920a2012d52fd8f4201495ea9411d0071b0ec22"}, + {file = "Pillow-8.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2a40d7d4b17db87f5b9a1efc0aff56000e1d0d5ece415090c102aafa0ccbe858"}, + {file = "Pillow-8.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:01bb0a34f1a6689b138c0089d670ae2e8f886d2666a9b2f2019031abdea673c4"}, + {file = "Pillow-8.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:43b3c859912e8bf754b3c5142df624794b18eb7ae07cfeddc917e1a9406a3ef2"}, + {file = "Pillow-8.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3b13d89d97b551e02549d1f0edf22bed6acfd6fd2e888cd1e9a953bf215f0e81"}, + {file = "Pillow-8.1.1-cp39-cp39-win32.whl", hash = "sha256:c143c409e7bc1db784471fe9d0bf95f37c4458e879ad84cfae640cb74ee11a26"}, + {file = "Pillow-8.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c5e3c36f02c815766ae9dd91899b1c5b4652f2a37b7a51609f3bd467c0f11fb"}, + {file = "Pillow-8.1.1-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:8cf77e458bd996dc85455f10fe443c0c946f5b13253773439bcbec08aa1aebc2"}, + {file = "Pillow-8.1.1-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:c10af40ee2f1a99e1ae755ab1f773916e8bca3364029a042cd9161c400416bd8"}, + {file = "Pillow-8.1.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:ff83dfeb04c98bb3e7948f876c17513a34e9a19fd92e292288649164924c1b39"}, + {file = "Pillow-8.1.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9af590adc1e46898a1276527f3cfe2da8048ae43fbbf9b1bf9395f6c99d9b47"}, + {file = "Pillow-8.1.1-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:172acfaf00434a28dddfe592d83f2980e22e63c769ff4a448ddf7b7a38ffd165"}, + {file = "Pillow-8.1.1-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:33fdbd4f5608c852d97264f9d2e3b54e9e9959083d008145175b86100b275e5b"}, + {file = "Pillow-8.1.1-pp37-pypy37_pp73-win32.whl", hash = "sha256:59445af66b59cc39530b4f810776928d75e95f41e945f0c32a3de4aceb93c15d"}, + {file = "Pillow-8.1.1.tar.gz", hash = "sha256:f6fc18f9c9c7959bf58e6faf801d14fafb6d4717faaf6f79a68c8bb2a13dcf20"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] protobuf = [ - {file = "protobuf-3.15.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:822341974a28d895f0b39df13b3e2f27577498c1d85b5e876ff1d53fbdf2ef97"}, - {file = "protobuf-3.15.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:31460706eb7c3bcc3c153877e580b78efa624b9626bd084fb882f20681ffa81a"}, - {file = "protobuf-3.15.3-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:405fbe27eeccd90b07e7cc20f2bcce477a86027435016aef71f15473dede92b5"}, - {file = "protobuf-3.15.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a98195be29b2622961896893a6e92a4e0812e4e367078ac20f0c7982e51ff7ea"}, - {file = "protobuf-3.15.3-cp35-cp35m-win32.whl", hash = "sha256:7141c37a5af565908c3da10575c517c59a8e67591c507cf36f2655590200ddfc"}, - {file = "protobuf-3.15.3-cp35-cp35m-win_amd64.whl", hash = "sha256:ffc556af23c7e1278b43719999dd215619f73f8d42f40275c55a1de09938214f"}, - {file = "protobuf-3.15.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9aa4c216e48236c6af4e71f64afc0c13c12401d3067a323b9fe543bb676bac3"}, - {file = "protobuf-3.15.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0c678e11b7b6d6e5baa9710f44de01d48bc81b7db617ad5283a76f1f4c73df99"}, - {file = "protobuf-3.15.3-cp36-cp36m-win32.whl", hash = "sha256:1fe832e1a5c51c71c2d6e949e597f3c47ef39c817264086293e4037941ab9bd7"}, - {file = "protobuf-3.15.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8a8157ff82760105cf435dbb8f4e7042a39c6d92f673fba8c2c815432b3f1063"}, - {file = "protobuf-3.15.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:970b9f648fc12d28ce6f1f10575bbf063e828e1fd8d95339602cad2312a4fefa"}, - {file = "protobuf-3.15.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:492beef9386706f84f683489fe18d1a7d8be5f4ab050782b3a484de1d1b01a69"}, - {file = "protobuf-3.15.3-cp37-cp37m-win32.whl", hash = "sha256:af760e4fe6f30e1af3d5dac6767444ff61ef621ac857b3405b8f3cd29f16ac55"}, - {file = "protobuf-3.15.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21911af1fc692e7ca6a73c0fab3912a5d792ed7603350dbabd34a9722cbfe4d5"}, - {file = "protobuf-3.15.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a3e18453d91040dad1985d1ea8a237fb7522a84fcefc17b452f756833b066d71"}, - {file = "protobuf-3.15.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:d7761bd18fc3d197e50459c37abb95b64cd614e7b9014239a1e7c952433e380b"}, - {file = "protobuf-3.15.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f52c66f361de32096ba88c73ad0ff53585dafc569d8bf11968412175ddf297c"}, - {file = "protobuf-3.15.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3bbaed8e63cd62533a80adfa51f91b34bf7da29ac0335412dbebd21dac2d68b9"}, - {file = "protobuf-3.15.3-py2.py3-none-any.whl", hash = "sha256:ad8e808b572e6ee38131e7b58d94aa5c438e3a3469d055e8989ea73a8e2308c0"}, - {file = "protobuf-3.15.3.tar.gz", hash = "sha256:f3348af83391cdb842030e774d9bb01565ed4c62c93554cd1c69723411ec5e9d"}, + {file = "protobuf-3.15.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:d26ed8dbdbe6b62cd24173c9ceb7588ae7831eec172ac002b095af091db01196"}, + {file = "protobuf-3.15.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9133b39924485ae43c02fc8274e57e5aa1706ad0970de49c72cfb8c0854d5f89"}, + {file = "protobuf-3.15.5-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:6bb44c15c98091e926a98362bff7fb24338bdf4001a6614834b8414c3b8593ee"}, + {file = "protobuf-3.15.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2d4cede5f2f2514df4a1eda1424a14d46daa5ea57963a1ea0fdab8d74ca2f9cd"}, + {file = "protobuf-3.15.5-cp35-cp35m-win32.whl", hash = "sha256:ab735b3a4342004afa60ff580ce2be0f2aa784f1f69ee7f08a23ef26d22d811d"}, + {file = "protobuf-3.15.5-cp35-cp35m-win_amd64.whl", hash = "sha256:a390e4bbb8232945fc8e4493c8b70949423a6dacee6f0353021b59c40b039e25"}, + {file = "protobuf-3.15.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dc7191b2e3361fdf2979e78a120a3a40e9d811318f6b2629036f53d9cb041c09"}, + {file = "protobuf-3.15.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:762f6b9fb8025db34f762a860fd2b1473dfc84bcd0c3e4f396a695c83d733729"}, + {file = "protobuf-3.15.5-cp36-cp36m-win32.whl", hash = "sha256:d1aab4d0aed36f7873734a243b46786d407cfa1010fae886249db56a1493a057"}, + {file = "protobuf-3.15.5-cp36-cp36m-win_amd64.whl", hash = "sha256:119b4d308c87e833b6265b3922d5f5927e9d804605fcb1c1f771aa4d17e03591"}, + {file = "protobuf-3.15.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c5b37b117ef89431149883d9b867c341a01f835142864722534885dcc1db6b1b"}, + {file = "protobuf-3.15.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f75aa0483fec2e4208bd4be18da0e3d7161dc74c65b6d6108f5968a8fe53a8ce"}, + {file = "protobuf-3.15.5-cp37-cp37m-win32.whl", hash = "sha256:5d52d89e26adf0ba65193b6be39025c7766740ccc57fe9d10ddb709220b360d9"}, + {file = "protobuf-3.15.5-cp37-cp37m-win_amd64.whl", hash = "sha256:87b5bc2ff944810a918628fc1f45f766acab23e1fecb0634fcf86cda554b30c4"}, + {file = "protobuf-3.15.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:282385b8dd168b0f71f2ffca74c1fb39377f42217830ab492a0b64cbe14f86c1"}, + {file = "protobuf-3.15.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f4445f197f779cd5b37c9d5d4aeb0d1999c1df7d143a9bce21d03dac8dba205"}, + {file = "protobuf-3.15.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ac7c7a2b271307787ccdc0a45278827f36f72aba5040eadefff129b869068797"}, + {file = "protobuf-3.15.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8090b77f0791560b3c01263f6222006fe4c1d1d526539344afc4ecd9bd3e56f2"}, + {file = "protobuf-3.15.5-py2.py3-none-any.whl", hash = "sha256:dbb98adb4281684eb54ce1f003b574bbc5768b9f614d7faa2c56f30e18519ec7"}, + {file = "protobuf-3.15.5.tar.gz", hash = "sha256:be8a929c6178bb6cbe9e2c858be62fa08966a39ae758a8493a88f0ed1efb6097"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, @@ -1969,8 +1980,8 @@ pygments = [ {file = "Pygments-2.8.0.tar.gz", hash = "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0"}, ] pylint = [ - {file = "pylint-2.7.1-py3-none-any.whl", hash = "sha256:a251b238db462b71d25948f940568bb5b3ae0e37dbaa05e10523f54f83e6cc7e"}, - {file = "pylint-2.7.1.tar.gz", hash = "sha256:81ce108f6342421169ea039ff1f528208c99d2e5a9c4ca95cfc5291be6dfd982"}, + {file = "pylint-2.7.2-py3-none-any.whl", hash = "sha256:d09b0b07ba06bcdff463958f53f23df25e740ecd81895f7d2699ec04bbd8dc3b"}, + {file = "pylint-2.7.2.tar.gz", hash = "sha256:0e21d3b80b96740909d77206d741aa3ce0b06b41be375d92e1f3244a274c1f8a"}, ] pymongo = [ {file = "pymongo-3.11.3-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:4d959e929cec805c2bf391418b1121590b4e7d5cb00af7b1ba521443d45a0918"}, @@ -2286,8 +2297,8 @@ wcwidth = [ {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] websocket-client = [ - {file = "websocket_client-0.57.0-py2.py3-none-any.whl", hash = "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549"}, - {file = "websocket_client-0.57.0.tar.gz", hash = "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"}, + {file = "websocket_client-0.58.0-py2.py3-none-any.whl", hash = "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663"}, + {file = "websocket_client-0.58.0.tar.gz", hash = "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"}, ] wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, @@ -2336,6 +2347,6 @@ yarl = [ {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, ] zipp = [ - {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, - {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, + {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, + {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, ] diff --git a/pyproject.toml b/pyproject.toml index 3a7ce4fc49..38b8239aa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ appdirs = "^1.4.3" blessed = "^1.17" # pype terminal formatting clique = "1.5.*" Click = "^7" +dnspython = "^2.1.0" ftrack-python-api = "2.0.*" google-api-python-client = "^1.12.8" # sync server google support (should be separate?) jsonschema = "^3.2.0" From 3d1ec0278a06d9b4bd565da1384da0f2232724cc Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 5 Mar 2021 20:41:35 +0100 Subject: [PATCH 40/68] add option to run w/o installation --- igniter/__init__.py | 10 +++++--- igniter/__main__.py | 5 ++-- igniter/bootstrap_repos.py | 4 +++ igniter/install_dialog.py | 29 ++++++++++++++++----- igniter/tools.py | 4 +-- start.py | 51 ++++++++++++++++++++++++------------- tools/build_dependencies.py | 7 ++++- 7 files changed, 76 insertions(+), 34 deletions(-) diff --git a/igniter/__init__.py b/igniter/__init__.py index 12f3b49457..9b2816a767 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -7,6 +7,7 @@ from Qt.QtCore import Signal # noqa from .install_dialog import InstallDialog from .bootstrap_repos import BootstrapRepos +from .version import __version__ as version RESULT = 0 @@ -18,18 +19,19 @@ def get_result(res: int): RESULT = res -def run(): +def open_dialog(): """Show Igniter dialog.""" app = QtWidgets.QApplication(sys.argv) d = InstallDialog() d.finished.connect(get_result) - d.show() - app.exec_() + d.open() + app.exec() return RESULT __all__ = [ "InstallDialog", "BootstrapRepos", - "run" + "open_dialog", + "version" ] diff --git a/igniter/__main__.py b/igniter/__main__.py index d56cc893a0..b453d29d5f 100644 --- a/igniter/__main__.py +++ b/igniter/__main__.py @@ -21,7 +21,6 @@ app = QtWidgets.QApplication(sys.argv) d = InstallDialog() d.finished.connect(get_result) -d.show() -app.exec_() -print(RESULT) +d.open() +app.exec() sys.exit(RESULT) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index ccf40bfafe..58d59afe88 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -337,6 +337,10 @@ class BootstrapRepos: else: version = self.get_version(repo_dir) + if not version: + self._print("Pype not found.", LOG_ERROR) + return + # create destination directory if not self.data_dir.exists(): self.data_dir.mkdir(parents=True) diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index f7bbc90fa3..9f9f921ac2 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -7,7 +7,7 @@ from Qt import QtCore, QtGui, QtWidgets # noqa from Qt.QtGui import QValidator # noqa from Qt.QtCore import QTimer # noqa -from .install_thread import InstallThread +from .install_thread import InstallThread, InstallResult from .tools import ( validate_path_string, validate_mongo_connection, @@ -107,11 +107,15 @@ class InstallDialog(QtWidgets.QDialog): self.pype_path_label = QtWidgets.QLabel( """This is Path to studio location where Pype versions - are stored. It will be pre-filled if your mongoDB connection is + are stored. It will be pre-filled if your MongoDB connection is already set and your studio defined this location.

- Leave it empty if you want to use Pype version that come with this - installation. + Leave it empty if you want to install Pype version that comes with + this installation. +

+

+ If you want to just try Pype without installing, hit the middle + button that states "run without installation".

""" ) @@ -465,6 +469,17 @@ class InstallDialog(QtWidgets.QDialog): else: self._mongo.set_valid() + if self._pype_run_ready: + self.done(3) + return + + if self.path != "": + valid, reason = validate_path_string(self.path) + + if not valid: + self.update_console(f"!!! {reason}", True) + return + self._disable_buttons() self._install_thread = InstallThread( self.install_result_callback_handler, self) @@ -475,9 +490,9 @@ class InstallDialog(QtWidgets.QDialog): self._install_thread.set_mongo(self._mongo.get_mongo_url()) self._install_thread.start() - def install_result_callback_handler(self, status): + def install_result_callback_handler(self, result: InstallResult): """Change button behaviour based on installation outcome.""" - self.update_console(f"--- {status}") + status = result.status if status >= 0: self.install_button.setText("Run installed Pype") self._pype_run_ready = True @@ -607,7 +622,7 @@ class MongoValidator(QValidator): QValidator.State.Invalid, "need mongodb schema", mongo) return self._return_state( - QValidator.State.Intermediate, "", mongo) + QValidator.State.Intermediate, "", mongo) class PathValidator(MongoValidator): diff --git a/igniter/tools.py b/igniter/tools.py index bd9b4577a0..5e071bbc18 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -176,7 +176,7 @@ def validate_path_string(path: str) -> (bool, str): if not path: return False, "empty string" - if Path(path).exists(): + if not Path(path).exists(): return False, "path doesn't exists" if not Path(path).is_dir(): @@ -251,6 +251,6 @@ def get_pype_path_from_db(url: str) -> Union[str, None]: col = db.settings global_settings = col.find_one( - {"type": "global_settings"}, {"data": 1}).get("data") + {"type": "global_settings"}, {"data": 1}).get("data", {}) return global_settings.get("pype_path", {}).get(platform.system().lower()) diff --git a/start.py b/start.py index 0bcbaff9e1..72c9fe969c 100644 --- a/start.py +++ b/start.py @@ -176,6 +176,7 @@ def run(arguments: list, env: dict = None) -> int: interpreter.extend(arguments) + print("|".join(interpreter)) p = subprocess.Popen(interpreter, env=env) p.wait() print(f">>> done [{p.returncode}]") @@ -276,8 +277,12 @@ def _process_arguments() -> tuple: # this is helper to run igniter before anything else if "igniter" in sys.argv: import igniter - igniter.run() + return_code = igniter.open_dialog() + # this is when we want to run Pype without installing anything. + # or we are ready to run. + if return_code not in [2, 3]: + sys.exit(return_code) return use_version, use_staging @@ -372,29 +377,41 @@ def _find_frozen_pype(use_version: str = None, pype_version = None pype_versions = bootstrap.find_pype(include_zips=True, staging=use_staging) - try: - # use latest one found (last in the list is latest) - pype_version = pype_versions[-1] - except IndexError: - # no pype version found, run Igniter and ask for them. - print('*** No Pype versions found.') - print("--- launching setup UI ...") - return_code = run(["igniter"]) - if return_code != 0: - raise RuntimeError("igniter crashed.") - print('>>> Finding Pype again ...') - pype_versions = bootstrap.find_pype(staging=use_staging) + if not os.getenv("PYPE_TRYOUT"): try: + # use latest one found (last in the list is latest) pype_version = pype_versions[-1] except IndexError: - print("!!! Something is wrong and we didn't found it again.") - pype_versions = None + # no pype version found, run Igniter and ask for them. + print('*** No Pype versions found.') + print("--- launching setup UI ...") + import igniter + return_code = igniter.open_dialog() + if return_code == 2: + os.environ["PYPE_TRYOUT"] = "1" + if return_code == 3: + # run Pype after installation + + print('>>> Finding Pype again ...') + pype_versions = bootstrap.find_pype(staging=use_staging) + try: + pype_version = pype_versions[-1] + except IndexError: + print(("!!! Something is wrong and we didn't " + "found it again.")) + pype_versions = None + sys.exit(1) + elif return_code != 2: + print(f" . finished ({return_code})") + sys.exit(return_code) if not pype_versions: # no Pype versions found anyway, lets use then the one # shipped with frozen Pype - print("*** Still no luck finding Pype.") - print("*** We'll try to use the one coming with Pype installation.") + if not os.getenv("PYPE_TRYOUT"): + print("*** Still no luck finding Pype.") + print(("*** We'll try to use the one coming " + "with Pype installation.")) version_path = _bootstrap_from_code(use_version) pype_version = PypeVersion( version=BootstrapRepos.get_version(version_path), diff --git a/tools/build_dependencies.py b/tools/build_dependencies.py index 0125de5211..ada786e96f 100644 --- a/tools/build_dependencies.py +++ b/tools/build_dependencies.py @@ -95,11 +95,16 @@ libs_dir = build_dir / "lib" to_delete = [] _print("Finding duplicates ...") +deps_items = list(deps_dir.iterdir()) for d in libs_dir.iterdir(): - if (deps_dir / d.name) in deps_dir.iterdir(): + if (deps_dir / d.name) in deps_items: to_delete.append(d) _print(f"found {d}", 3) +# add pype and igniter in libs too +to_delete.append(libs_dir / "pype") +to_delete.append(libs_dir / "igniter") + # delete duplicates _print(f"Deleting {len(to_delete)} duplicates ...") for d in to_delete: From 7c1118c31af4c33bc31fb76af809675139b64b21 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 10:40:46 +0100 Subject: [PATCH 41/68] added local settings view --- pype/tools/tray/pype_info_widget.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index d48dcb0f9c..0a105ede9f 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -7,6 +7,7 @@ from avalon import style from Qt import QtCore, QtGui, QtWidgets from pype.api import resources import pype.version +from pype.settings.lib import get_local_settings class EnvironmentsView(QtWidgets.QTreeView): @@ -69,8 +70,29 @@ class PypeInfoWidget(QtWidgets.QWidget): self.setWindowTitle("Pype info") main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(self._create_pype_info_widget()) - main_layout.addWidget(self._create_environ_widget()) + main_layout.addWidget(self._create_pype_info_widget(), 0) + main_layout.addWidget(self._create_local_settings_widget(), 0) + main_layout.addWidget(self._create_environ_widget(), 1) + + def _create_local_settings_widget(self): + local_settings = get_local_settings() + + local_settings_widget = QtWidgets.QWidget(self) + + label_widget = QtWidgets.QLabel( + "Local settings", local_settings_widget + ) + label_widget.setStyleSheet("font-weight: bold;") + settings_input = QtWidgets.QPlainTextEdit(local_settings_widget) + settings_input.setReadOnly(True) + settings_input.setMinimumHeight(20) + settings_input.setPlainText(json.dumps(local_settings, indent=4)) + + local_settings_layout = QtWidgets.QVBoxLayout(local_settings_widget) + local_settings_layout.addWidget(label_widget) + local_settings_layout.addWidget(settings_input) + + return local_settings_widget def _create_environ_widget(self): env_widget = QtWidgets.QWidget(self) From 0f02db0997acef1935c070bc517f8728f59982e0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 11:42:45 +0100 Subject: [PATCH 42/68] removed minimum height --- pype/tools/tray/pype_info_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 0a105ede9f..bd9bc8d3ee 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -85,7 +85,6 @@ class PypeInfoWidget(QtWidgets.QWidget): label_widget.setStyleSheet("font-weight: bold;") settings_input = QtWidgets.QPlainTextEdit(local_settings_widget) settings_input.setReadOnly(True) - settings_input.setMinimumHeight(20) settings_input.setPlainText(json.dumps(local_settings, indent=4)) local_settings_layout = QtWidgets.QVBoxLayout(local_settings_widget) From 4ed74637b2568519dde90e423a395789695804f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 11:55:40 +0100 Subject: [PATCH 43/68] envs and local settings are collapsible --- pype/tools/tray/pype_info_widget.py | 102 ++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index bd9bc8d3ee..b958aa637b 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -57,6 +57,88 @@ class EnvironmentsView(QtWidgets.QTreeView): return super(EnvironmentsView, self).keyPressEvent(event) +class ClickableWidget(QtWidgets.QWidget): + clicked = QtCore.Signal() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit() + super(ClickableWidget, self).mouseReleaseEvent(event) + + +class CollapsibleWidget(QtWidgets.QWidget): + def __init__(self, label, parent): + super(CollapsibleWidget, self).__init__(parent) + + self.content_widget = None + + top_part = ClickableWidget(parent=self) + + button_size = QtCore.QSize(5, 5) + button_toggle = QtWidgets.QToolButton(parent=top_part) + button_toggle.setIconSize(button_size) + button_toggle.setArrowType(QtCore.Qt.RightArrow) + button_toggle.setCheckable(True) + button_toggle.setChecked(False) + + label_widget = QtWidgets.QLabel(label, parent=top_part) + spacer_widget = QtWidgets.QWidget(top_part) + + top_part_layout = QtWidgets.QHBoxLayout(top_part) + top_part_layout.setContentsMargins(0, 0, 0, 0) + top_part_layout.addWidget(button_toggle) + top_part_layout.addWidget(label_widget) + top_part_layout.addWidget(spacer_widget, 1) + + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self.button_toggle = button_toggle + self.label_widget = label_widget + + top_part.clicked.connect(self._top_part_clicked) + self.button_toggle.clicked.connect(self._btn_clicked) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.setAlignment(QtCore.Qt.AlignTop) + main_layout.addWidget(top_part) + + self.main_layout = main_layout + + def set_content_widget(self, content_widget): + content_widget.setVisible(self.button_toggle.isChecked()) + self.main_layout.addWidget(content_widget) + self.content_widget = content_widget + + def _btn_clicked(self): + self.toggle_content(self.button_toggle.isChecked()) + + def _top_part_clicked(self): + self.toggle_content() + + def toggle_content(self, *args): + if len(args) > 0: + checked = args[0] + else: + checked = not self.button_toggle.isChecked() + arrow_type = QtCore.Qt.RightArrow + if checked: + arrow_type = QtCore.Qt.DownArrow + self.button_toggle.setChecked(checked) + self.button_toggle.setArrowType(arrow_type) + if self.content_widget: + self.content_widget.setVisible(checked) + self.parent().updateGeometry() + + def resizeEvent(self, event): + super(CollapsibleWidget, self).resizeEvent(event) + if self.content_widget: + self.content_widget.updateGeometry() + + class PypeInfoWidget(QtWidgets.QWidget): not_allowed = "N/A" @@ -70,6 +152,7 @@ class PypeInfoWidget(QtWidgets.QWidget): self.setWindowTitle("Pype info") main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setAlignment(QtCore.Qt.AlignTop) main_layout.addWidget(self._create_pype_info_widget(), 0) main_layout.addWidget(self._create_local_settings_widget(), 0) main_layout.addWidget(self._create_environ_widget(), 1) @@ -77,27 +160,18 @@ class PypeInfoWidget(QtWidgets.QWidget): def _create_local_settings_widget(self): local_settings = get_local_settings() - local_settings_widget = QtWidgets.QWidget(self) + local_settings_widget = CollapsibleWidget("Local settings", self) - label_widget = QtWidgets.QLabel( - "Local settings", local_settings_widget - ) - label_widget.setStyleSheet("font-weight: bold;") settings_input = QtWidgets.QPlainTextEdit(local_settings_widget) settings_input.setReadOnly(True) settings_input.setPlainText(json.dumps(local_settings, indent=4)) - local_settings_layout = QtWidgets.QVBoxLayout(local_settings_widget) - local_settings_layout.addWidget(label_widget) - local_settings_layout.addWidget(settings_input) + local_settings_widget.set_content_widget(settings_input) return local_settings_widget def _create_environ_widget(self): - env_widget = QtWidgets.QWidget(self) - - env_label_widget = QtWidgets.QLabel("Environments", env_widget) - env_label_widget.setStyleSheet("font-weight: bold;") + env_widget = CollapsibleWidget("Environments", self) env_model = QtGui.QStandardItemModel() @@ -114,9 +188,7 @@ class PypeInfoWidget(QtWidgets.QWidget): env_view = EnvironmentsView(env_model, env_widget) - env_layout = QtWidgets.QVBoxLayout(env_widget) - env_layout.addWidget(env_label_widget) - env_layout.addWidget(env_view) + env_widget.set_content_widget(env_view) return env_widget From b9e84d078f0dfc807fd929cb94fa14b496b7ded4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 11:56:45 +0100 Subject: [PATCH 44/68] don't center widget --- pype/tools/tray/pype_info_widget.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index b958aa637b..7bce740305 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -239,15 +239,3 @@ class PypeInfoWidget(QtWidgets.QWidget): value_label, row, 1, 1, 1 ) return info_widget - - def showEvent(self, event): - """Center widget to center of desktop on show.""" - result = super(PypeInfoWidget, self).showEvent(event) - screen_center = ( - QtWidgets.QApplication.desktop().availableGeometry(self).center() - ) - self.move( - screen_center.x() - (self.width() / 2), - screen_center.y() - (self.height() / 2) - ) - return result From 17a499225fc11f07b80c23d24c684cb8e0a6d81a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 12:00:29 +0100 Subject: [PATCH 45/68] minor modifications --- pype/tools/tray/pype_info_widget.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 7bce740305..7915e20be1 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -85,7 +85,7 @@ class CollapsibleWidget(QtWidgets.QWidget): spacer_widget = QtWidgets.QWidget(top_part) top_part_layout = QtWidgets.QHBoxLayout(top_part) - top_part_layout.setContentsMargins(0, 0, 0, 0) + top_part_layout.setContentsMargins(0, 0, 0, 5) top_part_layout.addWidget(button_toggle) top_part_layout.addWidget(label_widget) top_part_layout.addWidget(spacer_widget, 1) @@ -154,9 +154,18 @@ class PypeInfoWidget(QtWidgets.QWidget): main_layout = QtWidgets.QVBoxLayout(self) main_layout.setAlignment(QtCore.Qt.AlignTop) main_layout.addWidget(self._create_pype_info_widget(), 0) + main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_local_settings_widget(), 0) + main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_environ_widget(), 1) + def _create_separator(self): + separator_widget = QtWidgets.QWidget(self) + separator_widget.setStyleSheet("background: #222222;") + separator_widget.setMinimumHeight(2) + separator_widget.setMaximumHeight(2) + return separator_widget + def _create_local_settings_widget(self): local_settings = get_local_settings() From f181f0ba845486f46e6c38e7132d04a9069436f9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 8 Mar 2021 12:32:35 +0100 Subject: [PATCH 46/68] fix case when mongo url is not apriori set --- igniter/install_dialog.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index 9f9f921ac2..ca973d792c 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -44,7 +44,11 @@ class InstallDialog(QtWidgets.QDialog): super(InstallDialog, self).__init__(parent) self.registry = PypeSettingsRegistry() - self.mongo_url = os.getenv("PYPE_MONGO", "") or self.registry.get_secure_item("pypeMongo") or "" # noqa: E501 + self.mongo_url = "" + try: + self.mongo_url = os.getenv("PYPE_MONGO", "") or self.registry.get_secure_item("pypeMongo") # noqa: E501 + except ValueError: + pass self.setWindowTitle(f"Pype Igniter {__version__} - Pype installation") self._icon_path = os.path.join( From 02327ac39e719daa142cfc615c5d97c560844129 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 8 Mar 2021 12:54:11 +0100 Subject: [PATCH 47/68] fix error handlig --- igniter/install_thread.py | 2 +- start.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/igniter/install_thread.py b/igniter/install_thread.py index e3c26459a0..0e7cd13154 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -144,7 +144,7 @@ class InstallThread(QThread): PypeVersionInvalid, PypeVersionIOError) as e: self.message.emit(f"Installed failed: ", True) - self.message.emit(e, True) + self.message.emit(str(e), True) self.finished.emit(InstallResult(-1)) self.message.emit(f"Installed as {local_pype}", False) diff --git a/start.py b/start.py index 72c9fe969c..ede3d5eb45 100644 --- a/start.py +++ b/start.py @@ -176,7 +176,6 @@ def run(arguments: list, env: dict = None) -> int: interpreter.extend(arguments) - print("|".join(interpreter)) p = subprocess.Popen(interpreter, env=env) p.wait() print(f">>> done [{p.returncode}]") From 7ad4567534b815cd6a1d06a8dbc3ff864926d5d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 13:20:45 +0100 Subject: [PATCH 48/68] changed selection mode --- pype/tools/tray/pype_info_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 7915e20be1..0aa618c42b 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -18,7 +18,7 @@ class EnvironmentsView(QtWidgets.QTreeView): self.header().setSectionResizeMode( 0, QtWidgets.QHeaderView.ResizeToContents ) - self.setSelectionMode(QtWidgets.QTreeView.MultiSelection) + self.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection) def get_all_as_dict(self): pass From 44e03989d452d333e164229dbd133f9e81d547bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 20:01:07 +0100 Subject: [PATCH 49/68] added pype info getters to pype.lib --- pype/lib/pype_info.py | 73 +++++++++++++++++++++++++++++ pype/tools/tray/pype_info_widget.py | 29 +++++------- 2 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 pype/lib/pype_info.py diff --git a/pype/lib/pype_info.py b/pype/lib/pype_info.py new file mode 100644 index 0000000000..8db43a654f --- /dev/null +++ b/pype/lib/pype_info.py @@ -0,0 +1,73 @@ +import os +import sys +import json +import datetime +import platform +import getpass +import socket + +import pype.version +from pype.settings.lib import get_local_settings +from .execute import get_pype_execute_args +from .local_settings import get_local_site_id + + +def get_pype_version(): + return pype.version.__version__ + + +def get_pype_info(): + executable_args = get_pype_execute_args() + if len(executable_args) == 1: + version_type = "build" + else: + version_type = "code" + + return { + "version": get_pype_version(), + "version_type": version_type, + "executable": executable_args[-1], + "pype_root": os.environ["PYPE_ROOT"], + "mongo_url": os.environ["PYPE_MONGO"] + } + + +def get_workstation_info(): + host_name = socket.gethostname() + try: + host_ip = socket.gethostbyname(host_name) + except socket.gaierror: + host_ip = "127.0.0.1" + + return { + "hostname": host_name, + "hostip": host_ip, + "username": getpass.getuser(), + "system_name": platform.system(), + "local_id": get_local_site_id() + } + + +def get_all_data(): + return { + "pype": get_pype_info(), + "workstation": get_workstation_info(), + "env": os.environ.copy(), + "local_settings": get_local_settings() + } + + +def extract_pype_info_to_file(dirpath): + filename = "{}_{}_{}.json".format( + get_pype_version(), + get_local_site_id(), + datetime.datetime.now().strftime("%y%m%d%H%M%S") + ) + filepath = os.path.join(dirpath, filename) + data = get_all_data() + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + with open(filepath, "w") as file_stream: + json.dump(data, file_stream) + return filepath diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 0aa618c42b..d08cd404cd 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -8,6 +8,11 @@ from Qt import QtCore, QtGui, QtWidgets from pype.api import resources import pype.version from pype.settings.lib import get_local_settings +from pype.lib.pype_info import ( + get_pype_info, + get_workstation_info, + extract_pype_info_to_file +) class EnvironmentsView(QtWidgets.QTreeView): @@ -20,9 +25,6 @@ class EnvironmentsView(QtWidgets.QTreeView): ) self.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection) - def get_all_as_dict(self): - pass - def get_selection_as_dict(self): indexes = self.selectionModel().selectedIndexes() mapping = collections.defaultdict(dict) @@ -203,25 +205,20 @@ class PypeInfoWidget(QtWidgets.QWidget): def _create_pype_info_widget(self): """Create widget with information about pype application.""" - pype_root = os.environ["PYPE_ROOT"] - if getattr(sys, "frozen", False): - version_end = "build" - executable_path = sys.executable - else: - version_end = "code" - executable_path = os.path.join(pype_root, "start.py") - version_value = "{} ({})".format( - pype.version.__version__, version_end - ) + pype_info = get_pype_info() + version_value = "{} ({})".format( + pype_info["version"], + pype_info["version_type"] + ) lable_value = [ # Pype version ("Pype version:", version_value), - ("Pype executable:", executable_path), - ("Pype location:", pype_root), + ("Pype executable:", pype_info["executable"]), + ("Pype location:", pype_info["pype_root"]), # Mongo URL - ("Pype Mongo URL:", os.environ.get("PYPE_MONGO")) + ("Pype Mongo URL:", pype_info["mongo_url"]) ] info_widget = QtWidgets.QWidget(self) From 9cc95a9ac36ddf31c1e6a36534f3f38a1f05aefe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 20:18:47 +0100 Subject: [PATCH 50/68] fixed environments editability --- pype/tools/tray/pype_info_widget.py | 45 +++++++++++++++++++---------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index d08cd404cd..6e638616e5 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -15,11 +15,39 @@ from pype.lib.pype_info import ( ) +class EnvironmentValueDelegate(QtWidgets.QStyledItemDelegate): + def createEditor(self, parent, option, index): + edit_widget = QtWidgets.QLineEdit(parent) + edit_widget.setReadOnly(True) + return edit_widget + + class EnvironmentsView(QtWidgets.QTreeView): - def __init__(self, model, parent=None): + def __init__(self, parent=None): super(EnvironmentsView, self).__init__(parent) + + model = QtGui.QStandardItemModel() + + env = os.environ.copy() + keys = [] + values = [] + for key in sorted(env.keys()): + key_item = QtGui.QStandardItem(key) + key_item.setFlags( + QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsEnabled + ) + keys.append(key_item) + values.append(QtGui.QStandardItem(env[key])) + + model.appendColumn(keys) + model.appendColumn(values) + model.setHorizontalHeaderLabels(["Key", "Value"]) + self.setModel(model) self.setIndentation(0) + delegate = EnvironmentValueDelegate(self) + self.setItemDelegateForColumn(1, delegate) self.header().setSectionResizeMode( 0, QtWidgets.QHeaderView.ResizeToContents ) @@ -184,20 +212,7 @@ class PypeInfoWidget(QtWidgets.QWidget): def _create_environ_widget(self): env_widget = CollapsibleWidget("Environments", self) - env_model = QtGui.QStandardItemModel() - - env = os.environ.copy() - keys = [] - values = [] - for key in sorted(env.keys()): - keys.append(QtGui.QStandardItem(key)) - values.append(QtGui.QStandardItem(env[key])) - - env_model.appendColumn(keys) - env_model.appendColumn(values) - env_model.setHorizontalHeaderLabels(["Key", "Value"]) - - env_view = EnvironmentsView(env_model, env_widget) + env_view = EnvironmentsView(env_widget) env_widget.set_content_widget(env_view) From c7e8ab4d87fd10a4ac71dc69bdb83abf64b68cc9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 8 Mar 2021 23:00:57 +0100 Subject: [PATCH 51/68] fix return states --- igniter/install_thread.py | 8 ++++++++ start.py | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/igniter/install_thread.py b/igniter/install_thread.py index 0e7cd13154..bfed0aa336 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -80,6 +80,7 @@ class InstallThread(QThread): else: self._mongo = os.getenv("PYPE_MONGO") else: + self.message.emit("Saving mongo connection string ...", False) bs.registry.set_secure_item("pypeMongo", self._mongo) os.environ["PYPE_MONGO"] = self._mongo @@ -99,6 +100,7 @@ class InstallThread(QThread): if detected[-1].path.suffix.lower() == ".zip": bs.extract_pype(detected[-1]) self.finished.emit(InstallResult(0)) + return if PypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa self.message.emit(( @@ -107,6 +109,7 @@ class InstallThread(QThread): ), False) self.message.emit("Skipping Pype install ...", False) self.finished.emit(InstallResult(0)) + return self.message.emit(( "All installed versions are older then " @@ -122,10 +125,12 @@ class InstallThread(QThread): self.message.emit( f"!!! Install failed - {pype_version}", True) self.finished.emit(InstallResult(-1)) + return self.message.emit(f"Using: {pype_version}", False) bs.install_version(pype_version) self.message.emit(f"Installed as {pype_version}", False) self.finished.emit(InstallResult(1)) + return else: self.message.emit("None detected.", False) @@ -137,6 +142,7 @@ class InstallThread(QThread): self.message.emit( f"!!! Install failed - {local_pype}", True) self.finished.emit(InstallResult(-1)) + return try: bs.install_version(local_pype) @@ -146,6 +152,7 @@ class InstallThread(QThread): self.message.emit(f"Installed failed: ", True) self.message.emit(str(e), True) self.finished.emit(InstallResult(-1)) + return self.message.emit(f"Installed as {local_pype}", False) else: @@ -156,6 +163,7 @@ class InstallThread(QThread): self.message.emit( f"!!! invalid mongo url {self._mongo}", True) self.finished.emit(InstallResult(-1)) + return bs.registry.set_secure_item("pypeMongo", self._mongo) os.environ["PYPE_MONGO"] = self._mongo diff --git a/start.py b/start.py index ede3d5eb45..bb6e7a90be 100644 --- a/start.py +++ b/start.py @@ -308,9 +308,9 @@ def _determine_mongodb() -> str: except ValueError: print("*** No DB connection string specified.") print("--- launching setup UI ...") - return_code = run(["igniter"]) - if return_code != 0: - raise RuntimeError("mongodb is not set") + import igniter + igniter.open_dialog() + try: pype_mongo = bootstrap.registry.get_secure_item("pypeMongo") except ValueError: From 8defec44de21ae07d452f8fc08ffc3266b9be757 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 8 Mar 2021 23:31:49 +0100 Subject: [PATCH 52/68] ensure 100% progress bar when finished --- igniter/install_thread.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/igniter/install_thread.py b/igniter/install_thread.py index bfed0aa336..a184a19d36 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -129,6 +129,7 @@ class InstallThread(QThread): self.message.emit(f"Using: {pype_version}", False) bs.install_version(pype_version) self.message.emit(f"Installed as {pype_version}", False) + self.progress.emit(100) self.finished.emit(InstallResult(1)) return else: @@ -155,6 +156,8 @@ class InstallThread(QThread): return self.message.emit(f"Installed as {local_pype}", False) + self.progress.emit(100) + return else: # if we have mongo connection string, validate it, set it to # user settings and get PYPE_PATH from there. @@ -175,6 +178,7 @@ class InstallThread(QThread): self.finished.emit(InstallResult(-1)) return + self.progress.emit(100) self.finished.emit(InstallResult(1)) return From 276a7df3157c172176f6938d84e5e6177502813d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 09:45:33 +0100 Subject: [PATCH 53/68] added workstation info --- pype/tools/tray/pype_info_widget.py | 91 ++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 6e638616e5..f3ad668429 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -170,7 +170,7 @@ class CollapsibleWidget(QtWidgets.QWidget): class PypeInfoWidget(QtWidgets.QWidget): - not_allowed = "N/A" + not_applicable = "N/A" def __init__(self, parent=None): super(PypeInfoWidget, self).__init__(parent) @@ -185,6 +185,8 @@ class PypeInfoWidget(QtWidgets.QWidget): main_layout.setAlignment(QtCore.Qt.AlignTop) main_layout.addWidget(self._create_pype_info_widget(), 0) main_layout.addWidget(self._create_separator(), 0) + main_layout.addWidget(self._create_workstation_widget(), 0) + main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_local_settings_widget(), 0) main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_environ_widget(), 1) @@ -196,6 +198,56 @@ class PypeInfoWidget(QtWidgets.QWidget): separator_widget.setMaximumHeight(2) return separator_widget + def _create_workstation_widget(self): + key_label_mapping = { + "system_name": "System:", + "local_id": "Local ID:", + "username": "Username:", + "hostname": "Hostname:", + "hostip": "Host IP:" + } + keys_order = [ + "system_name", + "local_id", + "username", + "hostname", + "hostip" + ] + workstation_info = get_workstation_info() + for key in workstation_info.keys(): + if key not in keys_order: + keys_order.append(key) + + wokstation_info_widget = CollapsibleWidget("Workstation info", self) + + info_widget = QtWidgets.QWidget(self) + info_layout = QtWidgets.QGridLayout(info_widget) + # Add spacer to 3rd column + info_layout.addWidget(QtWidgets.QWidget(info_widget), 0, 2) + info_layout.setColumnStretch(2, 1) + + for key in keys_order: + if key not in workstation_info: + continue + + label = key_label_mapping.get(key, key) + value = workstation_info[key] + row = info_layout.rowCount() + info_layout.addWidget( + QtWidgets.QLabel(label), row, 0, 1, 1 + ) + value_label = QtWidgets.QLabel(value) + value_label.setTextInteractionFlags( + QtCore.Qt.TextSelectableByMouse + ) + info_layout.addWidget( + value_label, row, 1, 1, 1 + ) + + wokstation_info_widget.set_content_widget(info_widget) + + return wokstation_info_widget + def _create_local_settings_widget(self): local_settings = get_local_settings() @@ -221,21 +273,28 @@ class PypeInfoWidget(QtWidgets.QWidget): def _create_pype_info_widget(self): """Create widget with information about pype application.""" + # Get pype info data pype_info = get_pype_info() + # Modify version key/values version_value = "{} ({})".format( - pype_info["version"], - pype_info["version_type"] + pype_info.pop("version", self.not_applicable), + pype_info.pop("version_type", self.not_applicable) ) - lable_value = [ - # Pype version - ("Pype version:", version_value), - ("Pype executable:", pype_info["executable"]), - ("Pype location:", pype_info["pype_root"]), - - # Mongo URL - ("Pype Mongo URL:", pype_info["mongo_url"]) - ] + pype_info["version_value"] = version_value + # Prepare lable mapping + key_label_mapping = { + "version_value": "Pype version:", + "executable": "Pype executable:", + "pype_root": "Pype location:", + "mongo_url": "Pype Mongo URL:" + } + # Prepare keys order + keys_order = ["version_value", "executable", "pype_root", "mongo_url"] + for key in pype_info.keys(): + if key not in keys_order: + keys_order.append(key) + # Create widgets info_widget = QtWidgets.QWidget(self) info_layout = QtWidgets.QGridLayout(info_widget) # Add spacer to 3rd column @@ -247,12 +306,16 @@ class PypeInfoWidget(QtWidgets.QWidget): title_label.setStyleSheet("font-weight: bold;") info_layout.addWidget(title_label, 0, 0, 1, 2) - for label, value in lable_value: + for key in keys_order: + if key not in pype_info: + continue + value = pype_info[key] + label = key_label_mapping.get(key, key) row = info_layout.rowCount() info_layout.addWidget( QtWidgets.QLabel(label), row, 0, 1, 1 ) - value_label = QtWidgets.QLabel(value or self.not_allowed) + value_label = QtWidgets.QLabel(value) value_label.setTextInteractionFlags( QtCore.Qt.TextSelectableByMouse ) From 841c1b3918c9a7cbef8145eddb2ee1171cb26abf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 10:14:54 +0100 Subject: [PATCH 54/68] added buttons to copy or extract information --- pype/lib/pype_info.py | 2 +- pype/tools/tray/pype_info_widget.py | 53 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/pype/lib/pype_info.py b/pype/lib/pype_info.py index 8db43a654f..b52fc71635 100644 --- a/pype/lib/pype_info.py +++ b/pype/lib/pype_info.py @@ -69,5 +69,5 @@ def extract_pype_info_to_file(dirpath): os.makedirs(dirpath) with open(filepath, "w") as file_stream: - json.dump(data, file_stream) + json.dump(data, file_stream, indent=4) return filepath diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index f3ad668429..691bc12db5 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -9,6 +9,7 @@ from pype.api import resources import pype.version from pype.settings.lib import get_local_settings from pype.lib.pype_info import ( + get_all_data, get_pype_info, get_workstation_info, extract_pype_info_to_file @@ -190,6 +191,58 @@ class PypeInfoWidget(QtWidgets.QWidget): main_layout.addWidget(self._create_local_settings_widget(), 0) main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_environ_widget(), 1) + main_layout.addWidget(self._create_btns_section(), 0) + + def _create_btns_section(self): + btns_widget = QtWidgets.QWidget(self) + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + + copy_to_clipboard_btn = QtWidgets.QPushButton( + "Copy to clipboard", btns_widget + ) + export_to_file_btn = QtWidgets.QPushButton( + "Export", btns_widget + ) + btns_layout.addWidget(QtWidgets.QWidget(btns_widget)) + btns_layout.addWidget(copy_to_clipboard_btn) + btns_layout.addWidget(export_to_file_btn) + + copy_to_clipboard_btn.clicked.connect(self._on_copy_to_clipboard) + export_to_file_btn.clicked.connect(self._on_export_to_file) + + return btns_widget + + def _on_export_to_file(self): + dst_dir_path = QtWidgets.QFileDialog.getExistingDirectory( + self, + "Choose directory", + os.path.expanduser("~"), + QtWidgets.QFileDialog.ShowDirsOnly + ) + if not dst_dir_path or not os.path.exists(dst_dir_path): + return + + filepath = extract_pype_info_to_file(dst_dir_path) + title = "Extraction done" + message = "Extraction is done. Destination filepath is \"{}\"".format( + filepath.replace("\\", "/") + ) + dialog = QtWidgets.QMessageBox(self) + dialog.setIcon(QtWidgets.QMessageBox.NoIcon) + dialog.setWindowTitle(title) + dialog.setText(message) + dialog.exec_() + + def _on_copy_to_clipboard(self): + all_data = get_all_data() + all_data_str = json.dumps(all_data, indent=4) + + mime_data = QtCore.QMimeData() + mime_data.setText(all_data_str) + QtWidgets.QApplication.instance().clipboard().setMimeData( + mime_data + ) def _create_separator(self): separator_widget = QtWidgets.QWidget(self) From 24c49a7cc3e8e4101c84e64d05cf0918a6318899 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 10:21:44 +0100 Subject: [PATCH 55/68] added some docstrings --- pype/lib/pype_info.py | 19 +++++++++++++++++-- pype/tools/tray/pype_info_widget.py | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pype/lib/pype_info.py b/pype/lib/pype_info.py index b52fc71635..4c1bbccba0 100644 --- a/pype/lib/pype_info.py +++ b/pype/lib/pype_info.py @@ -13,10 +13,12 @@ from .local_settings import get_local_site_id def get_pype_version(): + """Version of pype that is currently used.""" return pype.version.__version__ def get_pype_info(): + """Information about currently used Pype process.""" executable_args = get_pype_execute_args() if len(executable_args) == 1: version_type = "build" @@ -33,6 +35,7 @@ def get_pype_info(): def get_workstation_info(): + """Basic information about workstation.""" host_name = socket.gethostname() try: host_ip = socket.gethostbyname(host_name) @@ -48,7 +51,8 @@ def get_workstation_info(): } -def get_all_data(): +def get_all_current_info(): + """All information about current process in one dictionary.""" return { "pype": get_pype_info(), "workstation": get_workstation_info(), @@ -58,13 +62,24 @@ def get_all_data(): def extract_pype_info_to_file(dirpath): + """Extract all current info to a file. + + It is possible to define onpy directory path. Filename is concatenated with + pype version, workstation site id and timestamp. + + Args: + dirpath (str): Path to directory where file will be stored. + + Returns: + filepath (str): Full path to file where data were extracted. + """ filename = "{}_{}_{}.json".format( get_pype_version(), get_local_site_id(), datetime.datetime.now().strftime("%y%m%d%H%M%S") ) filepath = os.path.join(dirpath, filename) - data = get_all_data() + data = get_all_current_info() if not os.path.exists(dirpath): os.makedirs(dirpath) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 691bc12db5..fcd820b0b2 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -9,7 +9,7 @@ from pype.api import resources import pype.version from pype.settings.lib import get_local_settings from pype.lib.pype_info import ( - get_all_data, + get_all_current_info, get_pype_info, get_workstation_info, extract_pype_info_to_file @@ -235,7 +235,7 @@ class PypeInfoWidget(QtWidgets.QWidget): dialog.exec_() def _on_copy_to_clipboard(self): - all_data = get_all_data() + all_data = get_all_current_info() all_data_str = json.dumps(all_data, indent=4) mime_data = QtCore.QMimeData() From 75fd04ca2b36cab4f183c80d6f39b1c0d3c09469 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 10:30:05 +0100 Subject: [PATCH 56/68] fixed buttons expandings --- pype/tools/tray/pype_info_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index fcd820b0b2..1a3fba0277 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -204,7 +204,7 @@ class PypeInfoWidget(QtWidgets.QWidget): export_to_file_btn = QtWidgets.QPushButton( "Export", btns_widget ) - btns_layout.addWidget(QtWidgets.QWidget(btns_widget)) + btns_layout.addWidget(QtWidgets.QWidget(btns_widget), 1) btns_layout.addWidget(copy_to_clipboard_btn) btns_layout.addWidget(export_to_file_btn) From aa62be3662f2c0bcec8346b839dfa6aaeaa279aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 10:54:50 +0100 Subject: [PATCH 57/68] added ability to show separated paths --- pype/tools/tray/pype_info_widget.py | 43 +++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index 1a3fba0277..dd59bc03f2 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -1,5 +1,4 @@ import os -import sys import json import collections @@ -15,6 +14,8 @@ from pype.lib.pype_info import ( extract_pype_info_to_file ) +IS_MAIN_ROLE = QtCore.Qt.UserRole + class EnvironmentValueDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): @@ -38,17 +39,36 @@ class EnvironmentsView(QtWidgets.QTreeView): QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled ) + key_item.setData(True, IS_MAIN_ROLE) keys.append(key_item) - values.append(QtGui.QStandardItem(env[key])) + + value = env[key] + value_item = QtGui.QStandardItem(value) + value_item.setData(True, IS_MAIN_ROLE) + values.append(value_item) + + value_parts = [ + part + for part in value.split(os.pathsep) if part + ] + if len(value_parts) < 2: + continue + + sub_parts = [] + for part_value in value_parts: + part_item = QtGui.QStandardItem(part_value) + part_item.setData(False, IS_MAIN_ROLE) + sub_parts.append(part_item) + key_item.appendRows(sub_parts) model.appendColumn(keys) model.appendColumn(values) model.setHorizontalHeaderLabels(["Key", "Value"]) self.setModel(model) - self.setIndentation(0) + # self.setIndentation(0) delegate = EnvironmentValueDelegate(self) - self.setItemDelegateForColumn(1, delegate) + self.setItemDelegate(delegate) self.header().setSectionResizeMode( 0, QtWidgets.QHeaderView.ResizeToContents ) @@ -56,17 +76,22 @@ class EnvironmentsView(QtWidgets.QTreeView): def get_selection_as_dict(self): indexes = self.selectionModel().selectedIndexes() - mapping = collections.defaultdict(dict) + + main_mapping = collections.defaultdict(dict) for index in indexes: + is_main = index.data(IS_MAIN_ROLE) + if not is_main: + continue row = index.row() value = index.data(QtCore.Qt.DisplayRole) if index.column() == 0: key = "key" else: key = "value" - mapping[row][key] = value + main_mapping[row][key] = value + result = {} - for item in mapping.values(): + for item in main_mapping.values(): result[item["key"]] = item["value"] return result @@ -75,8 +100,8 @@ class EnvironmentsView(QtWidgets.QTreeView): event.type() == QtGui.QKeyEvent.KeyPress and event.matches(QtGui.QKeySequence.Copy) ): - selected_dict = self.get_selection_as_dict() - selected_str = json.dumps(selected_dict, indent=4) + selected_data = self.get_selection_as_dict() + selected_str = json.dumps(selected_data, indent=4) mime_data = QtCore.QMimeData() mime_data.setText(selected_str) From 208d8301ed0df2a31848403e25731276baf0a449 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 9 Mar 2021 10:57:31 +0100 Subject: [PATCH 58/68] imports cleanup --- pype/lib/pype_info.py | 1 - pype/tools/tray/pype_info_widget.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pype/lib/pype_info.py b/pype/lib/pype_info.py index 4c1bbccba0..cbcc5811a0 100644 --- a/pype/lib/pype_info.py +++ b/pype/lib/pype_info.py @@ -1,5 +1,4 @@ import os -import sys import json import datetime import platform diff --git a/pype/tools/tray/pype_info_widget.py b/pype/tools/tray/pype_info_widget.py index dd59bc03f2..a4c52eb1d0 100644 --- a/pype/tools/tray/pype_info_widget.py +++ b/pype/tools/tray/pype_info_widget.py @@ -5,7 +5,6 @@ import collections from avalon import style from Qt import QtCore, QtGui, QtWidgets from pype.api import resources -import pype.version from pype.settings.lib import get_local_settings from pype.lib.pype_info import ( get_all_current_info, From acb18c45df1ec21348681267c6ca420fedceac3a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 Mar 2021 12:53:49 +0100 Subject: [PATCH 59/68] PS - fix create multiple groups instances --- .../websocket_server/stubs/photoshop_server_stub.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index 79a486a20c..5409120a65 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -224,11 +224,10 @@ class PhotoshopServerStub: layers: Returns: None """ - layer_ids = [layer.id for layer in layers] - + layers_id = [str(lay.id) for lay in layers] self.websocketserver.call(self.client.call - ('Photoshop.get_layers', - layers=layer_ids) + ('Photoshop.select_layers', + layers=json.dumps(layers_id)) ) def get_active_document_full_name(self): From c41a652e18d22ed97f03a2764a73016e7b7b8938 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 9 Mar 2021 22:41:32 +0100 Subject: [PATCH 60/68] updated avalon-core, few minor fixes --- igniter/install_dialog.py | 2 +- igniter/tools.py | 7 ++++--- repos/avalon-core | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index ca973d792c..0eb518c2e3 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -477,7 +477,7 @@ class InstallDialog(QtWidgets.QDialog): self.done(3) return - if self.path != "": + if self.path and len(self.path) > 0: valid, reason = validate_path_string(self.path) if not valid: diff --git a/igniter/tools.py b/igniter/tools.py index 5e071bbc18..4ed4ae67f4 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -250,7 +250,8 @@ def get_pype_path_from_db(url: str) -> Union[str, None]: db = client.pype col = db.settings - global_settings = col.find_one( - {"type": "global_settings"}, {"data": 1}).get("data", {}) - + global_settings = col.find_one({"type": "global_settings"}, {"data": 1}) + if not global_settings: + return None + global_settings.get("data", {}) return global_settings.get("pype_path", {}).get(platform.system().lower()) diff --git a/repos/avalon-core b/repos/avalon-core index eae14f2960..9e6b0d02e5 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit eae14f2960c4ccf2f0211e0726e88563129c0296 +Subproject commit 9e6b0d02e5a147cbafdcaeee7d786d4767e14c94 From 330f3cc4d3a98359f6b7a963ab5ff91afbe93194 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 12:55:29 +0100 Subject: [PATCH 61/68] there is no need of overriding taskChanged event only add new callback --- pype/hosts/maya/api/__init__.py | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/pype/hosts/maya/api/__init__.py b/pype/hosts/maya/api/__init__.py index 27e15e0b25..ea100dda0f 100644 --- a/pype/hosts/maya/api/__init__.py +++ b/pype/hosts/maya/api/__init__.py @@ -7,7 +7,7 @@ from maya import utils, cmds from avalon import api as avalon from avalon import pipeline from avalon.maya import suspended_refresh -from avalon.maya.pipeline import IS_HEADLESS, _on_task_changed +from avalon.maya.pipeline import IS_HEADLESS from avalon.tools import workfiles from pyblish import api as pyblish from pype.lib import any_outdated @@ -45,9 +45,7 @@ def install(): avalon.on("open", on_open) avalon.on("new", on_new) avalon.before("save", on_before_save) - - log.info("Overriding existing event 'taskChanged'") - override_event("taskChanged", on_task_changed) + avalon.on("taskChanged", on_task_changed) log.info("Setting default family states for loader..") avalon.data["familiesStateToggled"] = ["imagesequence"] @@ -61,24 +59,6 @@ def uninstall(): menu.uninstall() -def override_event(event, callback): - """ - Override existing event callback - Args: - event (str): name of the event - callback (function): callback to be triggered - - Returns: - None - - """ - - ref = weakref.WeakSet() - ref.add(callback) - - pipeline._registered_event_handlers[event] = ref - - def on_init(_): avalon.logger.info("Running callback on init..") @@ -215,7 +195,6 @@ def on_new(_): def on_task_changed(*args): """Wrapped function of app initialize and maya's on task changed""" # Run - _on_task_changed() with suspended_refresh(): lib.set_context_settings() lib.update_content_on_context_change() From 050429948a4c268a7598c9715acbe0d0cef001d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 10 Mar 2021 12:55:43 +0100 Subject: [PATCH 62/68] fixed widgets import in maya's implementation --- pype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/maya/api/lib.py b/pype/hosts/maya/api/lib.py index 56621949b3..44f1577f7f 100644 --- a/pype/hosts/maya/api/lib.py +++ b/pype/hosts/maya/api/lib.py @@ -2677,7 +2677,7 @@ def update_content_on_context_change(): def show_message(title, msg): from avalon.vendor.Qt import QtWidgets - from ...widgets import message_window + from pype.widgets import message_window # Find maya main window top_level_widgets = {w.objectName(): w for w in From ea678212f39631fd7d7eb498eb4f701be8a49bd2 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 10 Mar 2021 17:27:39 +0100 Subject: [PATCH 63/68] pull latest develop --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index eae14f2960..8d3364dc8a 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit eae14f2960c4ccf2f0211e0726e88563129c0296 +Subproject commit 8d3364dc8ae73a33726ba3279ff75adff73c6239 From 19c6181968d12dc01399e92a51c4cef7aeff4ab9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 11 Mar 2021 10:05:18 +0100 Subject: [PATCH 64/68] removed set_environments function as it is --- start.py | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/start.py b/start.py index bb6e7a90be..74cf4772d0 100644 --- a/start.py +++ b/start.py @@ -112,46 +112,13 @@ if getattr(sys, 'frozen', False): os.environ["PYTHONPATH"] = os.pathsep.join(paths) from igniter import BootstrapRepos # noqa: E402 -from igniter.tools import load_environments, get_pype_path_from_db # noqa +from igniter.tools import get_pype_path_from_db # noqa from igniter.bootstrap_repos import PypeVersion # noqa: E402 bootstrap = BootstrapRepos() silent_commands = ["run", "igniter", "standalonepublisher"] -def set_environments() -> None: - """Set loaded environments. - - .. todo: - better handling of environments - - """ - try: - import acre - except ImportError: - if getattr(sys, 'frozen', False): - sys.path.append(os.path.join( - os.path.dirname(sys.executable), - "dependencies" - )) - try: - import acre - except ImportError as e: - # giving up - print("!!! cannot import acre") - print(f"{e}") - sys.exit(1) - try: - env = load_environments(["global"]) - except OSError as e: - print(f"!!! {e}") - sys.exit(1) - - # acre must be available here - env = acre.merge(env, dict(os.environ)) # noqa - os.environ.clear() - os.environ.update(env) - def run(arguments: list, env: dict = None) -> int: """Use correct executable to run stuff. @@ -573,9 +540,6 @@ def boot(): else: os.environ["PYPE_ROOT"] = os.path.dirname(__file__) - # No environment loading from settings until Pype version is established. - # set_environments() - # Get Pype path from database and set it to environment so Pype can # find its versions there and bootstrap them. pype_path = get_pype_path_from_db(pype_mongo) From 11a0be709c251c7273d8a4d0ae40cbc306f4c5f7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 11 Mar 2021 10:09:59 +0100 Subject: [PATCH 65/68] added set_pype_global_environments which set environments from general and few hardcoded values --- start.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/start.py b/start.py index 74cf4772d0..b5e0fee847 100644 --- a/start.py +++ b/start.py @@ -119,6 +119,26 @@ bootstrap = BootstrapRepos() silent_commands = ["run", "igniter", "standalonepublisher"] +def set_pype_global_environments() -> None: + """Set global pype's environments.""" + import acre + + from pype.settings import get_environments + + all_env = get_environments() + + # TODO Global environments will be stored in "general" settings so loading + # will be modified and can be done in igniter. + env = acre.merge(all_env["global"], dict(os.environ)) + os.environ.clear() + os.environ.update(env) + + # Hardcoded default values + os.environ["PYBLISH_GUI"] = "pyblish_pype" + # Change scale factor only if is not set + if "QT_AUTO_SCREEN_SCALE_FACTOR" not in os.environ: + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" + def run(arguments: list, env: dict = None) -> int: """Use correct executable to run stuff. @@ -591,9 +611,11 @@ def boot(): from pype.lib import terminal as t from pype.version import __version__ print(">>> loading environments ...") - # Must happen before `set_modules_environments` + # Avalon environments must be set before avalon module is imported print(" - for Avalon ...") set_avalon_environments() + print(" - global Pype ...") + set_pype_global_environments() print(" - for modules ...") set_modules_environments() From 68540073fa93dfe5506b9d424c89b28e726c47b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 11 Mar 2021 10:16:24 +0100 Subject: [PATCH 66/68] removed al,ready hardcoded environments form defaults --- pype/settings/defaults/system_settings/general.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pype/settings/defaults/system_settings/general.json b/pype/settings/defaults/system_settings/general.json index db20be420f..35b450feee 100644 --- a/pype/settings/defaults/system_settings/general.json +++ b/pype/settings/defaults/system_settings/general.json @@ -23,15 +23,11 @@ "darwin": "{VIRTUAL_ENV}/bin/python" }, "PYPE_OCIO_CONFIG": "{STUDIO_SOFT}/OpenColorIO-Configs", - "PYBLISH_GUI": "pyblish_pype", - "QT_AUTO_SCREEN_SCALE_FACTOR": "1", "__environment_keys__": { "global": [ "FFMPEG_PATH", "PYPE_PYTHON_EXE", - "PYPE_OCIO_CONFIG", - "PYBLISH_GUI", - "QT_AUTO_SCREEN_SCALE_FACTOR" + "PYPE_OCIO_CONFIG" ] } } From fe09a88db36c64305a758a6e4a96b5dde7972f0e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 11 Mar 2021 10:17:48 +0100 Subject: [PATCH 67/68] removed PYPE_PYTHON_EXE from global environments --- pype/settings/defaults/system_settings/general.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pype/settings/defaults/system_settings/general.json b/pype/settings/defaults/system_settings/general.json index 35b450feee..bf2bb5def0 100644 --- a/pype/settings/defaults/system_settings/general.json +++ b/pype/settings/defaults/system_settings/general.json @@ -17,16 +17,10 @@ "darwin": "{PYPE_ROOT}/vendor/bin/ffmpeg_exec/darwin/bin", "linux": ":{PYPE_ROOT}/vendor/bin/ffmpeg_exec/linux" }, - "PYPE_PYTHON_EXE": { - "windows": "{VIRTUAL_ENV}/Scripts/python.exe", - "linux": "{VIRTUAL_ENV}/Scripts/python", - "darwin": "{VIRTUAL_ENV}/bin/python" - }, "PYPE_OCIO_CONFIG": "{STUDIO_SOFT}/OpenColorIO-Configs", "__environment_keys__": { "global": [ "FFMPEG_PATH", - "PYPE_PYTHON_EXE", "PYPE_OCIO_CONFIG" ] } From fe80ad7032ce06bee0cf509f22ee429d0f3aa4b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 11 Mar 2021 10:59:05 +0100 Subject: [PATCH 68/68] modified tvpaint's prelaunch hook to not use PYPE_PYTHON_EXE --- pype/hosts/tvpaint/hooks/pre_launch_args.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pype/hosts/tvpaint/hooks/pre_launch_args.py b/pype/hosts/tvpaint/hooks/pre_launch_args.py index ebb911220f..2bba9c950f 100644 --- a/pype/hosts/tvpaint/hooks/pre_launch_args.py +++ b/pype/hosts/tvpaint/hooks/pre_launch_args.py @@ -2,7 +2,10 @@ import os import shutil from pype.hosts import tvpaint -from pype.lib import PreLaunchHook +from pype.lib import ( + PreLaunchHook, + get_pype_execute_args +) import avalon @@ -20,18 +23,16 @@ class TvpaintPrelaunchHook(PreLaunchHook): def execute(self): # Pop tvpaint executable - tvpaint_executable = self.launch_context.launch_args.pop(0) + executable_path = self.launch_context.launch_args.pop(0) # Pop rest of launch arguments - There should not be other arguments! remainders = [] while self.launch_context.launch_args: remainders.append(self.launch_context.launch_args.pop(0)) - new_launch_args = [ - self.main_executable(), - self.launch_script_path(), - tvpaint_executable - ] + new_launch_args = get_pype_execute_args( + "run", self.launch_script_path(), executable_path + ) # Add workfile to launch arguments workfile_path = self.workfile_path() @@ -56,11 +57,6 @@ class TvpaintPrelaunchHook(PreLaunchHook): ).format(str(remainders))) self.launch_context.launch_args.extend(remainders) - def main_executable(self): - """Should lead to python executable.""" - # TODO change in Pype 3 - return os.path.normpath(os.environ["PYPE_PYTHON_EXE"]) - def launch_script_path(self): avalon_dir = os.path.dirname(os.path.abspath(avalon.__file__)) script_path = os.path.join(