on demand mongo url input, use Path

This commit is contained in:
Ondrej Samohel 2020-10-12 13:23:17 +02:00
parent a5c8b24988
commit 7065953166
No known key found for this signature in database
GPG key ID: 8A29C663C672C2B7
6 changed files with 280 additions and 76 deletions

View file

@ -13,7 +13,7 @@ from pathlib import Path
from appdirs import user_data_dir
from pype.version import __version__
from pype.lib import PypeSettingsRegistry
from igniter.tools import load_environments
from .tools import load_environments
class BootstrapRepos:
@ -26,7 +26,14 @@ class BootstrapRepos:
"""
def __init__(self):
def __init__(self, progress_callback: Callable = None):
"""Constructor.
Args:
progress_callback (callable): Optional callback method to report
progress.
"""
# vendor and app used to construct user data dir
self._vendor = "pypeclub"
self._app = "pype"
@ -34,6 +41,14 @@ class BootstrapRepos:
self.data_dir = Path(user_data_dir(self._app, self._vendor))
self.registry = PypeSettingsRegistry()
# dummy progress reporter
def empty_progress(x: int):
return x
if not progress_callback:
progress_callback = empty_progress
self._progress_callback = progress_callback
if getattr(sys, "frozen", False):
self.live_repo_dir = Path(sys.executable).parent / "repos"
else:
@ -44,25 +59,44 @@ class BootstrapRepos:
"""Get version of local Pype."""
return __version__
def install_live_repos(self, progress_callback=None) -> Union[Path, None]:
"""Copy zip created from local repositories to user data dir.
@staticmethod
def get_version(repo_dir: Path) -> Union[str, None]:
"""Get version of Pype in given directory.
Args:
progress_callback (callable): Optional callback method to report
progress.
repo_dir (Path): Path to Pype repo.
Returns:
str: version string.
None: if Pype is not found.
"""
# try to find version
version_file = Path(repo_dir) / "pype" / "version.py"
if not version_file.exists():
return None
version = {}
with version_file.open("r") as fp:
exec(fp.read(), version)
return version['__version__']
def install_live_repos(self, repo_dir: Path = None) -> Union[Path, None]:
"""Copy zip created from Pype repositories to user data dir.
Args:
repo_dir (Path, optional): Path to Pype repository.
Returns:
Path: path of installed repository file.
"""
# dummy progress reporter
def empty_progress(x: int):
return x
if not progress_callback:
progress_callback = empty_progress
# create zip from repositories
local_version = self.get_local_version()
if not repo_dir:
version = self.get_local_version()
repo_dir = self.live_repo_dir
else:
version = self.get_version(repo_dir)
# create destination directory
try:
@ -71,12 +105,10 @@ class BootstrapRepos:
self._log.error("directory already exists")
with tempfile.TemporaryDirectory() as temp_dir:
temp_zip = \
Path(temp_dir) / f"pype-repositories-v{local_version}.zip"
Path(temp_dir) / f"pype-repositories-v{version}.zip"
self._log.info(f"creating zip: {temp_zip}")
BootstrapRepos._create_pype_zip(
temp_zip, self.live_repo_dir,
progress_callback=progress_callback)
self._create_pype_zip(temp_zip, repo_dir)
if not os.path.exists(temp_zip):
self._log.error("make archive failed.")
return None
@ -98,10 +130,10 @@ class BootstrapRepos:
return None
return self.data_dir / temp_zip.name
@staticmethod
def _create_pype_zip(
self,
zip_path: Path, include_dir: Path,
progress_callback: Callable, include_pype: bool = True) -> None:
include_pype: bool = True) -> None:
"""Pack repositories and Pype into zip.
We are using :mod:`zipfile` instead :meth:`shutil.make_archive`
@ -113,10 +145,7 @@ class BootstrapRepos:
Args:
zip_path (str): path to zip file.
include_dir: repo directories to include.
progress_callback (Callable(progress: int): callback to
report progress back to UI progress bar. It takes progress
percents as argument.
include_dir (Path): repo directories to include.
include_pype (bool): add Pype module itself.
"""
@ -130,22 +159,22 @@ class BootstrapRepos:
else:
repo_inc = 98.0 / float(repo_files)
progress = 0
with ZipFile(zip_path, "w") as zip:
for root, _, files in os.walk(include_dir):
with ZipFile(zip_path, "w") as zip_file:
for root, _, files in os.walk(include_dir.as_posix()):
for file in files:
zip.write(
zip_file.write(
os.path.relpath(os.path.join(root, file),
os.path.join(include_dir, '..')),
os.path.relpath(os.path.join(root, file),
os.path.join(include_dir))
)
progress += repo_inc
progress_callback(int(progress))
self._progress_callback(int(progress))
# add pype itself
if include_pype:
for root, _, files in os.walk("pype"):
for file in files:
zip.write(
zip_file.write(
os.path.relpath(os.path.join(root, file),
os.path.join('pype', '..')),
os.path.join(
@ -154,9 +183,9 @@ class BootstrapRepos:
os.path.join('pype', '..')))
)
progress += pype_inc
progress_callback(int(progress))
zip.testzip()
progress_callback(100)
self._progress_callback(int(progress))
zip_file.testzip()
self._progress_callback(100)
@staticmethod
def add_paths_from_archive(archive: Path) -> None:
@ -169,7 +198,6 @@ class BootstrapRepos:
archive (str): path to archive.
"""
name_list = []
with ZipFile(archive, "r") as zip_file:
name_list = zip_file.namelist()
@ -249,13 +277,43 @@ class BootstrapRepos:
"""
os.environ["AVALON_MONGO"] = mongo_url
env = load_environments()
if not env.get("PYPE_ROOT"):
if not env.get("PYPE_PATH"):
return None
return Path(env.get("PYPE_ROOT"))
return Path(env.get("PYPE_PATH"))
def process_entered_path(self, location: str) -> str:
def process_entered_location(self, location: str) -> Union[Path, None]:
"""Process user entered location string.
It decides if location string is mongodb url or path.
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`.
Args:
location (str): User entered location.
Returns:
Path: to Pype zip produced from this location.
None: Zipping failed.
"""
pype_path = None
if location.startswith("mongodb"):
pype_path = self._get_pype_from_mongo(location)
if not pype_path:
self._log.error("cannot find PYPE_PATH in settings.")
return None
return pype_path
if not pype_path:
pype_path = Path(location)
if not pype_path.exists():
self._log.error(f"{pype_path} doesn't exists.")
return None
repo_file = self.install_live_repos(pype_path)
if not repo_file.exists():
self._log.error(f"installing zip {repo_file} failed.")
return None
return repo_file

View file

@ -124,6 +124,37 @@ class InstallDialog(QtWidgets.QDialog):
input_layout.addWidget(self.user_input)
input_layout.addWidget(self._btn_select)
# Mongo box | OK button
# --------------------------------------------------------------------
class MongoWidget(QtWidgets.QWidget):
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.setPlaceholderText("Mongo URL")
self._mongo_input.textChanged.connect(self._mongo_changed)
self._mongo_input.setStyleSheet(
("color: rgb(233, 233, 233);"
"background-color: rgb(64, 64, 64);"
"padding: 0.5em;"
"border: 1px solid rgb(32, 32, 32);")
)
mongo_layout.addWidget(self._mongo_input)
self.setLayout(mongo_layout)
def _mongo_changed(self, mongo: str):
self._mongo_url = mongo
def get_mongo_url(self):
return self._mongo_url
self._mongo = MongoWidget(self)
self._mongo.hide()
# Bottom button bar
# --------------------------------------------------------------------
bottom_widget = QtWidgets.QWidget()
@ -237,6 +268,7 @@ class InstallDialog(QtWidgets.QDialog):
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)
@ -292,10 +324,12 @@ class InstallDialog(QtWidgets.QDialog):
self._disable_buttons()
self._install_thread = InstallThread(self)
self._install_thread.ask_for_mongo.connect(self._show_mongo)
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_mongo(self._mongo.get_mongo_url())
self._install_thread.start()
def _update_progress(self, progress: int):
@ -304,10 +338,19 @@ class InstallDialog(QtWidgets.QDialog):
def _on_exit_clicked(self):
self.close()
def _show_mongo(self):
self._update_console("mongo showed")
def _path_changed(self, path: str) -> str:
"""Set path."""
self._path = path
return path
if not self._path.startswith("mongodb"):
self._mongo.setVisible(True)
else:
self._mongo.setVisible(False)
if len(self._path) < 1:
self._mongo.setVisible(False)
def _update_console(self, msg: str, error: bool = False) -> None:
"""Display message in console.
@ -425,6 +468,98 @@ class PathValidator(QValidator):
QValidator.State.Acceptable, reason, path, pos)
class CollapsibleWidget(QtWidgets.QWidget):
def __init__(self, parent=None, title: str = "", animation: int = 300):
self._mainLayout = QtWidgets.QGridLayout(parent)
self._toggleButton = QtWidgets.QToolButton(parent)
self._headerLine = QtWidgets.QFrame(parent)
self._toggleAnimation = QtCore.QParallelAnimationGroup(parent)
self._contentArea = QtWidgets.QScrollArea(parent)
self._animation = animation
self._title = title
super(CollapsibleWidget, self).__init__(parent)
self._initUi()
def _initUi(self):
self._toggleButton.setStyleSheet(
"""QToolButton {
border: none;
}
""")
self._toggleButton.setToolButtonStyle(
QtCore.Qt.ToolButtonTextBesideIcon)
self._toggleButton.setArrowType(QtCore.Qt.ArrowType.RightArrow)
self._toggleButton.setText(self._title)
self._toggleButton.setCheckable(True)
self._toggleButton.setChecked(False)
self._headerLine.setFrameShape(QtWidgets.QFrame.HLine)
self._headerLine.setFrameShadow(QtWidgets.QFrame.Sunken)
self._headerLine.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Maximum)
self._contentArea.setStyleSheet(
"""QScrollArea {
background-color: rgb(32, 32, 32);
border: none;
}
""")
self._contentArea.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Fixed)
self._contentArea.setMaximumHeight(0)
self._contentArea.setMinimumHeight(0)
self._toggleAnimation.addAnimation(
QtCore.QPropertyAnimation(self, b"minimumHeight"))
self._toggleAnimation.addAnimation(
QtCore.QPropertyAnimation(self, b"maximumHeight"))
self._toggleAnimation.addAnimation(
QtCore.QPropertyAnimation(self._contentArea, b"maximumHeight"))
self._mainLayout.setVerticalSpacing(0)
self._mainLayout.setContentsMargins(0, 0, 0, 0)
row = 0
self._mainLayout.addWidget(
self._toggleButton, row, 0, 1, 1, QtCore.Qt.AlignCenter)
self._mainLayout.addWidget(
self._headerLine, row, 2, 1, 1)
row += row
self._mainLayout.addWidget(self._contentArea, row, 0, 1, 3)
self.setLayout(self._mainLayout)
self._toggleButton.toggled.connect(self._toggle_action)
def _toggle_action(self, collapsed:bool):
arrow = QtCore.Qt.ArrowType.DownArrow if collapsed else QtCore.Qt.ArrowType.RightArrow # noqa: E501
direction = QtCore.QAbstractAnimation.Forward if collapsed else QtCore.QAbstractAnimation.Backward # noqa: E501
self._toggleButton.setArrowType(arrow)
self._toggleAnimation.setDirection(direction)
self._toggleAnimation.start()
def setContentLayout(self, content_layout: QtWidgets.QLayout):
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):
sec_anim = self._toggleAnimation.animationAt(i)
sec_anim.setDuration(self._animation)
sec_anim.setStartValue(collapsed_height)
sec_anim.setEndValue(collapsed_height + content_height)
con_anim = self._toggleAnimation.animationAt(
self._toggleAnimation.animationCount() - 1)
con_anim.setDuration(self._animation)
con_anim.setStartValue(0)
con_anim.setEndValue(32)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
d = InstallDialog()

View file

@ -5,6 +5,7 @@ from Qt.QtCore import QThread, Signal
from igniter.tools import load_environments
from .bootstrap_repos import BootstrapRepos
from .tools import validate_mongo_connection
class InstallThread(QThread):
@ -23,66 +24,53 @@ class InstallThread(QThread):
"""
progress = Signal(int)
message = Signal((str, bool))
def __init__(self, parent=None):
self.progress = Signal(int)
self.message = Signal((str, bool))
self._mongo = None
self._path = None
QThread.__init__(self, parent)
def run(self):
self.message.emit("Installing Pype ...", False)
# find local version of Pype
bs = BootstrapRepos()
bs = BootstrapRepos(progress_callback=self.set_progress)
local_version = bs.get_local_version()
# if user did entered nothing, we install Pype from local version.
# zip content of `repos`, copy it to user data dir and append
# version to it.
if not self._path:
self.ask_for_mongo.emit(True)
self.message.emit(
f"We will use local Pype version {local_version}", False)
repo_file = bs.install_live_repos(
progress_callback=self.set_progress)
repo_file = bs.install_live_repos()
if not repo_file:
self.message.emit(
f"!!! install failed - {repo_file}", True)
return
self.message.emit(f"installed as {repo_file}", False)
else:
pype_path = None
# find central pype location from database
if self._path.startswith("mongodb"):
self.message.emit("determining Pype location from db...")
os.environ["AVALON_MONGO"] = self._path
env = load_environments()
if not env.get("PYPE_ROOT"):
if self._mongo:
if not validate_mongo_connection(self._mongo):
self.message.emit(
"!!! cannot load path to Pype from db", True)
f"!!! invalid mongo url {self._mongo}", True)
return
bs.registry.set_secure_item("avalonMongo", self._mongo)
os.environ["AVALON_MONGO"] = self._mongo
self.message.emit(f"path loaded from database ...", False)
self.message.emit(env.get("PYPE_ROOT"), False)
if not os.path.exists(env.get("PYPE_ROOT")):
self.message.emit(f"!!! path doesn't exist", True)
return
pype_path = env.get("PYPE_ROOT")
if not pype_path:
pype_path = self._path
repo_file = bs.process_entered_location(self._path)
if not os.path.exists(pype_path):
self.message.emit(f"!!! path doesn't exist", True)
if not repo_file:
self.message.emit(f"!!! Cannot install", True)
return
# detect Pype in path
def set_path(self, path: str) -> None:
self._path = path
def set_mongo(self, mongo: str) -> None:
self._mongo = mongo
def set_progress(self, progress: int):
self.progress.emit(progress)

