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 |
6 |
--------------------------------------------------------------------------------
/src/allzpark/gui/resources/icons/hypnotize.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/allzpark/gui/resources/icons/hypnotize-on.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/allzpark/gui/resources/icons/hypnotize-dim.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
--------------------------------------------------------------------------------