From 946c93ad5d5bac0f80972c3b779a4ab5c20e0555 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Mar 2021 13:13:42 +0100 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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 4fa47cbe019e220a31076c258b8f6fb5f8218230 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 5 Mar 2021 17:06:39 +0100 Subject: [PATCH 07/15] 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 08/15] 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 c7ffc7ee4d8b3098cf6e7e16013aba3cb7f48679 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 5 Mar 2021 20:05:34 +0100 Subject: [PATCH 09/15] 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 10/15] 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 f181f0ba845486f46e6c38e7132d04a9069436f9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 8 Mar 2021 12:32:35 +0100 Subject: [PATCH 11/15] 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 12/15] 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 c7e8ab4d87fd10a4ac71dc69bdb83abf64b68cc9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 8 Mar 2021 23:00:57 +0100 Subject: [PATCH 13/15] 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 14/15] 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 c41a652e18d22ed97f03a2764a73016e7b7b8938 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 9 Mar 2021 22:41:32 +0100 Subject: [PATCH 15/15] 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