mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-26 05:42:15 +01:00
Merge branch 'develop' into feature/new_publisher_core
This commit is contained in:
commit
acae4f94d8
79 changed files with 2682 additions and 558 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
|
@ -9,4 +9,4 @@
|
|||
url = https://github.com/arrow-py/arrow.git
|
||||
[submodule "openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api"]
|
||||
path = openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api
|
||||
url = https://bitbucket.org/ftrack/ftrack-python-api.git
|
||||
url = https://bitbucket.org/ftrack/ftrack-python-api.git
|
||||
|
|
@ -12,6 +12,9 @@ from .version import __version__ as version
|
|||
|
||||
def open_dialog():
|
||||
"""Show Igniter dialog."""
|
||||
if os.getenv("OPENPYPE_HEADLESS_MODE"):
|
||||
print("!!! Can't open dialog in headless mode. Exiting.")
|
||||
sys.exit(1)
|
||||
from Qt import QtWidgets, QtCore
|
||||
from .install_dialog import InstallDialog
|
||||
|
||||
|
|
@ -28,8 +31,31 @@ def open_dialog():
|
|||
return d.result()
|
||||
|
||||
|
||||
def open_update_window(openpype_version):
|
||||
"""Open update window."""
|
||||
if os.getenv("OPENPYPE_HEADLESS_MODE"):
|
||||
print("!!! Can't open dialog in headless mode. Exiting.")
|
||||
sys.exit(1)
|
||||
from Qt import QtWidgets, QtCore
|
||||
from .update_window import UpdateWindow
|
||||
|
||||
scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None)
|
||||
if scale_attr is not None:
|
||||
QtWidgets.QApplication.setAttribute(scale_attr)
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
d = UpdateWindow(version=openpype_version)
|
||||
d.open()
|
||||
|
||||
app.exec_()
|
||||
version_path = d.get_version_path()
|
||||
return version_path
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BootstrapRepos",
|
||||
"open_dialog",
|
||||
"open_update_window",
|
||||
"version"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import sys
|
|||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Union, Callable, List, Tuple
|
||||
import hashlib
|
||||
|
||||
from zipfile import ZipFile, BadZipFile
|
||||
|
||||
|
|
@ -28,6 +29,25 @@ LOG_WARNING = 1
|
|||
LOG_ERROR = 3
|
||||
|
||||
|
||||
def sha256sum(filename):
|
||||
"""Calculate sha256 for content of the file.
|
||||
|
||||
Args:
|
||||
filename (str): Path to file.
|
||||
|
||||
Returns:
|
||||
str: hex encoded sha256
|
||||
|
||||
"""
|
||||
h = hashlib.sha256()
|
||||
b = bytearray(128 * 1024)
|
||||
mv = memoryview(b)
|
||||
with open(filename, 'rb', buffering=0) as f:
|
||||
for n in iter(lambda: f.readinto(mv), 0):
|
||||
h.update(mv[:n])
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
class OpenPypeVersion(semver.VersionInfo):
|
||||
"""Class for storing information about OpenPype version.
|
||||
|
||||
|
|
@ -261,7 +281,8 @@ class BootstrapRepos:
|
|||
self.live_repo_dir = Path(Path(__file__).parent / ".." / "repos")
|
||||
|
||||
@staticmethod
|
||||
def get_version_path_from_list(version: str, version_list: list) -> Path:
|
||||
def get_version_path_from_list(
|
||||
version: str, version_list: list) -> Union[Path, None]:
|
||||
"""Get path for specific version in list of OpenPype versions.
|
||||
|
||||
Args:
|
||||
|
|
@ -275,6 +296,7 @@ class BootstrapRepos:
|
|||
for v in version_list:
|
||||
if str(v) == version:
|
||||
return v.path
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_local_live_version() -> str:
|
||||
|
|
@ -487,6 +509,7 @@ class BootstrapRepos:
|
|||
openpype_root = openpype_path.resolve()
|
||||
# generate list of filtered paths
|
||||
dir_filter = [openpype_root / f for f in self.openpype_filter]
|
||||
checksums = []
|
||||
|
||||
file: Path
|
||||
for file in openpype_list:
|
||||
|
|
@ -508,12 +531,119 @@ class BootstrapRepos:
|
|||
processed_path = file
|
||||
self._print(f"- processing {processed_path}")
|
||||
|
||||
zip_file.write(file, file.resolve().relative_to(openpype_root))
|
||||
checksums.append(
|
||||
(
|
||||
sha256sum(file.as_posix()),
|
||||
file.resolve().relative_to(openpype_root)
|
||||
)
|
||||
)
|
||||
zip_file.write(
|
||||
file, file.resolve().relative_to(openpype_root))
|
||||
|
||||
checksums_str = ""
|
||||
for c in checksums:
|
||||
checksums_str += "{}:{}\n".format(c[0], c[1])
|
||||
zip_file.writestr("checksums", checksums_str)
|
||||
# test if zip is ok
|
||||
zip_file.testzip()
|
||||
self._progress_callback(100)
|
||||
|
||||
def validate_openpype_version(self, path: Path) -> tuple:
|
||||
"""Validate version directory or zip file.
|
||||
|
||||
This will load `checksums` file if present, calculate checksums
|
||||
of existing files in given path and compare. It will also compare
|
||||
lists of files together for missing files.
|
||||
|
||||
Args:
|
||||
path (Path): Path to OpenPype version to validate.
|
||||
|
||||
Returns:
|
||||
tuple(bool, str): with version validity as first item
|
||||
and string with reason as second.
|
||||
|
||||
"""
|
||||
if not path.exists():
|
||||
return False, "Path doesn't exist"
|
||||
|
||||
if path.is_file():
|
||||
return self._validate_zip(path)
|
||||
return self._validate_dir(path)
|
||||
|
||||
@staticmethod
|
||||
def _validate_zip(path: Path) -> tuple:
|
||||
"""Validate content of zip file."""
|
||||
with ZipFile(path, "r") as zip_file:
|
||||
# read checksums
|
||||
try:
|
||||
checksums_data = str(zip_file.read("checksums"))
|
||||
except IOError:
|
||||
# FIXME: This should be set to False sometimes in the future
|
||||
return True, "Cannot read checksums for archive."
|
||||
|
||||
# split it to the list of tuples
|
||||
checksums = [
|
||||
tuple(line.split(":"))
|
||||
for line in checksums_data.split("\n") if line
|
||||
]
|
||||
|
||||
# calculate and compare checksums in the zip file
|
||||
for file in checksums:
|
||||
h = hashlib.sha256()
|
||||
try:
|
||||
h.update(zip_file.read(file[1]))
|
||||
except FileNotFoundError:
|
||||
return False, f"Missing file [ {file[1]} ]"
|
||||
if h.hexdigest() != file[0]:
|
||||
return False, f"Invalid checksum on {file[1]}"
|
||||
|
||||
# get list of files in zip minus `checksums` file itself
|
||||
# and turn in to set to compare against list of files
|
||||
# from checksum file. If difference exists, something is
|
||||
# wrong
|
||||
files_in_zip = zip_file.namelist()
|
||||
files_in_zip.remove("checksums")
|
||||
files_in_zip = set(files_in_zip)
|
||||
files_in_checksum = set([file[1] for file in checksums])
|
||||
diff = files_in_zip.difference(files_in_checksum)
|
||||
if diff:
|
||||
return False, f"Missing files {diff}"
|
||||
|
||||
return True, "All ok"
|
||||
|
||||
@staticmethod
|
||||
def _validate_dir(path: Path) -> tuple:
|
||||
checksums_file = Path(path / "checksums")
|
||||
if not checksums_file.exists():
|
||||
# FIXME: This should be set to False sometimes in the future
|
||||
return True, "Cannot read checksums for archive."
|
||||
checksums_data = checksums_file.read_text()
|
||||
checksums = [
|
||||
tuple(line.split(":"))
|
||||
for line in checksums_data.split("\n") if line
|
||||
]
|
||||
files_in_dir = [
|
||||
file.relative_to(path).as_posix()
|
||||
for file in path.iterdir() if file.is_file()
|
||||
]
|
||||
files_in_dir.remove("checksums")
|
||||
files_in_dir = set(files_in_dir)
|
||||
files_in_checksum = set([file[1] for file in checksums])
|
||||
|
||||
for file in checksums:
|
||||
try:
|
||||
current = sha256sum((path / file[1]).as_posix())
|
||||
except FileNotFoundError:
|
||||
return False, f"Missing file [ {file[1]} ]"
|
||||
|
||||
if file[0] != current:
|
||||
return False, f"Invalid checksum on {file[1]}"
|
||||
diff = files_in_dir.difference(files_in_checksum)
|
||||
if diff:
|
||||
return False, f"Missing files {diff}"
|
||||
|
||||
return True, "All ok"
|
||||
|
||||
@staticmethod
|
||||
def add_paths_from_archive(archive: Path) -> None:
|
||||
"""Add first-level directory and 'repos' as paths to :mod:`sys.path`.
|
||||
|
|
@ -837,6 +967,7 @@ class BootstrapRepos:
|
|||
|
||||
# test if destination directory already exist, if so lets delete it.
|
||||
if destination.exists() and force:
|
||||
self._print("removing existing directory")
|
||||
try:
|
||||
shutil.rmtree(destination)
|
||||
except OSError as e:
|
||||
|
|
@ -846,6 +977,7 @@ class BootstrapRepos:
|
|||
raise OpenPypeVersionIOError(
|
||||
f"cannot remove existing {destination}") from e
|
||||
elif destination.exists() and not force:
|
||||
self._print("destination directory already exists")
|
||||
raise OpenPypeVersionExists(f"{destination} already exist.")
|
||||
else:
|
||||
# create destination parent directories even if they don't exist.
|
||||
|
|
@ -855,6 +987,7 @@ class BootstrapRepos:
|
|||
if openpype_version.path.is_dir():
|
||||
# create zip inside temporary directory.
|
||||
self._print("Creating zip from directory ...")
|
||||
self._progress_callback(0)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_zip = \
|
||||
Path(temp_dir) / f"openpype-v{openpype_version}.zip"
|
||||
|
|
@ -880,13 +1013,16 @@ class BootstrapRepos:
|
|||
raise OpenPypeVersionInvalid("Invalid file format")
|
||||
|
||||
if not self.is_inside_user_data(openpype_version.path):
|
||||
self._progress_callback(35)
|
||||
openpype_version.path = self._copy_zip(
|
||||
openpype_version.path, destination)
|
||||
|
||||
# extract zip there
|
||||
self._print("extracting zip to destination ...")
|
||||
with ZipFile(openpype_version.path, "r") as zip_ref:
|
||||
self._progress_callback(75)
|
||||
zip_ref.extractall(destination)
|
||||
self._progress_callback(100)
|
||||
|
||||
return destination
|
||||
|
||||
|
|
|
|||
|
|
@ -14,21 +14,13 @@ from .tools import (
|
|||
validate_mongo_connection,
|
||||
get_openpype_path_from_db
|
||||
)
|
||||
|
||||
from .nice_progress_bar import NiceProgressBar
|
||||
from .user_settings import OpenPypeSecureRegistry
|
||||
from .tools import load_stylesheet
|
||||
from .version import __version__
|
||||
|
||||
|
||||
def load_stylesheet():
|
||||
stylesheet_path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"stylesheet.css"
|
||||
)
|
||||
with open(stylesheet_path, "r") as file_stream:
|
||||
stylesheet = file_stream.read()
|
||||
|
||||
return stylesheet
|
||||
|
||||
|
||||
class ButtonWithOptions(QtWidgets.QFrame):
|
||||
option_clicked = QtCore.Signal(str)
|
||||
|
||||
|
|
@ -91,25 +83,6 @@ class ButtonWithOptions(QtWidgets.QFrame):
|
|||
self.option_clicked.emit(self._default_value)
|
||||
|
||||
|
||||
class NiceProgressBar(QtWidgets.QProgressBar):
|
||||
def __init__(self, parent=None):
|
||||
super(NiceProgressBar, self).__init__(parent)
|
||||
self._real_value = 0
|
||||
|
||||
def setValue(self, value):
|
||||
self._real_value = value
|
||||
if value != 0 and value < 11:
|
||||
value = 11
|
||||
|
||||
super(NiceProgressBar, self).setValue(value)
|
||||
|
||||
def value(self):
|
||||
return self._real_value
|
||||
|
||||
def text(self):
|
||||
return "{} %".format(self._real_value)
|
||||
|
||||
|
||||
class ConsoleWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(ConsoleWidget, self).__init__(parent)
|
||||
|
|
|
|||
20
igniter/nice_progress_bar.py
Normal file
20
igniter/nice_progress_bar.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from Qt import QtCore, QtGui, QtWidgets # noqa
|
||||
|
||||
|
||||
class NiceProgressBar(QtWidgets.QProgressBar):
|
||||
def __init__(self, parent=None):
|
||||
super(NiceProgressBar, self).__init__(parent)
|
||||
self._real_value = 0
|
||||
|
||||
def setValue(self, value):
|
||||
self._real_value = value
|
||||
if value != 0 and value < 11:
|
||||
value = 11
|
||||
|
||||
super(NiceProgressBar, self).setValue(value)
|
||||
|
||||
def value(self):
|
||||
return self._real_value
|
||||
|
||||
def text(self):
|
||||
return "{} %".format(self._real_value)
|
||||
|
|
@ -248,3 +248,15 @@ def get_openpype_path_from_db(url: str) -> Union[str, None]:
|
|||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def load_stylesheet() -> str:
|
||||
"""Load css style sheet.
|
||||
|
||||
Returns:
|
||||
str: content of the stylesheet
|
||||
|
||||
"""
|
||||
stylesheet_path = Path(__file__).parent.resolve() / "stylesheet.css"
|
||||
|
||||
return stylesheet_path.read_text()
|
||||
|
|
|
|||
61
igniter/update_thread.py
Normal file
61
igniter/update_thread.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Working thread for update."""
|
||||
from Qt.QtCore import QThread, Signal, QObject # noqa
|
||||
|
||||
from .bootstrap_repos import (
|
||||
BootstrapRepos,
|
||||
OpenPypeVersion
|
||||
)
|
||||
|
||||
|
||||
class UpdateThread(QThread):
|
||||
"""Install Worker thread.
|
||||
|
||||
This class takes care of finding OpenPype version on user entered path
|
||||
(or loading this path from database). If nothing is entered by user,
|
||||
OpenPype will create its zip files from repositories that comes with it.
|
||||
|
||||
If path contains plain repositories, they are zipped and installed to
|
||||
user data dir.
|
||||
|
||||
"""
|
||||
progress = Signal(int)
|
||||
message = Signal((str, bool))
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self._result = None
|
||||
self._openpype_version = None
|
||||
QThread.__init__(self, parent)
|
||||
|
||||
def set_version(self, openpype_version: OpenPypeVersion):
|
||||
self._openpype_version = openpype_version
|
||||
|
||||
def result(self):
|
||||
"""Result of finished installation."""
|
||||
return self._result
|
||||
|
||||
def _set_result(self, value):
|
||||
if self._result is not None:
|
||||
raise AssertionError("BUG: Result was set more than once!")
|
||||
self._result = value
|
||||
|
||||
def run(self):
|
||||
"""Thread entry point.
|
||||
|
||||
Using :class:`BootstrapRepos` to either install OpenPype as zip files
|
||||
or copy them from location specified by user or retrieved from
|
||||
database.
|
||||
"""
|
||||
bs = BootstrapRepos(
|
||||
progress_callback=self.set_progress, message=self.message)
|
||||
version_path = bs.install_version(self._openpype_version)
|
||||
self._set_result(version_path)
|
||||
|
||||
def set_progress(self, progress: int) -> None:
|
||||
"""Helper to set progress bar.
|
||||
|
||||
Args:
|
||||
progress (int): Progress in percents.
|
||||
|
||||
"""
|
||||
self.progress.emit(progress)
|
||||
136
igniter/update_window.py
Normal file
136
igniter/update_window.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Progress window to show when OpenPype is updating/installing locally."""
|
||||
import os
|
||||
from .update_thread import UpdateThread
|
||||
from Qt import QtCore, QtGui, QtWidgets # noqa
|
||||
from .bootstrap_repos import OpenPypeVersion
|
||||
from .nice_progress_bar import NiceProgressBar
|
||||
from .tools import load_stylesheet
|
||||
|
||||
|
||||
class UpdateWindow(QtWidgets.QDialog):
|
||||
"""OpenPype update window."""
|
||||
|
||||
_width = 500
|
||||
_height = 100
|
||||
|
||||
def __init__(self, version: OpenPypeVersion, parent=None):
|
||||
super(UpdateWindow, self).__init__(parent)
|
||||
self._openpype_version = version
|
||||
self._result_version_path = None
|
||||
|
||||
self.setWindowTitle(
|
||||
f"OpenPype is updating ..."
|
||||
)
|
||||
self.setModal(True)
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
roboto_font_path = os.path.join(current_dir, "RobotoMono-Regular.ttf")
|
||||
poppins_font_path = os.path.join(current_dir, "Poppins")
|
||||
icon_path = os.path.join(current_dir, "openpype_icon.png")
|
||||
|
||||
# Install roboto font
|
||||
QtGui.QFontDatabase.addApplicationFont(roboto_font_path)
|
||||
for filename in os.listdir(poppins_font_path):
|
||||
if os.path.splitext(filename)[1] == ".ttf":
|
||||
QtGui.QFontDatabase.addApplicationFont(filename)
|
||||
|
||||
# Load logo
|
||||
pixmap_openpype_logo = QtGui.QPixmap(icon_path)
|
||||
# Set logo as icon of window
|
||||
self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo))
|
||||
|
||||
self._pixmap_openpype_logo = pixmap_openpype_logo
|
||||
|
||||
self._update_thread = None
|
||||
|
||||
self.resize(QtCore.QSize(self._width, self._height))
|
||||
self._init_ui()
|
||||
|
||||
# Set stylesheet
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
self._run_update()
|
||||
|
||||
def _init_ui(self):
|
||||
|
||||
# Main info
|
||||
# --------------------------------------------------------------------
|
||||
main_label = QtWidgets.QLabel(
|
||||
f"<b>OpenPype</b> is updating to {self._openpype_version}", self)
|
||||
main_label.setWordWrap(True)
|
||||
main_label.setObjectName("MainLabel")
|
||||
|
||||
# Progress bar
|
||||
# --------------------------------------------------------------------
|
||||
progress_bar = NiceProgressBar(self)
|
||||
progress_bar.setAlignment(QtCore.Qt.AlignCenter)
|
||||
progress_bar.setTextVisible(False)
|
||||
|
||||
# add all to main
|
||||
main = QtWidgets.QVBoxLayout(self)
|
||||
main.addSpacing(15)
|
||||
main.addWidget(main_label, 0)
|
||||
main.addSpacing(15)
|
||||
main.addWidget(progress_bar, 0)
|
||||
main.addSpacing(15)
|
||||
|
||||
self._progress_bar = progress_bar
|
||||
|
||||
def _run_update(self):
|
||||
"""Start install process.
|
||||
|
||||
This will once again validate entered path and mongo if ok, start
|
||||
working thread that will do actual job.
|
||||
"""
|
||||
# Check if install thread is not already running
|
||||
if self._update_thread and self._update_thread.isRunning():
|
||||
return
|
||||
self._progress_bar.setRange(0, 0)
|
||||
update_thread = UpdateThread(self)
|
||||
update_thread.set_version(self._openpype_version)
|
||||
update_thread.message.connect(self.update_console)
|
||||
update_thread.progress.connect(self._update_progress)
|
||||
update_thread.finished.connect(self._installation_finished)
|
||||
|
||||
self._update_thread = update_thread
|
||||
|
||||
update_thread.start()
|
||||
|
||||
def get_version_path(self):
|
||||
return self._result_version_path
|
||||
|
||||
def _installation_finished(self):
|
||||
status = self._update_thread.result()
|
||||
self._result_version_path = status
|
||||
self._progress_bar.setRange(0, 1)
|
||||
self._update_progress(100)
|
||||
QtWidgets.QApplication.processEvents()
|
||||
self.done(0)
|
||||
|
||||
def _update_progress(self, progress: int):
|
||||
# not updating progress as we are not able to determine it
|
||||
# correctly now. Progress bar is set to un-deterministic mode
|
||||
# until we are able to get progress in better way.
|
||||
"""
|
||||
self._progress_bar.setRange(0, 0)
|
||||
self._progress_bar.setValue(progress)
|
||||
text_visible = self._progress_bar.isTextVisible()
|
||||
if progress == 0:
|
||||
if text_visible:
|
||||
self._progress_bar.setTextVisible(False)
|
||||
elif not text_visible:
|
||||
self._progress_bar.setTextVisible(True)
|
||||
"""
|
||||
return
|
||||
|
||||
def update_console(self, msg: str, error: bool = False) -> None:
|
||||
"""Display message in console.
|
||||
|
||||
Args:
|
||||
msg (str): message.
|
||||
error (bool): if True, print it red.
|
||||
"""
|
||||
print(msg)
|
||||
|
|
@ -24,7 +24,9 @@ from .lib import (
|
|||
get_latest_version,
|
||||
get_global_environments,
|
||||
get_local_site_id,
|
||||
change_openpype_mongo_url
|
||||
change_openpype_mongo_url,
|
||||
create_project_folders,
|
||||
get_project_basic_paths
|
||||
)
|
||||
|
||||
from .lib.mongo import (
|
||||
|
|
@ -72,6 +74,7 @@ __all__ = [
|
|||
"get_current_project_settings",
|
||||
"get_anatomy_settings",
|
||||
"get_environments",
|
||||
"get_project_basic_paths",
|
||||
|
||||
"SystemSettings",
|
||||
|
||||
|
|
@ -120,5 +123,9 @@ __all__ = [
|
|||
"get_global_environments",
|
||||
|
||||
"get_local_site_id",
|
||||
"change_openpype_mongo_url"
|
||||
"change_openpype_mongo_url",
|
||||
|
||||
"get_project_basic_paths",
|
||||
"create_project_folders"
|
||||
|
||||
]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ from .pype_commands import PypeCommands
|
|||
@click.option("--list-versions", is_flag=True, expose_value=False,
|
||||
help=("list all detected versions. Use With `--use-staging "
|
||||
"to list staging versions."))
|
||||
@click.option("--validate-version", expose_value=False,
|
||||
help="validate given version integrity")
|
||||
def main(ctx):
|
||||
"""Pype is main command serving as entry point to pipeline system.
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import pyblish.api
|
|||
class PreCollectClipEffects(pyblish.api.InstancePlugin):
|
||||
"""Collect soft effects instances."""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.579
|
||||
order = pyblish.api.CollectorOrder - 0.479
|
||||
label = "Precollect Clip Effects Instances"
|
||||
families = ["clip"]
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from pprint import pformat
|
|||
class PrecollectInstances(pyblish.api.ContextPlugin):
|
||||
"""Collect all Track items selection."""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.59
|
||||
order = pyblish.api.CollectorOrder - 0.49
|
||||
label = "Precollect Instances"
|
||||
hosts = ["hiero"]
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
"""Inject the current working file into context"""
|
||||
|
||||
label = "Precollect Workfile"
|
||||
order = pyblish.api.CollectorOrder - 0.6
|
||||
order = pyblish.api.CollectorOrder - 0.5
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
|
|||
|
|
@ -112,24 +112,26 @@ class ExtractThumbnail(openpype.api.Extractor):
|
|||
|
||||
# create write node
|
||||
write_node = nuke.createNode("Write")
|
||||
file = fhead + "jpeg"
|
||||
file = fhead + "jpg"
|
||||
name = "thumbnail"
|
||||
path = os.path.join(staging_dir, file).replace("\\", "/")
|
||||
instance.data["thumbnail"] = path
|
||||
write_node["file"].setValue(path)
|
||||
write_node["file_type"].setValue("jpeg")
|
||||
write_node["file_type"].setValue("jpg")
|
||||
write_node["raw"].setValue(1)
|
||||
write_node.setInput(0, previous_node)
|
||||
temporary_nodes.append(write_node)
|
||||
tags = ["thumbnail", "publish_on_farm"]
|
||||
|
||||
# retime for
|
||||
mid_frame = int((int(last_frame) - int(first_frame)) / 2) \
|
||||
+ int(first_frame)
|
||||
first_frame = int(last_frame) / 2
|
||||
last_frame = int(last_frame) / 2
|
||||
|
||||
repre = {
|
||||
'name': name,
|
||||
'ext': "jpeg",
|
||||
'ext': "jpg",
|
||||
"outputName": "thumb",
|
||||
'files': file,
|
||||
"stagingDir": staging_dir,
|
||||
|
|
@ -140,7 +142,7 @@ class ExtractThumbnail(openpype.api.Extractor):
|
|||
instance.data["representations"].append(repre)
|
||||
|
||||
# Render frames
|
||||
nuke.execute(write_node.name(), int(first_frame), int(last_frame))
|
||||
nuke.execute(write_node.name(), int(mid_frame), int(mid_frame))
|
||||
|
||||
self.log.debug(
|
||||
"representations: {}".format(instance.data["representations"]))
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from avalon.nuke import lib as anlib
|
|||
class PreCollectNukeInstances(pyblish.api.ContextPlugin):
|
||||
"""Collect all nodes with Avalon knob."""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.59
|
||||
order = pyblish.api.CollectorOrder - 0.49
|
||||
label = "Pre-collect Instances"
|
||||
hosts = ["nuke", "nukeassist"]
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ reload(anlib)
|
|||
class CollectWorkfile(pyblish.api.ContextPlugin):
|
||||
"""Collect current script for publish."""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.60
|
||||
order = pyblish.api.CollectorOrder - 0.50
|
||||
label = "Pre-collect Workfile"
|
||||
hosts = ['nuke']
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from avalon import io, api
|
|||
class CollectNukeWrites(pyblish.api.InstancePlugin):
|
||||
"""Collect all write nodes."""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.58
|
||||
order = pyblish.api.CollectorOrder - 0.48
|
||||
label = "Pre-collect Writes"
|
||||
hosts = ["nuke", "nukeassist"]
|
||||
families = ["write"]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from pprint import pformat
|
|||
class PrecollectInstances(pyblish.api.ContextPlugin):
|
||||
"""Collect all Track items selection."""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.59
|
||||
order = pyblish.api.CollectorOrder - 0.49
|
||||
label = "Precollect Instances"
|
||||
hosts = ["resolve"]
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
"""Precollect the current working file into context"""
|
||||
|
||||
label = "Precollect Workfile"
|
||||
order = pyblish.api.CollectorOrder - 0.6
|
||||
order = pyblish.api.CollectorOrder - 0.5
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import zipfile
|
|||
import pyblish.api
|
||||
from avalon import api, io
|
||||
import openpype.api
|
||||
from openpype.lib import get_workfile_template_key_from_context
|
||||
|
||||
|
||||
class ExtractHarmonyZip(openpype.api.Extractor):
|
||||
|
|
@ -65,10 +66,10 @@ class ExtractHarmonyZip(openpype.api.Extractor):
|
|||
|
||||
# Get Task types and Statuses for creation if needed
|
||||
self.task_types = self._get_all_task_types(project_entity)
|
||||
self.task_statuses = self.get_all_task_statuses(project_entity)
|
||||
self.task_statuses = self._get_all_task_statuses(project_entity)
|
||||
|
||||
# Get Statuses of AssetVersions
|
||||
self.assetversion_statuses = self.get_all_assetversion_statuses(
|
||||
self.assetversion_statuses = self._get_all_assetversion_statuses(
|
||||
project_entity
|
||||
)
|
||||
|
||||
|
|
@ -233,18 +234,28 @@ class ExtractHarmonyZip(openpype.api.Extractor):
|
|||
"version": 1,
|
||||
"ext": "zip",
|
||||
}
|
||||
host_name = "harmony"
|
||||
template_name = get_workfile_template_key_from_context(
|
||||
instance.data["asset"],
|
||||
instance.data.get("task"),
|
||||
host_name,
|
||||
project_name=project_entity["name"],
|
||||
dbcon=io
|
||||
)
|
||||
|
||||
# Get a valid work filename first with version 1
|
||||
file_template = anatomy.templates["work"]["file"]
|
||||
file_template = anatomy.templates[template_name]["file"]
|
||||
anatomy_filled = anatomy.format(data)
|
||||
work_path = anatomy_filled["work"]["path"]
|
||||
work_path = anatomy_filled[template_name]["path"]
|
||||
|
||||
# Get the final work filename with the proper version
|
||||
data["version"] = api.last_workfile_with_version(
|
||||
os.path.dirname(work_path), file_template, data, [".zip"]
|
||||
os.path.dirname(work_path),
|
||||
file_template,
|
||||
data,
|
||||
api.HOST_WORKFILE_EXTENSIONS[host_name]
|
||||
)[1]
|
||||
|
||||
work_path = anatomy_filled["work"]["path"]
|
||||
base_name = os.path.splitext(os.path.basename(work_path))[0]
|
||||
|
||||
staging_work_path = os.path.join(os.path.dirname(staging_scene),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from openpype.lib import get_subset_name
|
|||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
label = "Collect Instances"
|
||||
order = pyblish.api.CollectorOrder - 1
|
||||
order = pyblish.api.CollectorOrder - 0.4
|
||||
hosts = ["tvpaint"]
|
||||
|
||||
def process(self, context):
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from openpype.lib import get_subset_name
|
|||
|
||||
class CollectWorkfile(pyblish.api.ContextPlugin):
|
||||
label = "Collect Workfile"
|
||||
order = pyblish.api.CollectorOrder - 1
|
||||
order = pyblish.api.CollectorOrder - 0.4
|
||||
hosts = ["tvpaint"]
|
||||
|
||||
def process(self, context):
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class ResetTVPaintWorkfileMetadata(pyblish.api.Action):
|
|||
|
||||
class CollectWorkfileData(pyblish.api.ContextPlugin):
|
||||
label = "Collect Workfile Data"
|
||||
order = pyblish.api.CollectorOrder - 1.01
|
||||
order = pyblish.api.CollectorOrder - 0.45
|
||||
hosts = ["tvpaint"]
|
||||
actions = [ResetTVPaintWorkfileMetadata]
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,9 @@ from .plugin_tools import (
|
|||
from .path_tools import (
|
||||
version_up,
|
||||
get_version_from_path,
|
||||
get_last_version_from_path
|
||||
get_last_version_from_path,
|
||||
create_project_folders,
|
||||
get_project_basic_paths
|
||||
)
|
||||
|
||||
from .editorial import (
|
||||
|
|
@ -276,5 +278,7 @@ __all__ = [
|
|||
"range_from_frames",
|
||||
"frames_to_secons",
|
||||
"frames_to_timecode",
|
||||
"make_sequence_collection"
|
||||
"make_sequence_collection",
|
||||
"create_project_folders",
|
||||
"get_project_basic_paths"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ from .local_settings import get_openpype_username
|
|||
from .avalon_context import (
|
||||
get_workdir_data,
|
||||
get_workdir_with_workdir_data,
|
||||
get_workfile_template_key_from_context
|
||||
get_workfile_template_key
|
||||
)
|
||||
|
||||
from .python_module_tools import (
|
||||
|
|
@ -1226,8 +1226,12 @@ def prepare_context_environments(data):
|
|||
|
||||
# Load project specific environments
|
||||
project_name = project_doc["name"]
|
||||
project_settings = get_project_settings(project_name)
|
||||
data["project_settings"] = project_settings
|
||||
# Apply project specific environments on current env value
|
||||
apply_project_environments_value(project_name, data["env"])
|
||||
apply_project_environments_value(
|
||||
project_name, data["env"], project_settings
|
||||
)
|
||||
|
||||
app = data["app"]
|
||||
workdir_data = get_workdir_data(
|
||||
|
|
@ -1237,17 +1241,19 @@ def prepare_context_environments(data):
|
|||
|
||||
anatomy = data["anatomy"]
|
||||
|
||||
template_key = get_workfile_template_key_from_context(
|
||||
asset_doc["name"],
|
||||
task_name,
|
||||
asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
|
||||
task_info = asset_tasks.get(task_name) or {}
|
||||
task_type = task_info.get("type")
|
||||
workfile_template_key = get_workfile_template_key(
|
||||
task_type,
|
||||
app.host_name,
|
||||
project_name=project_name,
|
||||
dbcon=data["dbcon"]
|
||||
project_settings=project_settings
|
||||
)
|
||||
|
||||
try:
|
||||
workdir = get_workdir_with_workdir_data(
|
||||
workdir_data, anatomy, template_key=template_key
|
||||
workdir_data, anatomy, template_key=workfile_template_key
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
|
|
@ -1281,10 +1287,10 @@ def prepare_context_environments(data):
|
|||
)
|
||||
data["env"].update(context_env)
|
||||
|
||||
_prepare_last_workfile(data, workdir)
|
||||
_prepare_last_workfile(data, workdir, workfile_template_key)
|
||||
|
||||
|
||||
def _prepare_last_workfile(data, workdir):
|
||||
def _prepare_last_workfile(data, workdir, workfile_template_key):
|
||||
"""last workfile workflow preparation.
|
||||
|
||||
Function check if should care about last workfile workflow and tries
|
||||
|
|
@ -1345,7 +1351,7 @@ def _prepare_last_workfile(data, workdir):
|
|||
if extensions:
|
||||
anatomy = data["anatomy"]
|
||||
# Find last workfile
|
||||
file_template = anatomy.templates["work"]["file"]
|
||||
file_template = anatomy.templates[workfile_template_key]["file"]
|
||||
workdir_data.update({
|
||||
"version": 1,
|
||||
"user": get_openpype_username(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
|
||||
|
||||
from .anatomy import Anatomy
|
||||
from openpype.settings import get_project_settings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -77,7 +82,7 @@ def get_version_from_path(file):
|
|||
"""
|
||||
pattern = re.compile(r"[\._]v([0-9]+)", re.IGNORECASE)
|
||||
try:
|
||||
return pattern.findall(file)[0]
|
||||
return pattern.findall(file)[-1]
|
||||
except IndexError:
|
||||
log.error(
|
||||
"templates:get_version_from_workfile:"
|
||||
|
|
@ -119,3 +124,73 @@ def get_last_version_from_path(path_dir, filter):
|
|||
return filtred_files[-1]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def compute_paths(basic_paths_items, project_root):
|
||||
pattern_array = re.compile(r"\[.*\]")
|
||||
project_root_key = "__project_root__"
|
||||
output = []
|
||||
for path_items in basic_paths_items:
|
||||
clean_items = []
|
||||
for path_item in path_items:
|
||||
matches = re.findall(pattern_array, path_item)
|
||||
if len(matches) > 0:
|
||||
path_item = path_item.replace(matches[0], "")
|
||||
if path_item == project_root_key:
|
||||
path_item = project_root
|
||||
clean_items.append(path_item)
|
||||
output.append(os.path.normpath(os.path.sep.join(clean_items)))
|
||||
return output
|
||||
|
||||
|
||||
def create_project_folders(basic_paths, project_name):
|
||||
anatomy = Anatomy(project_name)
|
||||
roots_paths = []
|
||||
if isinstance(anatomy.roots, dict):
|
||||
for root in anatomy.roots.values():
|
||||
roots_paths.append(root.value)
|
||||
else:
|
||||
roots_paths.append(anatomy.roots.value)
|
||||
|
||||
for root_path in roots_paths:
|
||||
project_root = os.path.join(root_path, project_name)
|
||||
full_paths = compute_paths(basic_paths, project_root)
|
||||
# Create folders
|
||||
for path in full_paths:
|
||||
full_path = path.format(project_root=project_root)
|
||||
if os.path.exists(full_path):
|
||||
log.debug(
|
||||
"Folder already exists: {}".format(full_path)
|
||||
)
|
||||
else:
|
||||
log.debug("Creating folder: {}".format(full_path))
|
||||
os.makedirs(full_path)
|
||||
|
||||
|
||||
def _list_path_items(folder_structure):
|
||||
output = []
|
||||
for key, value in folder_structure.items():
|
||||
if not value:
|
||||
output.append(key)
|
||||
else:
|
||||
paths = _list_path_items(value)
|
||||
for path in paths:
|
||||
if not isinstance(path, (list, tuple)):
|
||||
path = [path]
|
||||
|
||||
output.append([key, *path])
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def get_project_basic_paths(project_name):
|
||||
project_settings = get_project_settings(project_name)
|
||||
folder_structure = (
|
||||
project_settings["global"]["project_folder_structure"]
|
||||
)
|
||||
if not folder_structure:
|
||||
return []
|
||||
|
||||
if isinstance(folder_structure, str):
|
||||
folder_structure = json.loads(folder_structure)
|
||||
return _list_path_items(folder_structure)
|
||||
|
|
|
|||
|
|
@ -165,7 +165,8 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None):
|
|||
if match == -1:
|
||||
profile_value = profile.get(key) or []
|
||||
logger.debug(
|
||||
"\"{}\" not found in {}".format(key, profile_value)
|
||||
"\"{}\" not found in \"{}\": {}".format(value, key,
|
||||
profile_value)
|
||||
)
|
||||
profile_points = -1
|
||||
break
|
||||
|
|
@ -192,13 +193,13 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None):
|
|||
])
|
||||
|
||||
if not matching_profiles:
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"None of profiles match your setup. {}".format(log_parts)
|
||||
)
|
||||
return None
|
||||
|
||||
if len(matching_profiles) > 1:
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"More than one profile match your setup. {}".format(log_parts)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,125 +1,143 @@
|
|||
# OpenPype modules/addons
|
||||
OpenPype modules should contain separated logic of specific kind of implementation, like Ftrack connection and usage code or Deadline farm rendering or may contain only special plugins. Addons work the same way currently there is no difference in module and addon.
|
||||
OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its usage code, Deadline farm rendering or may contain only special plugins. Addons work the same way currently, there is no difference between module and addon functionality.
|
||||
|
||||
## Modules concept
|
||||
- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the modulo located
|
||||
- modules or addons should never be imported directly even if you know possible full import path
|
||||
- it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts
|
||||
- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the module located
|
||||
- modules or addons should never be imported directly, even if you know possible full import path
|
||||
- it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts
|
||||
|
||||
### TODOs
|
||||
- add module/addon manifest
|
||||
- definition of module (not 100% defined content e.g. minimum require OpenPype version etc.)
|
||||
- defying that folder is content of a module or an addon
|
||||
- module/addon have it's settings schemas and default values outside OpenPype
|
||||
- add general setting of paths to modules
|
||||
- definition of module (not 100% defined content e.g. minimum required OpenPype version etc.)
|
||||
- defining a folder as a content of a module or an addon
|
||||
|
||||
## Base class `OpenPypeModule`
|
||||
- abstract class as base for each module
|
||||
- implementation should be module's api withou GUI parts
|
||||
- may implement `get_global_environments` method which should return dictionary of environments that are globally appliable and value is the same for whole studio if launched at any workstation (except os specific paths)
|
||||
- implementation should contain module's api without GUI parts
|
||||
- may implement `get_global_environments` method which should return dictionary of environments that are globally applicable and value is the same for whole studio if launched at any workstation (except os specific paths)
|
||||
- abstract parts:
|
||||
- `name` attribute - name of a module
|
||||
- `initialize` method - method for own initialization of a module (should not override `__init__`)
|
||||
- `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules
|
||||
- `__init__` should not be overriden and `initialize` should not do time consuming part but only prepare base data about module
|
||||
- also keep in mind that they may be initialized in headless mode
|
||||
- `name` attribute - name of a module
|
||||
- `initialize` method - method for own initialization of a module (should not override `__init__`)
|
||||
- `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules
|
||||
- `__init__` should not be overridden and `initialize` should not do time consuming part but only prepare base data about module
|
||||
- also keep in mind that they may be initialized in headless mode
|
||||
- connection with other modules is made with help of interfaces
|
||||
|
||||
## Addon class `OpenPypeAddOn`
|
||||
- inherits from `OpenPypeModule` but is enabled by default and doesn't have to implement `initialize` and `connect_with_modules` methods
|
||||
- that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...)
|
||||
|
||||
## How to add addons/modules
|
||||
- in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder
|
||||
- for openpype example addons use `{OPENPYPE_REPOS_ROOT}/openpype/modules/example_addons`
|
||||
|
||||
## Addon/module settings
|
||||
- addons/modules may have defined custom settings definitions with default values
|
||||
- it is based on settings type `dynamic_schema` which has `name`
|
||||
- that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults
|
||||
- they can't be added to any schema hierarchy
|
||||
- item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`)
|
||||
- addons may define it's dynamic schema items
|
||||
- they can be defined with class which inherits from `BaseModuleSettingsDef`
|
||||
- it is recommended to use pre implemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values
|
||||
- check it's docstring and check for `example_addon` in example addons
|
||||
- settings definition returns schemas by dynamic schemas names
|
||||
|
||||
# Interfaces
|
||||
- interface is class that has defined abstract methods to implement and may contain preimplemented helper methods
|
||||
- interface is class that has defined abstract methods to implement and may contain pre implemented helper methods
|
||||
- module that inherit from an interface must implement those abstract methods otherwise won't be initialized
|
||||
- it is easy to find which module object inherited from which interfaces withh 100% chance they have implemented required methods
|
||||
- it is easy to find which module object inherited from which interfaces with 100% chance they have implemented required methods
|
||||
- interfaces can be defined in `interfaces.py` inside module directory
|
||||
- the file can't use relative imports or import anything from other parts
|
||||
of module itself at the header of file
|
||||
- this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation
|
||||
- the file can't use relative imports or import anything from other parts
|
||||
of module itself at the header of file
|
||||
- this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation
|
||||
|
||||
## Base class `OpenPypeInterface`
|
||||
- has nothing implemented
|
||||
- has ABCMeta as metaclass
|
||||
- is defined to be able find out classes which inherit from this base to be
|
||||
able tell this is an Interface
|
||||
able tell this is an Interface
|
||||
|
||||
## Global interfaces
|
||||
- few interfaces are implemented for global usage
|
||||
|
||||
### IPluginPaths
|
||||
- module want to add directory path/s to avalon or publish plugins
|
||||
- module wants to add directory path/s to avalon or publish plugins
|
||||
- module must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"`
|
||||
- each key may contain list or string with path to directory with plugins
|
||||
- each key may contain list or string with a path to directory with plugins
|
||||
|
||||
### ITrayModule
|
||||
- module has more logic when used in tray
|
||||
- it is possible that module can be used only in tray
|
||||
- module has more logic when used in a tray
|
||||
- it is possible that module can be used only in the tray
|
||||
- abstract methods
|
||||
- `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules`
|
||||
- `tray_menu` - add actions to tray widget's menu that represent the module
|
||||
- `tray_start` - start of module's login in tray
|
||||
- module is initialized and connected with other modules
|
||||
- `tray_exit` - module's cleanup like stop and join threads etc.
|
||||
- order of calling is based on implementation this order is how it works with `TrayModulesManager`
|
||||
- it is recommended to import and use GUI implementaion only in these methods
|
||||
- `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules`
|
||||
- `tray_menu` - add actions to tray widget's menu that represent the module
|
||||
- `tray_start` - start of module's login in tray
|
||||
- module is initialized and connected with other modules
|
||||
- `tray_exit` - module's cleanup like stop and join threads etc.
|
||||
- order of calling is based on implementation this order is how it works with `TrayModulesManager`
|
||||
- it is recommended to import and use GUI implementation only in these methods
|
||||
- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init`
|
||||
- if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations
|
||||
- if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations
|
||||
|
||||
### ITrayService
|
||||
- inherit from `ITrayModule` and implement `tray_menu` method for you
|
||||
- add action to submenu "Services" in tray widget menu with icon and label
|
||||
- abstract atttribute `label`
|
||||
- label shown in menu
|
||||
- interface has preimplemented methods to change icon color
|
||||
- `set_service_running` - green icon
|
||||
- `set_service_failed` - red icon
|
||||
- `set_service_idle` - orange icon
|
||||
- these states must be set by module itself `set_service_running` is default state on initialization
|
||||
- inherits from `ITrayModule` and implements `tray_menu` method for you
|
||||
- adds action to submenu "Services" in tray widget menu with icon and label
|
||||
- abstract attribute `label`
|
||||
- label shown in menu
|
||||
- interface has pre implemented methods to change icon color
|
||||
- `set_service_running` - green icon
|
||||
- `set_service_failed` - red icon
|
||||
- `set_service_idle` - orange icon
|
||||
- these states must be set by module itself `set_service_running` is default state on initialization
|
||||
|
||||
### ITrayAction
|
||||
- inherit from `ITrayModule` and implement `tray_menu` method for you
|
||||
- add action to tray widget menu with label
|
||||
- abstract atttribute `label`
|
||||
- label shown in menu
|
||||
- inherits from `ITrayModule` and implements `tray_menu` method for you
|
||||
- adds action to tray widget menu with label
|
||||
- abstract attribute `label`
|
||||
- label shown in menu
|
||||
- abstract method `on_action_trigger`
|
||||
- what should happen when action is triggered
|
||||
- NOTE: It is good idea to implement logic in `on_action_trigger` to api method and trigger that methods on callbacks this gives ability to trigger that method outside tray
|
||||
- what should happen when an action is triggered
|
||||
- NOTE: It is a good idea to implement logic in `on_action_trigger` to the api method and trigger that method on callbacks. This gives ability to trigger that method outside tray
|
||||
|
||||
## Modules interfaces
|
||||
- modules may have defined their interfaces to be able recognize other modules that would want to use their features
|
||||
-
|
||||
### Example:
|
||||
- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which of other modules want to add paths to server/user event handlers
|
||||
- Clockify module use `IFtrackEventHandlerPaths` and return paths to clockify ftrack synchronizers
|
||||
- modules may have defined their own interfaces to be able to recognize other modules that would want to use their features
|
||||
|
||||
- Clockify has more inharitance it's class definition looks like
|
||||
### Example:
|
||||
- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which other modules want to add paths to server/user event handlers
|
||||
- Clockify module use `IFtrackEventHandlerPaths` and returns paths to clockify ftrack synchronizers
|
||||
|
||||
- Clockify inherits from more interfaces. It's class definition looks like:
|
||||
```
|
||||
class ClockifyModule(
|
||||
OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize.
|
||||
ITrayModule, # Says has special implementation when used in tray.
|
||||
IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher).
|
||||
IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server.
|
||||
ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module.
|
||||
OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize.
|
||||
ITrayModule, # Says has special implementation when used in tray.
|
||||
IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher).
|
||||
IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server.
|
||||
ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module.
|
||||
):
|
||||
```
|
||||
|
||||
### ModulesManager
|
||||
- collect module classes and tries to initialize them
|
||||
- collects module classes and tries to initialize them
|
||||
- important attributes
|
||||
- `modules` - list of available attributes
|
||||
- `modules_by_id` - dictionary of modules mapped by their ids
|
||||
- `modules_by_name` - dictionary of modules mapped by their names
|
||||
- all these attributes contain all found modules even if are not enabled
|
||||
- `modules` - list of available attributes
|
||||
- `modules_by_id` - dictionary of modules mapped by their ids
|
||||
- `modules_by_name` - dictionary of modules mapped by their names
|
||||
- all these attributes contain all found modules even if are not enabled
|
||||
- helper methods
|
||||
- `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them
|
||||
- `collect_plugin_paths` collect plugin paths from all enabled modules
|
||||
- output is always dictionary with all keys and values as list
|
||||
```
|
||||
{
|
||||
"publish": [],
|
||||
"create": [],
|
||||
"load": [],
|
||||
"actions": []
|
||||
}
|
||||
```
|
||||
- `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them
|
||||
- `collect_plugin_paths` collects plugin paths from all enabled modules
|
||||
- output is always dictionary with all keys and values as an list
|
||||
```
|
||||
{
|
||||
"publish": [],
|
||||
"create": [],
|
||||
"load": [],
|
||||
"actions": []
|
||||
}
|
||||
```
|
||||
|
||||
### TrayModulesManager
|
||||
- inherit from `ModulesManager`
|
||||
- has specific implementations for Pype Tray tool and handle `ITrayModule` methods
|
||||
- inherits from `ModulesManager`
|
||||
- has specific implementation for Pype Tray tool and handle `ITrayModule` methods
|
||||
|
|
@ -1,21 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .base import (
|
||||
OpenPypeModule,
|
||||
OpenPypeAddOn,
|
||||
OpenPypeInterface,
|
||||
|
||||
load_modules,
|
||||
|
||||
ModulesManager,
|
||||
TrayModulesManager
|
||||
TrayModulesManager,
|
||||
|
||||
BaseModuleSettingsDef,
|
||||
ModuleSettingsDef,
|
||||
JsonFilesSettingsDef,
|
||||
|
||||
get_module_settings_defs
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"OpenPypeModule",
|
||||
"OpenPypeAddOn",
|
||||
"OpenPypeInterface",
|
||||
|
||||
"load_modules",
|
||||
|
||||
"ModulesManager",
|
||||
"TrayModulesManager"
|
||||
"TrayModulesManager",
|
||||
|
||||
"BaseModuleSettingsDef",
|
||||
"ModuleSettingsDef",
|
||||
"JsonFilesSettingsDef",
|
||||
|
||||
"get_module_settings_defs"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
"""Base class for Pype Modules."""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import inspect
|
||||
import logging
|
||||
import platform
|
||||
import threading
|
||||
import collections
|
||||
from uuid import uuid4
|
||||
|
|
@ -12,7 +14,18 @@ from abc import ABCMeta, abstractmethod
|
|||
import six
|
||||
|
||||
import openpype
|
||||
from openpype.settings import get_system_settings
|
||||
from openpype.settings import (
|
||||
get_system_settings,
|
||||
SYSTEM_SETTINGS_KEY,
|
||||
PROJECT_SETTINGS_KEY,
|
||||
SCHEMA_KEY_SYSTEM_SETTINGS,
|
||||
SCHEMA_KEY_PROJECT_SETTINGS
|
||||
)
|
||||
|
||||
from openpype.settings.lib import (
|
||||
get_studio_system_settings_overrides,
|
||||
load_json_file
|
||||
)
|
||||
from openpype.lib import PypeLogger
|
||||
|
||||
|
||||
|
|
@ -115,11 +128,51 @@ def get_default_modules_dir():
|
|||
return os.path.join(current_dir, "default_modules")
|
||||
|
||||
|
||||
def get_dynamic_modules_dirs():
|
||||
"""Possible paths to OpenPype Addons of Modules.
|
||||
|
||||
Paths are loaded from studio settings under:
|
||||
`modules -> addon_paths -> {platform name}`
|
||||
|
||||
Path may contain environment variable as a formatting string.
|
||||
|
||||
They are not validated or checked their existence.
|
||||
|
||||
Returns:
|
||||
list: Paths loaded from studio overrides.
|
||||
"""
|
||||
output = []
|
||||
value = get_studio_system_settings_overrides()
|
||||
for key in ("modules", "addon_paths", platform.system().lower()):
|
||||
if key not in value:
|
||||
return output
|
||||
value = value[key]
|
||||
|
||||
for path in value:
|
||||
if not path:
|
||||
continue
|
||||
|
||||
try:
|
||||
path = path.format(**os.environ)
|
||||
except Exception:
|
||||
pass
|
||||
output.append(path)
|
||||
return output
|
||||
|
||||
|
||||
def get_module_dirs():
|
||||
"""List of paths where OpenPype modules can be found."""
|
||||
dirpaths = [
|
||||
get_default_modules_dir()
|
||||
]
|
||||
_dirpaths = []
|
||||
_dirpaths.append(get_default_modules_dir())
|
||||
_dirpaths.extend(get_dynamic_modules_dirs())
|
||||
|
||||
dirpaths = []
|
||||
for path in _dirpaths:
|
||||
if not path:
|
||||
continue
|
||||
normalized = os.path.normpath(path)
|
||||
if normalized not in dirpaths:
|
||||
dirpaths.append(normalized)
|
||||
return dirpaths
|
||||
|
||||
|
||||
|
|
@ -165,6 +218,9 @@ def _load_interfaces():
|
|||
os.path.join(get_default_modules_dir(), "interfaces.py")
|
||||
)
|
||||
for dirpath in dirpaths:
|
||||
if not os.path.exists(dirpath):
|
||||
continue
|
||||
|
||||
for filename in os.listdir(dirpath):
|
||||
if filename in ("__pycache__", ):
|
||||
continue
|
||||
|
|
@ -272,12 +328,19 @@ def _load_modules():
|
|||
|
||||
# TODO add more logic how to define if folder is module or not
|
||||
# - check manifest and content of manifest
|
||||
if os.path.isdir(fullpath):
|
||||
import_module_from_dirpath(dirpath, filename, modules_key)
|
||||
try:
|
||||
if os.path.isdir(fullpath):
|
||||
import_module_from_dirpath(dirpath, filename, modules_key)
|
||||
|
||||
elif ext in (".py", ):
|
||||
module = import_filepath(fullpath)
|
||||
setattr(openpype_modules, basename, module)
|
||||
elif ext in (".py", ):
|
||||
module = import_filepath(fullpath)
|
||||
setattr(openpype_modules, basename, module)
|
||||
|
||||
except Exception:
|
||||
log.error(
|
||||
"Failed to import '{}'.".format(fullpath),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
|
||||
class _OpenPypeInterfaceMeta(ABCMeta):
|
||||
|
|
@ -368,7 +431,16 @@ class OpenPypeModule:
|
|||
|
||||
|
||||
class OpenPypeAddOn(OpenPypeModule):
|
||||
pass
|
||||
# Enable Addon by default
|
||||
enabled = True
|
||||
|
||||
def initialize(self, module_settings):
|
||||
"""Initialization is not be required for most of addons."""
|
||||
pass
|
||||
|
||||
def connect_with_modules(self, enabled_modules):
|
||||
"""Do not require to implement connection with modules for addon."""
|
||||
pass
|
||||
|
||||
|
||||
class ModulesManager:
|
||||
|
|
@ -423,6 +495,7 @@ class ModulesManager:
|
|||
if (
|
||||
not inspect.isclass(modules_item)
|
||||
or modules_item is OpenPypeModule
|
||||
or modules_item is OpenPypeAddOn
|
||||
or not issubclass(modules_item, OpenPypeModule)
|
||||
):
|
||||
continue
|
||||
|
|
@ -920,3 +993,424 @@ class TrayModulesManager(ModulesManager):
|
|||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
|
||||
def get_module_settings_defs():
|
||||
"""Check loaded addons/modules for existence of thei settings definition.
|
||||
|
||||
Check if OpenPype addon/module as python module has class that inherit
|
||||
from `ModuleSettingsDef` in python module variables (imported
|
||||
in `__init__py`).
|
||||
|
||||
Returns:
|
||||
list: All valid and not abstract settings definitions from imported
|
||||
openpype addons and modules.
|
||||
"""
|
||||
# Make sure modules are loaded
|
||||
load_modules()
|
||||
|
||||
import openpype_modules
|
||||
|
||||
settings_defs = []
|
||||
|
||||
log = PypeLogger.get_logger("ModuleSettingsLoad")
|
||||
|
||||
for raw_module in openpype_modules:
|
||||
for attr_name in dir(raw_module):
|
||||
attr = getattr(raw_module, attr_name)
|
||||
if (
|
||||
not inspect.isclass(attr)
|
||||
or attr is ModuleSettingsDef
|
||||
or not issubclass(attr, ModuleSettingsDef)
|
||||
):
|
||||
continue
|
||||
|
||||
if inspect.isabstract(attr):
|
||||
# Find missing implementations by convetion on `abc` module
|
||||
not_implemented = []
|
||||
for attr_name in dir(attr):
|
||||
attr = getattr(attr, attr_name, None)
|
||||
abs_method = getattr(
|
||||
attr, "__isabstractmethod__", None
|
||||
)
|
||||
if attr and abs_method:
|
||||
not_implemented.append(attr_name)
|
||||
|
||||
# Log missing implementations
|
||||
log.warning((
|
||||
"Skipping abstract Class: {} in module {}."
|
||||
" Missing implementations: {}"
|
||||
).format(
|
||||
attr_name, raw_module.__name__, ", ".join(not_implemented)
|
||||
))
|
||||
continue
|
||||
|
||||
settings_defs.append(attr)
|
||||
|
||||
return settings_defs
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class BaseModuleSettingsDef:
|
||||
"""Definition of settings for OpenPype module or AddOn."""
|
||||
_id = None
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""ID created on initialization.
|
||||
|
||||
ID should be per created object. Helps to store objects.
|
||||
"""
|
||||
if self._id is None:
|
||||
self._id = uuid4()
|
||||
return self._id
|
||||
|
||||
@abstractmethod
|
||||
def get_settings_schemas(self, schema_type):
|
||||
"""Setting schemas for passed schema type.
|
||||
|
||||
These are main schemas by dynamic schema keys. If they're using
|
||||
sub schemas or templates they should be loaded with
|
||||
`get_dynamic_schemas`.
|
||||
|
||||
Returns:
|
||||
dict: Schema by `dynamic_schema` keys.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_dynamic_schemas(self, schema_type):
|
||||
"""Settings schemas and templates that can be used anywhere.
|
||||
|
||||
It is recommended to add prefix specific for addon/module to keys
|
||||
(e.g. "my_addon/real_schema_name").
|
||||
|
||||
Returns:
|
||||
dict: Schemas and templates by their keys.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_defaults(self, top_key):
|
||||
"""Default values for passed top key.
|
||||
|
||||
Top keys are (currently) "system_settings" or "project_settings".
|
||||
|
||||
Should return exactly what was passed with `save_defaults`.
|
||||
|
||||
Returns:
|
||||
dict: Default values by path to first key in OpenPype defaults.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_defaults(self, top_key, data):
|
||||
"""Save default values for passed top key.
|
||||
|
||||
Top keys are (currently) "system_settings" or "project_settings".
|
||||
|
||||
Passed data are by path to first key defined in main schemas.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ModuleSettingsDef(BaseModuleSettingsDef):
|
||||
"""Settings definiton with separated system and procect settings parts.
|
||||
|
||||
Reduce conditions that must be checked and adds predefined methods for
|
||||
each case.
|
||||
"""
|
||||
def get_defaults(self, top_key):
|
||||
"""Split method into 2 methods by top key."""
|
||||
if top_key == SYSTEM_SETTINGS_KEY:
|
||||
return self.get_default_system_settings() or {}
|
||||
elif top_key == PROJECT_SETTINGS_KEY:
|
||||
return self.get_default_project_settings() or {}
|
||||
return {}
|
||||
|
||||
def save_defaults(self, top_key, data):
|
||||
"""Split method into 2 methods by top key."""
|
||||
if top_key == SYSTEM_SETTINGS_KEY:
|
||||
self.save_system_defaults(data)
|
||||
elif top_key == PROJECT_SETTINGS_KEY:
|
||||
self.save_project_defaults(data)
|
||||
|
||||
def get_settings_schemas(self, schema_type):
|
||||
"""Split method into 2 methods by schema type."""
|
||||
if schema_type == SCHEMA_KEY_SYSTEM_SETTINGS:
|
||||
return self.get_system_settings_schemas() or {}
|
||||
elif schema_type == SCHEMA_KEY_PROJECT_SETTINGS:
|
||||
return self.get_project_settings_schemas() or {}
|
||||
return {}
|
||||
|
||||
def get_dynamic_schemas(self, schema_type):
|
||||
"""Split method into 2 methods by schema type."""
|
||||
if schema_type == SCHEMA_KEY_SYSTEM_SETTINGS:
|
||||
return self.get_system_dynamic_schemas() or {}
|
||||
elif schema_type == SCHEMA_KEY_PROJECT_SETTINGS:
|
||||
return self.get_project_dynamic_schemas() or {}
|
||||
return {}
|
||||
|
||||
@abstractmethod
|
||||
def get_system_settings_schemas(self):
|
||||
"""Schemas and templates usable in system settings schemas.
|
||||
|
||||
Returns:
|
||||
dict: Schemas and templates by it's names. Names must be unique
|
||||
across whole OpenPype.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_project_settings_schemas(self):
|
||||
"""Schemas and templates usable in project settings schemas.
|
||||
|
||||
Returns:
|
||||
dict: Schemas and templates by it's names. Names must be unique
|
||||
across whole OpenPype.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_system_dynamic_schemas(self):
|
||||
"""System schemas by dynamic schema name.
|
||||
|
||||
If dynamic schema name is not available in then schema will not used.
|
||||
|
||||
Returns:
|
||||
dict: Schemas or list of schemas by dynamic schema name.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_project_dynamic_schemas(self):
|
||||
"""Project schemas by dynamic schema name.
|
||||
|
||||
If dynamic schema name is not available in then schema will not used.
|
||||
|
||||
Returns:
|
||||
dict: Schemas or list of schemas by dynamic schema name.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_default_system_settings(self):
|
||||
"""Default system settings values.
|
||||
|
||||
Returns:
|
||||
dict: Default values by path to first key.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_default_project_settings(self):
|
||||
"""Default project settings values.
|
||||
|
||||
Returns:
|
||||
dict: Default values by path to first key.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_system_defaults(self, data):
|
||||
"""Save default system settings values.
|
||||
|
||||
Passed data are by path to first key defined in main schemas.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_project_defaults(self, data):
|
||||
"""Save default project settings values.
|
||||
|
||||
Passed data are by path to first key defined in main schemas.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class JsonFilesSettingsDef(ModuleSettingsDef):
|
||||
"""Preimplemented settings definition using json files and file structure.
|
||||
|
||||
Expected file structure:
|
||||
┕ root
|
||||
│
|
||||
│ # Default values
|
||||
┝ defaults
|
||||
│ ┝ system_settings.json
|
||||
│ ┕ project_settings.json
|
||||
│
|
||||
│ # Schemas for `dynamic_template` type
|
||||
┝ dynamic_schemas
|
||||
│ ┝ system_dynamic_schemas.json
|
||||
│ ┕ project_dynamic_schemas.json
|
||||
│
|
||||
│ # Schemas that can be used anywhere (enhancement for `dynamic_schemas`)
|
||||
┕ schemas
|
||||
┝ system_schemas
|
||||
│ ┝ <system schema.json> # Any schema or template files
|
||||
│ ┕ ...
|
||||
┕ project_schemas
|
||||
┝ <system schema.json> # Any schema or template files
|
||||
┕ ...
|
||||
|
||||
Schemas can be loaded with prefix to avoid duplicated schema/template names
|
||||
across all OpenPype addons/modules. Prefix can be defined with class
|
||||
attribute `schema_prefix`.
|
||||
|
||||
Only think which must be implemented in `get_settings_root_path` which
|
||||
should return directory path to `root` (in structure graph above).
|
||||
"""
|
||||
# Possible way how to define `schemas` prefix
|
||||
schema_prefix = ""
|
||||
|
||||
@abstractmethod
|
||||
def get_settings_root_path(self):
|
||||
"""Directory path where settings and it's schemas are located."""
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
settings_root_dir = self.get_settings_root_path()
|
||||
defaults_dir = os.path.join(
|
||||
settings_root_dir, "defaults"
|
||||
)
|
||||
dynamic_schemas_dir = os.path.join(
|
||||
settings_root_dir, "dynamic_schemas"
|
||||
)
|
||||
schemas_dir = os.path.join(
|
||||
settings_root_dir, "schemas"
|
||||
)
|
||||
|
||||
self.system_defaults_filepath = os.path.join(
|
||||
defaults_dir, "system_settings.json"
|
||||
)
|
||||
self.project_defaults_filepath = os.path.join(
|
||||
defaults_dir, "project_settings.json"
|
||||
)
|
||||
|
||||
self.system_dynamic_schemas_filepath = os.path.join(
|
||||
dynamic_schemas_dir, "system_dynamic_schemas.json"
|
||||
)
|
||||
self.project_dynamic_schemas_filepath = os.path.join(
|
||||
dynamic_schemas_dir, "project_dynamic_schemas.json"
|
||||
)
|
||||
|
||||
self.system_schemas_dir = os.path.join(
|
||||
schemas_dir, "system_schemas"
|
||||
)
|
||||
self.project_schemas_dir = os.path.join(
|
||||
schemas_dir, "project_schemas"
|
||||
)
|
||||
|
||||
def _load_json_file_data(self, path):
|
||||
if os.path.exists(path):
|
||||
return load_json_file(path)
|
||||
return {}
|
||||
|
||||
def get_default_system_settings(self):
|
||||
"""Default system settings values.
|
||||
|
||||
Returns:
|
||||
dict: Default values by path to first key.
|
||||
"""
|
||||
return self._load_json_file_data(self.system_defaults_filepath)
|
||||
|
||||
def get_default_project_settings(self):
|
||||
"""Default project settings values.
|
||||
|
||||
Returns:
|
||||
dict: Default values by path to first key.
|
||||
"""
|
||||
return self._load_json_file_data(self.project_defaults_filepath)
|
||||
|
||||
def _save_data_to_filepath(self, path, data):
|
||||
dirpath = os.path.dirname(path)
|
||||
if not os.path.exists(dirpath):
|
||||
os.makedirs(dirpath)
|
||||
|
||||
with open(path, "w") as file_stream:
|
||||
json.dump(data, file_stream, indent=4)
|
||||
|
||||
def save_system_defaults(self, data):
|
||||
"""Save default system settings values.
|
||||
|
||||
Passed data are by path to first key defined in main schemas.
|
||||
"""
|
||||
self._save_data_to_filepath(self.system_defaults_filepath, data)
|
||||
|
||||
def save_project_defaults(self, data):
|
||||
"""Save default project settings values.
|
||||
|
||||
Passed data are by path to first key defined in main schemas.
|
||||
"""
|
||||
self._save_data_to_filepath(self.project_defaults_filepath, data)
|
||||
|
||||
def get_system_dynamic_schemas(self):
|
||||
"""System schemas by dynamic schema name.
|
||||
|
||||
If dynamic schema name is not available in then schema will not used.
|
||||
|
||||
Returns:
|
||||
dict: Schemas or list of schemas by dynamic schema name.
|
||||
"""
|
||||
return self._load_json_file_data(self.system_dynamic_schemas_filepath)
|
||||
|
||||
def get_project_dynamic_schemas(self):
|
||||
"""Project schemas by dynamic schema name.
|
||||
|
||||
If dynamic schema name is not available in then schema will not used.
|
||||
|
||||
Returns:
|
||||
dict: Schemas or list of schemas by dynamic schema name.
|
||||
"""
|
||||
return self._load_json_file_data(self.project_dynamic_schemas_filepath)
|
||||
|
||||
def _load_files_from_path(self, path):
|
||||
output = {}
|
||||
if not path or not os.path.exists(path):
|
||||
return output
|
||||
|
||||
if os.path.isfile(path):
|
||||
filename = os.path.basename(path)
|
||||
basename, ext = os.path.splitext(filename)
|
||||
if ext == ".json":
|
||||
if self.schema_prefix:
|
||||
key = "{}/{}".format(self.schema_prefix, basename)
|
||||
else:
|
||||
key = basename
|
||||
output[key] = self._load_json_file_data(path)
|
||||
return output
|
||||
|
||||
path = os.path.normpath(path)
|
||||
for root, _, files in os.walk(path, topdown=False):
|
||||
for filename in files:
|
||||
basename, ext = os.path.splitext(filename)
|
||||
if ext != ".json":
|
||||
continue
|
||||
|
||||
json_path = os.path.join(root, filename)
|
||||
store_key = os.path.join(
|
||||
root.replace(path, ""), basename
|
||||
).replace("\\", "/")
|
||||
if self.schema_prefix:
|
||||
store_key = "{}/{}".format(self.schema_prefix, store_key)
|
||||
output[store_key] = self._load_json_file_data(json_path)
|
||||
|
||||
return output
|
||||
|
||||
def get_system_settings_schemas(self):
|
||||
"""Schemas and templates usable in system settings schemas.
|
||||
|
||||
Returns:
|
||||
dict: Schemas and templates by it's names. Names must be unique
|
||||
across whole OpenPype.
|
||||
"""
|
||||
return self._load_files_from_path(self.system_schemas_dir)
|
||||
|
||||
def get_project_settings_schemas(self):
|
||||
"""Schemas and templates usable in project settings schemas.
|
||||
|
||||
Returns:
|
||||
dict: Schemas and templates by it's names. Names must be unique
|
||||
across whole OpenPype.
|
||||
"""
|
||||
return self._load_files_from_path(self.project_schemas_dir)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import re
|
|||
import json
|
||||
|
||||
from openpype_modules.ftrack.lib import BaseAction, statics_icon
|
||||
from openpype.api import Anatomy, get_project_settings
|
||||
from openpype.api import get_project_basic_paths, create_project_folders
|
||||
|
||||
|
||||
class CreateProjectFolders(BaseAction):
|
||||
|
|
@ -72,25 +72,18 @@ class CreateProjectFolders(BaseAction):
|
|||
def launch(self, session, entities, event):
|
||||
# Get project entity
|
||||
project_entity = self.get_project_from_entity(entities[0])
|
||||
# Load settings for project
|
||||
project_name = project_entity["full_name"]
|
||||
project_settings = get_project_settings(project_name)
|
||||
project_folder_structure = (
|
||||
project_settings["global"]["project_folder_structure"]
|
||||
)
|
||||
if not project_folder_structure:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Project structure is not set."
|
||||
}
|
||||
|
||||
try:
|
||||
if isinstance(project_folder_structure, str):
|
||||
project_folder_structure = json.loads(project_folder_structure)
|
||||
|
||||
# Get paths based on presets
|
||||
basic_paths = self.get_path_items(project_folder_structure)
|
||||
self.create_folders(basic_paths, project_entity)
|
||||
basic_paths = get_project_basic_paths(project_name)
|
||||
if not basic_paths:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Project structure is not set."
|
||||
}
|
||||
|
||||
# Invoking OpenPype API to create the project folders
|
||||
create_project_folders(basic_paths, project_name)
|
||||
self.create_ftrack_entities(basic_paths, project_entity)
|
||||
|
||||
except Exception as exc:
|
||||
|
|
@ -195,58 +188,6 @@ class CreateProjectFolders(BaseAction):
|
|||
self.session.commit()
|
||||
return new_ent
|
||||
|
||||
def get_path_items(self, in_dict):
|
||||
output = []
|
||||
for key, value in in_dict.items():
|
||||
if not value:
|
||||
output.append(key)
|
||||
else:
|
||||
paths = self.get_path_items(value)
|
||||
for path in paths:
|
||||
if not isinstance(path, (list, tuple)):
|
||||
path = [path]
|
||||
|
||||
output.append([key, *path])
|
||||
|
||||
return output
|
||||
|
||||
def compute_paths(self, basic_paths_items, project_root):
|
||||
output = []
|
||||
for path_items in basic_paths_items:
|
||||
clean_items = []
|
||||
for path_item in path_items:
|
||||
matches = re.findall(self.pattern_array, path_item)
|
||||
if len(matches) > 0:
|
||||
path_item = path_item.replace(matches[0], "")
|
||||
if path_item == self.project_root_key:
|
||||
path_item = project_root
|
||||
clean_items.append(path_item)
|
||||
output.append(os.path.normpath(os.path.sep.join(clean_items)))
|
||||
return output
|
||||
|
||||
def create_folders(self, basic_paths, project):
|
||||
anatomy = Anatomy(project["full_name"])
|
||||
roots_paths = []
|
||||
if isinstance(anatomy.roots, dict):
|
||||
for root in anatomy.roots.values():
|
||||
roots_paths.append(root.value)
|
||||
else:
|
||||
roots_paths.append(anatomy.roots.value)
|
||||
|
||||
for root_path in roots_paths:
|
||||
project_root = os.path.join(root_path, project["full_name"])
|
||||
full_paths = self.compute_paths(basic_paths, project_root)
|
||||
# Create folders
|
||||
for path in full_paths:
|
||||
full_path = path.format(project_root=project_root)
|
||||
if os.path.exists(full_path):
|
||||
self.log.debug(
|
||||
"Folder already exists: {}".format(full_path)
|
||||
)
|
||||
else:
|
||||
self.log.debug("Creating folder: {}".format(full_path))
|
||||
os.makedirs(full_path)
|
||||
|
||||
|
||||
def register(session):
|
||||
CreateProjectFolders(session).register()
|
||||
|
|
|
|||
|
|
@ -68,9 +68,6 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin):
|
|||
instance.data["families"].append("ftrack")
|
||||
else:
|
||||
instance.data["families"] = ["ftrack"]
|
||||
else:
|
||||
self.log.debug("Instance '{}' doesn't match any profile".format(
|
||||
instance.data.get("family")))
|
||||
|
||||
def _get_add_ftrack_f_from_addit_filters(self,
|
||||
additional_filters,
|
||||
|
|
|
|||
|
|
@ -29,13 +29,35 @@ class AbstractProvider:
|
|||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_configurable_items(cls):
|
||||
def get_system_settings_schema(cls):
|
||||
"""
|
||||
Returns filtered dict of editable properties
|
||||
Returns dict for editable properties on system settings level
|
||||
|
||||
|
||||
Returns:
|
||||
(dict)
|
||||
(list) of dict
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_project_settings_schema(cls):
|
||||
"""
|
||||
Returns dict for editable properties on project settings level
|
||||
|
||||
|
||||
Returns:
|
||||
(list) of dict
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_local_settings_schema(cls):
|
||||
"""
|
||||
Returns dict for editable properties on local settings level
|
||||
|
||||
|
||||
Returns:
|
||||
(list) of dict
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import platform
|
|||
from openpype.api import Logger
|
||||
from openpype.api import get_system_settings
|
||||
from .abstract_provider import AbstractProvider
|
||||
from ..utils import time_function, ResumableError, EditableScopes
|
||||
from ..utils import time_function, ResumableError
|
||||
|
||||
log = Logger().get_logger("SyncServer")
|
||||
|
||||
|
|
@ -96,30 +96,61 @@ class GDriveHandler(AbstractProvider):
|
|||
return self.service is not None
|
||||
|
||||
@classmethod
|
||||
def get_configurable_items(cls):
|
||||
def get_system_settings_schema(cls):
|
||||
"""
|
||||
Returns filtered dict of editable properties.
|
||||
Returns dict for editable properties on system settings level
|
||||
|
||||
|
||||
Returns:
|
||||
(list) of dict
|
||||
"""
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_project_settings_schema(cls):
|
||||
"""
|
||||
Returns dict for editable properties on project settings level
|
||||
|
||||
|
||||
Returns:
|
||||
(list) of dict
|
||||
"""
|
||||
# {platform} tells that value is multiplatform and only specific OS
|
||||
# should be returned
|
||||
editable = [
|
||||
# credentials could be overriden on Project or User level
|
||||
{
|
||||
'key': "credentials_url",
|
||||
'label': "Credentials url",
|
||||
'type': 'text'
|
||||
},
|
||||
# roots could be overriden only on Project leve, User cannot
|
||||
{
|
||||
'key': "roots",
|
||||
'label': "Roots",
|
||||
'type': 'dict'
|
||||
}
|
||||
]
|
||||
return editable
|
||||
|
||||
@classmethod
|
||||
def get_local_settings_schema(cls):
|
||||
"""
|
||||
Returns dict for editable properties on local settings level
|
||||
|
||||
|
||||
Returns:
|
||||
(dict)
|
||||
"""
|
||||
# {platform} tells that value is multiplatform and only specific OS
|
||||
# should be returned
|
||||
editable = {
|
||||
editable = [
|
||||
# credentials could be override on Project or User level
|
||||
'credentials_url': {
|
||||
'scope': [EditableScopes.PROJECT,
|
||||
EditableScopes.LOCAL],
|
||||
{
|
||||
'key': "credentials_url",
|
||||
'label': "Credentials url",
|
||||
'type': 'text',
|
||||
'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501
|
||||
},
|
||||
# roots could be override only on Project leve, User cannot
|
||||
'root': {'scope': [EditableScopes.PROJECT],
|
||||
'label': "Roots",
|
||||
'type': 'dict'}
|
||||
}
|
||||
}
|
||||
]
|
||||
return editable
|
||||
|
||||
def get_roots_config(self, anatomy=None):
|
||||
|
|
|
|||
|
|
@ -76,6 +76,14 @@ class ProviderFactory:
|
|||
|
||||
return provider_info[0].get_configurable_items()
|
||||
|
||||
def get_provider_cls(self, provider_code):
|
||||
"""
|
||||
Returns class object for 'provider_code' to run class methods on.
|
||||
"""
|
||||
provider_info = self._get_creator_info(provider_code)
|
||||
|
||||
return provider_info[0]
|
||||
|
||||
def _get_creator_info(self, provider):
|
||||
"""
|
||||
Collect all necessary info for provider. Currently only creator
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import time
|
|||
from openpype.api import Logger, Anatomy
|
||||
from .abstract_provider import AbstractProvider
|
||||
|
||||
from ..utils import EditableScopes
|
||||
|
||||
log = Logger().get_logger("SyncServer")
|
||||
|
||||
|
||||
|
|
@ -30,18 +28,51 @@ class LocalDriveHandler(AbstractProvider):
|
|||
return True
|
||||
|
||||
@classmethod
|
||||
def get_configurable_items(cls):
|
||||
def get_system_settings_schema(cls):
|
||||
"""
|
||||
Returns filtered dict of editable properties
|
||||
Returns dict for editable properties on system settings level
|
||||
|
||||
|
||||
Returns:
|
||||
(list) of dict
|
||||
"""
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_project_settings_schema(cls):
|
||||
"""
|
||||
Returns dict for editable properties on project settings level
|
||||
|
||||
|
||||
Returns:
|
||||
(list) of dict
|
||||
"""
|
||||
# for non 'studio' sites, 'studio' is configured in Anatomy
|
||||
editable = [
|
||||
{
|
||||
'key': "roots",
|
||||
'label': "Roots",
|
||||
'type': 'dict'
|
||||
}
|
||||
]
|
||||
return editable
|
||||
|
||||
@classmethod
|
||||
def get_local_settings_schema(cls):
|
||||
"""
|
||||
Returns dict for editable properties on local settings level
|
||||
|
||||
|
||||
Returns:
|
||||
(dict)
|
||||
"""
|
||||
editable = {
|
||||
'root': {'scope': [EditableScopes.LOCAL],
|
||||
'label': "Roots",
|
||||
'type': 'dict'}
|
||||
}
|
||||
editable = [
|
||||
{
|
||||
'key': "roots",
|
||||
'label': "Roots",
|
||||
'type': 'dict'
|
||||
}
|
||||
]
|
||||
return editable
|
||||
|
||||
def upload_file(self, source_path, target_path,
|
||||
|
|
|
|||
|
|
@ -16,14 +16,13 @@ from openpype.api import (
|
|||
get_local_site_id)
|
||||
from openpype.lib import PypeLogger
|
||||
from openpype.settings.lib import (
|
||||
get_default_project_settings,
|
||||
get_default_anatomy_settings,
|
||||
get_anatomy_settings)
|
||||
|
||||
from .providers.local_drive import LocalDriveHandler
|
||||
from .providers import lib
|
||||
|
||||
from .utils import time_function, SyncStatus, EditableScopes
|
||||
from .utils import time_function, SyncStatus
|
||||
|
||||
|
||||
log = PypeLogger().get_logger("SyncServer")
|
||||
|
|
@ -399,204 +398,239 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
|
|||
|
||||
return remote_site
|
||||
|
||||
def get_local_settings_schema(self):
|
||||
"""Wrapper for Local settings - all projects incl. Default"""
|
||||
return self.get_configurable_items(EditableScopes.LOCAL)
|
||||
# Methods for Settings UI to draw appropriate forms
|
||||
@classmethod
|
||||
def get_system_settings_schema(cls):
|
||||
""" Gets system level schema of configurable items
|
||||
|
||||
def get_configurable_items(self, scope=None):
|
||||
Used for Setting UI to provide forms.
|
||||
"""
|
||||
Returns list of sites that could be configurable for all projects.
|
||||
ret_dict = {}
|
||||
for provider_code in lib.factory.providers:
|
||||
ret_dict[provider_code] = \
|
||||
lib.factory.get_provider_cls(provider_code). \
|
||||
get_system_settings_schema()
|
||||
|
||||
Could be filtered by 'scope' argument (list)
|
||||
return ret_dict
|
||||
|
||||
Args:
|
||||
scope (list of utils.EditableScope)
|
||||
@classmethod
|
||||
def get_project_settings_schema(cls):
|
||||
""" Gets project level schema of configurable items.
|
||||
|
||||
Returns:
|
||||
(dict of list of dict)
|
||||
{
|
||||
siteA : [
|
||||
{
|
||||
key:"root", label:"root",
|
||||
"value":"{'work': 'c:/projects'}",
|
||||
"type": "dict",
|
||||
"children":[
|
||||
{ "key": "work",
|
||||
"type": "text",
|
||||
"value": "c:/projects"}
|
||||
]
|
||||
},
|
||||
{
|
||||
key:"credentials_url", label:"Credentials url",
|
||||
"value":"'c:/projects/cred.json'", "type": "text",
|
||||
"namespace": "{project_setting}/global/sync_server/
|
||||
sites"
|
||||
}
|
||||
]
|
||||
}
|
||||
It is not using Setting! Used for Setting UI to provide forms.
|
||||
"""
|
||||
editable = {}
|
||||
applicable_projects = list(self.connection.projects())
|
||||
applicable_projects.append(None)
|
||||
for project in applicable_projects:
|
||||
project_name = None
|
||||
if project:
|
||||
project_name = project["name"]
|
||||
ret_dict = {}
|
||||
for provider_code in lib.factory.providers:
|
||||
ret_dict[provider_code] = \
|
||||
lib.factory.get_provider_cls(provider_code). \
|
||||
get_project_settings_schema()
|
||||
|
||||
items = self.get_configurable_items_for_project(project_name,
|
||||
scope)
|
||||
editable.update(items)
|
||||
return ret_dict
|
||||
|
||||
return editable
|
||||
@classmethod
|
||||
def get_local_settings_schema(cls):
|
||||
""" Gets local level schema of configurable items.
|
||||
|
||||
def get_local_settings_schema_for_project(self, project_name):
|
||||
"""Wrapper for Local settings - for specific 'project_name'"""
|
||||
return self.get_configurable_items_for_project(project_name,
|
||||
EditableScopes.LOCAL)
|
||||
|
||||
def get_configurable_items_for_project(self, project_name=None,
|
||||
scope=None):
|
||||
It is not using Setting! Used for Setting UI to provide forms.
|
||||
"""
|
||||
Returns list of items that could be configurable for specific
|
||||
'project_name'
|
||||
ret_dict = {}
|
||||
for provider_code in lib.factory.providers:
|
||||
ret_dict[provider_code] = \
|
||||
lib.factory.get_provider_cls(provider_code). \
|
||||
get_local_settings_schema()
|
||||
|
||||
Args:
|
||||
project_name (str) - None > default project,
|
||||
scope (list of utils.EditableScope)
|
||||
(optional, None is all scopes, default is LOCAL)
|
||||
return ret_dict
|
||||
|
||||
Returns:
|
||||
(dict of list of dict)
|
||||
{
|
||||
siteA : [
|
||||
{
|
||||
key:"root", label:"root",
|
||||
"type": "dict",
|
||||
"children":[
|
||||
{ "key": "work",
|
||||
"type": "text",
|
||||
"value": "c:/projects"}
|
||||
]
|
||||
},
|
||||
{
|
||||
key:"credentials_url", label:"Credentials url",
|
||||
"value":"'c:/projects/cred.json'", "type": "text",
|
||||
"namespace": "{project_setting}/global/sync_server/
|
||||
sites"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
allowed_sites = set()
|
||||
sites = self.get_all_site_configs(project_name)
|
||||
if project_name:
|
||||
# Local Settings can select only from allowed sites for project
|
||||
allowed_sites.update(set(self.get_active_sites(project_name)))
|
||||
allowed_sites.update(set(self.get_remote_sites(project_name)))
|
||||
|
||||
editable = {}
|
||||
for site_name in sites.keys():
|
||||
if allowed_sites and site_name not in allowed_sites:
|
||||
continue
|
||||
|
||||
items = self.get_configurable_items_for_site(project_name,
|
||||
site_name,
|
||||
scope)
|
||||
# Local Settings need 'local' instead of real value
|
||||
site_name = site_name.replace(get_local_site_id(), 'local')
|
||||
editable[site_name] = items
|
||||
|
||||
return editable
|
||||
|
||||
def get_local_settings_schema_for_site(self, project_name, site_name):
|
||||
"""Wrapper for Local settings - for particular 'site_name and proj."""
|
||||
return self.get_configurable_items_for_site(project_name,
|
||||
site_name,
|
||||
EditableScopes.LOCAL)
|
||||
|
||||
def get_configurable_items_for_site(self, project_name=None,
|
||||
site_name=None,
|
||||
scope=None):
|
||||
"""
|
||||
Returns list of items that could be configurable.
|
||||
|
||||
Args:
|
||||
project_name (str) - None > default project
|
||||
site_name (str)
|
||||
scope (list of utils.EditableScope)
|
||||
(optional, None is all scopes)
|
||||
|
||||
Returns:
|
||||
(list)
|
||||
[
|
||||
{
|
||||
key:"root", label:"root", type:"dict",
|
||||
"children":[
|
||||
{ "key": "work",
|
||||
"type": "text",
|
||||
"value": "c:/projects"}
|
||||
]
|
||||
}, ...
|
||||
]
|
||||
"""
|
||||
provider_name = self.get_provider_for_site(site=site_name)
|
||||
items = lib.factory.get_provider_configurable_items(provider_name)
|
||||
|
||||
if project_name:
|
||||
sync_s = self.get_sync_project_setting(project_name,
|
||||
exclude_locals=True,
|
||||
cached=False)
|
||||
else:
|
||||
sync_s = get_default_project_settings(exclude_locals=True)
|
||||
sync_s = sync_s["global"]["sync_server"]
|
||||
sync_s["sites"].update(
|
||||
self._get_default_site_configs(self.enabled))
|
||||
|
||||
editable = []
|
||||
if type(scope) is not list:
|
||||
scope = [scope]
|
||||
scope = set(scope)
|
||||
for key, properties in items.items():
|
||||
if scope is None or scope.intersection(set(properties["scope"])):
|
||||
val = sync_s.get("sites", {}).get(site_name, {}).get(key)
|
||||
|
||||
item = {
|
||||
"key": key,
|
||||
"label": properties["label"],
|
||||
"type": properties["type"]
|
||||
}
|
||||
|
||||
if properties.get("namespace"):
|
||||
item["namespace"] = properties.get("namespace")
|
||||
if "platform" in item["namespace"]:
|
||||
try:
|
||||
if val:
|
||||
val = val[platform.system().lower()]
|
||||
except KeyError:
|
||||
st = "{}'s field value {} should be".format(key, val) # noqa: E501
|
||||
log.error(st + " multiplatform dict")
|
||||
|
||||
item["namespace"] = item["namespace"].replace('{site}',
|
||||
site_name)
|
||||
children = []
|
||||
if properties["type"] == "dict":
|
||||
if val:
|
||||
for val_key, val_val in val.items():
|
||||
child = {
|
||||
"type": "text",
|
||||
"key": val_key,
|
||||
"value": val_val
|
||||
}
|
||||
children.append(child)
|
||||
|
||||
if properties["type"] == "dict":
|
||||
item["children"] = children
|
||||
else:
|
||||
item["value"] = val
|
||||
|
||||
editable.append(item)
|
||||
|
||||
return editable
|
||||
# Needs to be refactored after Settings are updated
|
||||
# # Methods for Settings to get appriate values to fill forms
|
||||
# def get_configurable_items(self, scope=None):
|
||||
# """
|
||||
# Returns list of sites that could be configurable for all projects
|
||||
#
|
||||
# Could be filtered by 'scope' argument (list)
|
||||
#
|
||||
# Args:
|
||||
# scope (list of utils.EditableScope)
|
||||
#
|
||||
# Returns:
|
||||
# (dict of list of dict)
|
||||
# {
|
||||
# siteA : [
|
||||
# {
|
||||
# key:"root", label:"root",
|
||||
# "value":"{'work': 'c:/projects'}",
|
||||
# "type": "dict",
|
||||
# "children":[
|
||||
# { "key": "work",
|
||||
# "type": "text",
|
||||
# "value": "c:/projects"}
|
||||
# ]
|
||||
# },
|
||||
# {
|
||||
# key:"credentials_url", label:"Credentials url",
|
||||
# "value":"'c:/projects/cred.json'", "type": "text", # noqa: E501
|
||||
# "namespace": "{project_setting}/global/sync_server/ # noqa: E501
|
||||
# sites"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# """
|
||||
# editable = {}
|
||||
# applicable_projects = list(self.connection.projects())
|
||||
# applicable_projects.append(None)
|
||||
# for project in applicable_projects:
|
||||
# project_name = None
|
||||
# if project:
|
||||
# project_name = project["name"]
|
||||
#
|
||||
# items = self.get_configurable_items_for_project(project_name,
|
||||
# scope)
|
||||
# editable.update(items)
|
||||
#
|
||||
# return editable
|
||||
#
|
||||
# def get_local_settings_schema_for_project(self, project_name):
|
||||
# """Wrapper for Local settings - for specific 'project_name'"""
|
||||
# return self.get_configurable_items_for_project(project_name,
|
||||
# EditableScopes.LOCAL)
|
||||
#
|
||||
# def get_configurable_items_for_project(self, project_name=None,
|
||||
# scope=None):
|
||||
# """
|
||||
# Returns list of items that could be configurable for specific
|
||||
# 'project_name'
|
||||
#
|
||||
# Args:
|
||||
# project_name (str) - None > default project,
|
||||
# scope (list of utils.EditableScope)
|
||||
# (optional, None is all scopes, default is LOCAL)
|
||||
#
|
||||
# Returns:
|
||||
# (dict of list of dict)
|
||||
# {
|
||||
# siteA : [
|
||||
# {
|
||||
# key:"root", label:"root",
|
||||
# "type": "dict",
|
||||
# "children":[
|
||||
# { "key": "work",
|
||||
# "type": "text",
|
||||
# "value": "c:/projects"}
|
||||
# ]
|
||||
# },
|
||||
# {
|
||||
# key:"credentials_url", label:"Credentials url",
|
||||
# "value":"'c:/projects/cred.json'", "type": "text",
|
||||
# "namespace": "{project_setting}/global/sync_server/
|
||||
# sites"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# """
|
||||
# allowed_sites = set()
|
||||
# sites = self.get_all_site_configs(project_name)
|
||||
# if project_name:
|
||||
# # Local Settings can select only from allowed sites for project
|
||||
# allowed_sites.update(set(self.get_active_sites(project_name)))
|
||||
# allowed_sites.update(set(self.get_remote_sites(project_name)))
|
||||
#
|
||||
# editable = {}
|
||||
# for site_name in sites.keys():
|
||||
# if allowed_sites and site_name not in allowed_sites:
|
||||
# continue
|
||||
#
|
||||
# items = self.get_configurable_items_for_site(project_name,
|
||||
# site_name,
|
||||
# scope)
|
||||
# # Local Settings need 'local' instead of real value
|
||||
# site_name = site_name.replace(get_local_site_id(), 'local')
|
||||
# editable[site_name] = items
|
||||
#
|
||||
# return editable
|
||||
#
|
||||
# def get_configurable_items_for_site(self, project_name=None,
|
||||
# site_name=None,
|
||||
# scope=None):
|
||||
# """
|
||||
# Returns list of items that could be configurable.
|
||||
#
|
||||
# Args:
|
||||
# project_name (str) - None > default project
|
||||
# site_name (str)
|
||||
# scope (list of utils.EditableScope)
|
||||
# (optional, None is all scopes)
|
||||
#
|
||||
# Returns:
|
||||
# (list)
|
||||
# [
|
||||
# {
|
||||
# key:"root", label:"root", type:"dict",
|
||||
# "children":[
|
||||
# { "key": "work",
|
||||
# "type": "text",
|
||||
# "value": "c:/projects"}
|
||||
# ]
|
||||
# }, ...
|
||||
# ]
|
||||
# """
|
||||
# provider_name = self.get_provider_for_site(site=site_name)
|
||||
# items = lib.factory.get_provider_configurable_items(provider_name)
|
||||
#
|
||||
# if project_name:
|
||||
# sync_s = self.get_sync_project_setting(project_name,
|
||||
# exclude_locals=True,
|
||||
# cached=False)
|
||||
# else:
|
||||
# sync_s = get_default_project_settings(exclude_locals=True)
|
||||
# sync_s = sync_s["global"]["sync_server"]
|
||||
# sync_s["sites"].update(
|
||||
# self._get_default_site_configs(self.enabled))
|
||||
#
|
||||
# editable = []
|
||||
# if type(scope) is not list:
|
||||
# scope = [scope]
|
||||
# scope = set(scope)
|
||||
# for key, properties in items.items():
|
||||
# if scope is None or scope.intersection(set(properties["scope"])):
|
||||
# val = sync_s.get("sites", {}).get(site_name, {}).get(key)
|
||||
#
|
||||
# item = {
|
||||
# "key": key,
|
||||
# "label": properties["label"],
|
||||
# "type": properties["type"]
|
||||
# }
|
||||
#
|
||||
# if properties.get("namespace"):
|
||||
# item["namespace"] = properties.get("namespace")
|
||||
# if "platform" in item["namespace"]:
|
||||
# try:
|
||||
# if val:
|
||||
# val = val[platform.system().lower()]
|
||||
# except KeyError:
|
||||
# st = "{}'s field value {} should be".format(key, val) # noqa: E501
|
||||
# log.error(st + " multiplatform dict")
|
||||
#
|
||||
# item["namespace"] = item["namespace"].replace('{site}',
|
||||
# site_name)
|
||||
# children = []
|
||||
# if properties["type"] == "dict":
|
||||
# if val:
|
||||
# for val_key, val_val in val.items():
|
||||
# child = {
|
||||
# "type": "text",
|
||||
# "key": val_key,
|
||||
# "value": val_val
|
||||
# }
|
||||
# children.append(child)
|
||||
#
|
||||
# if properties["type"] == "dict":
|
||||
# item["children"] = children
|
||||
# else:
|
||||
# item["value"] = val
|
||||
#
|
||||
# editable.append(item)
|
||||
#
|
||||
# return editable
|
||||
|
||||
def reset_timer(self):
|
||||
"""
|
||||
|
|
@ -611,7 +645,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
|
|||
enabled_projects = []
|
||||
|
||||
if self.enabled:
|
||||
for project in self.connection.projects():
|
||||
for project in self.connection.projects(projection={"name": 1}):
|
||||
project_name = project["name"]
|
||||
project_settings = self.get_sync_project_setting(project_name)
|
||||
if project_settings and project_settings.get("enabled"):
|
||||
|
|
|
|||
15
openpype/modules/example_addons/example_addon/__init__.py
Normal file
15
openpype/modules/example_addons/example_addon/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
""" Addon class definition and Settings definition must be imported here.
|
||||
|
||||
If addon class or settings definition won't be here their definition won't
|
||||
be found by OpenPype discovery.
|
||||
"""
|
||||
|
||||
from .addon import (
|
||||
AddonSettingsDef,
|
||||
ExampleAddon
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"AddonSettingsDef",
|
||||
"ExampleAddon"
|
||||
)
|
||||
132
openpype/modules/example_addons/example_addon/addon.py
Normal file
132
openpype/modules/example_addons/example_addon/addon.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
"""Addon definition is located here.
|
||||
|
||||
Import of python packages that may not be available should not be imported
|
||||
in global space here until are required or used.
|
||||
- Qt related imports
|
||||
- imports of Python 3 packages
|
||||
- we still support Python 2 hosts where addon definition should available
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from openpype.modules import (
|
||||
JsonFilesSettingsDef,
|
||||
OpenPypeAddOn
|
||||
)
|
||||
# Import interface defined by this addon to be able find other addons using it
|
||||
from openpype_interfaces import (
|
||||
IExampleInterface,
|
||||
IPluginPaths,
|
||||
ITrayAction
|
||||
)
|
||||
|
||||
|
||||
# Settings definition of this addon using `JsonFilesSettingsDef`
|
||||
# - JsonFilesSettingsDef is prepared settings definition using json files
|
||||
# to define settings and store default values
|
||||
class AddonSettingsDef(JsonFilesSettingsDef):
|
||||
# This will add prefixes to every schema and template from `schemas`
|
||||
# subfolder.
|
||||
# - it is not required to fill the prefix but it is highly
|
||||
# recommended as schemas and templates may have name clashes across
|
||||
# multiple addons
|
||||
# - it is also recommended that prefix has addon name in it
|
||||
schema_prefix = "example_addon"
|
||||
|
||||
def get_settings_root_path(self):
|
||||
"""Implemented abstract class of JsonFilesSettingsDef.
|
||||
|
||||
Return directory path where json files defying addon settings are
|
||||
located.
|
||||
"""
|
||||
return os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"settings"
|
||||
)
|
||||
|
||||
|
||||
class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction):
|
||||
"""This Addon has defined it's settings and interface.
|
||||
|
||||
This example has system settings with an enabled option. And use
|
||||
few other interfaces:
|
||||
- `IPluginPaths` to define custom plugin paths
|
||||
- `ITrayAction` to be shown in tray tool
|
||||
"""
|
||||
label = "Example Addon"
|
||||
name = "example_addon"
|
||||
|
||||
def initialize(self, settings):
|
||||
"""Initialization of addon."""
|
||||
module_settings = settings[self.name]
|
||||
# Enabled by settings
|
||||
self.enabled = module_settings.get("enabled", False)
|
||||
|
||||
# Prepare variables that can be used or set afterwards
|
||||
self._connected_modules = None
|
||||
# UI which must not be created at this time
|
||||
self._dialog = None
|
||||
|
||||
def tray_init(self):
|
||||
"""Implementation of abstract method for `ITrayAction`.
|
||||
|
||||
We're definitely in tray tool so we can pre create dialog.
|
||||
"""
|
||||
|
||||
self._create_dialog()
|
||||
|
||||
def connect_with_modules(self, enabled_modules):
|
||||
"""Method where you should find connected modules.
|
||||
|
||||
It is triggered by OpenPype modules manager at the best possible time.
|
||||
Some addons and modules may required to connect with other modules
|
||||
before their main logic is executed so changes would require to restart
|
||||
whole process.
|
||||
"""
|
||||
self._connected_modules = []
|
||||
for module in enabled_modules:
|
||||
if isinstance(module, IExampleInterface):
|
||||
self._connected_modules.append(module)
|
||||
|
||||
def _create_dialog(self):
|
||||
# Don't recreate dialog if already exists
|
||||
if self._dialog is not None:
|
||||
return
|
||||
|
||||
from .widgets import MyExampleDialog
|
||||
|
||||
self._dialog = MyExampleDialog()
|
||||
|
||||
def show_dialog(self):
|
||||
"""Show dialog with connected modules.
|
||||
|
||||
This can be called from anywhere but can also crash in headless mode.
|
||||
There is no way to prevent addon to do invalid operations if he's
|
||||
not handling them.
|
||||
"""
|
||||
# Make sure dialog is created
|
||||
self._create_dialog()
|
||||
# Change value of dialog by current state
|
||||
self._dialog.set_connected_modules(self.get_connected_modules())
|
||||
# Show dialog
|
||||
self._dialog.open()
|
||||
|
||||
def get_connected_modules(self):
|
||||
"""Custom implementation of addon."""
|
||||
names = set()
|
||||
if self._connected_modules is not None:
|
||||
for module in self._connected_modules:
|
||||
names.add(module.name)
|
||||
return names
|
||||
|
||||
def on_action_trigger(self):
|
||||
"""Implementation of abstract method for `ITrayAction`."""
|
||||
self.show_dialog()
|
||||
|
||||
def get_plugin_paths(self):
|
||||
"""Implementation of abstract method for `IPluginPaths`."""
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
return {
|
||||
"publish": [os.path.join(current_dir, "plugins", "publish")]
|
||||
}
|
||||
28
openpype/modules/example_addons/example_addon/interfaces.py
Normal file
28
openpype/modules/example_addons/example_addon/interfaces.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
""" Using interfaces is one way of connecting multiple OpenPype Addons/Modules.
|
||||
|
||||
Interfaces must be in `interfaces.py` file (or folder). Interfaces should not
|
||||
import module logic or other module in global namespace. That is because
|
||||
all of them must be imported before all OpenPype AddOns and Modules.
|
||||
|
||||
Ideally they should just define abstract and helper methods. If interface
|
||||
require any logic or connection it should be defined in module.
|
||||
|
||||
Keep in mind that attributes and methods will be added to other addon
|
||||
attributes and methods so they should be unique and ideally contain
|
||||
addon name in it's name.
|
||||
"""
|
||||
|
||||
from abc import abstractmethod
|
||||
from openpype.modules import OpenPypeInterface
|
||||
|
||||
|
||||
class IExampleInterface(OpenPypeInterface):
|
||||
"""Example interface of addon."""
|
||||
_example_module = None
|
||||
|
||||
def get_example_module(self):
|
||||
return self._example_module
|
||||
|
||||
@abstractmethod
|
||||
def example_method_of_example_interface(self):
|
||||
pass
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class CollectExampleAddon(pyblish.api.ContextPlugin):
|
||||
order = pyblish.api.CollectorOrder + 0.4
|
||||
label = "Collect Example Addon"
|
||||
|
||||
def process(self, context):
|
||||
self.log.info("I'm in example addon's plugin!")
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"project_settings/example_addon": {
|
||||
"number": 0,
|
||||
"color_1": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"color_2": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"modules/example_addon": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"project_settings/global": {
|
||||
"type": "schema",
|
||||
"name": "example_addon/main"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"system_settings/modules": {
|
||||
"type": "schema",
|
||||
"name": "example_addon/main"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"key": "example_addon",
|
||||
"label": "Example addon",
|
||||
"collapsible": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "number",
|
||||
"key": "number",
|
||||
"label": "This is your lucky number:",
|
||||
"minimum": 7,
|
||||
"maximum": 7,
|
||||
"decimals": 0
|
||||
},
|
||||
{
|
||||
"type": "template",
|
||||
"name": "example_addon/the_template",
|
||||
"template_data": [
|
||||
{
|
||||
"name": "color_1",
|
||||
"label": "Color 1"
|
||||
},
|
||||
{
|
||||
"name": "color_2",
|
||||
"label": "Color 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"type": "list-strict",
|
||||
"key": "{name}",
|
||||
"label": "{label}",
|
||||
"object_types": [
|
||||
{
|
||||
"label": "Red",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Green",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Blue",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"key": "example_addon",
|
||||
"label": "Example addon",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
openpype/modules/example_addons/example_addon/widgets.py
Normal file
39
openpype/modules/example_addons/example_addon/widgets.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from Qt import QtWidgets
|
||||
|
||||
from openpype.style import load_stylesheet
|
||||
|
||||
|
||||
class MyExampleDialog(QtWidgets.QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(MyExampleDialog, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("Connected modules")
|
||||
|
||||
label_widget = QtWidgets.QLabel(self)
|
||||
|
||||
ok_btn = QtWidgets.QPushButton("OK", self)
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(ok_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(label_widget)
|
||||
layout.addLayout(btns_layout)
|
||||
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
|
||||
self._label_widget = label_widget
|
||||
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
|
||||
def _on_ok_clicked(self):
|
||||
self.done(1)
|
||||
|
||||
def set_connected_modules(self, connected_modules):
|
||||
if connected_modules:
|
||||
message = "\n".join(connected_modules)
|
||||
else:
|
||||
message = (
|
||||
"Other enabled modules/addons are not using my interface."
|
||||
)
|
||||
self._label_widget.setText(message)
|
||||
9
openpype/modules/example_addons/tiny_addon.py
Normal file
9
openpype/modules/example_addons/tiny_addon.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from openpype.modules import OpenPypeAddOn
|
||||
|
||||
|
||||
class TinyAddon(OpenPypeAddOn):
|
||||
"""This is tiniest possible addon.
|
||||
|
||||
This addon won't do much but will exist in OpenPype modules environment.
|
||||
"""
|
||||
name = "tiniest_addon_ever"
|
||||
|
|
@ -14,7 +14,7 @@ class CollectHostName(pyblish.api.ContextPlugin):
|
|||
"""Collect avalon host name to context."""
|
||||
|
||||
label = "Collect Host Name"
|
||||
order = pyblish.api.CollectorOrder - 1
|
||||
order = pyblish.api.CollectorOrder - 0.5
|
||||
|
||||
def process(self, context):
|
||||
host_name = context.data.get("hostName")
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from openpype.api import get_system_settings
|
|||
|
||||
class StopTimer(pyblish.api.ContextPlugin):
|
||||
label = "Stop Timer"
|
||||
order = pyblish.api.ExtractorOrder - 0.5
|
||||
order = pyblish.api.ExtractorOrder - 0.49
|
||||
hosts = ["*"]
|
||||
|
||||
def process(self, context):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,21 @@
|
|||
from .constants import (
|
||||
GLOBAL_SETTINGS_KEY,
|
||||
SYSTEM_SETTINGS_KEY,
|
||||
PROJECT_SETTINGS_KEY,
|
||||
PROJECT_ANATOMY_KEY,
|
||||
LOCAL_SETTING_KEY,
|
||||
|
||||
SCHEMA_KEY_SYSTEM_SETTINGS,
|
||||
SCHEMA_KEY_PROJECT_SETTINGS,
|
||||
|
||||
KEY_ALLOWED_SYMBOLS,
|
||||
KEY_REGEX
|
||||
)
|
||||
from .exceptions import (
|
||||
SaveWarningExc
|
||||
)
|
||||
from .lib import (
|
||||
get_general_environments,
|
||||
get_system_settings,
|
||||
get_project_settings,
|
||||
get_current_project_settings,
|
||||
|
|
@ -16,15 +30,27 @@ from .entities import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"GLOBAL_SETTINGS_KEY",
|
||||
"SYSTEM_SETTINGS_KEY",
|
||||
"PROJECT_SETTINGS_KEY",
|
||||
"PROJECT_ANATOMY_KEY",
|
||||
"LOCAL_SETTING_KEY",
|
||||
|
||||
"SCHEMA_KEY_SYSTEM_SETTINGS",
|
||||
"SCHEMA_KEY_PROJECT_SETTINGS",
|
||||
|
||||
"KEY_ALLOWED_SYMBOLS",
|
||||
"KEY_REGEX",
|
||||
|
||||
"SaveWarningExc",
|
||||
|
||||
"get_general_environments",
|
||||
"get_system_settings",
|
||||
"get_project_settings",
|
||||
"get_current_project_settings",
|
||||
"get_anatomy_settings",
|
||||
"get_environments",
|
||||
"get_local_settings",
|
||||
|
||||
"SystemSettings",
|
||||
"ProjectSettings"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,13 +14,17 @@ METADATA_KEYS = (
|
|||
M_DYNAMIC_KEY_LABEL
|
||||
)
|
||||
|
||||
# File where studio's system overrides are stored
|
||||
# Keys where studio's system overrides are stored
|
||||
GLOBAL_SETTINGS_KEY = "global_settings"
|
||||
SYSTEM_SETTINGS_KEY = "system_settings"
|
||||
PROJECT_SETTINGS_KEY = "project_settings"
|
||||
PROJECT_ANATOMY_KEY = "project_anatomy"
|
||||
LOCAL_SETTING_KEY = "local_settings"
|
||||
|
||||
# Schema hub names
|
||||
SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema"
|
||||
SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema"
|
||||
|
||||
DEFAULT_PROJECT_KEY = "__default_project__"
|
||||
|
||||
KEY_ALLOWED_SYMBOLS = "a-zA-Z0-9-_ "
|
||||
|
|
@ -39,6 +43,9 @@ __all__ = (
|
|||
"PROJECT_ANATOMY_KEY",
|
||||
"LOCAL_SETTING_KEY",
|
||||
|
||||
"SCHEMA_KEY_SYSTEM_SETTINGS",
|
||||
"SCHEMA_KEY_PROJECT_SETTINGS",
|
||||
|
||||
"DEFAULT_PROJECT_KEY",
|
||||
|
||||
"KEY_ALLOWED_SYMBOLS",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"addon_paths": {
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
"avalon": {
|
||||
"AVALON_TIMEOUT": 1000,
|
||||
"AVALON_THUMBNAIL_ROOT": {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,6 @@ from .enum_entity import (
|
|||
AppsEnumEntity,
|
||||
ToolsEnumEntity,
|
||||
TaskTypeEnumEntity,
|
||||
ProvidersEnum,
|
||||
DeadlineUrlEnumEntity,
|
||||
AnatomyTemplatesEnumEntity
|
||||
)
|
||||
|
|
@ -113,7 +112,10 @@ from .enum_entity import (
|
|||
from .list_entity import ListEntity
|
||||
from .dict_immutable_keys_entity import DictImmutableKeysEntity
|
||||
from .dict_mutable_keys_entity import DictMutableKeysEntity
|
||||
from .dict_conditional import DictConditionalEntity
|
||||
from .dict_conditional import (
|
||||
DictConditionalEntity,
|
||||
SyncServerProviders
|
||||
)
|
||||
|
||||
from .anatomy_entities import AnatomyEntity
|
||||
|
||||
|
|
@ -161,7 +163,6 @@ __all__ = (
|
|||
"AppsEnumEntity",
|
||||
"ToolsEnumEntity",
|
||||
"TaskTypeEnumEntity",
|
||||
"ProvidersEnum",
|
||||
"DeadlineUrlEnumEntity",
|
||||
"AnatomyTemplatesEnumEntity",
|
||||
|
||||
|
|
@ -172,6 +173,7 @@ __all__ = (
|
|||
"DictMutableKeysEntity",
|
||||
|
||||
"DictConditionalEntity",
|
||||
"SyncServerProviders",
|
||||
|
||||
"AnatomyEntity"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,12 @@ class BaseItemEntity(BaseEntity):
|
|||
self.is_group = False
|
||||
# Entity's value will be stored into file with name of it's key
|
||||
self.is_file = False
|
||||
# Default values are not stored to an openpype file
|
||||
# - these must not be set through schemas directly
|
||||
self.dynamic_schema_id = None
|
||||
self.is_dynamic_schema_node = False
|
||||
self.is_in_dynamic_schema_node = False
|
||||
|
||||
# Reference to parent entity which has `is_group` == True
|
||||
# - stays as None if none of parents is group
|
||||
self.group_item = None
|
||||
|
|
@ -255,13 +261,22 @@ class BaseItemEntity(BaseEntity):
|
|||
)
|
||||
|
||||
# Group item can be only once in on hierarchy branch.
|
||||
if self.is_group and self.group_item:
|
||||
if self.is_group and self.group_item is not None:
|
||||
raise SchemeGroupHierarchyBug(self)
|
||||
|
||||
# Group item can be only once in on hierarchy branch.
|
||||
if self.group_item is not None and self.is_dynamic_schema_node:
|
||||
reason = (
|
||||
"Dynamic schema is inside grouped item {}."
|
||||
" Change group hierarchy or remove dynamic"
|
||||
" schema to be able work properly."
|
||||
).format(self.group_item.path)
|
||||
raise EntitySchemaError(self, reason)
|
||||
|
||||
# Validate that env group entities will be stored into file.
|
||||
# - env group entities must store metadata which is not possible if
|
||||
# metadata would be outside of file
|
||||
if not self.file_item and self.is_env_group:
|
||||
if self.file_item is None and self.is_env_group:
|
||||
reason = (
|
||||
"Environment item is not inside file"
|
||||
" item so can't store metadata for defaults."
|
||||
|
|
@ -478,7 +493,15 @@ class BaseItemEntity(BaseEntity):
|
|||
|
||||
@abstractmethod
|
||||
def settings_value(self):
|
||||
"""Value of an item without key."""
|
||||
"""Value of an item without key without dynamic items."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def collect_dynamic_schema_entities(self):
|
||||
"""Collect entities that are on top of dynamically added schemas.
|
||||
|
||||
This method make sence only when defaults are saved.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -808,6 +831,12 @@ class ItemEntity(BaseItemEntity):
|
|||
self.is_dynamic_item = is_dynamic_item
|
||||
|
||||
self.is_file = self.schema_data.get("is_file", False)
|
||||
# These keys have underscore as they must not be set in schemas
|
||||
self.dynamic_schema_id = self.schema_data.get(
|
||||
"_dynamic_schema_id", None
|
||||
)
|
||||
self.is_dynamic_schema_node = self.dynamic_schema_id is not None
|
||||
|
||||
self.is_group = self.schema_data.get("is_group", False)
|
||||
self.is_in_dynamic_item = bool(
|
||||
not self.is_dynamic_item
|
||||
|
|
@ -837,10 +866,20 @@ class ItemEntity(BaseItemEntity):
|
|||
self._require_restart_on_change = require_restart_on_change
|
||||
|
||||
# File item reference
|
||||
if self.parent.is_file:
|
||||
self.file_item = self.parent
|
||||
elif self.parent.file_item:
|
||||
self.file_item = self.parent.file_item
|
||||
if not self.is_dynamic_schema_node:
|
||||
self.is_in_dynamic_schema_node = (
|
||||
self.parent.is_dynamic_schema_node
|
||||
or self.parent.is_in_dynamic_schema_node
|
||||
)
|
||||
|
||||
if (
|
||||
not self.is_dynamic_schema_node
|
||||
and not self.is_in_dynamic_schema_node
|
||||
):
|
||||
if self.parent.is_file:
|
||||
self.file_item = self.parent
|
||||
elif self.parent.file_item:
|
||||
self.file_item = self.parent.file_item
|
||||
|
||||
# Group item reference
|
||||
if self.parent.is_group:
|
||||
|
|
@ -891,6 +930,18 @@ class ItemEntity(BaseItemEntity):
|
|||
def root_key(self):
|
||||
return self.root_item.root_key
|
||||
|
||||
@abstractmethod
|
||||
def collect_dynamic_schema_entities(self, collector):
|
||||
"""Collect entities that are on top of dynamically added schemas.
|
||||
|
||||
This method make sence only when defaults are saved.
|
||||
|
||||
Args:
|
||||
collector(DynamicSchemaValueCollector): Object where dynamic
|
||||
entities are stored.
|
||||
"""
|
||||
pass
|
||||
|
||||
def schema_validations(self):
|
||||
if not self.label and self.use_label_wrap:
|
||||
reason = (
|
||||
|
|
@ -899,7 +950,12 @@ class ItemEntity(BaseItemEntity):
|
|||
)
|
||||
raise EntitySchemaError(self, reason)
|
||||
|
||||
if self.is_file and self.file_item is not None:
|
||||
if (
|
||||
not self.is_dynamic_schema_node
|
||||
and not self.is_in_dynamic_schema_node
|
||||
and self.is_file
|
||||
and self.file_item is not None
|
||||
):
|
||||
reason = (
|
||||
"Entity has set `is_file` to true but"
|
||||
" it's parent is already marked as file item."
|
||||
|
|
|
|||
|
|
@ -469,6 +469,10 @@ class DictConditionalEntity(ItemEntity):
|
|||
return True
|
||||
return False
|
||||
|
||||
def collect_dynamic_schema_entities(self, collector):
|
||||
if self.is_dynamic_schema_node:
|
||||
collector.add_entity(self)
|
||||
|
||||
def settings_value(self):
|
||||
if self._override_state is OverrideState.NOT_DEFINED:
|
||||
return NOT_SET
|
||||
|
|
@ -482,13 +486,7 @@ class DictConditionalEntity(ItemEntity):
|
|||
|
||||
output = {}
|
||||
for key, child_obj in children_items:
|
||||
child_value = child_obj.settings_value()
|
||||
if not child_obj.is_file and not child_obj.file_item:
|
||||
for _key, _value in child_value.items():
|
||||
new_key = "/".join([key, _key])
|
||||
output[new_key] = _value
|
||||
else:
|
||||
output[key] = child_value
|
||||
output[key] = child_obj.settings_value()
|
||||
return output
|
||||
|
||||
if self.is_group:
|
||||
|
|
@ -726,3 +724,49 @@ class DictConditionalEntity(ItemEntity):
|
|||
for children in self.children.values():
|
||||
for child_entity in children:
|
||||
child_entity.reset_callbacks()
|
||||
|
||||
|
||||
class SyncServerProviders(DictConditionalEntity):
|
||||
schema_types = ["sync-server-providers"]
|
||||
|
||||
def _add_children(self):
|
||||
self.enum_key = "provider"
|
||||
self.enum_label = "Provider"
|
||||
|
||||
enum_children = self._get_enum_children()
|
||||
if not enum_children:
|
||||
enum_children.append({
|
||||
"key": None,
|
||||
"label": "< Nothing >"
|
||||
})
|
||||
self.enum_children = enum_children
|
||||
|
||||
super(SyncServerProviders, self)._add_children()
|
||||
|
||||
def _get_enum_children(self):
|
||||
from openpype_modules import sync_server
|
||||
|
||||
from openpype_modules.sync_server.providers import lib as lib_providers
|
||||
|
||||
provider_code_to_label = {}
|
||||
providers = lib_providers.factory.providers
|
||||
for provider_code, provider_info in providers.items():
|
||||
provider, _ = provider_info
|
||||
provider_code_to_label[provider_code] = provider.LABEL
|
||||
|
||||
system_settings_schema = (
|
||||
sync_server
|
||||
.SyncServerModule
|
||||
.get_system_settings_schema()
|
||||
)
|
||||
|
||||
enum_children = []
|
||||
for provider_code, configurables in system_settings_schema.items():
|
||||
label = provider_code_to_label.get(provider_code) or provider_code
|
||||
|
||||
enum_children.append({
|
||||
"key": provider_code,
|
||||
"label": label,
|
||||
"children": configurables
|
||||
})
|
||||
return enum_children
|
||||
|
|
|
|||
|
|
@ -330,15 +330,32 @@ class DictImmutableKeysEntity(ItemEntity):
|
|||
return True
|
||||
return False
|
||||
|
||||
def collect_dynamic_schema_entities(self, collector):
|
||||
for child_obj in self.non_gui_children.values():
|
||||
child_obj.collect_dynamic_schema_entities(collector)
|
||||
|
||||
if self.is_dynamic_schema_node:
|
||||
collector.add_entity(self)
|
||||
|
||||
def settings_value(self):
|
||||
if self._override_state is OverrideState.NOT_DEFINED:
|
||||
return NOT_SET
|
||||
|
||||
if self._override_state is OverrideState.DEFAULTS:
|
||||
is_dynamic_schema_node = (
|
||||
self.is_dynamic_schema_node or self.is_in_dynamic_schema_node
|
||||
)
|
||||
output = {}
|
||||
for key, child_obj in self.non_gui_children.items():
|
||||
if child_obj.is_dynamic_schema_node:
|
||||
continue
|
||||
|
||||
child_value = child_obj.settings_value()
|
||||
if not child_obj.is_file and not child_obj.file_item:
|
||||
if (
|
||||
not is_dynamic_schema_node
|
||||
and not child_obj.is_file
|
||||
and not child_obj.file_item
|
||||
):
|
||||
for _key, _value in child_value.items():
|
||||
new_key = "/".join([key, _key])
|
||||
output[new_key] = _value
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ class DictMutableKeysEntity(EndpointEntity):
|
|||
raise EntitySchemaError(self, reason)
|
||||
|
||||
# TODO Ability to store labels should be defined with different key
|
||||
if self.collapsible_key and not self.file_item:
|
||||
if self.collapsible_key and self.file_item is None:
|
||||
reason = (
|
||||
"Modifiable dictionary with collapsible keys is not under"
|
||||
" file item so can't store metadata."
|
||||
|
|
|
|||
|
|
@ -376,11 +376,16 @@ class TaskTypeEnumEntity(BaseEnumEntity):
|
|||
schema_types = ["task-types-enum"]
|
||||
|
||||
def _item_initalization(self):
|
||||
self.multiselection = True
|
||||
self.value_on_not_set = []
|
||||
self.multiselection = self.schema_data.get("multiselection", True)
|
||||
if self.multiselection:
|
||||
self.valid_value_types = (list, )
|
||||
self.value_on_not_set = []
|
||||
else:
|
||||
self.valid_value_types = (STRING_TYPE, )
|
||||
self.value_on_not_set = ""
|
||||
|
||||
self.enum_items = []
|
||||
self.valid_keys = set()
|
||||
self.valid_value_types = (list, )
|
||||
self.placeholder = None
|
||||
|
||||
def _get_enum_values(self):
|
||||
|
|
@ -396,53 +401,51 @@ class TaskTypeEnumEntity(BaseEnumEntity):
|
|||
|
||||
return enum_items, valid_keys
|
||||
|
||||
def _convert_value_for_current_state(self, source_value):
|
||||
if self.multiselection:
|
||||
output = []
|
||||
for key in source_value:
|
||||
if key in self.valid_keys:
|
||||
output.append(key)
|
||||
return output
|
||||
|
||||
if source_value not in self.valid_keys:
|
||||
# Take first item from enum items
|
||||
for item in self.enum_items:
|
||||
for key in item.keys():
|
||||
source_value = key
|
||||
break
|
||||
return source_value
|
||||
|
||||
def set_override_state(self, *args, **kwargs):
|
||||
super(TaskTypeEnumEntity, self).set_override_state(*args, **kwargs)
|
||||
|
||||
self.enum_items, self.valid_keys = self._get_enum_values()
|
||||
new_value = []
|
||||
for key in self._current_value:
|
||||
if key in self.valid_keys:
|
||||
new_value.append(key)
|
||||
self._current_value = new_value
|
||||
|
||||
if self.multiselection:
|
||||
new_value = []
|
||||
for key in self._current_value:
|
||||
if key in self.valid_keys:
|
||||
new_value.append(key)
|
||||
|
||||
class ProvidersEnum(BaseEnumEntity):
|
||||
schema_types = ["providers-enum"]
|
||||
if self._current_value != new_value:
|
||||
self.set(new_value)
|
||||
else:
|
||||
if not self.enum_items:
|
||||
self.valid_keys.add("")
|
||||
self.enum_items.append({"": "< Empty >"})
|
||||
|
||||
def _item_initalization(self):
|
||||
self.multiselection = False
|
||||
self.value_on_not_set = ""
|
||||
self.enum_items = []
|
||||
self.valid_keys = set()
|
||||
self.valid_value_types = (str, )
|
||||
self.placeholder = None
|
||||
for item in self.enum_items:
|
||||
for key in item.keys():
|
||||
value_on_not_set = key
|
||||
break
|
||||
|
||||
def _get_enum_values(self):
|
||||
from openpype_modules.sync_server.providers import lib as lib_providers
|
||||
|
||||
providers = lib_providers.factory.providers
|
||||
|
||||
valid_keys = set()
|
||||
valid_keys.add('')
|
||||
enum_items = [{'': 'Choose Provider'}]
|
||||
for provider_code, provider_info in providers.items():
|
||||
provider, _ = provider_info
|
||||
enum_items.append({provider_code: provider.LABEL})
|
||||
valid_keys.add(provider_code)
|
||||
|
||||
return enum_items, valid_keys
|
||||
|
||||
def set_override_state(self, *args, **kwargs):
|
||||
super(ProvidersEnum, self).set_override_state(*args, **kwargs)
|
||||
|
||||
self.enum_items, self.valid_keys = self._get_enum_values()
|
||||
|
||||
value_on_not_set = list(self.valid_keys)[0]
|
||||
if self._current_value is NOT_SET:
|
||||
self._current_value = value_on_not_set
|
||||
|
||||
self.value_on_not_set = value_on_not_set
|
||||
self.value_on_not_set = value_on_not_set
|
||||
if (
|
||||
self._current_value is NOT_SET
|
||||
or self._current_value not in self.valid_keys
|
||||
):
|
||||
self.set(value_on_not_set)
|
||||
|
||||
|
||||
class DeadlineUrlEnumEntity(BaseEnumEntity):
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ class EndpointEntity(ItemEntity):
|
|||
|
||||
super(EndpointEntity, self).schema_validations()
|
||||
|
||||
def collect_dynamic_schema_entities(self, collector):
|
||||
if self.is_dynamic_schema_node:
|
||||
collector.add_entity(self)
|
||||
|
||||
@abstractmethod
|
||||
def _settings_value(self):
|
||||
pass
|
||||
|
|
@ -121,7 +125,11 @@ class InputEntity(EndpointEntity):
|
|||
|
||||
def schema_validations(self):
|
||||
# Input entity must have file parent.
|
||||
if not self.file_item:
|
||||
if (
|
||||
not self.is_dynamic_schema_node
|
||||
and not self.is_in_dynamic_schema_node
|
||||
and self.file_item is None
|
||||
):
|
||||
raise EntitySchemaError(self, "Missing parent file entity.")
|
||||
|
||||
super(InputEntity, self).schema_validations()
|
||||
|
|
@ -369,6 +377,14 @@ class NumberEntity(InputEntity):
|
|||
self.valid_value_types = valid_value_types
|
||||
self.value_on_not_set = value_on_not_set
|
||||
|
||||
# UI specific attributes
|
||||
self.show_slider = self.schema_data.get("show_slider", False)
|
||||
steps = self.schema_data.get("steps", None)
|
||||
# Make sure that steps are not set to `0`
|
||||
if steps == 0:
|
||||
steps = None
|
||||
self.steps = steps
|
||||
|
||||
def _convert_to_valid_type(self, value):
|
||||
if isinstance(value, str):
|
||||
new_value = None
|
||||
|
|
|
|||
|
|
@ -115,6 +115,9 @@ class PathEntity(ItemEntity):
|
|||
def set(self, value):
|
||||
self.child_obj.set(value)
|
||||
|
||||
def collect_dynamic_schema_entities(self, *args, **kwargs):
|
||||
self.child_obj.collect_dynamic_schema_entities(*args, **kwargs)
|
||||
|
||||
def settings_value(self):
|
||||
if self._override_state is OverrideState.NOT_DEFINED:
|
||||
return NOT_SET
|
||||
|
|
@ -236,7 +239,12 @@ class ListStrictEntity(ItemEntity):
|
|||
|
||||
def schema_validations(self):
|
||||
# List entity must have file parent.
|
||||
if not self.file_item and not self.is_file:
|
||||
if (
|
||||
not self.is_dynamic_schema_node
|
||||
and not self.is_in_dynamic_schema_node
|
||||
and not self.is_file
|
||||
and self.file_item is None
|
||||
):
|
||||
raise EntitySchemaError(
|
||||
self, "Missing file entity in hierarchy."
|
||||
)
|
||||
|
|
@ -279,6 +287,10 @@ class ListStrictEntity(ItemEntity):
|
|||
for idx, item in enumerate(new_value):
|
||||
self.children[idx].set(item)
|
||||
|
||||
def collect_dynamic_schema_entities(self, collector):
|
||||
if self.is_dynamic_schema_node:
|
||||
collector.add_entity(self)
|
||||
|
||||
def settings_value(self):
|
||||
if self._override_state is OverrideState.NOT_DEFINED:
|
||||
return NOT_SET
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import re
|
|||
import json
|
||||
import copy
|
||||
import inspect
|
||||
import collections
|
||||
import contextlib
|
||||
|
||||
from .exceptions import (
|
||||
|
|
@ -10,6 +11,12 @@ from .exceptions import (
|
|||
SchemaDuplicatedEnvGroupKeys
|
||||
)
|
||||
|
||||
from openpype.settings.constants import (
|
||||
SYSTEM_SETTINGS_KEY,
|
||||
PROJECT_SETTINGS_KEY,
|
||||
SCHEMA_KEY_SYSTEM_SETTINGS,
|
||||
SCHEMA_KEY_PROJECT_SETTINGS
|
||||
)
|
||||
try:
|
||||
STRING_TYPE = basestring
|
||||
except Exception:
|
||||
|
|
@ -24,6 +31,10 @@ TEMPLATE_METADATA_KEYS = (
|
|||
DEFAULT_VALUES_KEY,
|
||||
)
|
||||
|
||||
SCHEMA_EXTEND_TYPES = (
|
||||
"schema", "template", "schema_template", "dynamic_schema"
|
||||
)
|
||||
|
||||
template_key_pattern = re.compile(r"(\{.*?[^{0]*\})")
|
||||
|
||||
|
||||
|
|
@ -102,8 +113,8 @@ class OverrideState:
|
|||
|
||||
|
||||
class SchemasHub:
|
||||
def __init__(self, schema_subfolder, reset=True):
|
||||
self._schema_subfolder = schema_subfolder
|
||||
def __init__(self, schema_type, reset=True):
|
||||
self._schema_type = schema_type
|
||||
|
||||
self._loaded_types = {}
|
||||
self._gui_types = tuple()
|
||||
|
|
@ -112,25 +123,60 @@ class SchemasHub:
|
|||
self._loaded_templates = {}
|
||||
self._loaded_schemas = {}
|
||||
|
||||
# Attributes for modules settings
|
||||
self._dynamic_schemas_defs_by_id = {}
|
||||
self._dynamic_schemas_by_id = {}
|
||||
|
||||
# Store validating and validated dynamic template or schemas
|
||||
self._validating_dynamic = set()
|
||||
self._validated_dynamic = set()
|
||||
|
||||
# It doesn't make sence to reload types on each reset as they can't be
|
||||
# changed
|
||||
self._load_types()
|
||||
|
||||
# Trigger reset
|
||||
if reset:
|
||||
self.reset()
|
||||
|
||||
@property
|
||||
def schema_type(self):
|
||||
return self._schema_type
|
||||
|
||||
def reset(self):
|
||||
self._load_modules_settings_defs()
|
||||
self._load_types()
|
||||
self._load_schemas()
|
||||
|
||||
def _load_modules_settings_defs(self):
|
||||
from openpype.modules import get_module_settings_defs
|
||||
|
||||
module_settings_defs = get_module_settings_defs()
|
||||
for module_settings_def_cls in module_settings_defs:
|
||||
module_settings_def = module_settings_def_cls()
|
||||
def_id = module_settings_def.id
|
||||
self._dynamic_schemas_defs_by_id[def_id] = module_settings_def
|
||||
|
||||
@property
|
||||
def gui_types(self):
|
||||
return self._gui_types
|
||||
|
||||
def resolve_dynamic_schema(self, dynamic_key):
|
||||
output = []
|
||||
for def_id, def_keys in self._dynamic_schemas_by_id.items():
|
||||
if dynamic_key in def_keys:
|
||||
def_schema = def_keys[dynamic_key]
|
||||
if not def_schema:
|
||||
continue
|
||||
|
||||
if isinstance(def_schema, dict):
|
||||
def_schema = [def_schema]
|
||||
|
||||
all_def_schema = []
|
||||
for item in def_schema:
|
||||
items = self.resolve_schema_data(item)
|
||||
for _item in items:
|
||||
_item["_dynamic_schema_id"] = def_id
|
||||
all_def_schema.extend(items)
|
||||
output.extend(all_def_schema)
|
||||
return output
|
||||
|
||||
def get_template_name(self, item_def, default=None):
|
||||
"""Get template name from passed item definition.
|
||||
|
||||
|
|
@ -260,7 +306,7 @@ class SchemasHub:
|
|||
list: Resolved schema data.
|
||||
"""
|
||||
schema_type = schema_data["type"]
|
||||
if schema_type not in ("schema", "template", "schema_template"):
|
||||
if schema_type not in SCHEMA_EXTEND_TYPES:
|
||||
return [schema_data]
|
||||
|
||||
if schema_type == "schema":
|
||||
|
|
@ -268,6 +314,9 @@ class SchemasHub:
|
|||
self.get_schema(schema_data["name"])
|
||||
)
|
||||
|
||||
if schema_type == "dynamic_schema":
|
||||
return self.resolve_dynamic_schema(schema_data["name"])
|
||||
|
||||
template_name = schema_data["name"]
|
||||
template_def = self.get_template(template_name)
|
||||
|
||||
|
|
@ -368,14 +417,16 @@ class SchemasHub:
|
|||
self._crashed_on_load = {}
|
||||
self._loaded_templates = {}
|
||||
self._loaded_schemas = {}
|
||||
self._dynamic_schemas_by_id = {}
|
||||
|
||||
dirpath = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"schemas",
|
||||
self._schema_subfolder
|
||||
self.schema_type
|
||||
)
|
||||
loaded_schemas = {}
|
||||
loaded_templates = {}
|
||||
dynamic_schemas_by_id = {}
|
||||
for root, _, filenames in os.walk(dirpath):
|
||||
for filename in filenames:
|
||||
basename, ext = os.path.splitext(filename)
|
||||
|
|
@ -425,8 +476,34 @@ class SchemasHub:
|
|||
)
|
||||
loaded_schemas[basename] = schema_data
|
||||
|
||||
defs_iter = self._dynamic_schemas_defs_by_id.items()
|
||||
for def_id, module_settings_def in defs_iter:
|
||||
dynamic_schemas_by_id[def_id] = (
|
||||
module_settings_def.get_dynamic_schemas(self.schema_type)
|
||||
)
|
||||
module_schemas = module_settings_def.get_settings_schemas(
|
||||
self.schema_type
|
||||
)
|
||||
for key, schema_data in module_schemas.items():
|
||||
if isinstance(schema_data, list):
|
||||
if key in loaded_templates:
|
||||
raise KeyError(
|
||||
"Duplicated template key \"{}\"".format(key)
|
||||
)
|
||||
loaded_templates[key] = schema_data
|
||||
else:
|
||||
if key in loaded_schemas:
|
||||
raise KeyError(
|
||||
"Duplicated schema key \"{}\"".format(key)
|
||||
)
|
||||
loaded_schemas[key] = schema_data
|
||||
|
||||
self._loaded_templates = loaded_templates
|
||||
self._loaded_schemas = loaded_schemas
|
||||
self._dynamic_schemas_by_id = dynamic_schemas_by_id
|
||||
|
||||
def get_dynamic_modules_settings_defs(self, schema_def_id):
|
||||
return self._dynamic_schemas_defs_by_id.get(schema_def_id)
|
||||
|
||||
def _fill_template(self, child_data, template_def):
|
||||
"""Fill template based on schema definition and template definition.
|
||||
|
|
@ -660,3 +737,38 @@ class SchemasHub:
|
|||
if found_idx is not None:
|
||||
metadata_item = template_def.pop(found_idx)
|
||||
return metadata_item
|
||||
|
||||
|
||||
class DynamicSchemaValueCollector:
|
||||
# Map schema hub type to store keys
|
||||
schema_hub_type_map = {
|
||||
SCHEMA_KEY_SYSTEM_SETTINGS: SYSTEM_SETTINGS_KEY,
|
||||
SCHEMA_KEY_PROJECT_SETTINGS: PROJECT_SETTINGS_KEY
|
||||
}
|
||||
|
||||
def __init__(self, schema_hub):
|
||||
self._schema_hub = schema_hub
|
||||
self._dynamic_entities = []
|
||||
|
||||
def add_entity(self, entity):
|
||||
self._dynamic_entities.append(entity)
|
||||
|
||||
def create_hierarchy(self):
|
||||
output = collections.defaultdict(dict)
|
||||
for entity in self._dynamic_entities:
|
||||
output[entity.dynamic_schema_id][entity.path] = (
|
||||
entity.settings_value()
|
||||
)
|
||||
return output
|
||||
|
||||
def save_values(self):
|
||||
hierarchy = self.create_hierarchy()
|
||||
|
||||
for schema_def_id, schema_def_value in hierarchy.items():
|
||||
schema_def = self._schema_hub.get_dynamic_modules_settings_defs(
|
||||
schema_def_id
|
||||
)
|
||||
top_key = self.schema_hub_type_map.get(
|
||||
self._schema_hub.schema_type
|
||||
)
|
||||
schema_def.save_defaults(top_key, schema_def_value)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ from .base_entity import BaseItemEntity
|
|||
from .lib import (
|
||||
NOT_SET,
|
||||
WRAPPER_TYPES,
|
||||
SCHEMA_KEY_SYSTEM_SETTINGS,
|
||||
SCHEMA_KEY_PROJECT_SETTINGS,
|
||||
OverrideState,
|
||||
SchemasHub
|
||||
SchemasHub,
|
||||
DynamicSchemaValueCollector
|
||||
)
|
||||
from .exceptions import (
|
||||
SchemaError,
|
||||
|
|
@ -28,6 +31,7 @@ from openpype.settings.lib import (
|
|||
DEFAULTS_DIR,
|
||||
|
||||
get_default_settings,
|
||||
reset_default_settings,
|
||||
|
||||
get_studio_system_settings_overrides,
|
||||
save_studio_settings,
|
||||
|
|
@ -265,6 +269,16 @@ class RootEntity(BaseItemEntity):
|
|||
output[key] = child_obj.value
|
||||
return output
|
||||
|
||||
def collect_dynamic_schema_entities(self):
|
||||
output = DynamicSchemaValueCollector(self.schema_hub)
|
||||
if self._override_state is not OverrideState.DEFAULTS:
|
||||
return output
|
||||
|
||||
for child_obj in self.non_gui_children.values():
|
||||
child_obj.collect_dynamic_schema_entities(output)
|
||||
|
||||
return output
|
||||
|
||||
def settings_value(self):
|
||||
"""Value for current override state with metadata.
|
||||
|
||||
|
|
@ -276,6 +290,8 @@ class RootEntity(BaseItemEntity):
|
|||
if self._override_state is not OverrideState.DEFAULTS:
|
||||
output = {}
|
||||
for key, child_obj in self.non_gui_children.items():
|
||||
if child_obj.is_dynamic_schema_node:
|
||||
continue
|
||||
value = child_obj.settings_value()
|
||||
if value is not NOT_SET:
|
||||
output[key] = value
|
||||
|
|
@ -374,6 +390,7 @@ class RootEntity(BaseItemEntity):
|
|||
|
||||
if self._override_state is OverrideState.DEFAULTS:
|
||||
self._save_default_values()
|
||||
reset_default_settings()
|
||||
|
||||
elif self._override_state is OverrideState.STUDIO:
|
||||
self._save_studio_values()
|
||||
|
|
@ -421,6 +438,9 @@ class RootEntity(BaseItemEntity):
|
|||
with open(output_path, "w") as file_stream:
|
||||
json.dump(value, file_stream, indent=4)
|
||||
|
||||
dynamic_values_item = self.collect_dynamic_schema_entities()
|
||||
dynamic_values_item.save_values()
|
||||
|
||||
@abstractmethod
|
||||
def _save_studio_values(self):
|
||||
"""Save studio override values."""
|
||||
|
|
@ -476,7 +496,7 @@ class SystemSettings(RootEntity):
|
|||
):
|
||||
if schema_hub is None:
|
||||
# Load system schemas
|
||||
schema_hub = SchemasHub("system_schema")
|
||||
schema_hub = SchemasHub(SCHEMA_KEY_SYSTEM_SETTINGS)
|
||||
|
||||
super(SystemSettings, self).__init__(schema_hub, reset)
|
||||
|
||||
|
|
@ -607,7 +627,7 @@ class ProjectSettings(RootEntity):
|
|||
|
||||
if schema_hub is None:
|
||||
# Load system schemas
|
||||
schema_hub = SchemasHub("projects_schema")
|
||||
schema_hub = SchemasHub(SCHEMA_KEY_PROJECT_SETTINGS)
|
||||
|
||||
super(ProjectSettings, self).__init__(schema_hub, reset)
|
||||
|
||||
|
|
|
|||
|
|
@ -112,6 +112,22 @@
|
|||
```
|
||||
- It is possible to define default values for unfilled fields to do so one of items in list must be dictionary with key `"__default_values__"` and value as dictionary with default key: values (as in example above).
|
||||
|
||||
### dynamic_schema
|
||||
- dynamic templates that can be defined by class of `ModuleSettingsDef`
|
||||
- example:
|
||||
```
|
||||
{
|
||||
"type": "dynamic_schema",
|
||||
"name": "project_settings/global"
|
||||
}
|
||||
```
|
||||
- all valid `ModuleSettingsDef` classes where calling of `get_settings_schemas`
|
||||
will return dictionary where is key "project_settings/global" with schemas
|
||||
will extend and replace this item
|
||||
- works almost the same way as templates
|
||||
- one item can be replaced by multiple items (or by 0 items)
|
||||
- goal is to dynamically loaded settings of OpenPype addons without having
|
||||
their schemas or default values in main repository
|
||||
|
||||
## Basic Dictionary inputs
|
||||
- these inputs wraps another inputs into {key: value} relation
|
||||
|
|
@ -300,6 +316,8 @@ How output of the schema could look like on save:
|
|||
- key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`)
|
||||
- key `"minimum"` as minimum allowed number to enter (Default: `-99999`)
|
||||
- key `"maxium"` as maximum allowed number to enter (Default: `99999`)
|
||||
- key `"steps"` will change single step value of UI inputs (using arrows and wheel scroll)
|
||||
- for UI it is possible to show slider to enable this option set `show_slider` to `true`
|
||||
```
|
||||
{
|
||||
"type": "number",
|
||||
|
|
@ -311,6 +329,18 @@ How output of the schema could look like on save:
|
|||
}
|
||||
```
|
||||
|
||||
```
|
||||
{
|
||||
"type": "number",
|
||||
"key": "ratio",
|
||||
"label": "Ratio"
|
||||
"decimal": 3,
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"show_slider": true
|
||||
}
|
||||
```
|
||||
|
||||
### text
|
||||
- simple text input
|
||||
- key `"multiline"` allows to enter multiple lines of text (Default: `False`)
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@
|
|||
{
|
||||
"type": "schema",
|
||||
"name": "schema_project_unreal"
|
||||
},
|
||||
{
|
||||
"type": "dynamic_schema",
|
||||
"name": "project_settings/global"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,6 +183,15 @@
|
|||
"minimum": -10,
|
||||
"maximum": -5
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "number_with_slider",
|
||||
"label": "Number with slider",
|
||||
"decimal": 2,
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0,
|
||||
"show_slider": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "singleline_text",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,18 @@
|
|||
"collapsible": true,
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "path",
|
||||
"key": "addon_paths",
|
||||
"label": "OpenPype AddOn Paths",
|
||||
"use_label_wrap": true,
|
||||
"multiplatform": true,
|
||||
"multipath": true,
|
||||
"require_restart": true
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "avalon",
|
||||
|
|
@ -16,7 +28,8 @@
|
|||
"type": "number",
|
||||
"key": "AVALON_TIMEOUT",
|
||||
"minimum": 0,
|
||||
"label": "Avalon Mongo Timeout (ms)"
|
||||
"label": "Avalon Mongo Timeout (ms)",
|
||||
"steps": 100
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
|
|
@ -109,14 +122,7 @@
|
|||
"collapsible_key": false,
|
||||
"object_type":
|
||||
{
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"type": "providers-enum",
|
||||
"key": "provider",
|
||||
"label": "Provider"
|
||||
}
|
||||
]
|
||||
"type": "sync-server-providers"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -230,6 +236,10 @@
|
|||
"label": "Enabled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dynamic_schema",
|
||||
"name": "system_settings/modules"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -329,6 +329,45 @@ def reset_default_settings():
|
|||
_DEFAULT_SETTINGS = None
|
||||
|
||||
|
||||
def _get_default_settings():
|
||||
from openpype.modules import get_module_settings_defs
|
||||
|
||||
defaults = load_openpype_default_settings()
|
||||
|
||||
module_settings_defs = get_module_settings_defs()
|
||||
for module_settings_def_cls in module_settings_defs:
|
||||
module_settings_def = module_settings_def_cls()
|
||||
system_defaults = module_settings_def.get_defaults(
|
||||
SYSTEM_SETTINGS_KEY
|
||||
) or {}
|
||||
for path, value in system_defaults.items():
|
||||
if not path:
|
||||
continue
|
||||
|
||||
subdict = defaults["system_settings"]
|
||||
path_items = list(path.split("/"))
|
||||
last_key = path_items.pop(-1)
|
||||
for key in path_items:
|
||||
subdict = subdict[key]
|
||||
subdict[last_key] = value
|
||||
|
||||
project_defaults = module_settings_def.get_defaults(
|
||||
PROJECT_SETTINGS_KEY
|
||||
) or {}
|
||||
for path, value in project_defaults.items():
|
||||
if not path:
|
||||
continue
|
||||
|
||||
subdict = defaults
|
||||
path_items = list(path.split("/"))
|
||||
last_key = path_items.pop(-1)
|
||||
for key in path_items:
|
||||
subdict = subdict[key]
|
||||
subdict[last_key] = value
|
||||
|
||||
return defaults
|
||||
|
||||
|
||||
def get_default_settings():
|
||||
"""Get default settings.
|
||||
|
||||
|
|
@ -338,12 +377,10 @@ def get_default_settings():
|
|||
Returns:
|
||||
dict: Loaded default settings.
|
||||
"""
|
||||
# TODO add cacher
|
||||
return load_openpype_default_settings()
|
||||
# global _DEFAULT_SETTINGS
|
||||
# if _DEFAULT_SETTINGS is None:
|
||||
# _DEFAULT_SETTINGS = load_jsons_from_dir(DEFAULTS_DIR)
|
||||
# return copy.deepcopy(_DEFAULT_SETTINGS)
|
||||
global _DEFAULT_SETTINGS
|
||||
if _DEFAULT_SETTINGS is None:
|
||||
_DEFAULT_SETTINGS = _get_default_settings()
|
||||
return copy.deepcopy(_DEFAULT_SETTINGS)
|
||||
|
||||
|
||||
def load_json_file(fpath):
|
||||
|
|
@ -380,8 +417,8 @@ def load_jsons_from_dir(path, *args, **kwargs):
|
|||
"data1": "CONTENT OF FILE"
|
||||
},
|
||||
"folder2": {
|
||||
"data1": {
|
||||
"subfolder1": "CONTENT OF FILE"
|
||||
"subfolder1": {
|
||||
"data2": "CONTENT OF FILE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from .base import (
|
|||
BaseWidget,
|
||||
InputWidget
|
||||
)
|
||||
from openpype.widgets.sliders import NiceSlider
|
||||
from openpype.tools.settings import CHILD_OFFSET
|
||||
|
||||
|
||||
|
|
@ -404,21 +405,53 @@ class TextWidget(InputWidget):
|
|||
|
||||
|
||||
class NumberWidget(InputWidget):
|
||||
_slider_widget = None
|
||||
|
||||
def _add_inputs_to_layout(self):
|
||||
kwargs = {
|
||||
"minimum": self.entity.minimum,
|
||||
"maximum": self.entity.maximum,
|
||||
"decimal": self.entity.decimal
|
||||
"decimal": self.entity.decimal,
|
||||
"steps": self.entity.steps
|
||||
}
|
||||
self.input_field = NumberSpinBox(self.content_widget, **kwargs)
|
||||
input_field_stretch = 1
|
||||
|
||||
slider_multiplier = 1
|
||||
if self.entity.show_slider:
|
||||
# Slider can't handle float numbers so all decimals are converted
|
||||
# to integer range.
|
||||
slider_multiplier = 10 ** self.entity.decimal
|
||||
slider_widget = NiceSlider(QtCore.Qt.Horizontal, self)
|
||||
slider_widget.setRange(
|
||||
int(self.entity.minimum * slider_multiplier),
|
||||
int(self.entity.maximum * slider_multiplier)
|
||||
)
|
||||
if self.entity.steps is not None:
|
||||
slider_widget.setSingleStep(
|
||||
self.entity.steps * slider_multiplier
|
||||
)
|
||||
|
||||
self.content_layout.addWidget(slider_widget, 1)
|
||||
|
||||
slider_widget.valueChanged.connect(self._on_slider_change)
|
||||
|
||||
self._slider_widget = slider_widget
|
||||
|
||||
input_field_stretch = 0
|
||||
|
||||
self._slider_multiplier = slider_multiplier
|
||||
|
||||
self.setFocusProxy(self.input_field)
|
||||
|
||||
self.content_layout.addWidget(self.input_field, 1)
|
||||
self.content_layout.addWidget(self.input_field, input_field_stretch)
|
||||
|
||||
self.input_field.valueChanged.connect(self._on_value_change)
|
||||
self.input_field.focused_in.connect(self._on_input_focus)
|
||||
|
||||
self._ignore_slider_change = False
|
||||
self._ignore_input_change = False
|
||||
|
||||
def _on_input_focus(self):
|
||||
self.focused_in()
|
||||
|
||||
|
|
@ -429,10 +462,25 @@ class NumberWidget(InputWidget):
|
|||
def set_entity_value(self):
|
||||
self.input_field.setValue(self.entity.value)
|
||||
|
||||
def _on_slider_change(self, new_value):
|
||||
if self._ignore_slider_change:
|
||||
return
|
||||
|
||||
self._ignore_input_change = True
|
||||
self.input_field.setValue(new_value / self._slider_multiplier)
|
||||
self._ignore_input_change = False
|
||||
|
||||
def _on_value_change(self):
|
||||
if self.ignore_input_changes:
|
||||
return
|
||||
self.entity.set(self.input_field.value())
|
||||
|
||||
value = self.input_field.value()
|
||||
if self._slider_widget is not None and not self._ignore_input_change:
|
||||
self._ignore_slider_change = True
|
||||
self._slider_widget.setValue(value * self._slider_multiplier)
|
||||
self._ignore_slider_change = False
|
||||
|
||||
self.entity.set(value)
|
||||
|
||||
|
||||
class RawJsonInput(SettingsPlainTextEdit):
|
||||
|
|
|
|||
|
|
@ -114,6 +114,30 @@ QPushButton[btn-type="expand-toggle"] {
|
|||
background: #21252B;
|
||||
}
|
||||
|
||||
/* SLider */
|
||||
QSlider::groove {
|
||||
border: 1px solid #464b54;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
QSlider::groove:horizontal {
|
||||
height: 8px;
|
||||
}
|
||||
QSlider::groove:vertical {
|
||||
width: 8px;
|
||||
}
|
||||
QSlider::handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
border-radius: 5px;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
margin: -2px 0;
|
||||
}
|
||||
QSlider::handle:vertical {
|
||||
margin: 0 -2px;
|
||||
}
|
||||
|
||||
#GroupWidget {
|
||||
border-bottom: 1px solid #21252B;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,11 +92,15 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox):
|
|||
min_value = kwargs.pop("minimum", -99999)
|
||||
max_value = kwargs.pop("maximum", 99999)
|
||||
decimals = kwargs.pop("decimal", 0)
|
||||
steps = kwargs.pop("steps", None)
|
||||
|
||||
super(NumberSpinBox, self).__init__(*args, **kwargs)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
self.setDecimals(decimals)
|
||||
self.setMinimum(min_value)
|
||||
self.setMaximum(max_value)
|
||||
if steps is not None:
|
||||
self.setSingleStep(steps)
|
||||
|
||||
def focusInEvent(self, event):
|
||||
super(NumberSpinBox, self).focusInEvent(event)
|
||||
|
|
|
|||
|
|
@ -430,7 +430,6 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
# Pype's anatomy object for current project
|
||||
self.anatomy = Anatomy(io.Session["AVALON_PROJECT"])
|
||||
# Template key used to get work template from anatomy templates
|
||||
# TODO change template key based on task
|
||||
self.template_key = "work"
|
||||
|
||||
# This is not root but workfile directory
|
||||
|
|
|
|||
139
openpype/widgets/sliders.py
Normal file
139
openpype/widgets/sliders.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
|
||||
class NiceSlider(QtWidgets.QSlider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NiceSlider, self).__init__(*args, **kwargs)
|
||||
self._mouse_clicked = False
|
||||
self._handle_size = 0
|
||||
|
||||
self._bg_brush = QtGui.QBrush(QtGui.QColor("#21252B"))
|
||||
self._fill_brush = QtGui.QBrush(QtGui.QColor("#5cadd6"))
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self._mouse_clicked = True
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._set_value_to_pos(event.pos())
|
||||
return event.accept()
|
||||
return super(NiceSlider, self).mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self._mouse_clicked:
|
||||
self._set_value_to_pos(event.pos())
|
||||
|
||||
super(NiceSlider, self).mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self._mouse_clicked = True
|
||||
super(NiceSlider, self).mouseReleaseEvent(event)
|
||||
|
||||
def _set_value_to_pos(self, pos):
|
||||
if self.orientation() == QtCore.Qt.Horizontal:
|
||||
self._set_value_to_pos_x(pos.x())
|
||||
else:
|
||||
self._set_value_to_pos_y(pos.y())
|
||||
|
||||
def _set_value_to_pos_x(self, pos_x):
|
||||
_range = self.maximum() - self.minimum()
|
||||
handle_size = self._handle_size
|
||||
half_handle = handle_size / 2
|
||||
pos_x -= half_handle
|
||||
width = self.width() - handle_size
|
||||
value = ((_range * pos_x) / width) + self.minimum()
|
||||
self.setValue(value)
|
||||
|
||||
def _set_value_to_pos_y(self, pos_y):
|
||||
_range = self.maximum() - self.minimum()
|
||||
handle_size = self._handle_size
|
||||
half_handle = handle_size / 2
|
||||
pos_y = self.height() - pos_y - half_handle
|
||||
height = self.height() - handle_size
|
||||
value = (_range * pos_y / height) + self.minimum()
|
||||
self.setValue(value)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter(self)
|
||||
opt = QtWidgets.QStyleOptionSlider()
|
||||
self.initStyleOption(opt)
|
||||
|
||||
painter.fillRect(event.rect(), QtCore.Qt.transparent)
|
||||
|
||||
painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
|
||||
|
||||
horizontal = self.orientation() == QtCore.Qt.Horizontal
|
||||
|
||||
rect = self.style().subControlRect(
|
||||
QtWidgets.QStyle.CC_Slider,
|
||||
opt,
|
||||
QtWidgets.QStyle.SC_SliderGroove,
|
||||
self
|
||||
)
|
||||
|
||||
_range = self.maximum() - self.minimum()
|
||||
_offset = self.value() - self.minimum()
|
||||
if horizontal:
|
||||
_handle_half = rect.height() / 2
|
||||
_handle_size = _handle_half * 2
|
||||
width = rect.width() - _handle_size
|
||||
pos_x = ((width / _range) * _offset)
|
||||
pos_y = rect.center().y() - _handle_half + 1
|
||||
else:
|
||||
_handle_half = rect.width() / 2
|
||||
_handle_size = _handle_half * 2
|
||||
height = rect.height() - _handle_size
|
||||
pos_x = rect.center().x() - _handle_half + 1
|
||||
pos_y = height - ((height / _range) * _offset)
|
||||
|
||||
handle_rect = QtCore.QRect(
|
||||
pos_x, pos_y, _handle_size, _handle_size
|
||||
)
|
||||
|
||||
self._handle_size = _handle_size
|
||||
_offset = 2
|
||||
_size = _handle_size - _offset
|
||||
if horizontal:
|
||||
if rect.height() > _size:
|
||||
new_rect = QtCore.QRect(0, 0, rect.width(), _size)
|
||||
center_point = QtCore.QPoint(
|
||||
rect.center().x(), handle_rect.center().y()
|
||||
)
|
||||
new_rect.moveCenter(center_point)
|
||||
rect = new_rect
|
||||
|
||||
ratio = rect.height() / 2
|
||||
fill_rect = QtCore.QRect(
|
||||
rect.x(),
|
||||
rect.y(),
|
||||
handle_rect.right() - rect.x(),
|
||||
rect.height()
|
||||
)
|
||||
|
||||
else:
|
||||
if rect.width() > _size:
|
||||
new_rect = QtCore.QRect(0, 0, _size, rect.height())
|
||||
center_point = QtCore.QPoint(
|
||||
handle_rect.center().x(), rect.center().y()
|
||||
)
|
||||
new_rect.moveCenter(center_point)
|
||||
rect = new_rect
|
||||
|
||||
ratio = rect.width() / 2
|
||||
fill_rect = QtCore.QRect(
|
||||
rect.x(),
|
||||
handle_rect.y(),
|
||||
rect.width(),
|
||||
rect.height() - handle_rect.y(),
|
||||
)
|
||||
|
||||
painter.save()
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(self._bg_brush)
|
||||
painter.drawRoundedRect(rect, ratio, ratio)
|
||||
|
||||
painter.setBrush(self._fill_brush)
|
||||
painter.drawRoundedRect(fill_rect, ratio, ratio)
|
||||
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(self._fill_brush)
|
||||
painter.drawEllipse(handle_rect)
|
||||
painter.restore()
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit f48fce09c0986c1fd7f6731de33907be46b436c5
|
||||
Subproject commit b3e49597786c931c13bca207769727d5fc56d5f6
|
||||
98
start.py
98
start.py
|
|
@ -179,6 +179,12 @@ else:
|
|||
ssl_cert_file = certifi.where()
|
||||
os.environ["SSL_CERT_FILE"] = ssl_cert_file
|
||||
|
||||
if "--headless" in sys.argv:
|
||||
os.environ["OPENPYPE_HEADLESS_MODE"] = "1"
|
||||
sys.argv.remove("--headless")
|
||||
else:
|
||||
if os.getenv("OPENPYPE_HEADLESS_MODE") != "1":
|
||||
os.environ.pop("OPENPYPE_HEADLESS_MODE", None)
|
||||
|
||||
import igniter # noqa: E402
|
||||
from igniter import BootstrapRepos # noqa: E402
|
||||
|
|
@ -343,7 +349,7 @@ def _process_arguments() -> tuple:
|
|||
# check for `--use-version=3.0.0` argument and `--use-staging`
|
||||
use_version = None
|
||||
use_staging = False
|
||||
print_versions = False
|
||||
commands = []
|
||||
for arg in sys.argv:
|
||||
if arg == "--use-version":
|
||||
_print("!!! Please use option --use-version like:")
|
||||
|
|
@ -366,17 +372,38 @@ def _process_arguments() -> tuple:
|
|||
" proper version string."))
|
||||
sys.exit(1)
|
||||
|
||||
if arg == "--validate-version":
|
||||
_print("!!! Please use option --validate-version like:")
|
||||
_print(" --validate-version=3.0.0")
|
||||
sys.exit(1)
|
||||
|
||||
if arg.startswith("--validate-version="):
|
||||
m = re.search(
|
||||
r"--validate-version=(?P<version>\d+\.\d+\.\d+(?:\S*)?)", arg)
|
||||
if m and m.group('version'):
|
||||
use_version = m.group('version')
|
||||
sys.argv.remove(arg)
|
||||
commands.append("validate")
|
||||
else:
|
||||
_print("!!! Requested version isn't in correct format.")
|
||||
_print((" Use --list-versions to find out"
|
||||
" proper version string."))
|
||||
sys.exit(1)
|
||||
|
||||
if "--use-staging" in sys.argv:
|
||||
use_staging = True
|
||||
sys.argv.remove("--use-staging")
|
||||
|
||||
if "--list-versions" in sys.argv:
|
||||
print_versions = True
|
||||
commands.append("print_versions")
|
||||
sys.argv.remove("--list-versions")
|
||||
|
||||
# handle igniter
|
||||
# this is helper to run igniter before anything else
|
||||
if "igniter" in sys.argv:
|
||||
if os.getenv("OPENPYPE_HEADLESS_MODE") == "1":
|
||||
_print("!!! Cannot open Igniter dialog in headless mode.")
|
||||
sys.exit(1)
|
||||
import igniter
|
||||
return_code = igniter.open_dialog()
|
||||
|
||||
|
|
@ -389,7 +416,7 @@ def _process_arguments() -> tuple:
|
|||
sys.argv.pop(idx)
|
||||
sys.argv.insert(idx, "tray")
|
||||
|
||||
return use_version, use_staging, print_versions
|
||||
return use_version, use_staging, commands
|
||||
|
||||
|
||||
def _determine_mongodb() -> str:
|
||||
|
|
@ -424,6 +451,11 @@ def _determine_mongodb() -> str:
|
|||
|
||||
if not openpype_mongo:
|
||||
_print("*** No DB connection string specified.")
|
||||
if os.getenv("OPENPYPE_HEADLESS_MODE") == "1":
|
||||
_print("!!! Cannot open Igniter dialog in headless mode.")
|
||||
_print(
|
||||
"!!! Please use `OPENPYPE_MONGO` to specify server address.")
|
||||
sys.exit(1)
|
||||
_print("--- launching setup UI ...")
|
||||
|
||||
result = igniter.open_dialog()
|
||||
|
|
@ -527,6 +559,9 @@ def _find_frozen_openpype(use_version: str = None,
|
|||
except IndexError:
|
||||
# no OpenPype version found, run Igniter and ask for them.
|
||||
_print('*** No OpenPype versions found.')
|
||||
if os.getenv("OPENPYPE_HEADLESS_MODE") == "1":
|
||||
_print("!!! Cannot open Igniter dialog in headless mode.")
|
||||
sys.exit(1)
|
||||
_print("--- launching setup UI ...")
|
||||
import igniter
|
||||
return_code = igniter.open_dialog()
|
||||
|
|
@ -590,8 +625,16 @@ def _find_frozen_openpype(use_version: str = None,
|
|||
|
||||
if not is_inside:
|
||||
# install latest version to user data dir
|
||||
version_path = bootstrap.install_version(
|
||||
openpype_version, force=True)
|
||||
if os.getenv("OPENPYPE_HEADLESS_MODE", "0") != "1":
|
||||
import igniter
|
||||
version_path = igniter.open_update_window(openpype_version)
|
||||
else:
|
||||
version_path = bootstrap.install_version(
|
||||
openpype_version, force=True)
|
||||
|
||||
openpype_version.path = version_path
|
||||
_initialize_environment(openpype_version)
|
||||
return openpype_version.path
|
||||
|
||||
if openpype_version.path.is_file():
|
||||
_print(">>> Extracting zip file ...")
|
||||
|
|
@ -738,7 +781,7 @@ def boot():
|
|||
# Process arguments
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
use_version, use_staging, print_versions = _process_arguments()
|
||||
use_version, use_staging, commands = _process_arguments()
|
||||
|
||||
if os.getenv("OPENPYPE_VERSION"):
|
||||
if use_version:
|
||||
|
|
@ -766,13 +809,47 @@ def boot():
|
|||
# Get openpype path from database and set it to environment so openpype can
|
||||
# find its versions there and bootstrap them.
|
||||
openpype_path = get_openpype_path_from_db(openpype_mongo)
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
local_version = bootstrap.get_version(Path(OPENPYPE_ROOT))
|
||||
else:
|
||||
local_version = bootstrap.get_local_live_version()
|
||||
|
||||
if "validate" in commands:
|
||||
_print(f">>> Validating version [ {use_version} ]")
|
||||
openpype_versions = bootstrap.find_openpype(include_zips=True,
|
||||
staging=True)
|
||||
openpype_versions += bootstrap.find_openpype(include_zips=True,
|
||||
staging=False)
|
||||
v: OpenPypeVersion
|
||||
found = [v for v in openpype_versions if str(v) == use_version]
|
||||
if not found:
|
||||
_print(f"!!! Version [ {use_version} ] not found.")
|
||||
list_versions(openpype_versions, local_version)
|
||||
sys.exit(1)
|
||||
|
||||
# print result
|
||||
result = bootstrap.validate_openpype_version(
|
||||
bootstrap.get_version_path_from_list(
|
||||
use_version, openpype_versions))
|
||||
|
||||
_print("{}{}".format(
|
||||
">>> " if result[0] else "!!! ",
|
||||
bootstrap.validate_openpype_version(
|
||||
bootstrap.get_version_path_from_list(
|
||||
use_version, openpype_versions)
|
||||
)[1])
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if not openpype_path:
|
||||
_print("*** Cannot get OpenPype path from database.")
|
||||
|
||||
if not os.getenv("OPENPYPE_PATH") and openpype_path:
|
||||
os.environ["OPENPYPE_PATH"] = openpype_path
|
||||
|
||||
if print_versions:
|
||||
if "print_versions" in commands:
|
||||
if not use_staging:
|
||||
_print("--- This will list only non-staging versions detected.")
|
||||
_print(" To see staging versions, use --use-staging argument.")
|
||||
|
|
@ -803,6 +880,13 @@ def boot():
|
|||
# no version to run
|
||||
_print(f"!!! {e}")
|
||||
sys.exit(1)
|
||||
# validate version
|
||||
_print(f">>> Validating version [ {str(version_path)} ]")
|
||||
result = bootstrap.validate_openpype_version(version_path)
|
||||
if not result[0]:
|
||||
_print(f"!!! Invalid version: {result[1]}")
|
||||
sys.exit(1)
|
||||
_print(f"--- version is valid")
|
||||
else:
|
||||
version_path = _bootstrap_from_code(use_version, use_staging)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,11 +18,14 @@ Running OpenPype without any commands will default to `tray`.
|
|||
```shell
|
||||
openpype_console --use-version=3.0.0-foo+bar
|
||||
```
|
||||
`--headless` - to run OpenPype in headless mode (without using graphical UI)
|
||||
|
||||
`--use-staging` - to use staging versions of OpenPype.
|
||||
|
||||
`--list-versions [--use-staging]` - to list available versions.
|
||||
|
||||
`--validate-version` to validate integrity of given version
|
||||
|
||||
For more information [see here](admin_use#run-openpype).
|
||||
|
||||
## Commands
|
||||
|
|
|
|||
|
|
@ -56,6 +56,19 @@ openpype_console --list-versions
|
|||
You can add `--use-staging` to list staging versions.
|
||||
:::
|
||||
|
||||
If you want to validate integrity of some available version, you can use:
|
||||
|
||||
```shell
|
||||
openpype_console --validate-version=3.3.0
|
||||
```
|
||||
|
||||
This will go through the version and validate file content against sha 256 hashes
|
||||
stored in `checksums` file.
|
||||
|
||||
:::tip Headless mode
|
||||
Add `--headless` to run OpenPype without graphical UI (useful on server or on automated tasks, etc.)
|
||||
:::
|
||||
|
||||
### Details
|
||||
When you run OpenPype from executable, few check are made:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue