├── tests ├── __init__.py ├── test_launch.py ├── test_docks.py ├── test_profiles.py ├── util.py └── test_apps.py ├── src └── allzpark │ ├── __init__.py │ ├── gui │ ├── __init__.py │ ├── _vendor │ │ ├── __init__.py │ │ ├── Qt5.py │ │ ├── qoverview.py │ │ └── qjsonmodel.py │ ├── resources │ │ ├── icons │ │ │ ├── sequence.png │ │ │ ├── sg_logo.png │ │ │ ├── _.svg │ │ │ ├── dot-red.svg │ │ │ ├── dot-red-on.svg │ │ │ ├── dot.svg │ │ │ ├── dash.svg │ │ │ ├── slash-lg.svg │ │ │ ├── plus.svg │ │ │ ├── chevron_up.svg │ │ │ ├── caret-down-fill.svg │ │ │ ├── exclamation-warn.svg │ │ │ ├── caret-down-fill-dim.svg │ │ │ ├── caret-down-fill-on.svg │ │ │ ├── caret-right-fill.svg │ │ │ ├── caret-right-fill-dim.svg │ │ │ ├── caret-right-fill-on.svg │ │ │ ├── chevron_left.svg │ │ │ ├── square.svg │ │ │ ├── chevron_down.svg │ │ │ ├── chevron_right.svg │ │ │ ├── square-dim.svg │ │ │ ├── door-closed.svg │ │ │ ├── toggle-on.svg │ │ │ ├── toggle-on-bright.svg │ │ │ ├── toggle-on-dim.svg │ │ │ ├── check-ok.svg │ │ │ ├── log-undefined.svg │ │ │ ├── toggle-off.svg │ │ │ ├── filter-on.svg │ │ │ ├── toggle-off-dim.svg │ │ │ ├── toggle-off-bright.svg │ │ │ ├── x.svg │ │ │ ├── filter.svg │ │ │ ├── filter-dim.svg │ │ │ ├── person-circle.svg │ │ │ ├── arrow-clockwise.svg │ │ │ ├── activity.svg │ │ │ ├── arrow-clockwise-on.svg │ │ │ ├── log-warning.svg │ │ │ ├── square-slash.svg │ │ │ ├── square-slash-dim.svg │ │ │ ├── collection.svg │ │ │ ├── command.svg │ │ │ ├── people-fill.svg │ │ │ ├── door-open.svg │ │ │ ├── people-fill-ok.svg │ │ │ ├── exclamation-triangle-fill.svg │ │ │ ├── chevron-double-left.svg │ │ │ ├── code-slash.svg │ │ │ ├── square-check.svg │ │ │ ├── terminal.svg │ │ │ ├── square-check-dim.svg │ │ │ ├── terminal-on.svg │ │ │ ├── terminal-dim.svg │ │ │ ├── log-error.svg │ │ │ ├── log-info.svg │ │ │ ├── box-seam.svg │ │ │ ├── card-text.svg │ │ │ ├── folder.svg │ │ │ ├── brightness-low-fill.svg │ │ │ ├── box-arrow-down.svg │ │ │ ├── log-critical.svg │ │ │ ├── folder-minus.svg │ │ │ ├── joystick.svg │ │ │ ├── card-list.svg │ │ │ ├── journal-plus.svg │ │ │ ├── server.svg │ │ │ ├── folder-x.svg │ │ │ ├── magic.svg │ │ │ ├── brightness-high-fill.svg │ │ │ ├── boxes.svg │ │ │ ├── clock-history.svg │ │ │ ├── hypnotize.svg │ │ │ ├── hypnotize-on.svg │ │ │ ├── hypnotize-dim.svg │ │ │ ├── avalon-logomark.svg │ │ │ └── rez_logo.svg │ │ ├── fonts │ │ │ ├── opensans │ │ │ │ ├── OpenSans-Bold.ttf │ │ │ │ ├── OpenSans-Italic.ttf │ │ │ │ ├── OpenSans-Regular.ttf │ │ │ │ └── LICENSE.txt │ │ │ └── jetbrainsmono │ │ │ │ ├── JetBrainsMono-Regular.ttf │ │ │ │ └── LICENSE │ │ ├── compile_qrc.py │ │ └── allzpark-rc.qrc │ ├── delegates.py │ ├── pages.py │ ├── window.py │ ├── widgets_sg_sync.py │ └── app.py │ ├── rezplugins │ ├── __init__.py │ └── command │ │ ├── __init__.py │ │ ├── rezconfig.py │ │ └── park.py │ ├── cli │ └── __init__.py │ ├── exceptions.py │ ├── backends.md │ ├── _version.py │ ├── __main__.py │ ├── report.py │ ├── util.py │ ├── lib.py │ ├── backend_sg_sync.py │ └── core.py ├── setup.py ├── pyproject.toml ├── MANIFEST.in ├── README.md ├── setup.cfg ├── .gitignore ├── azure-pipelines.yml └── LICENCE.txt /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/allzpark/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/allzpark/gui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/allzpark/gui/_vendor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/allzpark/rezplugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/allzpark/rezplugins/command/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | from setuptools import setup 3 | setup() 4 | -------------------------------------------------------------------------------- /src/allzpark/cli/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def main(): 4 | # todo: 5 | # * .. 6 | raise NotImplementedError 7 | -------------------------------------------------------------------------------- /src/allzpark/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ParkError(Exception): 4 | """""" 5 | 6 | 7 | class BackendError(ParkError): 8 | """""" 9 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoonShineVFX/park/HEAD/src/allzpark/gui/resources/icons/sequence.png -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/sg_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoonShineVFX/park/HEAD/src/allzpark/gui/resources/icons/sg_logo.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | # Minimum requirements for the build system to execute. 3 | requires = ["setuptools==60.9.3", "wheel"] # PEP 508 specifications. 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/fonts/opensans/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoonShineVFX/park/HEAD/src/allzpark/gui/resources/fonts/opensans/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/allzpark/gui/resources/fonts/opensans/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoonShineVFX/park/HEAD/src/allzpark/gui/resources/fonts/opensans/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /src/allzpark/gui/resources/fonts/opensans/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoonShineVFX/park/HEAD/src/allzpark/gui/resources/fonts/opensans/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/allzpark/gui/resources/fonts/jetbrainsmono/JetBrainsMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoonShineVFX/park/HEAD/src/allzpark/gui/resources/fonts/jetbrainsmono/JetBrainsMono-Regular.ttf -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/_.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | global-include *LICENCE* *LICENSE* *.txt *.md *.rst 3 | recursive-include allzpark/resources *.svg *.png *.css 4 | recursive-include allzpark/resources/fonts *.ttf 5 | include pyproject.toml 6 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/dot-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/dot-red-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/dot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/dash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/backends.md: -------------------------------------------------------------------------------- 1 | 2 | * Scope 3 | * iter_children() -> Iterator[Scope] 4 | * list_tools(suite) -> List[SuiteTool] 5 | * obtain_workspace(tool) -> str or None 6 | * additional_env(tool) -> dict 7 | * current_user_roles() -> List[str] 8 | 9 | * Entrance 10 | * backend 11 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/slash-lg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/chevron_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/caret-down-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/exclamation-warn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/caret-down-fill-dim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/caret-down-fill-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/caret-right-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/caret-right-fill-dim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/caret-right-fill-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/chevron_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/chevron_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/chevron_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/square-dim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/door-closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/toggle-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/toggle-on-bright.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/toggle-on-dim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/check-ok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/log-undefined.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/toggle-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/filter-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/toggle-off-dim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/toggle-off-bright.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/filter-dim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/person-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/arrow-clockwise.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/activity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/arrow-clockwise-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/log-warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/square-slash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/square-slash-dim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/collection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/command.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/people-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/door-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/people-fill-ok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/exclamation-triangle-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/chevron-double-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/code-slash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/square-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/terminal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/square-check-dim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/terminal-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/_version.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = "2.0.0" 3 | 4 | 5 | def package_info(): 6 | import allzpark 7 | return dict( 8 | name=allzpark.__package__, 9 | version=__version__, 10 | path=allzpark.__path__[0], 11 | ) 12 | 13 | 14 | def print_info(): 15 | import sys 16 | info = package_info() 17 | py = sys.version_info 18 | print(info["name"], 19 | info["version"], 20 | "from", info["path"], 21 | "(python {x}.{y})".format(x=py.major, y=py.minor)) 22 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/terminal-dim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/log-error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/log-info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/rezplugins/command/rezconfig.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def suite_roots(): 4 | """Return a dict of suite saving root path 5 | """ 6 | from collections import OrderedDict as odict 7 | from allzpark import util 8 | return odict([ 9 | ("local", util.normpath("~/rez/sweet/local")), 10 | ("release", util.normpath("~/rez/sweet/release")), 11 | ]) 12 | 13 | 14 | park = { 15 | # saved suite root paths 16 | "suite_roots": suite_roots(), 17 | 18 | # ordering tools by name 19 | "tool_ordering": [], 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/box-seam.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/card-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/brightness-low-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/box-arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/log-critical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/folder-minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/joystick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | # Debugging, measure start-up time 5 | # NOTE: Handle this prior to importing anything 6 | if os.getenv("ALLZPARK_STARTTIME"): 7 | 8 | try: 9 | t0 = float(os.getenv("ALLZPARK_STARTTIME")) 10 | t1 = time.time() 11 | 12 | except ValueError: 13 | raise ValueError( 14 | "ALLZPARK_STARTTIME must be in format time.time()" 15 | ) 16 | 17 | duration = t1 - t0 18 | print("shell to python: %.2f s" % duration) 19 | 20 | 21 | import sys 22 | from .rezplugins.command.park import standalone_cli 23 | sys.exit(standalone_cli()) 24 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/card-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/journal-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/folder-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/magic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/brightness-high-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/boxes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

Application launcher and environment management
forked from mottosso/allzpark, for 21st century games and digital post-production,
built with rez, rez-sweet and Qt5.py

5 |
6 | 7 |
8 | 9 |
10 | 11 | ### What is it? 12 | 13 | It's an application launcher based on `rez` package manager with [Avalon](https://github.com/getavalon/core) workspace integrated. 14 | 15 |
16 | -------------------------------------------------------------------------------- /src/allzpark/report.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | from colorama import init, Fore 4 | 5 | 6 | class ColorFormatter(logging.Formatter): 7 | Colors = { 8 | "DEBUG": Fore.BLUE, 9 | "INFO": Fore.GREEN, 10 | "WARNING": Fore.YELLOW, 11 | "ERROR": Fore.RED, 12 | "CRITICAL": Fore.MAGENTA, 13 | } 14 | 15 | def format(self, record): 16 | color = self.Colors.get(record.levelname, "") 17 | return color + logging.Formatter.format(self, record) 18 | 19 | 20 | def init_logging(): 21 | init(autoreset=True) 22 | 23 | formatter = ColorFormatter( 24 | fmt="%(asctime)s %(levelname)-8s %(message)s", 25 | datefmt="%X" 26 | ) 27 | 28 | handler = logging.StreamHandler() 29 | handler.set_name("stream") 30 | handler.setFormatter(formatter) 31 | handler.setLevel(logging.WARNING) 32 | 33 | logger = logging.getLogger("allzpark") 34 | logger.addHandler(handler) 35 | logger.setLevel(logging.DEBUG) 36 | 37 | logger = logging.getLogger("sweet") 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/compile_qrc.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | from pathlib import Path 4 | 5 | here = Path(__file__).parent 6 | resource = here 7 | allzpark_gui = resource.parent 8 | allzpark_pkg = allzpark_gui.parent 9 | allzpark_src = allzpark_pkg.parent 10 | allzpark_res = allzpark_src / "allzpark" / "gui" / "resources.py" 11 | 12 | 13 | def trigger_update(): 14 | """Trigger resource file update 15 | 16 | This doesn't update (compile) the qrc module, but update the timestamp 17 | in Resources class and the resource will be updated on next app launch. 18 | 19 | """ 20 | with open(allzpark_res, "r") as r: 21 | lines = r.readlines() 22 | for i, line in enumerate(lines): 23 | if "# !!!!" in line: 24 | t = int(time.time()) 25 | lines[i] = \ 26 | f" qrc_updated = {t} # !!!! don't touch\n" 27 | break 28 | else: 29 | raise Exception("Pragma not found.") 30 | 31 | with open(allzpark_res, "w") as w: 32 | w.write("".join(lines)) 33 | 34 | 35 | if __name__ == "__main__": 36 | trigger_update() 37 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/clock-history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/hypnotize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/hypnotize-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/hypnotize-dim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/allzpark/gui/delegates.py: -------------------------------------------------------------------------------- 1 | 2 | from rez.packages import iter_packages 3 | from ._vendor.Qt5 import QtCore, QtGui, QtWidgets 4 | 5 | 6 | class VersionDelegate(QtWidgets.QStyledItemDelegate): 7 | 8 | def createEditor(self, parent, option, index): 9 | editor = QtWidgets.QComboBox(parent) 10 | return editor 11 | 12 | def sizeHint(self, option, index): 13 | size = super(VersionDelegate, self).sizeHint(option, index) 14 | size.setWidth(size.width()+25) 15 | return size 16 | 17 | def setEditorData(self, editor, index): 18 | editor.clear() 19 | 20 | version_name = index.data(QtCore.Qt.DisplayRole) 21 | package_version = index.data(QtCore.Qt.UserRole) 22 | 23 | package_versions = sorted(list(iter_packages(package_version.name)), key=lambda v: str(v.version)) 24 | 25 | editor_index = 0 26 | for package_version in package_versions: 27 | label = str(package_version.version) 28 | requires = ",\n".join([str(r) for r in package_version.requires or []]) 29 | 30 | editor.addItem(label, userData=package_version) 31 | editor.setItemData(editor.count() - 1, requires, QtCore.Qt.ToolTipRole) 32 | 33 | if label == version_name: 34 | editor_index = editor.count() - 1 35 | 36 | editor.setCurrentIndex(editor_index) 37 | 38 | def setModelData(self, editor, model, index): 39 | package_version = editor.itemData(editor.currentIndex()) 40 | if package_version: 41 | label = str(package_version.version) 42 | model.setData(index, label) 43 | model.setData(index, package_version, QtCore.Qt.UserRole) 44 | -------------------------------------------------------------------------------- /tests/test_launch.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | from tests import util 4 | 5 | 6 | class TestLaunch(util.TestBase): 7 | 8 | def test_launch_subprocess(self): 9 | """Test launching subprocess command""" 10 | util.memory_repository({ 11 | "foo": { 12 | "1": { 13 | "name": "foo", 14 | "version": "1", 15 | "requires": ["~app"], 16 | } 17 | }, 18 | "app": { 19 | "1": { 20 | "name": "app", 21 | "version": "1", 22 | } 23 | }, 24 | }) 25 | self.ctrl_reset(["foo"]) 26 | 27 | with self.wait_signal(self.ctrl.state_changed, "ready"): 28 | self.ctrl.select_profile("foo") 29 | 30 | self.ctrl.select_application("app==1") 31 | self.assertEqual("app==1", self.ctrl.state["appRequest"]) 32 | 33 | commands = self.ctrl.state["commands"] 34 | self.assertEqual(len(commands), 0) 35 | 36 | stdout = list() 37 | stderr = list() 38 | command = ( 39 | '%s -c "' 40 | 'import sys;' 41 | 'sys.stdout.write(\'meow\')"' 42 | ) % sys.executable 43 | 44 | with self.wait_signal(self.ctrl.state_changed, "launching"): 45 | self.ctrl.launch(command=command, 46 | stdout=lambda m: stdout.append(m), 47 | stderr=lambda m: stderr.append(m)) 48 | 49 | self.assertEqual(len(commands), 1) 50 | 51 | with self.wait_signal(commands[0].killed): 52 | pass 53 | 54 | self.assertIn("meow", "\n".join(stdout)) 55 | self.assertEqual("", "\n".join(stderr)) 56 | -------------------------------------------------------------------------------- /src/allzpark/gui/_vendor/Qt5.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import types 4 | 5 | __version__ = "0.2.0.b2" 6 | __binding__ = "None" # added for working with `qjsonmodel.py` 7 | 8 | QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) 9 | QT_PREFERRED_BINDING = os.environ.get("QT_PREFERRED_BINDING") 10 | QtCompat = types.ModuleType("QtCompat") 11 | 12 | 13 | def _log(text): 14 | if QT_VERBOSE: 15 | sys.stdout.write(text + "\n") 16 | 17 | 18 | try: 19 | from PySide2 import ( 20 | QtWidgets, 21 | QtCore, 22 | QtGui, 23 | QtQml, 24 | QtQuick, 25 | QtMultimedia, 26 | QtOpenGL, 27 | ) 28 | 29 | from shiboken2 import wrapInstance, getCppPointer 30 | QtCompat.wrapInstance = wrapInstance 31 | QtCompat.getCppPointer = getCppPointer 32 | 33 | try: 34 | from PySide2 import QtUiTools 35 | QtCompat.loadUi = QtUiTools.QUiLoader 36 | 37 | except ImportError: 38 | _log("QtUiTools not provided.") 39 | 40 | __binding__ = "PySide2" 41 | 42 | 43 | except ImportError: 44 | try: 45 | from PyQt5 import ( 46 | QtWidgets, 47 | QtCore, 48 | QtGui, 49 | QtQml, 50 | QtQuick, 51 | QtMultimedia, 52 | QtOpenGL, 53 | ) 54 | 55 | QtCore.Signal = QtCore.pyqtSignal 56 | QtCore.Slot = QtCore.pyqtSlot 57 | QtCore.Property = QtCore.pyqtProperty 58 | 59 | from sip import wrapinstance, unwrapinstance 60 | QtCompat.wrapInstance = wrapinstance 61 | QtCompat.getCppPointer = unwrapinstance 62 | 63 | try: 64 | from PyQt5 import uic 65 | QtCompat.loadUi = uic.loadUi 66 | except ImportError: 67 | _log("uic not provided.") 68 | 69 | __binding__ = "PyQt5" 70 | 71 | except ImportError: 72 | 73 | # Used during tests and installers 74 | if QT_PREFERRED_BINDING == "None": 75 | _log("No binding found") 76 | else: 77 | raise 78 | 79 | __all__ = [ 80 | "__binding__", 81 | "QtWidgets", 82 | "QtCore", 83 | "QtGui", 84 | "QtQml", 85 | "QtQuick", 86 | "QtMultimedia", 87 | "QtCompat", 88 | "QtOpenGL", 89 | ] 90 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = allzpark 3 | version = attr: allzpark._version.__version__ 4 | description = A cross-platform launcher for film and games projects, built on Rez 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | url = https://github.com/mottosso/allzpark 8 | author = Marcus Ottosson 9 | author_email = konstruktion@gmail.com 10 | maintainer = davidlatwe 11 | maintainer_email = davidlatwe@gmail.com 12 | license = LGPLv3 13 | license_file = LICENCE.txt 14 | platforms = any 15 | classifiers = 16 | Development Status :: 4 - Beta 17 | Intended Audience :: Developers 18 | License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) 19 | Operating System :: MacOS :: MacOS X 20 | Operating System :: Microsoft :: Windows 21 | Operating System :: POSIX 22 | Programming Language :: Python :: 3 23 | Programming Language :: Python :: 3.6 24 | Programming Language :: Python :: 3.7 25 | Programming Language :: Python :: 3.8 26 | Programming Language :: Python :: 3.9 27 | Programming Language :: Python :: Implementation :: CPython 28 | Topic :: Utilities 29 | Topic :: Software Development 30 | Topic :: System :: Software Distribution 31 | keywords = launcher package resolve version software management 32 | project_urls = 33 | Source=https://github.com/mottosso/allzpark 34 | Tracker=https://github.com/mottosso/allzpark/issues 35 | 36 | [options] 37 | zip_safe = true 38 | python_requires = >=3, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* 39 | install_requires = 40 | pymongo 41 | colorama 42 | packages = find: 43 | package_dir = 44 | = src 45 | include_package_data = true 46 | 47 | [options.packages.find] 48 | where = src 49 | exclude = 50 | tests 51 | 52 | [options.entry_points] 53 | console_scripts = 54 | allzpark = allzpark.rezplugins.command.park:rez_cli 55 | azp = allzpark.rezplugins.command.park:rez_cli 56 | rez-park = allzpark.rezplugins.command.park:rez_cli 57 | 58 | [options.package_data] 59 | allzpark.gui = 60 | resources/*.css 61 | resources/*.svg 62 | resources/*.png 63 | resources/fonts/*/*.ttf 64 | resources/fonts/*/*LICENSE* 65 | 66 | [options.extras_require] 67 | gui = 68 | pyside2 69 | tests = 70 | pytest 71 | 72 | [sdist] 73 | formats = gztar 74 | 75 | [bdist_wheel] 76 | universal = true 77 | 78 | [nosetests] 79 | verbosity=3 80 | detailed-errors=1 81 | -------------------------------------------------------------------------------- /src/allzpark/rezplugins/command/park.py: -------------------------------------------------------------------------------- 1 | """ 2 | Rez suite based package tool/application launcher 3 | """ 4 | import sys 5 | import argparse 6 | try: 7 | from rez.command import Command 8 | except ImportError: 9 | Command = object 10 | 11 | 12 | command_behavior = {} 13 | 14 | 15 | def rez_cli(): 16 | from rez.cli._main import run 17 | from rez.cli._entry_points import check_production_install 18 | check_production_install() 19 | try: 20 | return run("park") 21 | except KeyError: 22 | pass 23 | # for rez version that doesn't have Command type plugin 24 | return standalone_cli() 25 | 26 | 27 | def standalone_cli(): 28 | # for running without rez's cli 29 | parser = argparse.ArgumentParser("allzpark", description=( 30 | "An application launcher built on Rez, " 31 | "pass --help for details" 32 | )) 33 | parser.add_argument("-v", "--verbose", action="count", default=0, help=( 34 | "Print additional information about Allzpark during operation. " 35 | "Pass -v for info, -vv for info and -vvv for debug messages")) 36 | setup_parser(parser) 37 | opts = parser.parse_args() 38 | return command(opts) 39 | 40 | 41 | def setup_parser(parser, completions=False): 42 | parser.add_argument("--clean", action="store_true", help=( 43 | "Start fresh with user preferences")) # todo: not implemented 44 | parser.add_argument("--version", action="store_true", 45 | help="Print out version of this plugin command.") 46 | parser.add_argument("--gui", action="store_true") 47 | 48 | 49 | def command(opts, parser=None, extra_arg_groups=None): 50 | import logging 51 | from allzpark import cli, report 52 | report.init_logging() 53 | 54 | if opts.debug: 55 | log = logging.getLogger("allzpark") 56 | stream_handler = next(h for h in log.handlers if h.name == "stream") 57 | stream_handler.setLevel(logging.DEBUG) 58 | 59 | if opts.version: 60 | from allzpark._version import print_info 61 | sys.exit(print_info()) 62 | 63 | if opts.gui: 64 | from allzpark.gui import app 65 | sys.exit(app.launch()) 66 | 67 | return cli.main() 68 | 69 | 70 | class AllzparkCommand(Command): 71 | schema_dict = {} 72 | 73 | @classmethod 74 | def name(cls): 75 | return "park" 76 | 77 | 78 | def register_plugin(): 79 | return AllzparkCommand 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyCharm 132 | .idea/ 133 | 134 | 135 | /src/allzpark/gui/allzpark_rc.py 136 | -------------------------------------------------------------------------------- /src/allzpark/gui/pages.py: -------------------------------------------------------------------------------- 1 | 2 | from ._vendor.Qt5 import QtCore, QtGui, QtWidgets 3 | from . import widgets 4 | 5 | 6 | class ProductionPage(widgets.BusyWidget): 7 | 8 | def __init__(self, *args, **kwargs): 9 | super(ProductionPage, self).__init__(*args, **kwargs) 10 | self.setObjectName("ProductionPage") 11 | 12 | # top 13 | head = QtWidgets.QWidget() 14 | head.setObjectName("ButtonBelt") 15 | clear_cache = widgets.ClearCacheWidget() 16 | work_dir = widgets.WorkDirWidget() 17 | # body 18 | body = QtWidgets.QWidget() 19 | # - left side tab #1 20 | workspace_view = widgets.WorkspaceWidget() 21 | tools_view = widgets.ToolsView() 22 | # - left side tab #2 23 | work_history = widgets.WorkHistoryWidget() 24 | # - right side 25 | tool_context = widgets.ToolContextWidget() 26 | 27 | work_split = QtWidgets.QSplitter() 28 | work_split.addWidget(workspace_view) 29 | work_split.addWidget(tools_view) 30 | 31 | work_split.setObjectName("WorkspaceToolSplitter") 32 | work_split.setOrientation(QtCore.Qt.Horizontal) 33 | work_split.setChildrenCollapsible(False) 34 | work_split.setContentsMargins(0, 0, 0, 0) 35 | work_split.setStretchFactor(0, 50) 36 | work_split.setStretchFactor(1, 50) 37 | 38 | tabs = QtWidgets.QTabBar() 39 | stack = QtWidgets.QStackedWidget() 40 | stack.setObjectName("TabStackWidgetLeft") 41 | tabs.setShape(tabs.RoundedWest) 42 | tabs.setDocumentMode(True) 43 | # QTabWidget's frame (pane border) will not be rendered if documentMode 44 | # is enabled, so we make our own with bar + stack with border. 45 | tabs.addTab(QtGui.QIcon(":/icons/magic.svg"), "Workspaces") 46 | stack.addWidget(work_split) 47 | tabs.addTab(QtGui.QIcon(":/icons/clock-history.svg"), "History") 48 | stack.addWidget(work_history) 49 | 50 | body_split = QtWidgets.QSplitter() 51 | body_split.addWidget(body) 52 | body_split.addWidget(tool_context) 53 | 54 | body_split.setObjectName("MainPageSplitter") 55 | body_split.setOrientation(QtCore.Qt.Horizontal) 56 | body_split.setChildrenCollapsible(False) 57 | body_split.setContentsMargins(0, 0, 0, 0) 58 | body_split.setStretchFactor(0, 70) 59 | body_split.setStretchFactor(1, 30) 60 | 61 | layout = QtWidgets.QHBoxLayout(head) 62 | layout.setContentsMargins(0, 0, 0, 0) 63 | layout.setSpacing(0) 64 | layout.addWidget(clear_cache) 65 | layout.addWidget(work_dir, stretch=True) 66 | 67 | layout = QtWidgets.QHBoxLayout(body) 68 | layout.setContentsMargins(0, 0, 0, 0) 69 | layout.setSpacing(0) 70 | layout.addWidget(tabs, alignment=QtCore.Qt.AlignTop) 71 | layout.addWidget(stack, stretch=True) 72 | 73 | layout = QtWidgets.QVBoxLayout(self) 74 | layout.setContentsMargins(0, 4, 0, 0) 75 | layout.addWidget(head) 76 | layout.addWidget(body_split, stretch=True) 77 | 78 | tabs.currentChanged.connect(stack.setCurrentIndex) 79 | -------------------------------------------------------------------------------- /src/allzpark/util.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import json 4 | import getpass 5 | import logging 6 | from contextlib import contextmanager 7 | from functools import singledispatch, update_wrapper 8 | 9 | log = logging.getLogger("allzpark") 10 | 11 | 12 | def normpath(path): 13 | return os.path.normpath( 14 | os.path.normcase(os.path.abspath(os.path.expanduser(path))) 15 | ).replace("\\", "/") 16 | 17 | 18 | def normpaths(*paths): 19 | return list(map(normpath, paths)) 20 | 21 | 22 | @contextmanager 23 | def log_level(level, name="stream"): 24 | stream_handler = next(h for h in log.handlers if h.name == name) 25 | current = stream_handler.level 26 | stream_handler.setLevel(level) 27 | yield 28 | stream_handler.setLevel(current) 29 | 30 | 31 | def elide(string, length=120): 32 | string = str(string) 33 | placeholder = "..." 34 | length -= len(placeholder) 35 | 36 | if len(string) <= length: 37 | return string 38 | 39 | half = int(length / 2) 40 | return string[:half] + placeholder + string[-half:] 41 | 42 | 43 | def subprocess_encoding(): 44 | """Codec that should be used to decode subprocess stdout/stderr 45 | 46 | See https://docs.python.org/3/library/codecs.html#standard-encodings 47 | 48 | Returns: 49 | str: name of codec 50 | 51 | """ 52 | # nerdvegas/rez sets `encoding='utf-8'` when `universal_newlines=True` and 53 | # `encoding` is not in Popen kwarg. 54 | return "utf-8" 55 | 56 | 57 | def unicode_decode_error_handler(): 58 | """Error handler for handling UnicodeDecodeError in subprocess 59 | 60 | See https://docs.python.org/3/library/codecs.html#error-handlers 61 | 62 | Returns: 63 | str: name of registered error handler 64 | 65 | """ 66 | import codecs 67 | import locale 68 | 69 | def decode_with_preferred_encoding(exception): 70 | # type: (UnicodeError) -> tuple[str, int] 71 | encoding = locale.getpreferredencoding(do_setlocale=False) 72 | invalid_bytes = exception.object[exception.start:] 73 | 74 | text = invalid_bytes.decode(encoding, 75 | # second fallback 76 | errors="backslashreplace") 77 | 78 | return text, len(exception.object) 79 | 80 | handler_name = "decode_with_preferred_encoding" 81 | try: 82 | codecs.lookup_error(handler_name) 83 | except LookupError: 84 | codecs.register_error(handler_name, decode_with_preferred_encoding) 85 | 86 | return handler_name 87 | 88 | 89 | def singledispatchmethod(func): 90 | """A decorator like `functools.singledispatch` but for class method 91 | 92 | This is for Python<3.8. 93 | For version 3.8+, there is `functools.singledispatchmethod`. 94 | 95 | https://stackoverflow.com/a/24602374 96 | 97 | :param func: 98 | :return: 99 | """ 100 | dispatcher = singledispatch(func) 101 | 102 | def wrapper(*args, **kw): 103 | return dispatcher.dispatch(args[1].__class__)(*args, **kw) 104 | 105 | wrapper.register = dispatcher.register 106 | update_wrapper(wrapper, func) 107 | return wrapper 108 | 109 | 110 | def get_user_task(): 111 | db = "T:/rez-studio/setup/configs/user_task.json" 112 | if not os.path.isfile(db): 113 | return '' 114 | 115 | with open(db, "rb") as f: 116 | user_docs = json.load(f) 117 | 118 | user = getpass.getuser().lower() 119 | task = user_docs.get(user, {}).get('task', '') 120 | return task 121 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/allzpark-rc.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icons/_.svg 5 | icons/activity.svg 6 | icons/arrow-clockwise-on.svg 7 | icons/arrow-clockwise.svg 8 | icons/avalon-logomark.svg 9 | icons/box-arrow-down.svg 10 | icons/box-seam.svg 11 | icons/boxes.svg 12 | icons/brightness-high-fill.svg 13 | icons/brightness-low-fill.svg 14 | icons/card-list.svg 15 | icons/card-text.svg 16 | icons/caret-down-fill-dim.svg 17 | icons/caret-down-fill-on.svg 18 | icons/caret-down-fill.svg 19 | icons/caret-right-fill-dim.svg 20 | icons/caret-right-fill-on.svg 21 | icons/caret-right-fill.svg 22 | icons/check-ok.svg 23 | icons/chevron-double-left.svg 24 | icons/chevron_down.svg 25 | icons/chevron_left.svg 26 | icons/chevron_right.svg 27 | icons/chevron_up.svg 28 | icons/clock-history.svg 29 | icons/code-slash.svg 30 | icons/collection.svg 31 | icons/command.svg 32 | icons/dash.svg 33 | icons/door-closed.svg 34 | icons/door-open.svg 35 | icons/dot-red-on.svg 36 | icons/dot-red.svg 37 | icons/dot.svg 38 | icons/episode.svg 39 | icons/exclamation-triangle-fill.svg 40 | icons/exclamation-warn.svg 41 | icons/filter-dim.svg 42 | icons/filter-on.svg 43 | icons/filter.svg 44 | icons/folder-minus.svg 45 | icons/folder-x.svg 46 | icons/folder.svg 47 | icons/hypnotize-dim.svg 48 | icons/hypnotize-on.svg 49 | icons/hypnotize.svg 50 | icons/journal-plus.svg 51 | icons/joystick.svg 52 | icons/log-critical.svg 53 | icons/log-error.svg 54 | icons/log-info.svg 55 | icons/log-undefined.svg 56 | icons/log-warning.svg 57 | icons/magic.svg 58 | icons/people-fill-ok.svg 59 | icons/people-fill.svg 60 | icons/person-circle.svg 61 | icons/plus.svg 62 | icons/rez_logo.svg 63 | icons/sequence.png 64 | icons/server.svg 65 | icons/sg_logo.png 66 | icons/slash-lg.svg 67 | icons/square-check-dim.svg 68 | icons/square-check.svg 69 | icons/square-dim.svg 70 | icons/square-slash-dim.svg 71 | icons/square-slash.svg 72 | icons/square.svg 73 | icons/terminal-dim.svg 74 | icons/terminal-on.svg 75 | icons/terminal.svg 76 | icons/toggle-off-bright.svg 77 | icons/toggle-off-dim.svg 78 | icons/toggle-off.svg 79 | icons/toggle-on-bright.svg 80 | icons/toggle-on-dim.svg 81 | icons/toggle-on.svg 82 | icons/x.svg 83 | fonts/opensans/OpenSans-Bold.ttf 84 | fonts/opensans/OpenSans-Italic.ttf 85 | fonts/opensans/OpenSans-Regular.ttf 86 | fonts/jetbrainsmono/JetBrainsMono-Regular.ttf 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/avalon-logomark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 12 | 13 | 21 | 22 | 23 | 24 | 27 | 30 | 32 | 34 | 36 | 38 | 39 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/test_docks.py: -------------------------------------------------------------------------------- 1 | 2 | from tests import util 3 | 4 | 5 | class TestDocks(util.TestBase): 6 | 7 | def test_feature_blocked_on_failed_app(self): 8 | """Test feature blocked if application is broken""" 9 | util.memory_repository({ 10 | "foo": { 11 | "1.0.0": { 12 | "name": "foo", 13 | "version": "1.0.0", 14 | "requires": [ 15 | "~app_A", # missing package (broken app) 16 | "~app_B", 17 | ], 18 | }, 19 | }, 20 | "app_B": {"1": {"name": "app_B", "version": "1"}}, 21 | }) 22 | self.ctrl_reset(["foo"]) 23 | 24 | self.set_preference("showAdvancedControls", True) 25 | 26 | context_a = self.ctrl.state["rezContexts"]["app_A==None"] 27 | context_b = self.ctrl.state["rezContexts"]["app_B==1"] 28 | 29 | self.assertFalse(context_a.success) 30 | self.assertTrue(context_b.success) 31 | 32 | for app, state in {"app_A==None": False, "app_B==1": True}.items(): 33 | self.select_application(app) 34 | 35 | dock = self.show_dock("environment", on_page="diagnose") 36 | self.assertEqual(dock._widgets["compute"].isEnabled(), state) 37 | 38 | dock = self.show_dock("context", on_page="code") 39 | self.assertEqual(dock._widgets["printCode"].isEnabled(), state) 40 | 41 | dock = self.show_dock("app") 42 | self.assertEqual(dock._widgets["launchBtn"].isEnabled(), state) 43 | 44 | def test_version_editable_on_show_all_versions(self): 45 | """Test version is editable when show all version enabled""" 46 | self._test_version_editable(show_all_version=True) 47 | 48 | def test_version_editable_on_not_show_all_versions(self): 49 | """Test version is not editable when show all version disabled""" 50 | self._test_version_editable(show_all_version=False) 51 | 52 | def _test_version_editable(self, show_all_version): 53 | util.memory_repository({ 54 | "foo": { 55 | "1": {"name": "foo", "version": "1", 56 | "requires": ["~app_A", "~app_B"]}, 57 | "2": {"name": "foo", "version": "2", 58 | "requires": ["~app_A", "~app_B"]}, 59 | }, 60 | "app_A": {"1": {"name": "app_A", "version": "1"}}, 61 | "app_B": {"1": {"name": "app_B", "version": "1", 62 | "requires": ["bar"]}}, 63 | "bar": {"1": {"name": "bar", "version": "1"}, 64 | "2": {"name": "bar", "version": "2"}} 65 | }) 66 | self.ctrl_reset(["foo"]) 67 | 68 | self.set_preference("showAdvancedControls", True) 69 | self.set_preference("showAllVersions", show_all_version) 70 | self.wait(200) # wait for reset 71 | 72 | self.select_application("app_B==1") 73 | 74 | dock = self.show_dock("packages") 75 | view = dock._widgets["view"] 76 | proxy = view.model() 77 | model = proxy.sourceModel() 78 | 79 | for pkg, state in {"foo": False, # profile can't change version here 80 | "bar": show_all_version, 81 | "app_B": False}.items(): 82 | index = model.findIndex(pkg) 83 | index = proxy.mapFromSource(index) 84 | 85 | rect = view.visualRect(index) 86 | position = rect.center() 87 | with util.patch_cursor_pos(view.mapToGlobal(position)): 88 | dock.on_right_click(position) 89 | menu = self.get_menu(dock) 90 | edit_action = next((a for a in menu.actions() 91 | if a.text() == "Edit"), None) 92 | if edit_action is None: 93 | self.fail("No version edit action.") 94 | 95 | self.assertEqual( 96 | edit_action.isEnabled(), state, 97 | "Package '%s' version edit state is incorrect." % pkg 98 | ) 99 | 100 | self.wait(200) 101 | menu.close() 102 | -------------------------------------------------------------------------------- /src/allzpark/gui/_vendor/qoverview.py: -------------------------------------------------------------------------------- 1 | 2 | from .Qt5 import QtCore, QtWidgets 3 | 4 | 5 | class VerticalExtendedTreeView(QtWidgets.QTreeView): 6 | """TreeView with vertical virtual space extended 7 | 8 | The last row in default TreeView always stays on bottom, this TreeView 9 | subclass extends the space so the last row can be scrolled on top of 10 | view. Which behaves like modern text editor that has virtual space after 11 | last line. 12 | 13 | """ 14 | _extended = None 15 | _on_key_search = False 16 | 17 | def __init__(self, parent=None): 18 | super(VerticalExtendedTreeView, self).__init__(parent=parent) 19 | # these are important 20 | self.setVerticalScrollMode(self.ScrollPerPixel) 21 | self.setSizeAdjustPolicy(self.AdjustIgnored) 22 | self.setUniformRowHeights(True) 23 | 24 | self.collapsed.connect(self.reset_extension) 25 | self.expanded.connect(self.reset_extension) 26 | 27 | self._on_key_search = False 28 | self._extended = None 29 | self._row_height = 0 30 | self._pos = 0 31 | 32 | def _compute_extension(self): 33 | # scrollArea's SizeAdjustPolicy is AdjustIgnored, so the extension 34 | # only need to set once until modelReset or item collapsed/expanded 35 | scroll = self.verticalScrollBar() 36 | height = self.viewport().height() 37 | row_unit = self.uniformed_row_height() 38 | current_max = scroll.maximum() 39 | 40 | self._extended = current_max + height - row_unit 41 | 42 | def paintEvent(self, event): 43 | if self._extended is None: 44 | self._compute_extension() 45 | 46 | if self._extended > 0: 47 | scroll = self.verticalScrollBar() 48 | current_max = scroll.maximum() 49 | 50 | resized = self._extended != current_max 51 | if resized and current_max > 0: 52 | scroll.setMaximum(self._extended) 53 | scroll.setSliderPosition(self._pos) 54 | else: 55 | self._pos = scroll.sliderPosition() 56 | 57 | return super(VerticalExtendedTreeView, self).paintEvent(event) 58 | 59 | def resizeEvent(self, event): 60 | self.reset_extension() 61 | return super(VerticalExtendedTreeView, self).resizeEvent(event) 62 | 63 | def setModel(self, model): 64 | super(VerticalExtendedTreeView, self).setModel(model) 65 | model.modelReset.connect(self.reset_extension) 66 | 67 | def uniformed_row_height(self): 68 | """Uniformed single row height, compute from first row and cached""" 69 | model = self.model() 70 | if model is not None and not self._row_height: 71 | first = model.index(0, 0) 72 | self._row_height = float(self.rowHeight(first)) 73 | # cached 74 | return self._row_height 75 | 76 | def reset_extension(self, *args, **kwargs): 77 | """Should be called on model reset or item collapsed/expanded""" 78 | self._extended = None 79 | 80 | def scroll_at_top(self, index): 81 | """Scroll to index and position at top 82 | Like `scrollTo` with `PositionAtTop` hint, but works better with 83 | extended view. 84 | """ 85 | if self._extended: 86 | scroll = self.verticalScrollBar() 87 | rect = self.visualRect(index) 88 | pos = rect.top() + self.verticalOffset() 89 | scroll.setSliderPosition(pos) 90 | else: 91 | hint = self.PositionAtTop 92 | super(VerticalExtendedTreeView, self).scrollTo(index, hint) 93 | 94 | def scrollTo(self, index, hint=None): 95 | hint = hint or self.EnsureVisible 96 | if hint == self.PositionAtTop or self._on_key_search: 97 | self.scroll_at_top(index) 98 | else: 99 | super(VerticalExtendedTreeView, self).scrollTo(index, hint) 100 | 101 | def keyboardSearch(self, string): 102 | self._on_key_search = True 103 | super(VerticalExtendedTreeView, self).keyboardSearch(string) 104 | self._on_key_search = False 105 | 106 | def top_scrolled_index(self, slider_pos): 107 | """Return the index of item that has been scrolled of top of view""" 108 | row_unit = self.uniformed_row_height() 109 | value = (slider_pos - self.verticalOffset()) / row_unit 110 | return self.indexAt(QtCore.QPoint(0, value)) 111 | -------------------------------------------------------------------------------- /src/allzpark/gui/window.py: -------------------------------------------------------------------------------- 1 | 2 | from ._vendor.Qt5 import QtCore, QtGui, QtWidgets 3 | from . import app, pages, resources as res 4 | 5 | 6 | class MainWindow(QtWidgets.QMainWindow): 7 | dark_toggled = QtCore.Signal(bool) 8 | 9 | def __init__(self, state): 10 | """ 11 | :param state: 12 | :type state: app.State 13 | """ 14 | super(MainWindow, self).__init__(flags=QtCore.Qt.Window) 15 | self.setWindowTitle("Allzpark") # todo: add version num 16 | self.setWindowIcon(QtGui.QIcon(":/icons/rez_logo.svg")) 17 | 18 | body = QtWidgets.QWidget() 19 | 20 | tabs = QtWidgets.QTabBar() 21 | stack = QtWidgets.QStackedWidget() 22 | stack.setObjectName("TabStackWidget") 23 | 24 | tabs.addTab("Production") 25 | stack.addWidget(pages.ProductionPage()) 26 | 27 | buttons = QtWidgets.QWidget() 28 | buttons.setObjectName("ButtonBelt") 29 | dark_btn = QtWidgets.QPushButton() 30 | dark_btn.setObjectName("DarkSwitch") 31 | dark_btn.setToolTip("Switch Theme") 32 | dark_btn.setCheckable(True) 33 | 34 | self.statusBar().setSizeGripEnabled(False) 35 | 36 | layout = QtWidgets.QHBoxLayout(buttons) 37 | layout.setContentsMargins(4, 0, 4, 0) 38 | layout.addWidget(dark_btn) 39 | 40 | layout = QtWidgets.QGridLayout(body) 41 | layout.setSpacing(0) 42 | layout.setContentsMargins(0, 0, 0, 0) 43 | layout.addWidget(tabs, 0, 0, 1, 1, 44 | QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom) 45 | layout.addWidget(buttons, 0, 1, 1, 1, QtCore.Qt.AlignRight) 46 | layout.addWidget(stack, 1, 0, 1, 2) 47 | 48 | tabs.currentChanged.connect(stack.setCurrentIndex) 49 | dark_btn.toggled.connect(self.dark_toggled.emit) 50 | self.statusBar().messageChanged.connect(self.on_status_changed) 51 | 52 | self._body = body 53 | self._state = state 54 | self._splitters = { 55 | s.objectName(): s 56 | for s in body.findChildren(QtWidgets.QSplitter) if s.objectName() 57 | } 58 | 59 | self.statusBar().show() 60 | self.setCentralWidget(body) 61 | self.setContentsMargins(6, 6, 6, 6) 62 | 63 | dark_btn.setChecked(state.retrieve_dark_mode()) 64 | tabs.setCurrentIndex(0) # production 65 | 66 | def on_status_changed(self, message): 67 | theme = res.current_theme() 68 | if theme is None: 69 | return 70 | if message.startswith("WARNING"): 71 | color = theme.palette.on_warning 72 | bg_cl = theme.palette.warning 73 | elif message.startswith("ERROR") or message.startswith("CRITICAL"): 74 | color = theme.palette.on_error 75 | bg_cl = theme.palette.error 76 | else: 77 | color = theme.palette.on_background 78 | bg_cl = theme.palette.background 79 | 80 | style = f"color: {color}; background-color: {bg_cl};" 81 | self.statusBar().setStyleSheet(style) 82 | 83 | @QtCore.Slot(str, int) # noqa 84 | def spoken(self, message, duration=5000): 85 | if message: 86 | self.statusBar().showMessage(message, duration) 87 | else: 88 | self.statusBar().clearMessage() 89 | 90 | def find(self, widget_cls, name=None): 91 | return self._body.findChild(widget_cls, name) 92 | 93 | def switch_tab(self, index): 94 | self._tabs.setCurrentIndex(index) 95 | 96 | def reset_layout(self): 97 | with self._state.group("default"): 98 | self._state.restore_layout(self, "mainWindow", keep_geo=True) 99 | for key, split in self._splitters.items(): 100 | self._state.restore_layout(split, key) 101 | 102 | def showEvent(self, event): 103 | super(MainWindow, self).showEvent(event) 104 | 105 | with self._state.group("default"): # for resetting layout 106 | self._state.preserve_layout(self, "mainWindow") 107 | for key, split in self._splitters.items(): 108 | self._state.preserve_layout(split, key) 109 | 110 | with self._state.group("current"): 111 | self._state.restore_layout(self, "mainWindow") 112 | for key, split in self._splitters.items(): 113 | self._state.restore_layout(split, key) 114 | 115 | def closeEvent(self, event): 116 | with self._state.group("current"): 117 | self._state.preserve_layout(self, "mainWindow") 118 | for key, split in self._splitters.items(): 119 | self._state.preserve_layout(split, key) 120 | 121 | return super(MainWindow, self).closeEvent(event) 122 | -------------------------------------------------------------------------------- /tests/test_profiles.py: -------------------------------------------------------------------------------- 1 | 2 | from unittest import mock 3 | from tests import util 4 | 5 | 6 | class TestProfiles(util.TestBase): 7 | 8 | def test_reset(self): 9 | """Test session reset 10 | """ 11 | util.memory_repository({ 12 | "foo": { 13 | "1.0.0": { 14 | "name": "foo", 15 | "version": "1.0.0", 16 | } 17 | }, 18 | "bar": { 19 | "1.0.0": { 20 | "name": "bar", 21 | "version": "1.0.0", 22 | } 23 | } 24 | }) 25 | with self.wait_signal(self.ctrl.resetted): 26 | self.ctrl.reset(["foo", "bar"]) 27 | self.wait(timeout=200) 28 | self.assertEqual(self.ctrl.state.state, "noapps") 29 | 30 | # last profile will be selected by default 31 | self.assertEqual("bar", self.ctrl.state["profileName"]) 32 | self.assertEqual(["foo", "bar"], list(self.ctrl.state["rezProfiles"])) 33 | 34 | def test_select_profile_with_out_apps(self): 35 | """Test selecting profile that has no apps 36 | """ 37 | util.memory_repository({ 38 | "foo": { 39 | "1.0.0": { 40 | "name": "foo", 41 | "version": "1.0.0", 42 | } 43 | }, 44 | "bar": { 45 | "1.0.0": { 46 | "name": "bar", 47 | "version": "1.0.0", 48 | } 49 | } 50 | }) 51 | with self.wait_signal(self.ctrl.resetted): 52 | self.ctrl.reset(["foo", "bar"]) 53 | self.wait(timeout=200) 54 | self.assertEqual(self.ctrl.state.state, "noapps") 55 | 56 | with self.wait_signal(self.ctrl.state_changed, "noapps"): 57 | self.ctrl.select_profile("foo") 58 | # wait enter 'noapps' state 59 | 60 | self.assertEqual("foo", self.ctrl.state["profileName"]) 61 | 62 | def test_profile_list_apps(self): 63 | """Test listing apps from profile 64 | """ 65 | util.memory_repository({ 66 | "foo": { 67 | "1.0.0": { 68 | "name": "foo", 69 | "version": "1.0.0", 70 | "requires": [ 71 | "lib_foo", 72 | "~app_A", 73 | "~app_B", 74 | ], 75 | } 76 | }, 77 | "app_A": { 78 | "1.0.0": { 79 | "name": "app_A", 80 | "version": "1.0.0", 81 | } 82 | }, 83 | "app_B": { 84 | "1.0.0": { 85 | "name": "app_B", 86 | "version": "1.0.0", 87 | } 88 | }, 89 | "lib_foo": { 90 | "1.0.0": { 91 | "name": "lib_foo", 92 | "version": "1.0.0", 93 | } 94 | }, 95 | }) 96 | self.ctrl_reset(["foo"]) 97 | 98 | with self.wait_signal(self.ctrl.state_changed, "ready"): 99 | self.ctrl.select_profile("foo") 100 | 101 | self.assertEqual( 102 | [ 103 | "app_A==1.0.0", 104 | "app_B==1.0.0", 105 | ], 106 | list(self.ctrl.state["rezApps"].keys()) 107 | ) 108 | 109 | def test_profile_listing_without_root_err(self): 110 | """Listing profile without root will raise AssertionError""" 111 | self.assertRaises(AssertionError, self.ctrl.reset) 112 | self.assertRaises(AssertionError, self.ctrl.list_profiles) 113 | 114 | def test_profile_listing_callable_root_err(self): 115 | """Listing profile with bad callable will prompt error message""" 116 | import traceback 117 | import logging 118 | from allzpark import control 119 | 120 | traceback.print_exc = mock.MagicMock(name="traceback.print_exc") 121 | self.ctrl.error = mock.MagicMock(name="Controller.error") 122 | 123 | def bad_root(): 124 | raise Exception("This should be caught.") 125 | self.ctrl.list_profiles(bad_root) 126 | 127 | # ctrl.error must be called in all cases 128 | self.ctrl.error.assert_called_once() 129 | # traceback.print_exc should be called if logging level is set 130 | # lower than INFO, e.g. DEBUG or NOTSET 131 | if control.log.level < logging.INFO: 132 | traceback.print_exc.assert_called_once() 133 | 134 | def test_profile_listing_invalid_type_root_err(self): 135 | """Listing profile with invalid input type will raise TypeError""" 136 | self.assertRaises(TypeError, self.ctrl.list_profiles, {"foo"}) 137 | 138 | def test_profile_listing_filter_out_empty_names(self): 139 | """Listing profile with empty names will be filtered""" 140 | expected = ["foo", "bar"] 141 | profiles = self.ctrl.list_profiles(expected + [None, ""]) 142 | self.assertEqual(profiles, expected) 143 | -------------------------------------------------------------------------------- /src/allzpark/gui/widgets_sg_sync.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | from typing import List, Union 4 | from ._vendor.Qt5 import QtCore, QtGui, QtWidgets 5 | from ..backend_sg_sync import Entrance, Project 6 | from ..util import elide 7 | from ..core import AbstractScope 8 | from .models import BaseScopeModel, BaseProxyModel 9 | 10 | log = logging.getLogger("allzpark") 11 | 12 | 13 | class ShotGridSyncWidget(QtWidgets.QWidget): 14 | icon_path = ":/icons/sg_logo.png" 15 | tools_requested = QtCore.Signal(AbstractScope) 16 | workspace_changed = QtCore.Signal(AbstractScope) 17 | workspace_refreshed = QtCore.Signal(AbstractScope, bool) 18 | 19 | def __init__(self, *args, **kwargs): 20 | super(ShotGridSyncWidget, self).__init__(*args, **kwargs) 21 | 22 | label = QtWidgets.QLabel("ShotGrid Sync") 23 | label.setObjectName("BackendLabel") 24 | 25 | project_list = ProjectListWidget() 26 | 27 | layout = QtWidgets.QVBoxLayout(self) 28 | layout.setContentsMargins(0, 0, 0, 0) 29 | layout.addWidget(label) 30 | layout.addWidget(project_list) 31 | 32 | project_list.scope_selected.connect(self._on_project_scope_changed) 33 | project_list.scope_deselected.connect(self._on_project_scope_changed) 34 | 35 | self.__inited = False 36 | self._entrance = None # type: Entrance or None 37 | self._projects = project_list 38 | self._entered_scope = None 39 | 40 | def _on_project_scope_changed(self, scope=None): 41 | if scope is None and self._entrance is None: 42 | return 43 | scope = scope or self._entrance 44 | self.workspace_changed.emit(scope) 45 | 46 | def _workspace_refreshed(self, scope, cache_clear=False): 47 | self.workspace_refreshed.emit(scope, cache_clear) 48 | 49 | def enter_workspace(self, 50 | scope: Union[Entrance, Project], 51 | backend_changed: bool) -> None: 52 | if isinstance(scope, Entrance): 53 | self._entrance = scope 54 | if backend_changed and self.__inited: 55 | return # shotgrid slow, only auto update on start 56 | 57 | elif isinstance(scope, Project): 58 | pass 59 | 60 | else: 61 | return 62 | 63 | self.tools_requested.emit(scope) 64 | self._workspace_refreshed(scope) 65 | self._entered_scope = scope 66 | 67 | def update_workspace(self, scopes: List[Project]) -> None: 68 | if not scopes: 69 | log.debug("No scopes to update.") 70 | return 71 | upstream = scopes[0].upstream # take first scope as sample 72 | 73 | if isinstance(upstream, Entrance): 74 | self._projects.model().refresh(scopes) 75 | self.__inited = True 76 | else: 77 | raise NotImplementedError(f"Invalid upstream {elide(upstream)!r}") 78 | 79 | def on_cache_cleared(self): 80 | if self._entered_scope is not None: 81 | self._workspace_refreshed(self._entered_scope) 82 | 83 | 84 | class ProjectListWidget(QtWidgets.QWidget): 85 | scope_selected = QtCore.Signal(AbstractScope) 86 | scope_deselected = QtCore.Signal() 87 | 88 | def __init__(self, *args, **kwargs): 89 | super(ProjectListWidget, self).__init__(*args, **kwargs) 90 | self.setObjectName("ShotGridProjectView") 91 | 92 | search_bar = QtWidgets.QLineEdit() 93 | search_bar.setPlaceholderText("search projects..") 94 | search_bar.setClearButtonEnabled(True) 95 | 96 | # todo: toggle tank_name <-> name 97 | 98 | model = ProjectListModel() 99 | proxy = BaseProxyModel() 100 | proxy.setSourceModel(model) 101 | view = QtWidgets.QListView() 102 | view.setModel(proxy) 103 | selection = view.selectionModel() 104 | 105 | layout = QtWidgets.QVBoxLayout(self) 106 | layout.setContentsMargins(0, 4, 0, 4) 107 | layout.addWidget(search_bar) 108 | layout.addWidget(view) 109 | 110 | search_bar.textChanged.connect(self._on_project_searched) 111 | selection.selectionChanged.connect(self._on_selection_changed) 112 | 113 | self._view = view 114 | self._proxy = proxy 115 | self._model = model 116 | 117 | def model(self): 118 | return self._model 119 | 120 | def _on_project_searched(self, text): 121 | self._proxy.setFilterRegExp(text) 122 | 123 | def _on_selection_changed(self, selected, _): 124 | indexes = selected.indexes() 125 | if indexes and indexes[0].isValid(): 126 | index = indexes[0] # SingleSelection view 127 | index = self._proxy.mapToSource(index) 128 | scope = index.data(BaseScopeModel.ScopeRole) 129 | self.scope_selected.emit(scope) 130 | else: 131 | self.scope_deselected.emit() 132 | 133 | 134 | class ProjectListModel(BaseScopeModel): 135 | Headers = ["Name"] 136 | 137 | def refresh(self, scopes): 138 | self.reset() 139 | 140 | for project in sorted(scopes, key=lambda s: s.name): 141 | item = QtGui.QStandardItem() 142 | item.setText(project.name) 143 | item.setData(project, self.ScopeRole) 144 | 145 | self.appendRow(item) 146 | -------------------------------------------------------------------------------- /src/allzpark/lib.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import webbrowser 4 | import subprocess 5 | from rez.packages import Variant 6 | from rez.rex import ActionInterpreter 7 | from rez.resolved_context import ResolvedContext 8 | 9 | 10 | def open_file_location(fname): 11 | if os.path.exists(fname): 12 | if os.name == "nt": 13 | fname = os.path.normpath(fname) 14 | subprocess.Popen("explorer /select,%s" % fname) 15 | else: 16 | webbrowser.open(os.path.dirname(fname)) 17 | else: 18 | raise OSError("%s did not exist" % fname) 19 | 20 | 21 | class Singleton(type): 22 | """A metaclass for creating singleton 23 | https://stackoverflow.com/q/6760685/14054728 24 | """ 25 | _instances = {} 26 | 27 | def __call__(cls, *args, **kwargs): 28 | if cls not in cls._instances: 29 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 30 | return cls._instances[cls] 31 | 32 | 33 | class ContextEnvInspector(ActionInterpreter): 34 | """A rex interpreter for inspecting context environ vars 35 | 36 | By parsing rex commenting actions, trace which environ key-value 37 | was set/append/prepend by which package or by rez. 38 | 39 | Example 1: 40 | >>> from rez.resolved_context import ResolvedContext 41 | >>> 42 | >>> context = ResolvedContext(["maya-2020", "python"]) 43 | >>> interp = ContextEnvInspector() 44 | >>> executor = context._create_executor(interp, parent_environ=None) 45 | >>> context._execute(executor) 46 | >>> executor.get_output() 47 | profit!! 48 | 49 | Example 2: 50 | >>> from rez.resolved_context import ResolvedContext 51 | >>> 52 | >>> context = ResolvedContext(["maya-2020", "python"]) 53 | >>> ContextEnvInspector.inspect(context) 54 | easy profit!!! 55 | 56 | """ 57 | expand_env_vars = True 58 | 59 | def __init__(self, context: ResolvedContext = None): 60 | self._scope = None 61 | self._envs = [] 62 | self._pkgs = {} 63 | 64 | if context and context.success: 65 | for pkg in context.resolved_packages: 66 | self._pkgs[pkg.qualified_name] = pkg 67 | 68 | @classmethod 69 | def inspect(cls, context): 70 | interp = cls(context=context) 71 | executor = context._create_executor(interp, parent_environ=None) 72 | context._execute(executor) 73 | return executor.get_output() 74 | 75 | def get_output(self, style=None): 76 | """ 77 | :param style: 78 | :rtype: list[tuple[Variant or str or None, str, str]] 79 | """ 80 | return [ 81 | (self._pkgs.get(scope, scope), key, value) 82 | for scope, key, value in self._envs 83 | ] 84 | 85 | def setenv(self, key, value): 86 | self._envs.append((self._scope, key, value)) 87 | if key.startswith("REZ_") and key.endswith("_ORIG_ROOT"): 88 | # is a cached package (just a note for now) 89 | pass 90 | 91 | def prependenv(self, key, value): 92 | self._envs.append((self._scope, key, value)) 93 | 94 | def appendenv(self, key, value): 95 | self._envs.append((self._scope, key, value)) 96 | 97 | def unsetenv(self, key): 98 | pass 99 | 100 | def resetenv(self, key, value, friends=None): 101 | pass 102 | 103 | def info(self, value): 104 | pass 105 | 106 | def error(self, value): 107 | pass 108 | 109 | def command(self, value): 110 | pass 111 | 112 | def comment(self, value): 113 | # header comment 114 | sys_setup = "system setup" 115 | variables = "package variables" 116 | pre_commands = "pre_commands" 117 | commands = "commands" 118 | post_commands = "post_commands" 119 | ephemeral = "ephemeral variables" 120 | post_sys_setup = "post system setup" 121 | # minor header comment 122 | pkg_variables = "variables for package " 123 | pkg_pre_commands = "pre_commands from package " 124 | pkg_commands = "commands from package " 125 | pkg_post_commands = "post_commands from package " 126 | 127 | if value in (sys_setup, variables): 128 | self._scope = "system" 129 | 130 | elif value in (pre_commands, commands, post_commands): 131 | pass 132 | 133 | elif value.startswith(pkg_variables): 134 | self._scope = value[len(pkg_variables):] 135 | 136 | elif value.startswith(pkg_pre_commands): 137 | self._scope = value[len(pkg_pre_commands):] 138 | 139 | elif value.startswith(pkg_commands): 140 | self._scope = value[len(pkg_commands):] 141 | 142 | elif value.startswith(pkg_post_commands): 143 | self._scope = value[len(pkg_post_commands):] 144 | 145 | elif value in (ephemeral, post_sys_setup): 146 | self._scope = "post-system" 147 | 148 | def source(self, value): 149 | pass 150 | 151 | def alias(self, key, value): 152 | pass 153 | 154 | def shebang(self): 155 | pass 156 | 157 | def get_key_token(self, key): 158 | return "${%s}" % key # It's just here because the API requires it. 159 | 160 | def _bind_interactive_rez(self): 161 | pass 162 | 163 | def _saferefenv(self, key): 164 | pass 165 | -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import time 4 | import unittest 5 | import contextlib 6 | 7 | 8 | MEMORY_LOCATION = "memory@any" 9 | 10 | 11 | def memory_repository(packages): 12 | from rezplugins.package_repository import memory 13 | from allzpark import _rezapi as rez 14 | 15 | class MemoryVariantRes(memory.MemoryVariantResource): 16 | def _root(self): # implement `root` to work with localz 17 | return MEMORY_LOCATION 18 | 19 | manager = rez.package_repository_manager 20 | repository = manager.get_repository(MEMORY_LOCATION) 21 | repository.pool.resource_classes[MemoryVariantRes.key] = MemoryVariantRes 22 | repository.data = packages 23 | 24 | 25 | class TestBase(unittest.TestCase): 26 | 27 | def setUp(self): 28 | from allzpark import cli 29 | 30 | os.environ["ALLZPARK_PREFERENCES_NAME"] = "preferences_test" 31 | os.environ["REZ_PACKAGES_PATH"] = MEMORY_LOCATION 32 | 33 | app, ctrl = cli.initialize(clean=True, verbose=3) 34 | window = cli.launch(ctrl) 35 | 36 | size = window.size() 37 | window.resize(size.width() + 80, size.height() + 80) 38 | 39 | self.app = app 40 | self.ctrl = ctrl 41 | self.window = window 42 | self.patched_allzparkconfig = dict() 43 | 44 | self.wait(timeout=50) 45 | 46 | def tearDown(self): 47 | self.wait(timeout=500) 48 | self.window.close() 49 | self.ctrl.deleteLater() 50 | self.window.deleteLater() 51 | self._restore_allzparkconfig() 52 | time.sleep(0.1) 53 | 54 | def _restore_allzparkconfig(self): 55 | from allzpark import allzparkconfig 56 | 57 | for name, value in self.patched_allzparkconfig.items(): 58 | setattr(allzparkconfig, name, value) 59 | 60 | self.patched_allzparkconfig.clear() 61 | 62 | def patch_allzparkconfig(self, name, value): 63 | from allzpark import allzparkconfig 64 | 65 | if name not in self.patched_allzparkconfig: 66 | original = getattr(allzparkconfig, name) 67 | self.patched_allzparkconfig[name] = original 68 | 69 | setattr(allzparkconfig, name, value) 70 | 71 | def set_preference(self, name, value): 72 | preferences = self.window._docks["preferences"] 73 | arg = next((opt for opt in preferences.options 74 | if opt["name"] == name), None) 75 | if not arg: 76 | self.fail("Preference doesn't have this setting: %s" % name) 77 | 78 | try: 79 | arg.write(value) 80 | except Exception as e: 81 | self.fail("Preference '%s' set failed: %s" % (name, str(e))) 82 | 83 | def show_dock(self, name, on_page=None): 84 | dock = self.window._docks[name] 85 | dock.toggle.setChecked(True) 86 | dock.toggle.clicked.emit() 87 | self.wait(timeout=50) 88 | 89 | if on_page is not None: 90 | tabs = dock._panels["central"] 91 | page = dock._pages[on_page] 92 | index = tabs.indexOf(page) 93 | tabs.tabBar().setCurrentIndex(index) 94 | 95 | return dock 96 | 97 | def ctrl_reset(self, profiles): 98 | with self.wait_signal(self.ctrl.resetted): 99 | self.ctrl.reset(profiles) 100 | self.wait(timeout=200) 101 | self.assertEqual(self.ctrl.state.state, "ready") 102 | 103 | def select_application(self, app_request): 104 | apps = self.window._widgets["apps"] 105 | proxy = apps.model() 106 | model = proxy.sourceModel() 107 | index = model.findIndex(app_request) 108 | index = proxy.mapFromSource(index) 109 | 110 | sel_model = apps.selectionModel() 111 | sel_model.select(index, sel_model.ClearAndSelect | sel_model.Rows) 112 | self.wait(50) 113 | 114 | def wait(self, timeout=1000): 115 | from allzpark.vendor.Qt import QtCore 116 | 117 | loop = QtCore.QEventLoop(self.window) 118 | timer = QtCore.QTimer(self.window) 119 | 120 | def on_timeout(): 121 | timer.stop() 122 | loop.quit() 123 | 124 | timer.timeout.connect(on_timeout) 125 | timer.start(timeout) 126 | loop.exec_() 127 | 128 | @contextlib.contextmanager 129 | def wait_signal(self, signal, on_value=None, timeout=1000): 130 | from allzpark.vendor.Qt import QtCore 131 | 132 | loop = QtCore.QEventLoop(self.window) 133 | timer = QtCore.QTimer(self.window) 134 | state = {"received": False} 135 | 136 | if on_value is None: 137 | def trigger(*args): 138 | state["received"] = True 139 | timer.stop() 140 | loop.quit() 141 | else: 142 | def trigger(value): 143 | if value == on_value: 144 | state["received"] = True 145 | timer.stop() 146 | loop.quit() 147 | 148 | def on_timeout(): 149 | timer.stop() 150 | loop.quit() 151 | self.fail("Signal waiting timeout.") 152 | 153 | signal.connect(trigger) 154 | timer.timeout.connect(on_timeout) 155 | 156 | try: 157 | yield 158 | finally: 159 | if not state["received"]: 160 | timer.start(timeout) 161 | loop.exec_() 162 | 163 | def get_menu(self, widget): 164 | from allzpark.vendor.Qt import QtWidgets 165 | menus = widget.findChildren(QtWidgets.QMenu, "") 166 | menu = next((m for m in menus if m.isVisible()), None) 167 | if menu: 168 | return menu 169 | else: 170 | self.fail("This widget doesn't have menu.") 171 | 172 | 173 | @contextlib.contextmanager 174 | def patch_cursor_pos(point): 175 | from allzpark.vendor.Qt import QtGui 176 | 177 | origin_pos = getattr(QtGui.QCursor, "pos") 178 | setattr(QtGui.QCursor, "pos", lambda: point) 179 | try: 180 | yield 181 | finally: 182 | setattr(QtGui.QCursor, "pos", origin_pos) 183 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/icons/rez_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 64 | 69 | 70 | 76 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: 3 | 4 | # Already default, but made explicit here 5 | branches: 6 | include: ["*"] 7 | 8 | # Ensure Azure triggers a build on a new tag 9 | # We use these for GitHub releases 10 | tags: 11 | include: ["*"] 12 | 13 | paths: 14 | # Do not trigger a build on changes at these paths 15 | exclude: 16 | - docs/* 17 | - .gitignore 18 | - LICENSE.txt 19 | - README.md 20 | 21 | 22 | jobs: 23 | 24 | # ----------------------------------------------------------------------- 25 | # 26 | # Test 27 | # 28 | # ----------------------------------------------------------------------- 29 | 30 | - job: Ubuntu 31 | pool: 32 | vmImage: "ubuntu-20.04" # Focal 33 | strategy: 34 | matrix: 35 | Py35-Rez: 36 | python.version: "3.5" 37 | rez.project: "rez" 38 | 39 | Py35-BleedingRez: 40 | python.version: "3.5" 41 | rez.project: "bleeding-rez" 42 | 43 | Py36-Rez: 44 | python.version: "3.6" 45 | rez.project: "rez" 46 | 47 | Py36-BleedingRez: 48 | python.version: "3.6" 49 | rez.project: "bleeding-rez" 50 | 51 | Py37-Rez: 52 | python.version: "3.7" 53 | rez.project: "rez" 54 | 55 | Py37-BleedingRez: 56 | python.version: "3.7" 57 | rez.project: "bleeding-rez" 58 | 59 | steps: 60 | - task: UsePythonVersion@0 61 | inputs: 62 | versionSpec: "$(python.version)" 63 | displayName: "Use Python $(python.version)" 64 | 65 | - script: | 66 | git clone https://github.com/nerdvegas/rez.git rez-src 67 | cd rez-src 68 | sudo pip install . 69 | condition: eq(variables['rez.project'], 'rez') 70 | displayName: "Install rez (pip for API)" 71 | 72 | - script: | 73 | sudo pip install bleeding-rez 74 | condition: eq(variables['rez.project'], 'bleeding-rez') 75 | displayName: "Install bleeding-rez" 76 | 77 | - script: | 78 | sudo apt-get install python-pyside 79 | sudo python -c "from PySide import QtCore;print(QtCore.__version__)" 80 | condition: startsWith(variables['python.version'], '2.') 81 | displayName: "Install PySide" 82 | 83 | - script: | 84 | sudo apt-get install python3-pyside2.qtcore \ 85 | python3-pyside2.qtgui \ 86 | python3-pyside2.qtwidgets \ 87 | python3-pyside2.qtsvg 88 | sudo pip install pyside2 89 | sudo python -c "from PySide2 import QtCore;print(QtCore.__version__)" 90 | condition: startsWith(variables['python.version'], '3.') 91 | displayName: "Install PySide2" 92 | 93 | - script: | 94 | sudo pip install nose 95 | displayName: "Install test tools" 96 | 97 | - script: | 98 | sudo pip install . --no-deps 99 | displayName: "Install allzpark" 100 | 101 | - script: | 102 | sudo apt-get install xvfb 103 | displayName: "Setup Xvfb" 104 | 105 | - script: | 106 | export DISPLAY=:99 107 | xvfb-run sudo nosetests 108 | displayName: "Run tests" 109 | 110 | 111 | - job: MacOS 112 | pool: 113 | vmImage: "macOS-10.15" 114 | strategy: 115 | matrix: 116 | Py37-Rez: 117 | python.version: "3.7" 118 | rez.project: "rez" 119 | 120 | Py37-BleedingRez: 121 | python.version: "3.7" 122 | rez.project: "bleeding-rez" 123 | 124 | steps: 125 | - task: UsePythonVersion@0 126 | inputs: 127 | versionSpec: "$(python.version)" 128 | displayName: "Use Python $(python.version)" 129 | 130 | - script: | 131 | git clone https://github.com/nerdvegas/rez.git rez-src 132 | cd rez-src 133 | pip install . 134 | condition: eq(variables['rez.project'], 'rez') 135 | displayName: "Install rez (pip for API)" 136 | 137 | - script: | 138 | pip install bleeding-rez 139 | condition: eq(variables['rez.project'], 'bleeding-rez') 140 | displayName: "Install bleeding-rez" 141 | 142 | - script: | 143 | brew tap cartr/qt4 144 | brew install qt@4 145 | pip install PySide 146 | condition: startsWith(variables['python.version'], '2.') 147 | displayName: "Install PySide" 148 | 149 | - script: | 150 | pip install PySide2 151 | condition: startsWith(variables['python.version'], '3.') 152 | displayName: "Install PySide2" 153 | 154 | - script: | 155 | pip install nose 156 | displayName: "Install test tools" 157 | 158 | - script: | 159 | pip install . --no-deps 160 | displayName: "Install allzpark" 161 | 162 | - script: | 163 | nosetests 164 | displayName: "Run tests" 165 | 166 | - job: Windows 167 | pool: 168 | vmImage: vs2017-win2016 169 | strategy: 170 | matrix: 171 | Py37-Rez: 172 | python.version: "3.7" 173 | rez.project: "rez" 174 | 175 | Py37-BleedingRez: 176 | python.version: "3.7" 177 | rez.project: "bleeding-rez" 178 | 179 | steps: 180 | - task: UsePythonVersion@0 181 | inputs: 182 | versionSpec: "$(python.version)" 183 | displayName: "Use Python $(python.version)" 184 | 185 | - script: | 186 | git clone https://github.com/nerdvegas/rez.git rez-src 187 | cd rez-src 188 | pip install . 189 | condition: eq(variables['rez.project'], 'rez') 190 | displayName: "Install rez (pip for API)" 191 | 192 | - script: | 193 | pip install bleeding-rez 194 | condition: eq(variables['rez.project'], 'bleeding-rez') 195 | displayName: "Install bleeding-rez" 196 | 197 | - script: | 198 | pip install PySide 199 | condition: startsWith(variables['python.version'], '2.') 200 | displayName: "Install PySide" 201 | 202 | - script: | 203 | pip install PySide2 204 | condition: startsWith(variables['python.version'], '3.') 205 | displayName: "Install PySide2" 206 | 207 | - script: | 208 | pip install nose 209 | displayName: "Install test tools" 210 | 211 | - script: | 212 | pip install . --no-deps 213 | displayName: "Install allzpark" 214 | 215 | - script: | 216 | nosetests 217 | displayName: "Run tests" 218 | 219 | 220 | # ----------------------------------------------------------------------- 221 | # 222 | # Deploy to PyPI 223 | # 224 | # ----------------------------------------------------------------------- 225 | 226 | - job: Deploy 227 | condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') 228 | pool: 229 | vmImage: "ubuntu-latest" 230 | strategy: 231 | matrix: 232 | Python37: 233 | python.version: "3.7" 234 | 235 | steps: 236 | - task: UsePythonVersion@0 237 | inputs: 238 | versionSpec: "$(python.version)" 239 | displayName: "Use Python $(python.version)" 240 | 241 | - script: | 242 | pip install wheel twine 243 | python setup.py sdist bdist_wheel 244 | echo [distutils] > ~/.pypirc 245 | echo index-servers=pypi >> ~/.pypirc 246 | echo [pypi] >> ~/.pypirc 247 | echo username=$_LOGIN >> ~/.pypirc 248 | echo password=$_PASSWORD >> ~/.pypirc 249 | twine upload dist/* 250 | displayName: "Deploy to PyPI" 251 | 252 | # Decrypt secret variables provided by Azure web console 253 | env: 254 | _LOGIN: $(PYPI_LOGIN) 255 | _PASSWORD: $(PYPI_PASSWORD) 256 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/allzpark/gui/_vendor/qjsonmodel.py: -------------------------------------------------------------------------------- 1 | """Python adaptation of https://github.com/dridk/QJsonModel 2 | 3 | Supports Python 2 and 3 with PySide, PySide2, PyQt4 or PyQt5. 4 | Requires https://github.com/mottosso/Qt.py 5 | 6 | Usage: 7 | Use it like you would the C++ version. 8 | 9 | >>> import qjsonmodel 10 | >>> model = qjsonmodel.QJsonModel() 11 | >>> model.load({"key": "value"}) 12 | 13 | Test: 14 | Run the provided example to sanity check your Python, 15 | dependencies and Qt binding. 16 | 17 | $ python qjsonmodel.py 18 | 19 | Changes: 20 | This module differs from the C++ version in the following ways. 21 | 22 | 1. Setters and getters are replaced by Python properties 23 | 2. Objects are sorted by default, disabled via load(sort=False) 24 | 3. load() takes a Python dictionary as opposed to 25 | a string or file handle. 26 | 27 | - To load from a string, use built-in `json.loads()` 28 | >>> import json 29 | >>> document = json.loads("{'key': 'value'}") 30 | >>> model.load(document) 31 | 32 | - To load from a file, use `with open(fname)` 33 | >>> import json 34 | >>> with open("file.json") as f: 35 | ... document = json.load(f) 36 | ... model.load(document) 37 | 38 | """ 39 | 40 | import json 41 | 42 | from .Qt5 import QtWidgets, QtCore, __binding__ 43 | 44 | 45 | class QJsonTreeItem(object): 46 | def __init__(self, parent=None): 47 | self._parent = parent 48 | 49 | self._key = "" 50 | self._value = "" 51 | self._type = None 52 | self._children = list() 53 | 54 | def appendChild(self, item): 55 | self._children.append(item) 56 | 57 | def child(self, row): 58 | return self._children[row] 59 | 60 | def parent(self): 61 | return self._parent 62 | 63 | def childCount(self): 64 | return len(self._children) 65 | 66 | def row(self): 67 | return ( 68 | self._parent._children.index(self) 69 | if self._parent else 0 70 | ) 71 | 72 | @property 73 | def key(self): 74 | return self._key 75 | 76 | @key.setter 77 | def key(self, key): 78 | self._key = key 79 | 80 | @property 81 | def value(self): 82 | return self._value 83 | 84 | @value.setter 85 | def value(self, value): 86 | self._value = value 87 | 88 | @property 89 | def type(self): 90 | return self._type 91 | 92 | @type.setter 93 | def type(self, typ): 94 | self._type = typ 95 | 96 | @classmethod 97 | def load(self, value, parent=None, sort=True): 98 | rootItem = QJsonTreeItem(parent) 99 | rootItem.key = "root" 100 | 101 | if isinstance(value, dict): 102 | items = ( 103 | sorted(value.items()) 104 | if sort else value.items() 105 | ) 106 | 107 | for key, value in items: 108 | child = self.load(value, rootItem) 109 | child.key = key 110 | child.type = type(value) 111 | rootItem.appendChild(child) 112 | 113 | elif isinstance(value, list): 114 | for index, value in enumerate(value): 115 | child = self.load(value, rootItem) 116 | child.key = index 117 | child.type = type(value) 118 | rootItem.appendChild(child) 119 | 120 | else: 121 | rootItem.value = value 122 | rootItem.type = type(value) 123 | 124 | return rootItem 125 | 126 | 127 | class QJsonModel(QtCore.QAbstractItemModel): 128 | def __init__(self, parent=None): 129 | super(QJsonModel, self).__init__(parent) 130 | 131 | self._rootItem = QJsonTreeItem() 132 | self._headers = ("key", "value") 133 | 134 | def clear(self): 135 | self.load({}) 136 | 137 | def load(self, document): 138 | """Load from dictionary 139 | 140 | Arguments: 141 | document (dict): JSON-compatible dictionary 142 | 143 | """ 144 | 145 | assert isinstance(document, (dict, list, tuple)), ( 146 | "`document` must be of dict, list or tuple, " 147 | "not %s" % type(document) 148 | ) 149 | 150 | self.beginResetModel() 151 | 152 | self._rootItem = QJsonTreeItem.load(document) 153 | self._rootItem.type = type(document) 154 | 155 | self.endResetModel() 156 | 157 | return True 158 | 159 | def json(self, root=None): 160 | """Serialise model as JSON-compliant dictionary 161 | 162 | Arguments: 163 | root (QJsonTreeItem, optional): Serialise from here 164 | defaults to the the top-level item 165 | 166 | Returns: 167 | model as dict 168 | 169 | """ 170 | 171 | root = root or self._rootItem 172 | return self.genJson(root) 173 | 174 | def data(self, index, role): 175 | if not index.isValid(): 176 | return None 177 | 178 | item = index.internalPointer() 179 | 180 | if role == QtCore.Qt.DisplayRole: 181 | if index.column() == 0: 182 | return item.key 183 | 184 | if index.column() == 1: 185 | return item.value 186 | 187 | elif role == QtCore.Qt.EditRole: 188 | if index.column() == 1: 189 | return item.value 190 | 191 | def setData(self, index, value, role): 192 | if role == QtCore.Qt.EditRole: 193 | if index.column() == 1: 194 | item = index.internalPointer() 195 | item.value = str(value) 196 | 197 | if __binding__ in ("PySide", "PyQt4"): 198 | self.dataChanged.emit(index, index) 199 | else: 200 | self.dataChanged.emit(index, index, [QtCore.Qt.EditRole]) 201 | 202 | return True 203 | 204 | return False 205 | 206 | def headerData(self, section, orientation, role): 207 | if role != QtCore.Qt.DisplayRole: 208 | return None 209 | 210 | if orientation == QtCore.Qt.Horizontal: 211 | return self._headers[section] 212 | 213 | def index(self, row, column, parent=QtCore.QModelIndex()): 214 | if not self.hasIndex(row, column, parent): 215 | return QtCore.QModelIndex() 216 | 217 | if not parent.isValid(): 218 | parentItem = self._rootItem 219 | else: 220 | parentItem = parent.internalPointer() 221 | 222 | childItem = parentItem.child(row) 223 | if childItem: 224 | return self.createIndex(row, column, childItem) 225 | else: 226 | return QtCore.QModelIndex() 227 | 228 | def parent(self, index): 229 | if not index.isValid(): 230 | return QtCore.QModelIndex() 231 | 232 | childItem = index.internalPointer() 233 | parentItem = childItem.parent() 234 | 235 | if parentItem == self._rootItem: 236 | return QtCore.QModelIndex() 237 | 238 | return self.createIndex(parentItem.row(), 0, parentItem) 239 | 240 | def rowCount(self, parent=QtCore.QModelIndex()): 241 | if parent.column() > 0: 242 | return 0 243 | 244 | if not parent.isValid(): 245 | parentItem = self._rootItem 246 | else: 247 | parentItem = parent.internalPointer() 248 | 249 | return parentItem.childCount() 250 | 251 | def columnCount(self, parent=QtCore.QModelIndex()): 252 | return 2 253 | 254 | def flags(self, index): 255 | flags = super(QJsonModel, self).flags(index) 256 | 257 | if index.column() == 1: 258 | return QtCore.Qt.ItemIsEditable | flags 259 | else: 260 | return flags 261 | 262 | def genJson(self, item): 263 | nchild = item.childCount() 264 | 265 | if item.type is dict: 266 | document = {} 267 | for i in range(nchild): 268 | ch = item.child(i) 269 | document[ch.key] = self.genJson(ch) 270 | return document 271 | 272 | elif item.type == list: 273 | document = [] 274 | for i in range(nchild): 275 | ch = item.child(i) 276 | document.append(self.genJson(ch)) 277 | return document 278 | 279 | else: 280 | return item.value 281 | 282 | 283 | if __name__ == '__main__': 284 | import sys 285 | 286 | app = QtWidgets.QApplication(sys.argv) 287 | view = QtWidgets.QTreeView() 288 | model = QJsonModel() 289 | 290 | view.setModel(model) 291 | 292 | document = json.loads("""\ 293 | { 294 | "firstName": "John", 295 | "lastName": "Smith", 296 | "age": 25, 297 | "address": { 298 | "streetAddress": "21 2nd Street", 299 | "city": "New York", 300 | "state": "NY", 301 | "postalCode": "10021" 302 | }, 303 | "phoneNumber": [ 304 | { 305 | "type": "home", 306 | "number": "212 555-1234" 307 | }, 308 | { 309 | "type": "fax", 310 | "number": "646 555-4567" 311 | } 312 | ] 313 | } 314 | """) 315 | 316 | model.load(document) 317 | model.clear() 318 | model.load(document) 319 | 320 | # Sanity check 321 | assert ( 322 | json.dumps(model.json(), sort_keys=True) == 323 | json.dumps(document, sort_keys=True) 324 | ) 325 | 326 | view.show() 327 | view.resize(500, 300) 328 | app.exec_() 329 | -------------------------------------------------------------------------------- /src/allzpark/gui/app.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | import json 5 | import logging 6 | import traceback 7 | import signal as py_signal 8 | from typing import Optional 9 | from importlib import reload 10 | from contextlib import contextmanager 11 | 12 | from .. import core, util 13 | from ..exceptions import BackendError 14 | from ._vendor.Qt5 import QtCore, QtWidgets 15 | from . import control, window, widgets, resources 16 | 17 | 18 | if sys.platform == "darwin": 19 | os.environ["QT_MAC_WANTS_LAYER"] = "1" # MacOS BigSur 20 | 21 | 22 | log = logging.getLogger("allzpark") 23 | 24 | 25 | def launch(app_name="park-gui"): 26 | """GUI entry point 27 | 28 | :param app_name: Application name. Used to compose the location of current 29 | user specific settings file. Default is "park-gui". 30 | :type app_name: str or None 31 | :return: QApplication exit code 32 | :rtype: int 33 | """ 34 | with util.log_level(logging.INFO): 35 | ses = Session(app_name=app_name) 36 | ses.show() 37 | return ses.app.exec_() 38 | 39 | 40 | class Session(object): 41 | 42 | def __init__(self, app_name="park-gui"): 43 | app = QtWidgets.QApplication.instance() 44 | if app is None: 45 | app = QtWidgets.QApplication([]) 46 | app.setStyle(AppProxyStyle()) 47 | 48 | # allow user to interrupt with Ctrl+C 49 | def sigint_handler(signals, frame): 50 | sys.exit(app.exit(-1)) 51 | 52 | py_signal.signal(py_signal.SIGINT, sigint_handler) 53 | 54 | # sharpen icons/images 55 | # * the .svg file ext is needed in file path for Qt to auto scale it. 56 | # * without file ext given for svg file, may need to hard-coding attr 57 | # like width/height/viewBox attr in that svg file. 58 | # * without the Qt attr below, .svg may being rendered as they were 59 | # low-res. 60 | app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) 61 | 62 | # init 63 | 64 | storage = QtCore.QSettings(QtCore.QSettings.IniFormat, 65 | QtCore.QSettings.UserScope, 66 | app_name, "preferences") 67 | log.info("Preference file: %s" % storage.fileName()) 68 | 69 | state = State(storage=storage) 70 | resources.load_themes() 71 | 72 | try: 73 | backend_entrances = core.init_backends() 74 | except BackendError as e: 75 | log.error(str(e)) 76 | sys.exit(1) 77 | 78 | try: 79 | history = state.retrieve_history() 80 | except Exception as e: 81 | log.error(traceback.format_exc()) 82 | log.error(f"Failed to retrieve workspace history: {str(e)}") 83 | history = [] 84 | 85 | ctrl = control.Controller(backends=backend_entrances) 86 | view_ = window.MainWindow(state=state) 87 | 88 | # signals 89 | 90 | workspace = view_.find(widgets.WorkspaceWidget) 91 | work_dir = view_.find(widgets.WorkDirWidget) 92 | work_history = view_.find(widgets.WorkHistoryWidget) 93 | tool_list = view_.find(widgets.ToolsView) 94 | tool_context = view_.find(widgets.ToolContextWidget) 95 | tool_launcher = view_.find(widgets.ToolLaunchWidget) 96 | clear_cache = view_.find(widgets.ClearCacheWidget) 97 | busy_filter = widgets.BusyEventFilterSingleton() 98 | 99 | # view -> control 100 | workspace.workspace_changed.connect(ctrl.on_workspace_changed) 101 | workspace.workspace_refreshed.connect(ctrl.on_workspace_refreshed) 102 | workspace.backend_changed.connect(ctrl.on_backend_changed) 103 | workspace.tools_requested.connect(ctrl.on_scope_tools_requested) 104 | clear_cache.clear_clicked.connect(ctrl.on_cache_clear_clicked) 105 | tool_list.tool_selected.connect(ctrl.on_tool_selected) 106 | tool_list.tool_launched.connect(tool_launcher.launch_tool) 107 | work_history.tool_selected.connect(ctrl.on_tool_selected) 108 | work_history.tool_launched.connect(tool_launcher.launch_tool) 109 | tool_launcher.tool_launched.connect(ctrl.on_tool_launched) 110 | tool_launcher.shell_launched.connect(ctrl.on_shell_launched) 111 | 112 | # control -> view 113 | ctrl.workspace_entered.connect(workspace.on_workspace_entered) 114 | ctrl.workspace_updated.connect(workspace.on_workspace_updated) 115 | ctrl.tools_updated.connect(tool_list.on_tools_updated) 116 | ctrl.work_dir_obtained.connect(work_dir.on_work_dir_obtained) 117 | ctrl.work_dir_resetted.connect(work_dir.on_work_dir_resetted) 118 | ctrl.tool_selected.connect(tool_context.on_tool_selected) 119 | ctrl.cache_cleared.connect(tool_list.on_cache_cleared) 120 | ctrl.cache_cleared.connect(workspace.on_cache_cleared) 121 | ctrl.history_made.connect(work_history.on_history_made) 122 | ctrl.history_updated.connect(work_history.on_history_updated) 123 | 124 | # view -> view 125 | view_.dark_toggled.connect(self.on_dark_toggled) 126 | tool_list.tool_cleared.connect(tool_context.on_tool_cleared) 127 | work_history.tool_cleared.connect(tool_context.on_tool_cleared) 128 | work_history.history_saved.connect(self.on_history_saved) 129 | 130 | # status bar messages 131 | ctrl.status_message.connect(view_.spoken) 132 | busy_filter.overwhelmed.connect(view_.spoken) 133 | tool_context.env_hovered.connect(view_.spoken) 134 | 135 | self._app = app 136 | self._ctrl = ctrl 137 | self._view = view_ 138 | self._state = state 139 | 140 | # kick start 141 | workspace.register_backends(names=[ 142 | name for name, _ in backend_entrances] 143 | ) 144 | ctrl.on_history_refreshed(history) 145 | 146 | self.apply_theme() 147 | 148 | @property 149 | def app(self): 150 | return self._app 151 | 152 | @property 153 | def ctrl(self): 154 | return self._ctrl 155 | 156 | @property 157 | def view(self): 158 | return self._view 159 | 160 | @property 161 | def state(self): 162 | return self._state 163 | 164 | def on_history_saved(self, history): 165 | self._state.store_history(history) 166 | 167 | def on_dark_toggled(self, value): 168 | self._state.store_dark_mode(value) 169 | self.apply_theme(dark=value) 170 | self._view.on_status_changed(self._view.statusBar().currentMessage()) 171 | 172 | def apply_theme(self, name=None, dark=None): 173 | view = self._view 174 | name = name or self.state.retrieve("theme") 175 | dark = self.state.retrieve_dark_mode() if dark is None else dark 176 | qss = resources.get_style_sheet(name, dark) 177 | view.setStyleSheet(qss) 178 | view.style().unpolish(view) 179 | view.style().polish(view) 180 | self.state.store("theme", resources.current_theme().name) 181 | 182 | def reload_theme(self): 183 | """For look-dev""" 184 | reload(resources) 185 | resources.load_themes() 186 | self.apply_theme() 187 | 188 | def show(self): 189 | view = self._view 190 | view.show() 191 | 192 | # If the window is minimized then un-minimize it. 193 | if view.windowState() & QtCore.Qt.WindowMinimized: 194 | view.setWindowState(QtCore.Qt.WindowActive) 195 | 196 | view.raise_() # for MacOS 197 | view.activateWindow() # for Windows 198 | 199 | def process(self, events=QtCore.QEventLoop.AllEvents): 200 | self._app.eventDispatcher().processEvents(events) 201 | 202 | def close(self): 203 | self._app.closeAllWindows() 204 | self._app.quit() 205 | 206 | 207 | class State(object): 208 | """Store/re-store Application status in/between sessions""" 209 | 210 | def __init__(self, storage): 211 | """ 212 | :param storage: An QtCore.QSettings instance for save/load settings 213 | between sessions. 214 | :type storage: QtCore.QSettings 215 | """ 216 | self._storage = storage 217 | 218 | def _f(self, value): 219 | # Account for poor serialisation format 220 | true = ["2", "1", "true", True, 1, 2] 221 | false = ["0", "false", False, 0] 222 | 223 | if value in true: 224 | value = True 225 | 226 | if value in false: 227 | value = False 228 | 229 | if value and str(value).isnumeric(): 230 | value = float(value) 231 | 232 | return value 233 | 234 | @contextmanager 235 | def group(self, key): 236 | self._storage.beginGroup(key) 237 | try: 238 | yield 239 | finally: 240 | self._storage.endGroup() 241 | 242 | def is_writeable(self): 243 | return self._storage.isWritable() 244 | 245 | def store(self, key, value): 246 | self._storage.setValue(key, value) 247 | 248 | def retrieve(self, key, default=None): 249 | value = self._storage.value(key) 250 | if value is None: 251 | value = default 252 | return self._f(value) 253 | 254 | def retrieve_dark_mode(self): 255 | return bool(self.retrieve("theme.on_dark")) 256 | 257 | def store_dark_mode(self, value): 258 | self.store("theme.on_dark", bool(value)) 259 | 260 | def retrieve_history(self): 261 | sep = "" 262 | history = self.retrieve("workspace.history", "") 263 | return [ 264 | json.loads(entry) 265 | for entry in history.split(sep) 266 | if entry 267 | ] 268 | 269 | def store_history(self, history): 270 | sep = "" 271 | self.store("workspace.history", 272 | sep.join(json.dumps(entry) for entry in history)) 273 | 274 | def preserve_layout(self, widget, group): 275 | # type: (QtWidgets.QWidget, str) -> None 276 | if not self.is_writeable(): 277 | # todo: prompt warning 278 | return 279 | 280 | self._storage.beginGroup(group) 281 | 282 | self.store("geometry", widget.saveGeometry()) 283 | if hasattr(widget, "saveState"): 284 | self.store("state", widget.saveState()) 285 | if hasattr(widget, "directory"): # QtWidgets.QFileDialog 286 | self.store("directory", widget.directory()) 287 | 288 | self._storage.endGroup() 289 | 290 | def restore_layout(self, widget, group, keep_geo=False): 291 | # type: (QtWidgets.QWidget, str, bool) -> None 292 | self._storage.beginGroup(group) 293 | 294 | keys = self._storage.allKeys() 295 | 296 | if not keep_geo and "geometry" in keys: 297 | widget.restoreGeometry(self.retrieve("geometry")) 298 | if "state" in keys and hasattr(widget, "restoreState"): 299 | widget.restoreState(self.retrieve("state")) 300 | if "directory" in keys and hasattr(widget, "setDirectory"): 301 | widget.setDirectory(self.retrieve("directory")) 302 | 303 | self._storage.endGroup() 304 | 305 | 306 | class AppProxyStyle(QtWidgets.QProxyStyle): 307 | """For styling QComboBox 308 | https://stackoverflow.com/a/21019371 309 | """ 310 | def styleHint( 311 | self, 312 | hint: QtWidgets.QStyle.StyleHint, 313 | option: Optional[QtWidgets.QStyleOption] = ..., 314 | widget: Optional[QtWidgets.QWidget] = ..., 315 | returnData: Optional[QtWidgets.QStyleHintReturn] = ...,) -> int: 316 | 317 | if hint == QtWidgets.QStyle.SH_ComboBox_Popup: 318 | return 0 319 | 320 | return super(AppProxyStyle, self).styleHint( 321 | hint, option, widget, returnData) 322 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/fonts/jetbrainsmono/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/allzpark/gui/resources/fonts/opensans/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/allzpark/backend_sg_sync.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import time 4 | import logging 5 | import getpass 6 | from shotgun_api3 import Shotgun 7 | from dataclasses import dataclass 8 | from functools import singledispatch 9 | from collections import MutableMapping 10 | from typing import Union, Iterator, Callable, Set, overload 11 | from rez.config import config as rezconfig 12 | from .core import SuiteTool, AbstractScope 13 | from .util import elide 14 | from .exceptions import BackendError 15 | 16 | # typing 17 | ToolFilterCallable = Callable[[SuiteTool], bool] 18 | 19 | log = logging.getLogger("allzpark") 20 | 21 | 22 | parkconfig = rezconfig.plugins.command.park 23 | 24 | 25 | def get_entrance(sg_server=None, api_key=None, script_name=None): 26 | """ 27 | 28 | :param str sg_server: 29 | :param str api_key: 30 | :param str script_name: 31 | :return: 32 | :rtype: Entrance 33 | """ 34 | sg_server = sg_server or os.getenv("SHOTGRID_SERVER") 35 | api_key = api_key or os.getenv("SHOTGRID_APIKEY") 36 | script_name = script_name or os.getenv("SHOTGRID_SCRIPT") 37 | 38 | if not sg_server: 39 | raise BackendError("ShotGrid server URL not given.") 40 | if not api_key: 41 | raise BackendError("ShotGrid API key not given.") 42 | if not script_name: 43 | raise BackendError("ShotGrid script-name not given.") 44 | 45 | return Entrance(sg_server=sg_server, 46 | api_key=api_key, 47 | script_name=script_name) 48 | 49 | 50 | MANAGER_ROLE = "admin" 51 | 52 | 53 | @dataclass(frozen=True) 54 | class _Scope(AbstractScope): 55 | 56 | def exists(self) -> bool: 57 | return True 58 | 59 | def iter_children(self: "Entrance") -> Iterator["Project"]: 60 | """Iter child scopes 61 | """ 62 | return iter_shotgrid_scopes(self) 63 | 64 | @overload 65 | def suite_path(self: "Entrance") -> str: 66 | ... 67 | 68 | @overload 69 | def suite_path(self: "Project") -> None: 70 | ... 71 | 72 | def suite_path(self): 73 | """Iter child scopes 74 | 75 | :type self: Entrance or Project 76 | :return: 77 | :rtype: Union[str, None] 78 | """ 79 | return scope_suite_path(self) 80 | 81 | def make_tool_filter( 82 | self: Union["Entrance", "Project"] 83 | ) -> ToolFilterCallable: 84 | """ 85 | :return: 86 | """ 87 | return tool_filter_factory(self) 88 | 89 | def obtain_workspace( 90 | self: Union["Entrance", "Project"], 91 | tool: SuiteTool = None, 92 | ) -> Union[str, None]: 93 | """ 94 | 95 | :param tool: 96 | :type tool: SuiteTool 97 | :type self: Entrance or Project or Asset or Task 98 | :return: 99 | :rtype: str or None 100 | """ 101 | return obtain_workspace(self, tool) 102 | 103 | def additional_env( 104 | self: Union["Entrance", "Project"], tool: SuiteTool 105 | ) -> dict: 106 | """ 107 | 108 | :param tool: 109 | :type tool: SuiteTool 110 | :type self: Entrance or Project or Asset or Task 111 | :return: 112 | :rtype: dict 113 | """ 114 | if isinstance(self, Project): 115 | return { 116 | # for sg_sync 117 | "AVALON_PROJECTS": self.sg_project_root, 118 | "AVALON_PROJECT": self.tank_name, 119 | "SG_PROJECT_ID": self.id, 120 | } 121 | return dict() 122 | 123 | def current_user_roles( 124 | self: Union["Entrance", "Project", "Asset", "Task"] 125 | ) -> list: 126 | """ 127 | :type self: Entrance or Project or Asset or Task 128 | :return: 129 | :rtype: list 130 | """ 131 | return [] 132 | 133 | @dataclass(frozen=True) 134 | class Entrance(_Scope): 135 | name = "sg_sync" 136 | upstream = None 137 | sg_server: str 138 | api_key: str 139 | script_name: str 140 | 141 | def __repr__(self): 142 | return f"Entrance(name={self.name}, sg_server={self.sg_server})" 143 | 144 | def __hash__(self): 145 | return hash(repr(self)) 146 | 147 | 148 | @dataclass(frozen=True) 149 | class Project(_Scope): 150 | name: str 151 | upstream: Entrance 152 | roles: Set[str] 153 | code: str 154 | id: str 155 | tank_name: str 156 | sg_project_root: str 157 | 158 | def __repr__(self): 159 | return f"Project(" \ 160 | f"name={self.name}, tank_name={self.tank_name}, " \ 161 | f"upstream={self.upstream})" 162 | 163 | def __hash__(self): 164 | return hash(repr(self)) 165 | 166 | 167 | @singledispatch 168 | def iter_shotgrid_scopes(scope): 169 | raise NotImplementedError(f"Unknown scope type: {type(scope)}") 170 | 171 | 172 | @iter_shotgrid_scopes.register 173 | def _(scope: Entrance) -> Iterator[Project]: 174 | server = ShotGridConn(scope.sg_server, 175 | scope.script_name, 176 | scope.api_key, 177 | entrance=scope) 178 | return iter_shotgrid_projects(server) 179 | 180 | 181 | @iter_shotgrid_scopes.register 182 | def _(scope: Project) -> tuple: 183 | log.debug(f"Endpoint reached: {elide(scope)}") 184 | return () 185 | 186 | 187 | @singledispatch 188 | def scope_suite_path(scope): 189 | raise NotImplementedError(f"Unknown scope type: {type(scope)}") 190 | 191 | 192 | @scope_suite_path.register 193 | def _(scope: Entrance) -> str: 194 | _ = scope 195 | return os.getenv("SHOTGRID_ENTRANCE_SUITE") 196 | 197 | 198 | @scope_suite_path.register 199 | def _(scope: Project) -> Union[str, None]: 200 | roots = parkconfig.suite_roots # type: dict 201 | if not isinstance(roots, MutableMapping): 202 | raise BackendError("Invalid configuration, 'suite_roots' should be " 203 | f"dict-like type value, not {type(roots)}.") 204 | 205 | shotgrid_suite_root = roots.get("shotgrid") 206 | if not shotgrid_suite_root: 207 | log.debug("No suite root for ShotGrid.") 208 | return 209 | 210 | suite_path = os.path.join(shotgrid_suite_root, scope.name) 211 | if not os.path.isdir(suite_path): 212 | log.debug(f"No suite root for ShotGrid project {scope.name}") 213 | return 214 | 215 | return suite_path 216 | 217 | 218 | @singledispatch 219 | def tool_filter_factory(scope) -> ToolFilterCallable: 220 | raise NotImplementedError(f"Unknown scope type: {type(scope)}") 221 | 222 | 223 | @tool_filter_factory.register 224 | def _(scope: Entrance) -> ToolFilterCallable: 225 | def _filter(tool: SuiteTool) -> bool: 226 | required_roles = tool.metadata.required_roles 227 | ctx_category = tool.ctx_name.split(".", 1)[0] 228 | categories = {"entrance"} 229 | return ( 230 | not tool.metadata.hidden 231 | and ctx_category in categories 232 | and (not required_roles 233 | or getpass.getuser() in required_roles) 234 | ) 235 | _ = scope # consume unused arg 236 | return _filter 237 | 238 | 239 | @tool_filter_factory.register 240 | def _(scope: Project) -> ToolFilterCallable: 241 | def _filter(tool: SuiteTool) -> bool: 242 | required_roles = tool.metadata.required_roles 243 | ctx_category = tool.ctx_name.split(".", 1)[0] 244 | categories = {"project", "entrance"} 245 | return ( 246 | not tool.metadata.hidden 247 | and ctx_category in categories 248 | and (not required_roles 249 | or scope.roles.intersection(required_roles)) 250 | ) 251 | return _filter 252 | 253 | 254 | @singledispatch 255 | def obtain_workspace(scope, tool=None): 256 | _ = tool # consume unused arg 257 | raise NotImplementedError(f"Unknown scope type: {type(scope)}") 258 | 259 | 260 | @obtain_workspace.register 261 | def _(scope: Entrance, tool: SuiteTool = None) -> None: 262 | _tool = f" for {tool.name!r}" if tool else "" 263 | log.debug(f"No workspace{_tool} in scope {elide(scope)}.") 264 | return None 265 | 266 | 267 | @obtain_workspace.register 268 | def _(scope: Project, tool: SuiteTool = None) -> Union[str, None]: 269 | _ = tool 270 | root = scope.sg_project_root 271 | root += "/" if ":" in root else "" 272 | # note: instead of checking root.endswith ':', just seeing if ':' in 273 | # string let us also check if there is redundant path sep written 274 | # in ShotGrid. We are on Windows. 275 | return os.path.join(root, scope.tank_name) 276 | 277 | 278 | def iter_shotgrid_projects(server: "ShotGridConn"): 279 | for d in server.iter_valid_projects(): 280 | roles = set() 281 | username = getpass.getuser() 282 | sg_user_ids = set([ 283 | _["id"] for _ in (d.get("sg_cg_lead", []) + d.get("sg_pc", [])) 284 | ]) 285 | if username in server.find_human_logins(list(sg_user_ids)): 286 | roles.add(MANAGER_ROLE) 287 | # allow assigning personnel directly in package 288 | roles.add(username) 289 | 290 | yield Project( 291 | name=d["name"], 292 | upstream=server.entrance, 293 | roles=roles, 294 | code=d["code"], 295 | id=str(d["id"]), 296 | tank_name=d["tank_name"], 297 | sg_project_root=d["sg_project_root"] 298 | ) 299 | 300 | 301 | class ShotGridConn(object): 302 | """ShotGrid connector 303 | """ 304 | def __init__(self, sg_server, script_name, api_key, entrance=None): 305 | """ 306 | :param str sg_server: ShotGrid server URL 307 | :param str script_name: ShotGrid server URL 308 | :param str api_key: MongoDB connection timeout, default 1000 309 | :param entrance: The entrance scope that used to open this 310 | connection. Optional. 311 | :type entrance: Entrance or None 312 | """ 313 | conn = Shotgun(sg_server, 314 | script_name=script_name, 315 | api_key=api_key, 316 | connect=False) 317 | conn.config.timeout_secs = 1 318 | 319 | self.conn = conn 320 | self.sg_server = sg_server 321 | self.api_key = api_key 322 | self.script_name = script_name 323 | self.entrance = entrance 324 | 325 | def iter_valid_projects(self): 326 | fields = [ 327 | "id", 328 | "code", 329 | "name", 330 | "tank_name", 331 | "sg_project_root", 332 | "sg_cg_lead", 333 | "sg_pc", 334 | ] 335 | filters = [ 336 | ["archived", "is", False], 337 | ["is_template", "is", False], 338 | ["tank_name", "is_not", None], 339 | ["sg_project_root", "is_not", None], 340 | ["sg_status", 'is', 'active'] 341 | ] 342 | for doc in self.conn.find("Project", filters, fields): 343 | lower_name = doc["name"].lower() 344 | if lower_name.startswith("test"): 345 | continue 346 | yield doc 347 | 348 | def find_human_logins(self, user_ids): 349 | if not user_ids: 350 | return [] 351 | fields = ["login"] 352 | filters = [["id", "in", user_ids]] 353 | docs = self.conn.find("HumanUser", filters=filters, fields=fields) 354 | return [d["login"] for d in docs] 355 | 356 | 357 | def ping(server, retry=3): 358 | """Test shotgrid server connection with retry 359 | 360 | :param ShotGridConn server: An ShotGrid connection instance 361 | :param int retry: Max retry times, default 3 362 | :return: None 363 | :raises IOError: If not able to connect in given retry times 364 | """ 365 | e = None 366 | for i in range(retry): 367 | try: 368 | t1 = time.time() 369 | server.conn.info() 370 | 371 | except Exception as e: 372 | log.error(f"Retrying..[{i}]") 373 | time.sleep(1) 374 | 375 | else: 376 | break 377 | 378 | else: 379 | raise IOError(f"ERROR: Couldn't connect to {server.sg_server!r} " 380 | f"due to: {str(e)}") 381 | 382 | log.info( 383 | f"ShotGrid server {server.sg_server!r} connected, " 384 | f"delay {time.time() - t1:.3f}" 385 | ) 386 | -------------------------------------------------------------------------------- /src/allzpark/core.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | import functools 4 | import traceback 5 | from typing import Set, Union, Iterator, Callable 6 | from dataclasses import dataclass 7 | from rez.packages import Variant 8 | from rez.config import config as rezconfig 9 | from rez.package_repository import package_repository_manager 10 | from sweet.core import RollingContext, SweetSuite 11 | from .exceptions import BackendError 12 | 13 | log = logging.getLogger("allzpark") 14 | 15 | 16 | def _load_backends(): 17 | 18 | def try_avalon_backend(): 19 | from . import backend_avalon as avalon 20 | 21 | scope = avalon.get_entrance() 22 | avalon.ping( 23 | avalon.AvalonMongo(scope.uri, scope.timeout, entrance=scope) 24 | ) 25 | return scope # type: avalon.Entrance 26 | 27 | def try_sg_sync_backend(): 28 | from . import backend_sg_sync as shotgrid 29 | 30 | scope = shotgrid.get_entrance() 31 | shotgrid.ping( 32 | shotgrid.ShotGridConn(scope.sg_server, 33 | scope.script_name, 34 | scope.api_key, 35 | entrance=scope) 36 | ) 37 | return scope # type: shotgrid.Entrance 38 | 39 | return [ 40 | ("avalon", try_avalon_backend), 41 | ("sg_sync", try_sg_sync_backend), 42 | # could be ftrack, or shotgrid, could be... 43 | ] 44 | 45 | 46 | def init_backends(no_warning=False): 47 | """ 48 | 49 | :param bool no_warning: 50 | :return: A list of available backend name and entrance object pair 51 | :rtype: list[tuple[str, Entrance]] 52 | """ 53 | possible_backends = _load_backends() 54 | available_backends = [] 55 | 56 | for name, entrance_getter in possible_backends: 57 | log.info(f"> Init backend {name!r}..") 58 | try: 59 | entrance = entrance_getter() 60 | except Exception as e: 61 | if no_warning: 62 | continue 63 | log.warning( 64 | f"Cannot get entrance from backend {name!r}: {str(e)}" 65 | ) 66 | else: 67 | available_backends.append((name, entrance)) 68 | log.info(f"- Backend {name!r} connected.") 69 | 70 | if available_backends: 71 | return available_backends 72 | 73 | raise BackendError("* No available backend.") 74 | 75 | 76 | def load_suite(path): 77 | """Load one saved suite from path 78 | 79 | :param str path: 80 | :return: 81 | :rtype: ReadOnlySuite or None 82 | """ 83 | log.debug(f"Loading suite: {path}") 84 | suite = ReadOnlySuite.load(path) 85 | if suite.is_live(): 86 | log.debug(f"Re-resolve contexts in suite..") 87 | suite.re_resolve_rxt_contexts() 88 | 89 | return suite 90 | 91 | 92 | class AbstractScope: 93 | name: str 94 | upstream: Union["AbstractScope", None] 95 | 96 | def exists(self) -> bool: 97 | """Query backend to check if this scope exists""" 98 | raise NotImplementedError 99 | 100 | def iter_children(self) -> Iterator["AbstractScope"]: 101 | """Iter child scopes""" 102 | raise NotImplementedError 103 | 104 | def suite_path(self) -> Union[str, None]: 105 | """Returns a load path of a suite that is for this scope, if any""" 106 | raise NotImplementedError 107 | 108 | def make_tool_filter(self) -> Callable[["SuiteTool"], bool]: 109 | """Returns a callable for filtering tools 110 | 111 | Example: 112 | 113 | >>> @dataclass(frozen=True) 114 | ... class ProjectScope(AbstractScope): # noqa 115 | ... def make_tool_filter(self): 116 | ... def _filter(tool: SuiteTool) -> bool: 117 | ... required_roles = tool.metadata.required_roles 118 | ... return ( 119 | ... not tool.metadata.hidden 120 | ... and self.roles.intersection(required_roles) # noqa 121 | ... ) 122 | ... return _filter 123 | 124 | """ 125 | raise NotImplementedError 126 | 127 | def obtain_workspace(self, tool: "SuiteTool" = None) -> Union[str, None]: 128 | """Returns a working directory for this scope, if allowed""" 129 | raise NotImplementedError 130 | 131 | def additional_env(self, tool: "SuiteTool") -> dict: 132 | """Returns environ that will be applied to the tool context""" 133 | raise NotImplementedError 134 | 135 | def generate_breadcrumb(self) -> dict: 136 | return {} 137 | 138 | 139 | def generate_tool_breadcrumb(tool: "SuiteTool") -> Union[dict, None]: 140 | breadcrumb = tool.scope.generate_breadcrumb() 141 | if not breadcrumb: 142 | return 143 | breadcrumb["tool_alias"] = tool.alias 144 | return breadcrumb 145 | 146 | 147 | def get_tool_from_breadcrumb( 148 | breadcrumb: dict, 149 | backends: dict 150 | ) -> Union["SuiteTool", None]: 151 | """ 152 | """ 153 | log.debug(f"Parsing breadcrumb: {breadcrumb}") 154 | 155 | backend_name = breadcrumb.get("entrance") 156 | if not backend_name: 157 | log.error("No backend found in breadcrumb.") 158 | return 159 | 160 | tool_alias = breadcrumb.get("tool_alias") 161 | if not tool_alias: 162 | log.error("No tool alias found in breadcrumb.") 163 | return 164 | 165 | backend = backends.get(backend_name) 166 | if not backend: 167 | log.error(f"Backend {backend_name!r} is currently not available.") 168 | return 169 | 170 | if not callable(getattr(backend, "get_scope_from_breadcrumb", None)): 171 | log.critical(f"Backend {backend_name!r} doesn't have " 172 | "'get_scope_from_breadcrumb()' implemented.") 173 | return 174 | 175 | scope = backend.get_scope_from_breadcrumb(breadcrumb) # type: AbstractScope 176 | if scope is None: 177 | log.warning(f"Unable to get scope from backend: {breadcrumb}") 178 | return 179 | log.debug(f"Searching tool {tool_alias!r} in scope {scope}") 180 | 181 | tool = None 182 | for tool in _tools_iter(scope, caching=True): 183 | if tool.alias == tool_alias: 184 | break 185 | else: 186 | log.debug("No matched tool found in scope.") 187 | 188 | return tool 189 | 190 | 191 | @dataclass(frozen=True) 192 | class ToolMetadata: 193 | label: str 194 | icon: str 195 | color: str 196 | hidden: bool 197 | required_roles: Set[str] 198 | no_console: bool 199 | start_new_session: bool 200 | remember_me: bool 201 | 202 | 203 | @dataclass(frozen=True) 204 | class SuiteTool: 205 | name: str 206 | alias: str 207 | ctx_name: str 208 | variant: Variant 209 | scope: Union[AbstractScope, None] 210 | 211 | @property 212 | def context(self) -> RollingContext: 213 | return self.variant.context 214 | 215 | @property 216 | def metadata(self) -> ToolMetadata: 217 | data = getattr(self.variant, "_data", {}).copy() 218 | tool = data.get("override", {}).get(self.name) # e.g. pre tool icon 219 | data.update(tool or {}) 220 | return ToolMetadata( 221 | label=data.get("label", self.variant.name), 222 | icon=data.get("icon"), 223 | color=data.get("color"), 224 | hidden=data.get("hidden", False), 225 | required_roles=set(data.get("required_roles", [])), 226 | no_console=data.get("no_console", True), 227 | start_new_session=data.get("start_new_session", True), 228 | remember_me=data.get("remember_me", True), 229 | ) 230 | 231 | 232 | def cache_clear(): 233 | log.debug("Cleaning caches..") 234 | 235 | # clear cached packages 236 | for path in rezconfig.packages_path: 237 | log.debug(f"Cleaning package repository cache: {path}") 238 | repo = package_repository_manager.get_repository(path) 239 | repo.clear_caches() 240 | 241 | # clear cached suites and tools 242 | log.debug("Cleaning cached suites and tools") 243 | _load_suite.cache_clear() 244 | list_tools.cache_clear() 245 | 246 | log.debug("Core cache cleared.") 247 | 248 | 249 | def _tools_iter(scope, filtering=None, caching=False): 250 | _get_suite = _load_suite if caching else load_suite 251 | 252 | def _iter_tools(_scope): 253 | try: 254 | suite_path = _scope.suite_path() 255 | except BackendError as e: 256 | log.error(str(e)) 257 | else: 258 | if suite_path: 259 | try: 260 | suite = _get_suite(suite_path) 261 | except Exception as e: 262 | log.error(traceback.format_exc()) 263 | log.error(f"Failed load suite: {str(e)}") 264 | else: 265 | for _tool in suite.iter_tools(scope=scope): 266 | yield _tool 267 | if _scope.upstream is not None: 268 | for _tool in _iter_tools(_scope.upstream): 269 | yield _tool 270 | 271 | if filtering is False: 272 | func = None 273 | elif callable(filtering): 274 | func = filtering 275 | else: 276 | func = scope.make_tool_filter() 277 | 278 | for tool in filter(func, _iter_tools(scope)): 279 | yield tool 280 | 281 | 282 | @functools.lru_cache(maxsize=None) 283 | def _load_suite(path): 284 | return load_suite(path) 285 | 286 | 287 | @functools.lru_cache(maxsize=None) 288 | def list_tools(scope, filtering=None): 289 | """List tools within scope and upstream scopes with lru cached 290 | 291 | :param scope: where to iter tools from 292 | :param filtering: If None, the default, tools will be filtered by 293 | the scope. If False, no filtering. Or if a callable is given, 294 | filtering tools with it instead. 295 | :type scope: AbstractScope 296 | :type filtering: bool or Callable or None 297 | :rtype: Iterator[SuiteTool] 298 | """ 299 | return list(_tools_iter(scope, filtering, caching=True)) 300 | 301 | 302 | def iter_tools(scope, filtering=None): 303 | """Iterate tools within scope and upstream scopes 304 | 305 | :param scope: where to iter tools from 306 | :param filtering: If None, the default, tools will be filtered by 307 | the scope. If False, no filtering. Or if a callable is given, 308 | filtering tools with it instead. 309 | :type scope: AbstractScope 310 | :type filtering: bool or Callable or None 311 | :rtype: Iterator[SuiteTool] 312 | """ 313 | return _tools_iter(scope, filtering, caching=False) 314 | 315 | 316 | class ReadOnlySuite(SweetSuite): 317 | """A Read-Only SweetSuite""" 318 | 319 | def _invalid_operation(self, *_, **__): 320 | raise RuntimeError("Invalid operation, this suite is Read-Only.") 321 | 322 | _update_context = SweetSuite.update_context 323 | add_context = \ 324 | update_context = \ 325 | remove_context = \ 326 | rename_context = \ 327 | set_context_prefix = \ 328 | remove_context_prefix = \ 329 | set_context_suffix = \ 330 | remove_context_suffix = \ 331 | bump_context = \ 332 | hide_tool = \ 333 | unhide_tool = \ 334 | alias_tool = \ 335 | unalias_tool = \ 336 | set_live = \ 337 | set_description = \ 338 | save = _invalid_operation 339 | 340 | def re_resolve_rxt_contexts(self): 341 | """Re-resolve all contexts that loaded from .rxt files 342 | :return: 343 | """ 344 | for name in list(self.contexts.keys()): 345 | context = self.context(name) 346 | if context.load_path: 347 | self._update_context(name, re_resolve_rxt(context)) 348 | 349 | def iter_tools(self, scope=None): 350 | """Iter tools in this suite 351 | :return: 352 | :rtype: collections.Iterator[SuiteTool] 353 | """ 354 | for alias, entry in self.get_tools().items(): 355 | yield SuiteTool( 356 | name=entry["tool_name"], 357 | alias=entry["tool_alias"], 358 | ctx_name=entry["context_name"], 359 | variant=entry["variant"], 360 | scope=scope, 361 | ) 362 | 363 | def get_tools(self): 364 | self._update_tools(suppress_err=True) 365 | return self.tools 366 | 367 | 368 | def re_resolve_rxt(context): 369 | """Re-resolve context loaded from .rxt file 370 | 371 | This takes following entries from input context to resolve a new one: 372 | - package_requests 373 | - timestamp 374 | - package_paths 375 | - package_filters 376 | - package_orderers 377 | - building 378 | 379 | :param context: .rxt loaded context 380 | :type context: ResolvedContext 381 | :return: new resolved context 382 | :rtype: RollingContext 383 | :raises AssertionError: If no context.load_path (not loaded from .rxt) 384 | """ 385 | assert context.load_path, "Not a loaded context." 386 | rxt = context 387 | new = RollingContext( 388 | package_requests=rxt.requested_packages(), 389 | timestamp=rxt.requested_timestamp, 390 | # todo: ignore this so the local package can be used 391 | # package_paths=rxt.package_paths, 392 | package_filter=rxt.package_filter, 393 | package_orderers=rxt.package_orderers, 394 | building=rxt.building, 395 | ) 396 | new.suite_context_name = rxt.suite_context_name 397 | return new 398 | -------------------------------------------------------------------------------- /tests/test_apps.py: -------------------------------------------------------------------------------- 1 | 2 | from tests import util 3 | 4 | 5 | class TestApps(util.TestBase): 6 | 7 | def test_select_app(self): 8 | """Test app selecting behavior 9 | """ 10 | util.memory_repository({ 11 | "foo": { 12 | "1.0.0": { 13 | "name": "foo", 14 | "version": "1.0.0", 15 | "requires": [ 16 | "~app_A", 17 | "~app_B", 18 | ], 19 | } 20 | }, 21 | "app_A": { 22 | "1.0.0": { 23 | "name": "app_A", 24 | "version": "1.0.0", 25 | } 26 | }, 27 | "app_B": { 28 | "1.0.0": { 29 | "name": "app_B", 30 | "version": "1.0.0", 31 | } 32 | }, 33 | }) 34 | self.ctrl_reset(["foo"]) 35 | 36 | with self.wait_signal(self.ctrl.state_changed, "ready"): 37 | self.ctrl.select_profile("foo") 38 | 39 | env = self.ctrl.state["rezEnvirons"] 40 | 41 | # first app will be selected if no preference loaded 42 | self.assertEqual("app_A==1.0.0", self.ctrl.state["appRequest"]) 43 | self.assertIn("app_A==1.0.0", env) 44 | self.assertNotIn("app_B==1.0.0", env) 45 | 46 | self.ctrl.select_application("app_B==1.0.0") 47 | 48 | self.assertEqual("app_B==1.0.0", self.ctrl.state["appRequest"]) 49 | self.assertIn("app_A==1.0.0", env) 50 | self.assertIn("app_B==1.0.0", env) 51 | 52 | def test_app_environ(self): 53 | """Test resolved environment in each app 54 | """ 55 | util.memory_repository({ 56 | "foo": { 57 | "1.0.0": { 58 | "name": "foo", 59 | "version": "1.0.0", 60 | "requires": [ 61 | "~app_A", 62 | "~app_B", 63 | ], 64 | "commands": "env.FOO='BAR'" 65 | } 66 | }, 67 | "app_A": { 68 | "1.0.0": { 69 | "name": "app_A", 70 | "version": "1.0.0", 71 | "commands": "env.THIS_A='1'" 72 | } 73 | }, 74 | "app_B": { 75 | "1.0.0": { 76 | "name": "app_B", 77 | "version": "1.0.0", 78 | "commands": "env.THIS_B='1'" 79 | } 80 | }, 81 | }) 82 | self.ctrl_reset(["foo"]) 83 | 84 | with self.wait_signal(self.ctrl.state_changed, "ready"): 85 | self.ctrl.select_profile("foo") 86 | 87 | env = self.ctrl.state["rezEnvirons"] 88 | 89 | for app_request in ["app_A==1.0.0", "app_B==1.0.0"]: 90 | self.ctrl.select_application(app_request) 91 | 92 | self.assertIn("app_A==1.0.0", env) 93 | self.assertIn("app_B==1.0.0", env) 94 | 95 | # profile env will apply to all apps 96 | self.assertIn("FOO", env["app_A==1.0.0"]) 97 | self.assertIn("FOO", env["app_B==1.0.0"]) 98 | self.assertEqual(env["app_A==1.0.0"]["FOO"], "BAR") 99 | self.assertEqual(env["app_B==1.0.0"]["FOO"], "BAR") 100 | 101 | self.assertIn("THIS_A", env["app_A==1.0.0"]) 102 | self.assertNotIn("THIS_A", env["app_B==1.0.0"]) 103 | 104 | self.assertIn("THIS_B", env["app_B==1.0.0"]) 105 | self.assertNotIn("THIS_B", env["app_A==1.0.0"]) 106 | 107 | def test_app_failed_independently_1(self): 108 | """Test app resolve failure doesn't fail whole profile""" 109 | util.memory_repository({ 110 | "foo": { 111 | "1.0.0": { 112 | "name": "foo", 113 | "version": "1.0.0", 114 | "requires": [ 115 | "anti_A", 116 | "~app_A", # fail by reduction 117 | "~app_B", 118 | ], 119 | }, 120 | }, 121 | "anti_A": { 122 | "1": { 123 | "name": "anti_A", 124 | "version": "1", 125 | "requires": ["!app_A"], 126 | } 127 | }, 128 | "app_A": {"1": {"name": "app_A", "version": "1"}}, 129 | "app_B": {"1": {"name": "app_B", "version": "1"}}, 130 | }) 131 | self.ctrl_reset(["foo"]) 132 | 133 | context_a = self.ctrl.state["rezContexts"]["app_A==1"] 134 | context_b = self.ctrl.state["rezContexts"]["app_B==1"] 135 | 136 | self.assertFalse(context_a.success) 137 | self.assertTrue(context_b.success) 138 | 139 | def test_app_failed_independently_2(self): 140 | """Test app missing doesn't fail whole profile""" 141 | util.memory_repository({ 142 | "foo": { 143 | "1.0.0": { 144 | "name": "foo", 145 | "version": "1.0.0", 146 | "requires": [ 147 | "~app_A", # missing package family 148 | "~app_B", 149 | ], 150 | }, 151 | }, 152 | "app_B": {"1": {"name": "app_B", "version": "1"}}, 153 | }) 154 | self.ctrl_reset(["foo"]) 155 | 156 | context_a = self.ctrl.state["rezContexts"]["app_A==None"] 157 | context_b = self.ctrl.state["rezContexts"]["app_B==1"] 158 | 159 | self.assertFalse(context_a.success) # broken context 160 | self.assertTrue(context_b.success) 161 | 162 | def test_app_failed_independently_3(self): 163 | """Test app missing dependency doesn't fail whole profile""" 164 | util.memory_repository({ 165 | "foo": { 166 | "1.0.0": { 167 | "name": "foo", 168 | "version": "1.0.0", 169 | "requires": [ 170 | "~app_A", # has missing requires 171 | "~app_B", 172 | ], 173 | }, 174 | }, 175 | "app_A": { 176 | "1": { 177 | "name": "app_A", 178 | "version": "1", 179 | "requires": ["missing"], 180 | } 181 | }, 182 | "app_B": {"1": {"name": "app_B", "version": "1"}}, 183 | }) 184 | self.ctrl_reset(["foo"]) 185 | 186 | context_a = self.ctrl.state["rezContexts"]["app_A==1"] 187 | context_b = self.ctrl.state["rezContexts"]["app_B==1"] 188 | 189 | self.assertFalse(context_a.success) 190 | self.assertTrue(context_b.success) 191 | 192 | def test_app_failed_independently_4(self): 193 | """Test app missing version/variant doesn't fail whole profile""" 194 | util.memory_repository({ 195 | "foo": { 196 | "1.0.0": { 197 | "name": "foo", 198 | "version": "1.0.0", 199 | "requires": [ 200 | "~app_A==2", # missing package 201 | "~app_B", 202 | ], 203 | }, 204 | }, 205 | "app_A": {"1": {"name": "app_A", "version": "1"}}, 206 | "app_B": {"1": {"name": "app_B", "version": "1"}}, 207 | }) 208 | self.ctrl_reset(["foo"]) 209 | 210 | context_a = self.ctrl.state["rezContexts"]["app_A==2"] 211 | context_b = self.ctrl.state["rezContexts"]["app_B==1"] 212 | 213 | self.assertFalse(context_a.success) 214 | self.assertTrue(context_b.success) 215 | 216 | def test_app_changing_version(self): 217 | """Test application version can be changed in view""" 218 | util.memory_repository({ 219 | "foo": { 220 | "1": {"name": "foo", "version": "1", 221 | "requires": ["~app_A", "~app_B"]} 222 | }, 223 | "app_A": {"1": {"name": "app_A", "version": "1"}}, 224 | "app_B": {"1": {"name": "app_B", "version": "1"}, 225 | "2": {"name": "app_B", "version": "2"}} 226 | }) 227 | self.ctrl_reset(["foo"]) 228 | self.show_dock("app") 229 | 230 | apps = self.window._widgets["apps"] 231 | 232 | def get_version_editor(app_request): 233 | self.select_application(app_request) 234 | proxy = apps.model() 235 | model = proxy.sourceModel() 236 | index = model.findIndex(app_request, column=1) 237 | index = proxy.mapFromSource(index) 238 | apps.edit(index) 239 | 240 | return apps.indexWidget(index), apps.itemDelegate(index) 241 | 242 | editor, delegate = get_version_editor("app_A==1") 243 | self.assertIsNone( 244 | editor, "No version editing if App has only one version.") 245 | 246 | editor, delegate = get_version_editor("app_B==2") 247 | self.assertIsNotNone( 248 | editor, "Version should be editable if App has versions.") 249 | 250 | # for visual 251 | editor.showPopup() 252 | self.wait(100) 253 | view = editor.view() 254 | index = view.model().index(0, 0) 255 | sel_model = view.selectionModel() 256 | sel_model.select(index, sel_model.ClearAndSelect) 257 | self.wait(150) 258 | # change version 259 | editor.setCurrentIndex(0) 260 | delegate.commitData.emit(editor) 261 | self.wait(200) # wait patch 262 | 263 | self.assertEqual("app_B==1", self.ctrl.state["appRequest"]) 264 | 265 | def test_app_no_version_change_if_flattened(self): 266 | """No version edit if versions are flattened with allzparkconfig""" 267 | 268 | def applications_from_package(variant): 269 | # From https://allzpark.com/gui/#multiple-application-versions 270 | from allzpark import _rezapi as rez 271 | 272 | requirements = variant.requires or [] 273 | apps = list( 274 | str(req) 275 | for req in requirements 276 | if req.weak 277 | ) 278 | apps = [rez.PackageRequest(req.strip("~")) for req in apps] 279 | flattened = list() 280 | for request in apps: 281 | flattened += rez.find( 282 | request.name, 283 | range_=request.range, 284 | ) 285 | apps = list( 286 | "%s==%s" % (package.name, package.version) 287 | for package in flattened 288 | ) 289 | return apps 290 | 291 | # patch config 292 | self.patch_allzparkconfig("applications_from_package", 293 | applications_from_package) 294 | # start 295 | util.memory_repository({ 296 | "foo": { 297 | "1": {"name": "foo", "version": "1", 298 | "requires": ["~app_A"]} 299 | }, 300 | "app_A": {"1": {"name": "app_A", "version": "1"}, 301 | "2": {"name": "app_A", "version": "2"}} 302 | }) 303 | self.ctrl_reset(["foo"]) 304 | self.show_dock("app") 305 | 306 | apps = self.window._widgets["apps"] 307 | 308 | def get_version_editor(app_request): 309 | self.select_application(app_request) 310 | proxy = apps.model() 311 | model = proxy.sourceModel() 312 | index = model.findIndex(app_request, column=1) 313 | index = proxy.mapFromSource(index) 314 | apps.edit(index) 315 | 316 | return apps.indexWidget(index), apps.itemDelegate(index) 317 | 318 | editor, delegate = get_version_editor("app_A==1") 319 | self.assertIsNone( 320 | editor, "No version editing if versions are flattened.") 321 | 322 | editor, delegate = get_version_editor("app_A==2") 323 | self.assertIsNone( 324 | editor, "No version editing if versions are flattened.") 325 | 326 | def test_app_exclusion_filter(self): 327 | """Test app is available when latest version excluded by filter""" 328 | util.memory_repository({ 329 | "foo": { 330 | "1.0.0": { 331 | "name": "foo", 332 | "version": "1.0.0", 333 | "requires": [ 334 | "~app_A-1" 335 | ] 336 | } 337 | }, 338 | "app_A": { 339 | "1.0.0": { 340 | "name": "app_A", 341 | "version": "1.0.0" 342 | }, 343 | "1.0.0.beta": { 344 | "name": "app_A", 345 | "version": "1.0.0.beta" 346 | # latest app_A version matches exclusion filter 347 | } 348 | } 349 | }) 350 | self.ctrl_reset(["foo"]) 351 | 352 | self.set_preference("exclusionFilter", "*.beta") 353 | self.wait(200) # wait for reset 354 | 355 | # App was added 356 | self.assertIn("app_A==1.0.0", self.ctrl.state["rezContexts"]) 357 | context_a = self.ctrl.state["rezContexts"]["app_A==1.0.0"] 358 | self.assertTrue(context_a.success) 359 | 360 | # Latest non-beta version was chosen 361 | resolved_pkgs = [p for p in context_a.resolved_packages 362 | if "app_A" == p.name and "1.0.0" == str(p.version)] 363 | self.assertEqual(1, len(resolved_pkgs)) 364 | --------------------------------------------------------------------------------