View file

@ -11,6 +11,15 @@ from pymongo.errors import ServerSelectionTimeoutError, InvalidURI
def validate_mongo_connection(cnx: str) -> (bool, str):
"""Check if provided mongodb URL is valid.
Args:
cnx (str): URL to validate.
Returns:
(bool, str): True if ok, False if not and reason in str.
"""
parsed = urlparse(cnx)
if parsed.scheme in ["mongodb", "mongodb+srv"]:
# we have mongo connection string. Let's try if we can connect.
@ -25,7 +34,7 @@ def validate_mongo_connection(cnx: str) -> (bool, str):
try:
client = MongoClient(**mongo_args)
# client.server_info()
client.server_info()
except ServerSelectionTimeoutError as e:
return False, f"Cannot connect to server {cnx} - {e}"
except ValueError:
@ -70,6 +79,15 @@ def validate_path_string(path: str) -> (bool, str):
def load_environments() -> dict:
"""Load environments from Pype.
This will load environments from database, process them with
:mod:`acre` and return them as flattened dictionary.
Returns;
dict of str: loaded and processed environments.
"""
try:
import acre
except ImportError:

13
pype.py
View file

@ -87,10 +87,15 @@ def boot():
break
if not os.getenv("AVALON_MONGO"):
print("*** No DB connection string specified.")
import igniter
igniter.run()
set_environments()
try:
avalon_mongo = bootstrap.registry.get_secure_item("avalonMongo")
except ValueError:
print("*** No DB connection string specified.")
import igniter
igniter.run()
set_environments()
else:
os.environ["AVALON_MONGO"] = avalon_mongo
if getattr(sys, 'frozen', False):
if not pype_versions:

View file

@ -12,7 +12,7 @@ from pype.lib import PypeSettingsRegistry
@pytest.fixture
def fix_bootstrap(tmp_path):
bs = BootstrapRepos()
bs.live_repo_dir = os.path.abspath('repos')
bs.live_repo_dir = Path(os.path.abspath('repos'))
bs.data_dir = tmp_path
return bs