mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
Merge branch 'develop' into bugfix/OP-6416_3dsmax-container-tab
This commit is contained in:
commit
d47a27f0cf
38 changed files with 2137 additions and 400 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,7 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.16.4-nightly.2
|
||||
- 3.16.4-nightly.1
|
||||
- 3.16.3
|
||||
- 3.16.3-nightly.5
|
||||
|
|
@ -134,7 +135,6 @@ body:
|
|||
- 3.14.7
|
||||
- 3.14.7-nightly.8
|
||||
- 3.14.7-nightly.7
|
||||
- 3.14.7-nightly.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ from openpype.pipeline import (
|
|||
LegacyCreator,
|
||||
LoaderPlugin,
|
||||
get_representation_path,
|
||||
|
||||
legacy_io,
|
||||
)
|
||||
from openpype.pipeline.load import LoadError
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline.create import get_subset_name
|
||||
|
||||
from . import lib
|
||||
from .lib import imprint, read
|
||||
|
|
@ -405,14 +405,21 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase):
|
|||
# No existing scene instance node for this layer. Note that
|
||||
# this instance will not have the `instance_node` data yet
|
||||
# until it's been saved/persisted at least once.
|
||||
# TODO: Correctly define the subset name using templates
|
||||
prefix = self.layer_instance_prefix or self.family
|
||||
subset_name = "{}{}".format(prefix, layer.name())
|
||||
project_name = self.create_context.get_current_project_name()
|
||||
|
||||
instance_data = {
|
||||
"asset": legacy_io.Session["AVALON_ASSET"],
|
||||
"task": legacy_io.Session["AVALON_TASK"],
|
||||
"asset": self.create_context.get_current_asset_name(),
|
||||
"task": self.create_context.get_current_task_name(),
|
||||
"variant": layer.name(),
|
||||
}
|
||||
asset_doc = get_asset_by_name(project_name,
|
||||
instance_data["asset"])
|
||||
subset_name = self.get_subset_name(
|
||||
layer.name(),
|
||||
instance_data["task"],
|
||||
asset_doc,
|
||||
project_name)
|
||||
|
||||
instance = CreatedInstance(
|
||||
family=self.family,
|
||||
subset_name=subset_name,
|
||||
|
|
@ -519,6 +526,22 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase):
|
|||
if node and cmds.objExists(node):
|
||||
cmds.delete(node)
|
||||
|
||||
def get_subset_name(
|
||||
self,
|
||||
variant,
|
||||
task_name,
|
||||
asset_doc,
|
||||
project_name,
|
||||
host_name=None,
|
||||
instance=None
|
||||
):
|
||||
# creator.family != 'render' as expected
|
||||
return get_subset_name(self.layer_instance_prefix,
|
||||
variant,
|
||||
task_name,
|
||||
asset_doc,
|
||||
project_name)
|
||||
|
||||
|
||||
class Loader(LoaderPlugin):
|
||||
hosts = ["maya"]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin
|
|||
from openpype.hosts.maya.api import plugin
|
||||
from openpype.hosts.maya.api.lib import read
|
||||
|
||||
from openpype.client import get_asset_by_name
|
||||
|
||||
from maya import cmds
|
||||
from maya.app.renderSetup.model import renderSetup
|
||||
|
||||
|
|
@ -135,6 +137,18 @@ class MayaLegacyConvertor(SubsetConvertorPlugin,
|
|||
# "rendering" family being converted to "renderlayer" family)
|
||||
original_data["family"] = creator.family
|
||||
|
||||
# recreate subset name as without it would be
|
||||
# `renderingMain` vs correct `renderMain`
|
||||
project_name = self.create_context.get_current_project_name()
|
||||
asset_doc = get_asset_by_name(project_name,
|
||||
original_data["asset"])
|
||||
subset_name = creator.get_subset_name(
|
||||
original_data["variant"],
|
||||
data["task"],
|
||||
asset_doc,
|
||||
project_name)
|
||||
original_data["subset"] = subset_name
|
||||
|
||||
# Convert to creator attributes when relevant
|
||||
creator_attributes = {}
|
||||
for key in list(original_data.keys()):
|
||||
|
|
|
|||
|
|
@ -304,9 +304,9 @@ class CollectMayaRender(pyblish.api.InstancePlugin):
|
|||
|
||||
if self.sync_workfile_version:
|
||||
data["version"] = context.data["version"]
|
||||
for instance in context:
|
||||
if instance.data['family'] == "workfile":
|
||||
instance.data["version"] = context.data["version"]
|
||||
for _instance in context:
|
||||
if _instance.data['family'] == "workfile":
|
||||
_instance.data["version"] = context.data["version"]
|
||||
|
||||
# Define nice label
|
||||
label = "{0} ({1})".format(layer_name, instance.data["asset"])
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ class LoadImage(load.LoaderPlugin):
|
|||
|
||||
file = file.replace("\\", "/")
|
||||
|
||||
repr_cont = context["representation"]["context"]
|
||||
representation = context["representation"]
|
||||
repr_cont = representation["context"]
|
||||
frame = repr_cont.get("frame")
|
||||
if frame:
|
||||
padding = len(frame)
|
||||
|
|
@ -104,16 +105,7 @@ class LoadImage(load.LoaderPlugin):
|
|||
frame,
|
||||
format(frame_number, "0{}".format(padding)))
|
||||
|
||||
name_data = {
|
||||
"asset": repr_cont["asset"],
|
||||
"subset": repr_cont["subset"],
|
||||
"representation": context["representation"]["name"],
|
||||
"ext": repr_cont["representation"],
|
||||
"id": context["representation"]["_id"],
|
||||
"class_name": self.__class__.__name__
|
||||
}
|
||||
|
||||
read_name = self.node_name_template.format(**name_data)
|
||||
read_name = self._get_node_name(representation)
|
||||
|
||||
# Create the Loader with the filename path set
|
||||
with viewer_update_and_undo_stop():
|
||||
|
|
@ -212,6 +204,8 @@ class LoadImage(load.LoaderPlugin):
|
|||
last = first = int(frame_number)
|
||||
|
||||
# Set the global in to the start frame of the sequence
|
||||
read_name = self._get_node_name(representation)
|
||||
node["name"].setValue(read_name)
|
||||
node["file"].setValue(file)
|
||||
node["origfirst"].setValue(first)
|
||||
node["first"].setValue(first)
|
||||
|
|
@ -250,3 +244,17 @@ class LoadImage(load.LoaderPlugin):
|
|||
|
||||
with viewer_update_and_undo_stop():
|
||||
nuke.delete(node)
|
||||
|
||||
def _get_node_name(self, representation):
|
||||
|
||||
repre_cont = representation["context"]
|
||||
name_data = {
|
||||
"asset": repre_cont["asset"],
|
||||
"subset": repre_cont["subset"],
|
||||
"representation": representation["name"],
|
||||
"ext": repre_cont["representation"],
|
||||
"id": representation["_id"],
|
||||
"class_name": self.__class__.__name__
|
||||
}
|
||||
|
||||
return self.node_name_template.format(**name_data)
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
environment["OPENPYPE_PUBLISH_JOB"] = "1"
|
||||
environment["OPENPYPE_RENDER_JOB"] = "0"
|
||||
environment["OPENPYPE_REMOTE_PUBLISH"] = "0"
|
||||
deadline_plugin = "Openpype"
|
||||
deadline_plugin = "OpenPype"
|
||||
# Add OpenPype version if we are running from build.
|
||||
if is_running_from_build():
|
||||
self.environ_keys.append("OPENPYPE_VERSION")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from .constants import (
|
|||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
DEFAULT_SUBSET_TEMPLATE,
|
||||
PRE_CREATE_THUMBNAIL_KEY,
|
||||
DEFAULT_VARIANT_VALUE,
|
||||
)
|
||||
|
||||
from .utils import (
|
||||
|
|
@ -50,6 +51,7 @@ __all__ = (
|
|||
"SUBSET_NAME_ALLOWED_SYMBOLS",
|
||||
"DEFAULT_SUBSET_TEMPLATE",
|
||||
"PRE_CREATE_THUMBNAIL_KEY",
|
||||
"DEFAULT_VARIANT_VALUE",
|
||||
|
||||
"get_last_versions_for_instances",
|
||||
"get_next_versions_for_instances",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_."
|
||||
DEFAULT_SUBSET_TEMPLATE = "{family}{Variant}"
|
||||
PRE_CREATE_THUMBNAIL_KEY = "thumbnail_source"
|
||||
DEFAULT_VARIANT_VALUE = "Main"
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SUBSET_NAME_ALLOWED_SYMBOLS",
|
||||
"DEFAULT_SUBSET_TEMPLATE",
|
||||
"PRE_CREATE_THUMBNAIL_KEY",
|
||||
"DEFAULT_VARIANT_VALUE",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import os
|
||||
import copy
|
||||
import collections
|
||||
|
||||
|
|
@ -20,6 +19,7 @@ from openpype.pipeline.plugin_discover import (
|
|||
deregister_plugin_path
|
||||
)
|
||||
|
||||
from .constants import DEFAULT_VARIANT_VALUE
|
||||
from .subset_name import get_subset_name
|
||||
from .utils import get_next_versions_for_instances
|
||||
from .legacy_create import LegacyCreator
|
||||
|
|
@ -517,7 +517,7 @@ class Creator(BaseCreator):
|
|||
default_variants = []
|
||||
|
||||
# Default variant used in 'get_default_variant'
|
||||
default_variant = None
|
||||
_default_variant = None
|
||||
|
||||
# Short description of family
|
||||
# - may not be used if `get_description` is overriden
|
||||
|
|
@ -543,6 +543,21 @@ class Creator(BaseCreator):
|
|||
# - similar to instance attribute definitions
|
||||
pre_create_attr_defs = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
cls = self.__class__
|
||||
|
||||
# Fix backwards compatibility for plugins which override
|
||||
# 'default_variant' attribute directly
|
||||
if not isinstance(cls.default_variant, property):
|
||||
# Move value from 'default_variant' to '_default_variant'
|
||||
self._default_variant = self.default_variant
|
||||
# Create property 'default_variant' on the class
|
||||
cls.default_variant = property(
|
||||
cls._get_default_variant_wrap,
|
||||
cls._set_default_variant_wrap
|
||||
)
|
||||
super(Creator, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def show_order(self):
|
||||
"""Order in which is creator shown in UI.
|
||||
|
|
@ -595,10 +610,10 @@ class Creator(BaseCreator):
|
|||
def get_default_variants(self):
|
||||
"""Default variant values for UI tooltips.
|
||||
|
||||
Replacement of `defatults` attribute. Using method gives ability to
|
||||
have some "logic" other than attribute values.
|
||||
Replacement of `default_variants` attribute. Using method gives
|
||||
ability to have some "logic" other than attribute values.
|
||||
|
||||
By default returns `default_variants` value.
|
||||
By default, returns `default_variants` value.
|
||||
|
||||
Returns:
|
||||
List[str]: Whisper variants for user input.
|
||||
|
|
@ -606,17 +621,63 @@ class Creator(BaseCreator):
|
|||
|
||||
return copy.deepcopy(self.default_variants)
|
||||
|
||||
def get_default_variant(self):
|
||||
def get_default_variant(self, only_explicit=False):
|
||||
"""Default variant value that will be used to prefill variant input.
|
||||
|
||||
This is for user input and value may not be content of result from
|
||||
`get_default_variants`.
|
||||
|
||||
Can return `None`. In that case first element from
|
||||
`get_default_variants` should be used.
|
||||
Note:
|
||||
This method does not allow to have empty string as
|
||||
default variant.
|
||||
|
||||
Args:
|
||||
only_explicit (Optional[bool]): If True, only explicit default
|
||||
variant from '_default_variant' will be returned.
|
||||
|
||||
Returns:
|
||||
str: Variant value.
|
||||
"""
|
||||
|
||||
return self.default_variant
|
||||
if only_explicit or self._default_variant:
|
||||
return self._default_variant
|
||||
|
||||
for variant in self.get_default_variants():
|
||||
return variant
|
||||
return DEFAULT_VARIANT_VALUE
|
||||
|
||||
def _get_default_variant_wrap(self):
|
||||
"""Default variant value that will be used to prefill variant input.
|
||||
|
||||
Wrapper for 'get_default_variant'.
|
||||
|
||||
Notes:
|
||||
This method is wrapper for 'get_default_variant'
|
||||
for 'default_variant' property, so creator can override
|
||||
the method.
|
||||
|
||||
Returns:
|
||||
str: Variant value.
|
||||
"""
|
||||
|
||||
return self.get_default_variant()
|
||||
|
||||
def _set_default_variant_wrap(self, variant):
|
||||
"""Set default variant value.
|
||||
|
||||
This method is needed for automated settings overrides which are
|
||||
changing attributes based on keys in settings.
|
||||
|
||||
Args:
|
||||
variant (str): New default variant value.
|
||||
"""
|
||||
|
||||
self._default_variant = variant
|
||||
|
||||
default_variant = property(
|
||||
_get_default_variant_wrap,
|
||||
_set_default_variant_wrap
|
||||
)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
"""Plugin attribute definitions needed for creation.
|
||||
|
|
|
|||
|
|
@ -568,9 +568,15 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
|
|||
col = list(cols[0])
|
||||
|
||||
# create subset name `familyTaskSubset_AOV`
|
||||
group_name = 'render{}{}{}{}'.format(
|
||||
task[0].upper(), task[1:],
|
||||
subset[0].upper(), subset[1:])
|
||||
# TODO refactor/remove me
|
||||
family = skeleton["family"]
|
||||
if not subset.startswith(family):
|
||||
group_name = '{}{}{}{}{}'.format(
|
||||
family,
|
||||
task[0].upper(), task[1:],
|
||||
subset[0].upper(), subset[1:])
|
||||
else:
|
||||
group_name = subset
|
||||
|
||||
# if there are multiple cameras, we need to add camera name
|
||||
if isinstance(col, (list, tuple)):
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from openpype import AYON_SERVER_ENABLED
|
|||
from openpype.pipeline.create import (
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
PRE_CREATE_THUMBNAIL_KEY,
|
||||
DEFAULT_VARIANT_VALUE,
|
||||
TaskNotSetError,
|
||||
)
|
||||
|
||||
|
|
@ -626,7 +627,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
|
||||
default_variants = creator_item.default_variants
|
||||
if not default_variants:
|
||||
default_variants = ["Main"]
|
||||
default_variants = [DEFAULT_VARIANT_VALUE]
|
||||
|
||||
default_variant = creator_item.default_variant
|
||||
if not default_variant:
|
||||
|
|
@ -642,7 +643,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
elif variant:
|
||||
self.variant_hints_menu.addAction(variant)
|
||||
|
||||
variant_text = default_variant or "Main"
|
||||
variant_text = default_variant or DEFAULT_VARIANT_VALUE
|
||||
# Make sure subset name is updated to new plugin
|
||||
if variant_text == self.variant_input.text():
|
||||
self._on_variant_change()
|
||||
|
|
|
|||
BIN
openpype/tools/publisher/widgets/images/browse.png
Normal file
BIN
openpype/tools/publisher/widgets/images/browse.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
openpype/tools/publisher/widgets/images/options.png
Normal file
BIN
openpype/tools/publisher/widgets/images/options.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
openpype/tools/publisher/widgets/images/paste.png
Normal file
BIN
openpype/tools/publisher/widgets/images/paste.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
BIN
openpype/tools/publisher/widgets/images/take_screenshot.png
Normal file
BIN
openpype/tools/publisher/widgets/images/take_screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
314
openpype/tools/publisher/widgets/screenshot_widget.py
Normal file
314
openpype/tools/publisher/widgets/screenshot_widget.py
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class ScreenMarquee(QtWidgets.QDialog):
|
||||
"""Dialog to interactively define screen area.
|
||||
|
||||
This allows to select a screen area through a marquee selection.
|
||||
|
||||
You can use any of its classmethods for easily saving an image,
|
||||
capturing to QClipboard or returning a QPixmap, respectively
|
||||
`capture_to_file`, `capture_to_clipboard` and `capture_to_pixmap`.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ScreenMarquee, self).__init__(parent=parent)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.FramelessWindowHint
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
| QtCore.Qt.CustomizeWindowHint
|
||||
| QtCore.Qt.Tool)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.setCursor(QtCore.Qt.CrossCursor)
|
||||
self.setMouseTracking(True)
|
||||
|
||||
fade_anim = QtCore.QVariantAnimation()
|
||||
fade_anim.setStartValue(0)
|
||||
fade_anim.setEndValue(50)
|
||||
fade_anim.setDuration(200)
|
||||
fade_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic)
|
||||
fade_anim.start(QtCore.QAbstractAnimation.DeleteWhenStopped)
|
||||
|
||||
fade_anim.valueChanged.connect(self._on_fade_anim)
|
||||
|
||||
app = QtWidgets.QApplication.instance()
|
||||
if hasattr(app, "screenAdded"):
|
||||
app.screenAdded.connect(self._on_screen_added)
|
||||
app.screenRemoved.connect(self._fit_screen_geometry)
|
||||
elif hasattr(app, "desktop"):
|
||||
desktop = app.desktop()
|
||||
desktop.screenCountChanged.connect(self._fit_screen_geometry)
|
||||
|
||||
for screen in QtWidgets.QApplication.screens():
|
||||
screen.geometryChanged.connect(self._fit_screen_geometry)
|
||||
|
||||
self._opacity = fade_anim.currentValue()
|
||||
self._click_pos = None
|
||||
self._capture_rect = None
|
||||
|
||||
self._fade_anim = fade_anim
|
||||
|
||||
def get_captured_pixmap(self):
|
||||
if self._capture_rect is None:
|
||||
return QtGui.QPixmap()
|
||||
|
||||
return self.get_desktop_pixmap(self._capture_rect)
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Paint event"""
|
||||
|
||||
# Convert click and current mouse positions to local space.
|
||||
mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())
|
||||
click_pos = None
|
||||
if self._click_pos is not None:
|
||||
click_pos = self.mapFromGlobal(self._click_pos)
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
|
||||
# Draw background. Aside from aesthetics, this makes the full
|
||||
# tool region accept mouse events.
|
||||
painter.setBrush(QtGui.QColor(0, 0, 0, self._opacity))
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.drawRect(event.rect())
|
||||
|
||||
# Clear the capture area
|
||||
if click_pos is not None:
|
||||
capture_rect = QtCore.QRect(click_pos, mouse_pos)
|
||||
painter.setCompositionMode(
|
||||
QtGui.QPainter.CompositionMode_Clear)
|
||||
painter.drawRect(capture_rect)
|
||||
painter.setCompositionMode(
|
||||
QtGui.QPainter.CompositionMode_SourceOver)
|
||||
|
||||
pen_color = QtGui.QColor(255, 255, 255, 64)
|
||||
pen = QtGui.QPen(pen_color, 1, QtCore.Qt.DotLine)
|
||||
painter.setPen(pen)
|
||||
|
||||
# Draw cropping markers at click position
|
||||
rect = event.rect()
|
||||
if click_pos is not None:
|
||||
painter.drawLine(
|
||||
rect.left(), click_pos.y(),
|
||||
rect.right(), click_pos.y()
|
||||
)
|
||||
painter.drawLine(
|
||||
click_pos.x(), rect.top(),
|
||||
click_pos.x(), rect.bottom()
|
||||
)
|
||||
|
||||
# Draw cropping markers at current mouse position
|
||||
painter.drawLine(
|
||||
rect.left(), mouse_pos.y(),
|
||||
rect.right(), mouse_pos.y()
|
||||
)
|
||||
painter.drawLine(
|
||||
mouse_pos.x(), rect.top(),
|
||||
mouse_pos.x(), rect.bottom()
|
||||
)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Mouse click event"""
|
||||
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
# Begin click drag operation
|
||||
self._click_pos = event.globalPos()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""Mouse release event"""
|
||||
if (
|
||||
self._click_pos is not None
|
||||
and event.button() == QtCore.Qt.LeftButton
|
||||
):
|
||||
# End click drag operation and commit the current capture rect
|
||||
self._capture_rect = QtCore.QRect(
|
||||
self._click_pos, event.globalPos()
|
||||
).normalized()
|
||||
self._click_pos = None
|
||||
self.close()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""Mouse move event"""
|
||||
self.repaint()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Mouse press event"""
|
||||
if event.key() == QtCore.Qt.Key_Escape:
|
||||
self._click_pos = None
|
||||
self._capture_rect = None
|
||||
self.close()
|
||||
return
|
||||
return super(ScreenMarquee, self).mousePressEvent(event)
|
||||
|
||||
def showEvent(self, event):
|
||||
self._fit_screen_geometry()
|
||||
self._fade_anim.start()
|
||||
|
||||
def _fit_screen_geometry(self):
|
||||
# Compute the union of all screen geometries, and resize to fit.
|
||||
workspace_rect = QtCore.QRect()
|
||||
for screen in QtWidgets.QApplication.screens():
|
||||
workspace_rect = workspace_rect.united(screen.geometry())
|
||||
self.setGeometry(workspace_rect)
|
||||
|
||||
def _on_fade_anim(self):
|
||||
"""Animation callback for opacity."""
|
||||
|
||||
self._opacity = self._fade_anim.currentValue()
|
||||
self.repaint()
|
||||
|
||||
def _on_screen_added(self):
|
||||
for screen in QtGui.QGuiApplication.screens():
|
||||
screen.geometryChanged.connect(self._fit_screen_geometry)
|
||||
|
||||
@classmethod
|
||||
def get_desktop_pixmap(cls, rect):
|
||||
"""Performs a screen capture on the specified rectangle.
|
||||
|
||||
Args:
|
||||
rect (QtCore.QRect): The rectangle to capture.
|
||||
|
||||
Returns:
|
||||
QtGui.QPixmap: Captured pixmap image
|
||||
"""
|
||||
|
||||
if rect.width() < 1 or rect.height() < 1:
|
||||
return QtGui.QPixmap()
|
||||
|
||||
screen_pixes = []
|
||||
for screen in QtWidgets.QApplication.screens():
|
||||
screen_geo = screen.geometry()
|
||||
if not screen_geo.intersects(rect):
|
||||
continue
|
||||
|
||||
screen_pix_rect = screen_geo.intersected(rect)
|
||||
screen_pix = screen.grabWindow(
|
||||
0,
|
||||
screen_pix_rect.x() - screen_geo.x(),
|
||||
screen_pix_rect.y() - screen_geo.y(),
|
||||
screen_pix_rect.width(), screen_pix_rect.height()
|
||||
)
|
||||
paste_point = QtCore.QPoint(
|
||||
screen_pix_rect.x() - rect.x(),
|
||||
screen_pix_rect.y() - rect.y()
|
||||
)
|
||||
screen_pixes.append((screen_pix, paste_point))
|
||||
|
||||
output_pix = QtGui.QPixmap(rect.width(), rect.height())
|
||||
output_pix.fill(QtCore.Qt.transparent)
|
||||
pix_painter = QtGui.QPainter()
|
||||
pix_painter.begin(output_pix)
|
||||
for item in screen_pixes:
|
||||
(screen_pix, offset) = item
|
||||
pix_painter.drawPixmap(offset, screen_pix)
|
||||
|
||||
pix_painter.end()
|
||||
|
||||
return output_pix
|
||||
|
||||
@classmethod
|
||||
def capture_to_pixmap(cls):
|
||||
"""Take screenshot with marquee into pixmap.
|
||||
|
||||
Note:
|
||||
The pixmap can be invalid (use 'isNull' to check).
|
||||
|
||||
Returns:
|
||||
QtGui.QPixmap: Captured pixmap image.
|
||||
"""
|
||||
|
||||
tool = cls()
|
||||
tool.exec_()
|
||||
return tool.get_captured_pixmap()
|
||||
|
||||
@classmethod
|
||||
def capture_to_file(cls, filepath=None):
|
||||
"""Take screenshot with marquee into file.
|
||||
|
||||
Args:
|
||||
filepath (Optional[str]): Path where screenshot will be saved.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to the saved screenshot, or None if user
|
||||
cancelled the operation.
|
||||
"""
|
||||
|
||||
pixmap = cls.capture_to_pixmap()
|
||||
if pixmap.isNull():
|
||||
return None
|
||||
|
||||
if filepath is None:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
prefix="screenshot_", suffix=".png", delete=False
|
||||
) as tmpfile:
|
||||
filepath = tmpfile.name
|
||||
|
||||
else:
|
||||
output_dir = os.path.dirname(filepath)
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
pixmap.save(filepath)
|
||||
return filepath
|
||||
|
||||
@classmethod
|
||||
def capture_to_clipboard(cls):
|
||||
"""Take screenshot with marquee into clipboard.
|
||||
|
||||
Notes:
|
||||
Screenshot is not in clipboard if user cancelled the operation.
|
||||
|
||||
Returns:
|
||||
bool: Screenshot was added to clipboard.
|
||||
"""
|
||||
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
pixmap = cls.capture_to_pixmap()
|
||||
if pixmap.isNull():
|
||||
return False
|
||||
image = pixmap.toImage()
|
||||
clipboard.setImage(image, QtGui.QClipboard.Clipboard)
|
||||
return True
|
||||
|
||||
|
||||
def capture_to_pixmap():
|
||||
"""Take screenshot with marquee into pixmap.
|
||||
|
||||
Note:
|
||||
The pixmap can be invalid (use 'isNull' to check).
|
||||
|
||||
Returns:
|
||||
QtGui.QPixmap: Captured pixmap image.
|
||||
"""
|
||||
|
||||
return ScreenMarquee.capture_to_pixmap()
|
||||
|
||||
|
||||
def capture_to_file(filepath=None):
|
||||
"""Take screenshot with marquee into file.
|
||||
|
||||
Args:
|
||||
filepath (Optional[str]): Path where screenshot will be saved.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to the saved screenshot, or None if user
|
||||
cancelled the operation.
|
||||
"""
|
||||
|
||||
return ScreenMarquee.capture_to_file(filepath)
|
||||
|
||||
|
||||
def capture_to_clipboard():
|
||||
"""Take screenshot with marquee into clipboard.
|
||||
|
||||
Notes:
|
||||
Screenshot is not in clipboard if user cancelled the operation.
|
||||
|
||||
Returns:
|
||||
bool: Screenshot was added to clipboard.
|
||||
"""
|
||||
|
||||
return ScreenMarquee.capture_to_clipboard()
|
||||
|
|
@ -22,6 +22,7 @@ from openpype.tools.utils import (
|
|||
from openpype.tools.publisher.control import CardMessageTypes
|
||||
|
||||
from .icons import get_image
|
||||
from .screenshot_widget import capture_to_file
|
||||
|
||||
|
||||
class ThumbnailPainterWidget(QtWidgets.QWidget):
|
||||
|
|
@ -306,20 +307,43 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
|
||||
thumbnail_painter = ThumbnailPainterWidget(self)
|
||||
|
||||
icon_color = get_objected_colors("bg-view-selection").get_qcolor()
|
||||
icon_color.setAlpha(255)
|
||||
|
||||
buttons_widget = QtWidgets.QWidget(self)
|
||||
buttons_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
icon_color = get_objected_colors("bg-view-selection").get_qcolor()
|
||||
icon_color.setAlpha(255)
|
||||
clear_image = get_image("clear_thumbnail")
|
||||
clear_pix = paint_image_with_color(clear_image, icon_color)
|
||||
|
||||
clear_button = PixmapButton(clear_pix, buttons_widget)
|
||||
clear_button.setObjectName("ThumbnailPixmapHoverButton")
|
||||
clear_button.setToolTip("Clear thumbnail")
|
||||
|
||||
take_screenshot_image = get_image("take_screenshot")
|
||||
take_screenshot_pix = paint_image_with_color(
|
||||
take_screenshot_image, icon_color)
|
||||
take_screenshot_btn = PixmapButton(
|
||||
take_screenshot_pix, buttons_widget)
|
||||
take_screenshot_btn.setObjectName("ThumbnailPixmapHoverButton")
|
||||
take_screenshot_btn.setToolTip("Take screenshot")
|
||||
|
||||
paste_image = get_image("paste")
|
||||
paste_pix = paint_image_with_color(paste_image, icon_color)
|
||||
paste_btn = PixmapButton(paste_pix, buttons_widget)
|
||||
paste_btn.setObjectName("ThumbnailPixmapHoverButton")
|
||||
paste_btn.setToolTip("Paste from clipboard")
|
||||
|
||||
browse_image = get_image("browse")
|
||||
browse_pix = paint_image_with_color(browse_image, icon_color)
|
||||
browse_btn = PixmapButton(browse_pix, buttons_widget)
|
||||
browse_btn.setObjectName("ThumbnailPixmapHoverButton")
|
||||
browse_btn.setToolTip("Browse...")
|
||||
|
||||
buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)
|
||||
buttons_layout.setContentsMargins(3, 3, 3, 3)
|
||||
buttons_layout.addStretch(1)
|
||||
buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
buttons_layout.addWidget(take_screenshot_btn, 0)
|
||||
buttons_layout.addWidget(paste_btn, 0)
|
||||
buttons_layout.addWidget(browse_btn, 0)
|
||||
buttons_layout.addWidget(clear_button, 0)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
|
|
@ -327,6 +351,9 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
layout.addWidget(thumbnail_painter)
|
||||
|
||||
clear_button.clicked.connect(self._on_clear_clicked)
|
||||
take_screenshot_btn.clicked.connect(self._on_take_screenshot)
|
||||
paste_btn.clicked.connect(self._on_paste_from_clipboard)
|
||||
browse_btn.clicked.connect(self._on_browse_clicked)
|
||||
|
||||
self._controller = controller
|
||||
self._output_dir = controller.get_thumbnail_temp_dir_path()
|
||||
|
|
@ -338,9 +365,16 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
self._adapted_to_size = True
|
||||
self._last_width = None
|
||||
self._last_height = None
|
||||
self._hide_on_finish = False
|
||||
|
||||
self._buttons_widget = buttons_widget
|
||||
self._thumbnail_painter = thumbnail_painter
|
||||
self._clear_button = clear_button
|
||||
self._take_screenshot_btn = take_screenshot_btn
|
||||
self._paste_btn = paste_btn
|
||||
self._browse_btn = browse_btn
|
||||
|
||||
clear_button.setEnabled(False)
|
||||
|
||||
@property
|
||||
def width_ratio(self):
|
||||
|
|
@ -430,13 +464,75 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
|
||||
self._thumbnail_painter.clear_cache()
|
||||
|
||||
def _set_current_thumbails(self, thumbnail_paths):
|
||||
self._thumbnail_painter.set_current_thumbnails(thumbnail_paths)
|
||||
self._update_buttons_position()
|
||||
|
||||
def set_current_thumbnails(self, thumbnail_paths=None):
|
||||
self._thumbnail_painter.set_current_thumbnails(thumbnail_paths)
|
||||
self._update_buttons_position()
|
||||
self._clear_button.setEnabled(self._thumbnail_painter.has_pixes)
|
||||
|
||||
def _on_clear_clicked(self):
|
||||
self.set_current_thumbnails()
|
||||
self.thumbnail_cleared.emit()
|
||||
self._clear_button.setEnabled(False)
|
||||
|
||||
def _on_take_screenshot(self):
|
||||
window = self.window()
|
||||
state = window.windowState()
|
||||
window.setWindowState(QtCore.Qt.WindowMinimized)
|
||||
output_path = os.path.join(
|
||||
self._output_dir, uuid.uuid4().hex + ".png")
|
||||
if capture_to_file(output_path):
|
||||
self.thumbnail_created.emit(output_path)
|
||||
# restore original window state
|
||||
window.setWindowState(state)
|
||||
|
||||
def _on_paste_from_clipboard(self):
|
||||
"""Set thumbnail from a pixmap image in the system clipboard"""
|
||||
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
pixmap = clipboard.pixmap()
|
||||
if pixmap.isNull():
|
||||
return
|
||||
|
||||
# Save as temporary file
|
||||
output_path = os.path.join(
|
||||
self._output_dir, uuid.uuid4().hex + ".png")
|
||||
|
||||
output_dir = os.path.dirname(output_path)
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
if pixmap.save(output_path):
|
||||
self.thumbnail_created.emit(output_path)
|
||||
|
||||
def _on_browse_clicked(self):
|
||||
ext_filter = "Source (*{0})".format(
|
||||
" *".join(self._review_extensions)
|
||||
)
|
||||
filepath, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self, "Choose thumbnail", os.path.expanduser("~"), ext_filter
|
||||
)
|
||||
if not filepath:
|
||||
return
|
||||
valid_path = False
|
||||
ext = os.path.splitext(filepath)[-1].lower()
|
||||
if ext in self._review_extensions:
|
||||
valid_path = True
|
||||
|
||||
output = None
|
||||
if valid_path:
|
||||
output = export_thumbnail(filepath, self._output_dir)
|
||||
|
||||
if output:
|
||||
self.thumbnail_created.emit(output)
|
||||
else:
|
||||
self._controller.emit_card_message(
|
||||
"Couldn't convert the source for thumbnail",
|
||||
CardMessageTypes.error
|
||||
)
|
||||
|
||||
def _adapt_to_size(self):
|
||||
if not self._adapted_to_size:
|
||||
|
|
@ -452,13 +548,25 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
self._thumbnail_painter.clear_cache()
|
||||
|
||||
def _update_buttons_position(self):
|
||||
self._buttons_widget.setVisible(self._thumbnail_painter.has_pixes)
|
||||
size = self.size()
|
||||
my_width = size.width()
|
||||
my_height = size.height()
|
||||
height = self._buttons_widget.sizeHint().height()
|
||||
buttons_sh = self._buttons_widget.sizeHint()
|
||||
buttons_height = buttons_sh.height()
|
||||
buttons_width = buttons_sh.width()
|
||||
pos_x = my_width - (buttons_width + 3)
|
||||
pos_y = my_height - (buttons_height + 3)
|
||||
if pos_x < 0:
|
||||
pos_x = 0
|
||||
buttons_width = my_width
|
||||
if pos_y < 0:
|
||||
pos_y = 0
|
||||
buttons_height = my_height
|
||||
self._buttons_widget.setGeometry(
|
||||
0, my_height - height,
|
||||
size.width(), height
|
||||
pos_x,
|
||||
pos_y,
|
||||
buttons_width,
|
||||
buttons_height
|
||||
)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class InventoryModel(TreeModel):
|
|||
self.remote_provider = remote_provider
|
||||
self._site_icons = {
|
||||
provider: QtGui.QIcon(icon_path)
|
||||
for provider, icon_path in self.get_site_icons().items()
|
||||
for provider, icon_path in sync_server.get_site_icons().items()
|
||||
}
|
||||
if "active_site" not in self.Columns:
|
||||
self.Columns.append("active_site")
|
||||
|
|
|
|||
|
|
@ -410,6 +410,18 @@ class PixmapButtonPainter(QtWidgets.QWidget):
|
|||
|
||||
self._pixmap = pixmap
|
||||
self._cached_pixmap = None
|
||||
self._disabled = False
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PixmapButtonPainter, self).resizeEvent(event)
|
||||
self._cached_pixmap = None
|
||||
self.repaint()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
if self._disabled != enabled:
|
||||
return
|
||||
self._disabled = not enabled
|
||||
self.repaint()
|
||||
|
||||
def set_pixmap(self, pixmap):
|
||||
self._pixmap = pixmap
|
||||
|
|
@ -444,6 +456,8 @@ class PixmapButtonPainter(QtWidgets.QWidget):
|
|||
if self._cached_pixmap is None:
|
||||
self._cache_pixmap()
|
||||
|
||||
if self._disabled:
|
||||
painter.setOpacity(0.5)
|
||||
painter.drawPixmap(0, 0, self._cached_pixmap)
|
||||
|
||||
painter.end()
|
||||
|
|
@ -464,6 +478,10 @@ class PixmapButton(ClickableFrame):
|
|||
layout.setContentsMargins(*args)
|
||||
self._update_painter_geo()
|
||||
|
||||
def setEnabled(self, enabled):
|
||||
self._button_painter.set_enabled(enabled)
|
||||
super(PixmapButton, self).setEnabled(enabled)
|
||||
|
||||
def set_pixmap(self, pixmap):
|
||||
self._button_painter.set_pixmap(pixmap)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ from ._api import (
|
|||
set_client_version,
|
||||
get_default_settings_variant,
|
||||
set_default_settings_variant,
|
||||
get_sender,
|
||||
set_sender,
|
||||
|
||||
get_base_url,
|
||||
get_rest_url,
|
||||
|
|
@ -92,6 +94,7 @@ from ._api import (
|
|||
get_users,
|
||||
|
||||
get_attributes_for_type,
|
||||
get_attributes_fields_for_type,
|
||||
get_default_fields_for_type,
|
||||
|
||||
get_project_anatomy_preset,
|
||||
|
|
@ -110,6 +113,11 @@ from ._api import (
|
|||
get_addons_project_settings,
|
||||
get_addons_settings,
|
||||
|
||||
get_secrets,
|
||||
get_secret,
|
||||
save_secret,
|
||||
delete_secret,
|
||||
|
||||
get_project_names,
|
||||
get_projects,
|
||||
get_project,
|
||||
|
|
@ -124,6 +132,8 @@ from ._api import (
|
|||
get_folders_hierarchy,
|
||||
|
||||
get_tasks,
|
||||
get_task_by_id,
|
||||
get_task_by_name,
|
||||
|
||||
get_folder_ids_with_products,
|
||||
get_product_by_id,
|
||||
|
|
@ -154,6 +164,7 @@ from ._api import (
|
|||
get_workfile_info,
|
||||
get_workfile_info_by_id,
|
||||
|
||||
get_thumbnail_by_id,
|
||||
get_thumbnail,
|
||||
get_folder_thumbnail,
|
||||
get_version_thumbnail,
|
||||
|
|
@ -216,6 +227,8 @@ __all__ = (
|
|||
"set_client_version",
|
||||
"get_default_settings_variant",
|
||||
"set_default_settings_variant",
|
||||
"get_sender",
|
||||
"set_sender",
|
||||
|
||||
"get_base_url",
|
||||
"get_rest_url",
|
||||
|
|
@ -278,6 +291,7 @@ __all__ = (
|
|||
"get_users",
|
||||
|
||||
"get_attributes_for_type",
|
||||
"get_attributes_fields_for_type",
|
||||
"get_default_fields_for_type",
|
||||
|
||||
"get_project_anatomy_preset",
|
||||
|
|
@ -295,6 +309,11 @@ __all__ = (
|
|||
"get_addons_project_settings",
|
||||
"get_addons_settings",
|
||||
|
||||
"get_secrets",
|
||||
"get_secret",
|
||||
"save_secret",
|
||||
"delete_secret",
|
||||
|
||||
"get_project_names",
|
||||
"get_projects",
|
||||
"get_project",
|
||||
|
|
@ -308,6 +327,8 @@ __all__ = (
|
|||
"get_folders",
|
||||
|
||||
"get_tasks",
|
||||
"get_task_by_id",
|
||||
"get_task_by_name",
|
||||
|
||||
"get_folder_ids_with_products",
|
||||
"get_product_by_id",
|
||||
|
|
@ -338,6 +359,7 @@ __all__ = (
|
|||
"get_workfile_info",
|
||||
"get_workfile_info_by_id",
|
||||
|
||||
"get_thumbnail_by_id",
|
||||
"get_thumbnail",
|
||||
"get_folder_thumbnail",
|
||||
"get_version_thumbnail",
|
||||
|
|
|
|||
62
openpype/vendor/python/common/ayon_api/_api.py
vendored
62
openpype/vendor/python/common/ayon_api/_api.py
vendored
|
|
@ -392,6 +392,28 @@ def set_default_settings_variant(variant):
|
|||
return con.set_default_settings_variant(variant)
|
||||
|
||||
|
||||
def get_sender():
|
||||
"""Sender used to send requests.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Sender name or None.
|
||||
"""
|
||||
|
||||
con = get_server_api_connection()
|
||||
return con.get_sender()
|
||||
|
||||
|
||||
def set_sender(sender):
|
||||
"""Change sender used for requests.
|
||||
|
||||
Args:
|
||||
sender (Union[str, None]): Sender name or None.
|
||||
"""
|
||||
|
||||
con = get_server_api_connection()
|
||||
return con.set_sender(sender)
|
||||
|
||||
|
||||
def get_base_url():
|
||||
con = get_server_api_connection()
|
||||
return con.get_base_url()
|
||||
|
|
@ -704,6 +726,26 @@ def get_addons_settings(*args, **kwargs):
|
|||
return con.get_addons_settings(*args, **kwargs)
|
||||
|
||||
|
||||
def get_secrets(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.get_secrets(*args, **kwargs)
|
||||
|
||||
|
||||
def get_secret(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.delete_secret(*args, **kwargs)
|
||||
|
||||
|
||||
def save_secret(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.delete_secret(*args, **kwargs)
|
||||
|
||||
|
||||
def delete_secret(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.delete_secret(*args, **kwargs)
|
||||
|
||||
|
||||
def get_project_names(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.get_project_names(*args, **kwargs)
|
||||
|
|
@ -734,6 +776,16 @@ def get_tasks(*args, **kwargs):
|
|||
return con.get_tasks(*args, **kwargs)
|
||||
|
||||
|
||||
def get_task_by_id(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.get_task_by_id(*args, **kwargs)
|
||||
|
||||
|
||||
def get_task_by_name(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.get_task_by_name(*args, **kwargs)
|
||||
|
||||
|
||||
def get_folder_by_id(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.get_folder_by_id(*args, **kwargs)
|
||||
|
|
@ -904,6 +956,11 @@ def delete_project(project_name):
|
|||
return con.delete_project(project_name)
|
||||
|
||||
|
||||
def get_thumbnail_by_id(project_name, thumbnail_id):
|
||||
con = get_server_api_connection()
|
||||
con.get_thumbnail_by_id(project_name, thumbnail_id)
|
||||
|
||||
|
||||
def get_thumbnail(project_name, entity_type, entity_id, thumbnail_id=None):
|
||||
con = get_server_api_connection()
|
||||
con.get_thumbnail(project_name, entity_type, entity_id, thumbnail_id)
|
||||
|
|
@ -934,6 +991,11 @@ def update_thumbnail(project_name, thumbnail_id, src_filepath):
|
|||
return con.update_thumbnail(project_name, thumbnail_id, src_filepath)
|
||||
|
||||
|
||||
def get_attributes_fields_for_type(entity_type):
|
||||
con = get_server_api_connection()
|
||||
return con.get_attributes_fields_for_type(entity_type)
|
||||
|
||||
|
||||
def get_default_fields_for_type(entity_type):
|
||||
con = get_server_api_connection()
|
||||
return con.get_default_fields_for_type(entity_type)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,25 @@ SERVER_API_ENV_KEY = "AYON_API_KEY"
|
|||
# Backwards compatibility
|
||||
SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY
|
||||
|
||||
# --- User ---
|
||||
DEFAULT_USER_FIELDS = {
|
||||
"roles",
|
||||
"name",
|
||||
"isService",
|
||||
"isManager",
|
||||
"isGuest",
|
||||
"isAdmin",
|
||||
"defaultRoles",
|
||||
"createdAt",
|
||||
"active",
|
||||
"hasPassword",
|
||||
"updatedAt",
|
||||
"apiKeyPreview",
|
||||
"attrib.avatarUrl",
|
||||
"attrib.email",
|
||||
"attrib.fullName",
|
||||
}
|
||||
|
||||
# --- Product types ---
|
||||
DEFAULT_PRODUCT_TYPE_FIELDS = {
|
||||
"name",
|
||||
|
|
|
|||
748
openpype/vendor/python/common/ayon_api/entity_hub.py
vendored
748
openpype/vendor/python/common/ayon_api/entity_hub.py
vendored
|
|
@ -1,10 +1,11 @@
|
|||
import re
|
||||
import copy
|
||||
import collections
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import six
|
||||
from ._api import get_server_api_connection
|
||||
from .utils import create_entity_id, convert_entity_id
|
||||
from .utils import create_entity_id, convert_entity_id, slugify_string
|
||||
|
||||
UNKNOWN_VALUE = object()
|
||||
PROJECT_PARENT_ID = object()
|
||||
|
|
@ -545,6 +546,7 @@ class EntityHub(object):
|
|||
library=project["library"],
|
||||
folder_types=project["folderTypes"],
|
||||
task_types=project["taskTypes"],
|
||||
statuses=project["statuses"],
|
||||
name=project["name"],
|
||||
attribs=project["ownAttrib"],
|
||||
data=project["data"],
|
||||
|
|
@ -775,8 +777,7 @@ class EntityHub(object):
|
|||
"projects/{}".format(self.project_name),
|
||||
**project_changes
|
||||
)
|
||||
if response.status_code != 204:
|
||||
raise ValueError("Failed to update project")
|
||||
response.raise_for_status()
|
||||
|
||||
self.project_entity.lock()
|
||||
|
||||
|
|
@ -1485,6 +1486,722 @@ class BaseEntity(object):
|
|||
self._children_ids = set(children_ids)
|
||||
|
||||
|
||||
class ProjectStatus:
|
||||
"""Project status class.
|
||||
|
||||
Args:
|
||||
name (str): Name of the status. e.g. 'In progress'
|
||||
short_name (Optional[str]): Short name of the status. e.g. 'IP'
|
||||
state (Optional[Literal[not_started, in_progress, done, blocked]]): A
|
||||
state of the status.
|
||||
icon (Optional[str]): Icon of the status. e.g. 'play_arrow'.
|
||||
color (Optional[str]): Color of the status. e.g. '#eeeeee'.
|
||||
index (Optional[int]): Index of the status.
|
||||
project_statuses (Optional[_ProjectStatuses]): Project statuses
|
||||
wrapper.
|
||||
"""
|
||||
|
||||
valid_states = ("not_started", "in_progress", "done", "blocked")
|
||||
color_regex = re.compile(r"#([a-f0-9]{6})$")
|
||||
default_state = "in_progress"
|
||||
default_color = "#eeeeee"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
short_name=None,
|
||||
state=None,
|
||||
icon=None,
|
||||
color=None,
|
||||
index=None,
|
||||
project_statuses=None,
|
||||
is_new=None,
|
||||
):
|
||||
short_name = short_name or ""
|
||||
icon = icon or ""
|
||||
state = state or self.default_state
|
||||
color = color or self.default_color
|
||||
self._name = name
|
||||
self._short_name = short_name
|
||||
self._icon = icon
|
||||
self._slugified_name = None
|
||||
self._state = None
|
||||
self._color = None
|
||||
self.set_state(state)
|
||||
self.set_color(color)
|
||||
|
||||
self._original_name = name
|
||||
self._original_short_name = short_name
|
||||
self._original_icon = icon
|
||||
self._original_state = state
|
||||
self._original_color = color
|
||||
self._original_index = index
|
||||
|
||||
self._index = index
|
||||
self._project_statuses = project_statuses
|
||||
if is_new is None:
|
||||
is_new = index is None or project_statuses is None
|
||||
self._is_new = is_new
|
||||
|
||||
def __str__(self):
|
||||
short_name = ""
|
||||
if self.short_name:
|
||||
short_name = "({})".format(self.short_name)
|
||||
return "<{} {}{}>".format(
|
||||
self.__class__.__name__, self.name, short_name
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in {
|
||||
"name", "short_name", "icon", "state", "color", "slugified_name"
|
||||
}:
|
||||
return getattr(self, key)
|
||||
raise KeyError(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in {"name", "short_name", "icon", "state", "color"}:
|
||||
return setattr(self, key, value)
|
||||
raise KeyError(key)
|
||||
|
||||
def lock(self):
|
||||
"""Lock status.
|
||||
|
||||
Changes were commited and current values are now the original values.
|
||||
"""
|
||||
|
||||
self._is_new = False
|
||||
self._original_name = self.name
|
||||
self._original_short_name = self.short_name
|
||||
self._original_icon = self.icon
|
||||
self._original_state = self.state
|
||||
self._original_color = self.color
|
||||
self._original_index = self.index
|
||||
|
||||
@staticmethod
|
||||
def slugify_name(name):
|
||||
"""Slugify status name for name comparison.
|
||||
|
||||
Args:
|
||||
name (str): Name of the status.
|
||||
|
||||
Returns:
|
||||
str: Slugified name.
|
||||
"""
|
||||
|
||||
return slugify_string(name.lower())
|
||||
|
||||
def get_project_statuses(self):
|
||||
"""Internal logic method.
|
||||
|
||||
Returns:
|
||||
_ProjectStatuses: Project statuses object.
|
||||
"""
|
||||
|
||||
return self._project_statuses
|
||||
|
||||
def set_project_statuses(self, project_statuses):
|
||||
"""Internal logic method to change parent object.
|
||||
|
||||
Args:
|
||||
project_statuses (_ProjectStatuses): Project statuses object.
|
||||
"""
|
||||
|
||||
self._project_statuses = project_statuses
|
||||
|
||||
def unset_project_statuses(self, project_statuses):
|
||||
"""Internal logic method to unset parent object.
|
||||
|
||||
Args:
|
||||
project_statuses (_ProjectStatuses): Project statuses object.
|
||||
"""
|
||||
|
||||
if self._project_statuses is project_statuses:
|
||||
self._project_statuses = None
|
||||
self._index = None
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
"""Status has changed.
|
||||
|
||||
Returns:
|
||||
bool: Status has changed.
|
||||
"""
|
||||
|
||||
return (
|
||||
self._is_new
|
||||
or self._original_name != self._name
|
||||
or self._original_short_name != self._short_name
|
||||
or self._original_index != self._index
|
||||
or self._original_state != self._state
|
||||
or self._original_icon != self._icon
|
||||
or self._original_color != self._color
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
"""Remove status from project statuses object."""
|
||||
|
||||
if self._project_statuses is not None:
|
||||
self._project_statuses.remove(self)
|
||||
|
||||
def get_index(self):
|
||||
"""Get index of status.
|
||||
|
||||
Returns:
|
||||
Union[int, None]: Index of status or None if status is not under
|
||||
project.
|
||||
"""
|
||||
|
||||
return self._index
|
||||
|
||||
def set_index(self, index, **kwargs):
|
||||
"""Change status index.
|
||||
|
||||
Returns:
|
||||
Union[int, None]: Index of status or None if status is not under
|
||||
project.
|
||||
"""
|
||||
|
||||
if kwargs.get("from_parent"):
|
||||
self._index = index
|
||||
else:
|
||||
self._project_statuses.set_status_index(self, index)
|
||||
|
||||
def get_name(self):
|
||||
"""Status name.
|
||||
|
||||
Returns:
|
||||
str: Status name.
|
||||
"""
|
||||
|
||||
return self._name
|
||||
|
||||
def set_name(self, name):
|
||||
"""Change status name.
|
||||
|
||||
Args:
|
||||
name (str): New status name.
|
||||
"""
|
||||
|
||||
if not isinstance(name, six.string_types):
|
||||
raise TypeError("Name must be a string.")
|
||||
if name == self._name:
|
||||
return
|
||||
self._name = name
|
||||
self._slugified_name = None
|
||||
|
||||
def get_short_name(self):
|
||||
"""Status short name 3 letters tops.
|
||||
|
||||
Returns:
|
||||
str: Status short name.
|
||||
"""
|
||||
|
||||
return self._short_name
|
||||
|
||||
def set_short_name(self, short_name):
|
||||
"""Change status short name.
|
||||
|
||||
Args:
|
||||
short_name (str): New status short name. 3 letters tops.
|
||||
"""
|
||||
|
||||
if not isinstance(short_name, six.string_types):
|
||||
raise TypeError("Short name must be a string.")
|
||||
self._short_name = short_name
|
||||
|
||||
def get_icon(self):
|
||||
"""Name of icon to use for status.
|
||||
|
||||
Returns:
|
||||
str: Name of the icon.
|
||||
"""
|
||||
|
||||
return self._icon
|
||||
|
||||
def set_icon(self, icon):
|
||||
"""Change status icon name.
|
||||
|
||||
Args:
|
||||
icon (str): Name of the icon.
|
||||
"""
|
||||
|
||||
if icon is None:
|
||||
icon = ""
|
||||
if not isinstance(icon, six.string_types):
|
||||
raise TypeError("Icon name must be a string.")
|
||||
self._icon = icon
|
||||
|
||||
@property
|
||||
def slugified_name(self):
|
||||
"""Slugified and lowere status name.
|
||||
|
||||
Can be used for comparison of existing statuses. e.g. 'In Progress'
|
||||
vs. 'in-progress'.
|
||||
|
||||
Returns:
|
||||
str: Slugified and lower status name.
|
||||
"""
|
||||
|
||||
if self._slugified_name is None:
|
||||
self._slugified_name = self.slugify_name(self.name)
|
||||
return self._slugified_name
|
||||
|
||||
def get_state(self):
|
||||
"""Get state of project status.
|
||||
|
||||
Return:
|
||||
Literal[not_started, in_progress, done, blocked]: General
|
||||
state of status.
|
||||
"""
|
||||
|
||||
return self._state
|
||||
|
||||
def set_state(self, state):
|
||||
"""Set color of project status.
|
||||
|
||||
Args:
|
||||
state (Literal[not_started, in_progress, done, blocked]): General
|
||||
state of status.
|
||||
"""
|
||||
|
||||
if state not in self.valid_states:
|
||||
raise ValueError("Invalid state '{}'".format(str(state)))
|
||||
self._state = state
|
||||
|
||||
def get_color(self):
|
||||
"""Get color of project status.
|
||||
|
||||
Returns:
|
||||
str: Status color.
|
||||
"""
|
||||
|
||||
return self._color
|
||||
|
||||
def set_color(self, color):
|
||||
"""Set color of project status.
|
||||
|
||||
Args:
|
||||
color (str): Color in hex format. Example: '#ff0000'.
|
||||
"""
|
||||
|
||||
if not isinstance(color, six.string_types):
|
||||
raise TypeError(
|
||||
"Color must be string got '{}'".format(type(color)))
|
||||
color = color.lower()
|
||||
if self.color_regex.fullmatch(color) is None:
|
||||
raise ValueError("Invalid color value '{}'".format(color))
|
||||
self._color = color
|
||||
|
||||
name = property(get_name, set_name)
|
||||
short_name = property(get_short_name, set_short_name)
|
||||
project_statuses = property(get_project_statuses, set_project_statuses)
|
||||
index = property(get_index, set_index)
|
||||
state = property(get_state, set_state)
|
||||
color = property(get_color, set_color)
|
||||
icon = property(get_icon, set_icon)
|
||||
|
||||
def _validate_other_p_statuses(self, other):
|
||||
"""Validate if other status can be used for move.
|
||||
|
||||
To be able to work with other status, and position them in relation,
|
||||
they must belong to same existing object of '_ProjectStatuses'.
|
||||
|
||||
Args:
|
||||
other (ProjectStatus): Other status to validate.
|
||||
"""
|
||||
|
||||
o_project_statuses = other.project_statuses
|
||||
m_project_statuses = self.project_statuses
|
||||
if o_project_statuses is None and m_project_statuses is None:
|
||||
raise ValueError("Both statuses are not assigned to a project.")
|
||||
|
||||
missing_status = None
|
||||
if o_project_statuses is None:
|
||||
missing_status = other
|
||||
elif m_project_statuses is None:
|
||||
missing_status = self
|
||||
if missing_status is not None:
|
||||
raise ValueError(
|
||||
"Status '{}' is not assigned to a project.".format(
|
||||
missing_status.name))
|
||||
if m_project_statuses is not o_project_statuses:
|
||||
raise ValueError(
|
||||
"Statuse are assigned to different projects."
|
||||
" Cannot execute move."
|
||||
)
|
||||
|
||||
def move_before(self, other):
|
||||
"""Move status before other status.
|
||||
|
||||
Args:
|
||||
other (ProjectStatus): Status to move before.
|
||||
"""
|
||||
|
||||
self._validate_other_p_statuses(other)
|
||||
self._project_statuses.set_status_index(self, other.index)
|
||||
|
||||
def move_after(self, other):
|
||||
"""Move status after other status.
|
||||
|
||||
Args:
|
||||
other (ProjectStatus): Status to move after.
|
||||
"""
|
||||
|
||||
self._validate_other_p_statuses(other)
|
||||
self._project_statuses.set_status_index(self, other.index + 1)
|
||||
|
||||
def to_data(self):
|
||||
"""Convert status to data.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Status data.
|
||||
"""
|
||||
|
||||
output = {
|
||||
"name": self.name,
|
||||
"shortName": self.short_name,
|
||||
"state": self.state,
|
||||
"icon": self.icon,
|
||||
"color": self.color,
|
||||
}
|
||||
if (
|
||||
not self._is_new
|
||||
and self._original_name
|
||||
and self.name != self._original_name
|
||||
):
|
||||
output["original_name"] = self._original_name
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data, index=None, project_statuses=None):
|
||||
"""Create project status from data.
|
||||
|
||||
Args:
|
||||
data (dict[str, str]): Status data.
|
||||
index (Optional[int]): Status index.
|
||||
project_statuses (Optional[ProjectStatuses]): Project statuses
|
||||
object which wraps the status for a project.
|
||||
"""
|
||||
|
||||
return cls(
|
||||
data["name"],
|
||||
data.get("shortName", data.get("short_name")),
|
||||
data.get("state"),
|
||||
data.get("icon"),
|
||||
data.get("color"),
|
||||
index=index,
|
||||
project_statuses=project_statuses
|
||||
)
|
||||
|
||||
|
||||
class _ProjectStatuses:
|
||||
"""Wrapper for project statuses.
|
||||
|
||||
Supports basic methods to add, change or remove statuses from a project.
|
||||
|
||||
To add new statuses use 'create' or 'add_status' methods. To change
|
||||
statuses receive them by one of the getter methods and change their
|
||||
values.
|
||||
|
||||
Todos:
|
||||
Validate if statuses are duplicated.
|
||||
"""
|
||||
|
||||
def __init__(self, statuses):
|
||||
self._statuses = [
|
||||
ProjectStatus.from_data(status, idx, self)
|
||||
for idx, status in enumerate(statuses)
|
||||
]
|
||||
self._orig_status_length = len(self._statuses)
|
||||
self._set_called = False
|
||||
|
||||
def __len__(self):
|
||||
return len(self._statuses)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over statuses.
|
||||
|
||||
Yields:
|
||||
ProjectStatus: Project status.
|
||||
"""
|
||||
|
||||
for status in self._statuses:
|
||||
yield status
|
||||
|
||||
def create(
|
||||
self,
|
||||
name,
|
||||
short_name=None,
|
||||
state=None,
|
||||
icon=None,
|
||||
color=None,
|
||||
):
|
||||
"""Create project status.
|
||||
|
||||
Args:
|
||||
name (str): Name of the status. e.g. 'In progress'
|
||||
short_name (Optional[str]): Short name of the status. e.g. 'IP'
|
||||
state (Optional[Literal[not_started, in_progress, done, blocked]]): A
|
||||
state of the status.
|
||||
icon (Optional[str]): Icon of the status. e.g. 'play_arrow'.
|
||||
color (Optional[str]): Color of the status. e.g. '#eeeeee'.
|
||||
|
||||
Returns:
|
||||
ProjectStatus: Created project status.
|
||||
"""
|
||||
|
||||
status = ProjectStatus(
|
||||
name, short_name, state, icon, color, is_new=True
|
||||
)
|
||||
self.append(status)
|
||||
return status
|
||||
|
||||
def lock(self):
|
||||
"""Lock statuses.
|
||||
|
||||
Changes were commited and current values are now the original values.
|
||||
"""
|
||||
|
||||
self._orig_status_length = len(self._statuses)
|
||||
self._set_called = False
|
||||
for status in self._statuses:
|
||||
status.lock()
|
||||
|
||||
def to_data(self):
|
||||
"""Convert to project statuses data."""
|
||||
|
||||
return [
|
||||
status.to_data()
|
||||
for status in self._statuses
|
||||
]
|
||||
|
||||
def set(self, statuses):
|
||||
"""Explicitly override statuses.
|
||||
|
||||
This method does not handle if statuses changed or not.
|
||||
|
||||
Args:
|
||||
statuses (list[dict[str, str]]): List of statuses data.
|
||||
"""
|
||||
|
||||
self._set_called = True
|
||||
self._statuses = [
|
||||
ProjectStatus.from_data(status, idx, self)
|
||||
for idx, status in enumerate(statuses)
|
||||
]
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
"""Statuses have changed.
|
||||
|
||||
Returns:
|
||||
bool: True if statuses changed, False otherwise.
|
||||
"""
|
||||
|
||||
if self._set_called:
|
||||
return True
|
||||
|
||||
# Check if status length changed
|
||||
# - when all statuses are removed it is a changed
|
||||
if self._orig_status_length != len(self._statuses):
|
||||
return True
|
||||
# Go through all statuses and check if any of them changed
|
||||
for status in self._statuses:
|
||||
if status.changed:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get(self, name, default=None):
|
||||
"""Get status by name.
|
||||
|
||||
Args:
|
||||
name (str): Status name.
|
||||
default (Any): Default value of status is not found.
|
||||
|
||||
Returns:
|
||||
Union[ProjectStatus, Any]: Status or default value.
|
||||
"""
|
||||
|
||||
return next(
|
||||
(
|
||||
status
|
||||
for status in self._statuses
|
||||
if status.name == name
|
||||
),
|
||||
default
|
||||
)
|
||||
|
||||
get_status_by_name = get
|
||||
|
||||
def index(self, status, **kwargs):
|
||||
"""Get status index.
|
||||
|
||||
Args:
|
||||
status (ProjectStatus): Status to get index of.
|
||||
default (Optional[Any]): Default value if status is not found.
|
||||
|
||||
Returns:
|
||||
Union[int, Any]: Status index.
|
||||
|
||||
Raises:
|
||||
ValueError: If status is not found and default value is not
|
||||
defined.
|
||||
"""
|
||||
|
||||
output = next(
|
||||
(
|
||||
idx
|
||||
for idx, st in enumerate(self._statuses)
|
||||
if st is status
|
||||
),
|
||||
None
|
||||
)
|
||||
if output is not None:
|
||||
return output
|
||||
|
||||
if "default" in kwargs:
|
||||
return kwargs["default"]
|
||||
raise ValueError("Status '{}' not found".format(status.name))
|
||||
|
||||
def get_status_by_slugified_name(self, name):
|
||||
"""Get status by slugified name.
|
||||
|
||||
Args:
|
||||
name (str): Status name. Is slugified before search.
|
||||
|
||||
Returns:
|
||||
Union[ProjectStatus, None]: Status or None if not found.
|
||||
"""
|
||||
|
||||
slugified_name = ProjectStatus.slugify_name(name)
|
||||
return next(
|
||||
(
|
||||
status
|
||||
for status in self._statuses
|
||||
if status.slugified_name == slugified_name
|
||||
),
|
||||
None
|
||||
)
|
||||
|
||||
def remove_by_name(self, name, ignore_missing=False):
|
||||
"""Remove status by name.
|
||||
|
||||
Args:
|
||||
name (str): Status name.
|
||||
ignore_missing (Optional[bool]): If True, no error is raised if
|
||||
status is not found.
|
||||
|
||||
Returns:
|
||||
ProjectStatus: Removed status.
|
||||
"""
|
||||
|
||||
matching_status = self.get(name)
|
||||
if matching_status is None:
|
||||
if ignore_missing:
|
||||
return
|
||||
raise ValueError(
|
||||
"Status '{}' not found in project".format(name))
|
||||
return self.remove(matching_status)
|
||||
|
||||
def remove(self, status, ignore_missing=False):
|
||||
"""Remove status.
|
||||
|
||||
Args:
|
||||
status (ProjectStatus): Status to remove.
|
||||
ignore_missing (Optional[bool]): If True, no error is raised if
|
||||
status is not found.
|
||||
|
||||
Returns:
|
||||
Union[ProjectStatus, None]: Removed status.
|
||||
"""
|
||||
|
||||
index = self.index(status, default=None)
|
||||
if index is None:
|
||||
if ignore_missing:
|
||||
return None
|
||||
raise ValueError("Status '{}' not in project".format(status))
|
||||
|
||||
return self.pop(index)
|
||||
|
||||
def pop(self, index):
|
||||
"""Remove status by index.
|
||||
|
||||
Args:
|
||||
index (int): Status index.
|
||||
|
||||
Returns:
|
||||
ProjectStatus: Removed status.
|
||||
"""
|
||||
|
||||
status = self._statuses.pop(index)
|
||||
status.unset_project_statuses(self)
|
||||
for st in self._statuses[index:]:
|
||||
st.set_index(st.index - 1, from_parent=True)
|
||||
return status
|
||||
|
||||
def insert(self, index, status):
|
||||
"""Insert status at index.
|
||||
|
||||
Args:
|
||||
index (int): Status index.
|
||||
status (Union[ProjectStatus, dict[str, str]]): Status to insert.
|
||||
Can be either status object or status data.
|
||||
|
||||
Returns:
|
||||
ProjectStatus: Inserted status.
|
||||
"""
|
||||
|
||||
if not isinstance(status, ProjectStatus):
|
||||
status = ProjectStatus.from_data(status)
|
||||
|
||||
start_index = index
|
||||
end_index = len(self._statuses) + 1
|
||||
matching_index = self.index(status, default=None)
|
||||
if matching_index is not None:
|
||||
if matching_index == index:
|
||||
status.set_index(index, from_parent=True)
|
||||
return
|
||||
|
||||
self._statuses.pop(matching_index)
|
||||
if matching_index < index:
|
||||
start_index = matching_index
|
||||
end_index = index + 1
|
||||
else:
|
||||
end_index -= 1
|
||||
|
||||
status.set_project_statuses(self)
|
||||
self._statuses.insert(index, status)
|
||||
for idx, st in enumerate(self._statuses[start_index:end_index]):
|
||||
st.set_index(start_index + idx, from_parent=True)
|
||||
return status
|
||||
|
||||
def append(self, status):
|
||||
"""Add new status to the end of the list.
|
||||
|
||||
Args:
|
||||
status (Union[ProjectStatus, dict[str, str]]): Status to insert.
|
||||
Can be either status object or status data.
|
||||
|
||||
Returns:
|
||||
ProjectStatus: Inserted status.
|
||||
"""
|
||||
|
||||
return self.insert(len(self._statuses), status)
|
||||
|
||||
def set_status_index(self, status, index):
|
||||
"""Set status index.
|
||||
|
||||
Args:
|
||||
status (ProjectStatus): Status to set index.
|
||||
index (int): New status index.
|
||||
"""
|
||||
|
||||
return self.insert(index, status)
|
||||
|
||||
|
||||
class ProjectEntity(BaseEntity):
|
||||
"""Entity representing project on AYON server.
|
||||
|
||||
|
|
@ -1514,7 +2231,14 @@ class ProjectEntity(BaseEntity):
|
|||
default_task_type_icon = "task_alt"
|
||||
|
||||
def __init__(
|
||||
self, project_code, library, folder_types, task_types, *args, **kwargs
|
||||
self,
|
||||
project_code,
|
||||
library,
|
||||
folder_types,
|
||||
task_types,
|
||||
statuses,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
super(ProjectEntity, self).__init__(*args, **kwargs)
|
||||
|
||||
|
|
@ -1522,11 +2246,13 @@ class ProjectEntity(BaseEntity):
|
|||
self._library_project = library
|
||||
self._folder_types = folder_types
|
||||
self._task_types = task_types
|
||||
self._statuses_obj = _ProjectStatuses(statuses)
|
||||
|
||||
self._orig_project_code = project_code
|
||||
self._orig_library_project = library
|
||||
self._orig_folder_types = copy.deepcopy(folder_types)
|
||||
self._orig_task_types = copy.deepcopy(task_types)
|
||||
self._orig_statuses = copy.deepcopy(statuses)
|
||||
|
||||
def _prepare_entity_id(self, entity_id):
|
||||
if entity_id != self.project_name:
|
||||
|
|
@ -1573,13 +2299,24 @@ class ProjectEntity(BaseEntity):
|
|||
new_task_types.append(task_type)
|
||||
self._task_types = new_task_types
|
||||
|
||||
def get_orig_statuses(self):
|
||||
return copy.deepcopy(self._orig_statuses)
|
||||
|
||||
def get_statuses(self):
|
||||
return self._statuses_obj
|
||||
|
||||
def set_statuses(self, statuses):
|
||||
self._statuses_obj.set(statuses)
|
||||
|
||||
folder_types = property(get_folder_types, set_folder_types)
|
||||
task_types = property(get_task_types, set_task_types)
|
||||
statuses = property(get_statuses, set_statuses)
|
||||
|
||||
def lock(self):
|
||||
super(ProjectEntity, self).lock()
|
||||
self._orig_folder_types = copy.deepcopy(self._folder_types)
|
||||
self._orig_task_types = copy.deepcopy(self._task_types)
|
||||
self._statuses_obj.lock()
|
||||
|
||||
@property
|
||||
def changes(self):
|
||||
|
|
@ -1590,6 +2327,9 @@ class ProjectEntity(BaseEntity):
|
|||
if self._orig_task_types != self._task_types:
|
||||
changes["taskTypes"] = self.get_task_types()
|
||||
|
||||
if self._statuses_obj.changed:
|
||||
changes["statuses"] = self._statuses_obj.to_data()
|
||||
|
||||
return changes
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -462,3 +462,28 @@ def events_graphql_query(fields):
|
|||
for k, v in value.items():
|
||||
query_queue.append((k, v, field))
|
||||
return query
|
||||
|
||||
|
||||
def users_graphql_query(fields):
|
||||
query = GraphQlQuery("Users")
|
||||
names_var = query.add_variable("userNames", "[String!]")
|
||||
|
||||
users_field = query.add_field_with_edges("users")
|
||||
users_field.set_filter("names", names_var)
|
||||
|
||||
nested_fields = fields_to_dict(set(fields))
|
||||
|
||||
query_queue = collections.deque()
|
||||
for key, value in nested_fields.items():
|
||||
query_queue.append((key, value, users_field))
|
||||
|
||||
while query_queue:
|
||||
item = query_queue.popleft()
|
||||
key, value, parent = item
|
||||
field = parent.add_field(key)
|
||||
if value is FIELD_VALUE:
|
||||
continue
|
||||
|
||||
for k, v in value.items():
|
||||
query_queue.append((k, v, field))
|
||||
return query
|
||||
|
|
|
|||
117
openpype/vendor/python/common/ayon_api/operations.py
vendored
117
openpype/vendor/python/common/ayon_api/operations.py
vendored
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import copy
|
||||
import collections
|
||||
import uuid
|
||||
|
|
@ -22,6 +23,8 @@ def new_folder_entity(
|
|||
name,
|
||||
folder_type,
|
||||
parent_id=None,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
data=None,
|
||||
thumbnail_id=None,
|
||||
|
|
@ -32,12 +35,14 @@ def new_folder_entity(
|
|||
Args:
|
||||
name (str): Is considered as unique identifier of folder in project.
|
||||
folder_type (str): Type of folder.
|
||||
parent_id (Optional[str]]): Id of parent folder.
|
||||
parent_id (Optional[str]): Parent folder id.
|
||||
status (Optional[str]): Product status.
|
||||
tags (Optional[List[str]]): List of tags.
|
||||
attribs (Optional[Dict[str, Any]]): Explicitly set attributes
|
||||
of folder.
|
||||
data (Optional[Dict[str, Any]]): Custom folder data. Empty dictionary
|
||||
is used if not passed.
|
||||
thumbnail_id (Optional[str]): Id of thumbnail related to folder.
|
||||
thumbnail_id (Optional[str]): Thumbnail id related to folder.
|
||||
entity_id (Optional[str]): Predefined id of entity. New id is
|
||||
created if not passed.
|
||||
|
||||
|
|
@ -54,7 +59,7 @@ def new_folder_entity(
|
|||
if parent_id is not None:
|
||||
parent_id = _create_or_convert_to_id(parent_id)
|
||||
|
||||
return {
|
||||
output = {
|
||||
"id": _create_or_convert_to_id(entity_id),
|
||||
"name": name,
|
||||
# This will be ignored
|
||||
|
|
@ -64,6 +69,11 @@ def new_folder_entity(
|
|||
"attrib": attribs,
|
||||
"thumbnailId": thumbnail_id
|
||||
}
|
||||
if status:
|
||||
output["status"] = status
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
return output
|
||||
|
||||
|
||||
def new_product_entity(
|
||||
|
|
@ -71,6 +81,7 @@ def new_product_entity(
|
|||
product_type,
|
||||
folder_id,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
data=None,
|
||||
entity_id=None
|
||||
|
|
@ -81,8 +92,9 @@ def new_product_entity(
|
|||
name (str): Is considered as unique identifier of
|
||||
product under folder.
|
||||
product_type (str): Product type.
|
||||
folder_id (str): Id of parent folder.
|
||||
folder_id (str): Parent folder id.
|
||||
status (Optional[str]): Product status.
|
||||
tags (Optional[List[str]]): List of tags.
|
||||
attribs (Optional[Dict[str, Any]]): Explicitly set attributes
|
||||
of product.
|
||||
data (Optional[Dict[str, Any]]): product entity data. Empty dictionary
|
||||
|
|
@ -110,6 +122,8 @@ def new_product_entity(
|
|||
}
|
||||
if status:
|
||||
output["status"] = status
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
return output
|
||||
|
||||
|
||||
|
|
@ -119,6 +133,8 @@ def new_version_entity(
|
|||
task_id=None,
|
||||
thumbnail_id=None,
|
||||
author=None,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
data=None,
|
||||
entity_id=None
|
||||
|
|
@ -128,10 +144,12 @@ def new_version_entity(
|
|||
Args:
|
||||
version (int): Is considered as unique identifier of version
|
||||
under product.
|
||||
product_id (str): Id of parent product.
|
||||
task_id (Optional[str]]): Id of task under which product was created.
|
||||
thumbnail_id (Optional[str]]): Thumbnail related to version.
|
||||
author (Optional[str]]): Name of version author.
|
||||
product_id (str): Parent product id.
|
||||
task_id (Optional[str]): Task id under which product was created.
|
||||
thumbnail_id (Optional[str]): Thumbnail related to version.
|
||||
author (Optional[str]): Name of version author.
|
||||
status (Optional[str]): Version status.
|
||||
tags (Optional[List[str]]): List of tags.
|
||||
attribs (Optional[Dict[str, Any]]): Explicitly set attributes
|
||||
of version.
|
||||
data (Optional[Dict[str, Any]]): Version entity custom data.
|
||||
|
|
@ -164,6 +182,10 @@ def new_version_entity(
|
|||
output["thumbnailId"] = thumbnail_id
|
||||
if author:
|
||||
output["author"] = author
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
if status:
|
||||
output["status"] = status
|
||||
return output
|
||||
|
||||
|
||||
|
|
@ -173,6 +195,8 @@ def new_hero_version_entity(
|
|||
task_id=None,
|
||||
thumbnail_id=None,
|
||||
author=None,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
data=None,
|
||||
entity_id=None
|
||||
|
|
@ -182,10 +206,12 @@ def new_hero_version_entity(
|
|||
Args:
|
||||
version (int): Is considered as unique identifier of version
|
||||
under product. Should be same as standard version if there is any.
|
||||
product_id (str): Id of parent product.
|
||||
task_id (Optional[str]): Id of task under which product was created.
|
||||
product_id (str): Parent product id.
|
||||
task_id (Optional[str]): Task id under which product was created.
|
||||
thumbnail_id (Optional[str]): Thumbnail related to version.
|
||||
author (Optional[str]): Name of version author.
|
||||
status (Optional[str]): Version status.
|
||||
tags (Optional[List[str]]): List of tags.
|
||||
attribs (Optional[Dict[str, Any]]): Explicitly set attributes
|
||||
of version.
|
||||
data (Optional[Dict[str, Any]]): Version entity data.
|
||||
|
|
@ -215,18 +241,32 @@ def new_hero_version_entity(
|
|||
output["thumbnailId"] = thumbnail_id
|
||||
if author:
|
||||
output["author"] = author
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
if status:
|
||||
output["status"] = status
|
||||
return output
|
||||
|
||||
|
||||
def new_representation_entity(
|
||||
name, version_id, attribs=None, data=None, entity_id=None
|
||||
name,
|
||||
version_id,
|
||||
files,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
data=None,
|
||||
entity_id=None
|
||||
):
|
||||
"""Create skeleton data of representation entity.
|
||||
|
||||
Args:
|
||||
name (str): Representation name considered as unique identifier
|
||||
of representation under version.
|
||||
version_id (str): Id of parent version.
|
||||
version_id (str): Parent version id.
|
||||
files (list[dict[str, str]]): List of files in representation.
|
||||
status (Optional[str]): Representation status.
|
||||
tags (Optional[List[str]]): List of tags.
|
||||
attribs (Optional[Dict[str, Any]]): Explicitly set attributes
|
||||
of representation.
|
||||
data (Optional[Dict[str, Any]]): Representation entity data.
|
||||
|
|
@ -243,27 +283,42 @@ def new_representation_entity(
|
|||
if data is None:
|
||||
data = {}
|
||||
|
||||
return {
|
||||
output = {
|
||||
"id": _create_or_convert_to_id(entity_id),
|
||||
"versionId": _create_or_convert_to_id(version_id),
|
||||
"files": files,
|
||||
"name": name,
|
||||
"data": data,
|
||||
"attrib": attribs
|
||||
}
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
if status:
|
||||
output["status"] = status
|
||||
return output
|
||||
|
||||
|
||||
def new_workfile_info_doc(
|
||||
filename, folder_id, task_name, files, data=None, entity_id=None
|
||||
def new_workfile_info(
|
||||
filepath,
|
||||
task_id,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
description=None,
|
||||
data=None,
|
||||
entity_id=None
|
||||
):
|
||||
"""Create skeleton data of workfile info entity.
|
||||
|
||||
Workfile entity is at this moment used primarily for artist notes.
|
||||
|
||||
Args:
|
||||
filename (str): Filename of workfile.
|
||||
folder_id (str): Id of folder under which workfile live.
|
||||
task_name (str): Task under which was workfile created.
|
||||
files (List[str]): List of rootless filepaths related to workfile.
|
||||
filepath (str): Rootless workfile filepath.
|
||||
task_id (str): Task under which was workfile created.
|
||||
status (Optional[str]): Workfile status.
|
||||
tags (Optional[List[str]]): Workfile tags.
|
||||
attribs (Options[dic[str, Any]]): Explicitly set attributes.
|
||||
description (Optional[str]): Workfile description.
|
||||
data (Optional[Dict[str, Any]]): Additional metadata.
|
||||
entity_id (Optional[str]): Predefined id of entity. New id is created
|
||||
if not passed.
|
||||
|
|
@ -272,17 +327,31 @@ def new_workfile_info_doc(
|
|||
Dict[str, Any]: Skeleton of workfile info entity.
|
||||
"""
|
||||
|
||||
if attribs is None:
|
||||
attribs = {}
|
||||
|
||||
if "extension" not in attribs:
|
||||
attribs["extension"] = os.path.splitext(filepath)[-1]
|
||||
|
||||
if description:
|
||||
attribs["description"] = description
|
||||
|
||||
if not data:
|
||||
data = {}
|
||||
|
||||
return {
|
||||
output = {
|
||||
"id": _create_or_convert_to_id(entity_id),
|
||||
"parent": _create_or_convert_to_id(folder_id),
|
||||
"task_name": task_name,
|
||||
"filename": filename,
|
||||
"taskId": task_id,
|
||||
"path": filepath,
|
||||
"data": data,
|
||||
"files": files
|
||||
"attrib": attribs
|
||||
}
|
||||
if status:
|
||||
output["status"] = status
|
||||
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
return output
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
|
|
|
|||
416
openpype/vendor/python/common/ayon_api/server_api.py
vendored
416
openpype/vendor/python/common/ayon_api/server_api.py
vendored
|
|
@ -14,7 +14,16 @@ except ImportError:
|
|||
HTTPStatus = None
|
||||
|
||||
import requests
|
||||
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
|
||||
try:
|
||||
# This should be used if 'requests' have it available
|
||||
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
|
||||
except ImportError:
|
||||
# Older versions of 'requests' don't have custom exception for json
|
||||
# decode error
|
||||
try:
|
||||
from simplejson import JSONDecodeError as RequestsJSONDecodeError
|
||||
except ImportError:
|
||||
from json import JSONDecodeError as RequestsJSONDecodeError
|
||||
|
||||
from .constants import (
|
||||
DEFAULT_PRODUCT_TYPE_FIELDS,
|
||||
|
|
@ -27,8 +36,8 @@ from .constants import (
|
|||
REPRESENTATION_FILES_FIELDS,
|
||||
DEFAULT_WORKFILE_INFO_FIELDS,
|
||||
DEFAULT_EVENT_FIELDS,
|
||||
DEFAULT_USER_FIELDS,
|
||||
)
|
||||
from .thumbnails import ThumbnailCache
|
||||
from .graphql import GraphQlQuery, INTROSPECTION_QUERY
|
||||
from .graphql_queries import (
|
||||
project_graphql_query,
|
||||
|
|
@ -43,6 +52,7 @@ from .graphql_queries import (
|
|||
representations_parents_qraphql_query,
|
||||
workfiles_info_graphql_query,
|
||||
events_graphql_query,
|
||||
users_graphql_query,
|
||||
)
|
||||
from .exceptions import (
|
||||
FailedOperations,
|
||||
|
|
@ -61,6 +71,7 @@ from .utils import (
|
|||
failed_json_default,
|
||||
TransferProgress,
|
||||
create_dependency_package_basename,
|
||||
ThumbnailContent,
|
||||
)
|
||||
|
||||
PatternType = type(re.compile(""))
|
||||
|
|
@ -319,6 +330,8 @@ class ServerAPI(object):
|
|||
default_settings_variant (Optional[Literal["production", "staging"]]):
|
||||
Settings variant used by default if a method for settings won't
|
||||
get any (by default is 'production').
|
||||
sender (Optional[str]): Sender of requests. Used in server logs and
|
||||
propagated into events.
|
||||
ssl_verify (Union[bool, str, None]): Verify SSL certificate
|
||||
Looks for env variable value 'AYON_CA_FILE' by default. If not
|
||||
available then 'True' is used.
|
||||
|
|
@ -335,6 +348,7 @@ class ServerAPI(object):
|
|||
site_id=None,
|
||||
client_version=None,
|
||||
default_settings_variant=None,
|
||||
sender=None,
|
||||
ssl_verify=None,
|
||||
cert=None,
|
||||
create_session=True,
|
||||
|
|
@ -354,6 +368,7 @@ class ServerAPI(object):
|
|||
default_settings_variant
|
||||
or "production"
|
||||
)
|
||||
self._sender = sender
|
||||
|
||||
if ssl_verify is None:
|
||||
# Custom AYON env variable for CA file or 'True'
|
||||
|
|
@ -390,7 +405,6 @@ class ServerAPI(object):
|
|||
self._entity_type_attributes_cache = {}
|
||||
|
||||
self._as_user_stack = _AsUserStack()
|
||||
self._thumbnail_cache = ThumbnailCache(True)
|
||||
|
||||
# Create session
|
||||
if self._access_token and create_session:
|
||||
|
|
@ -559,6 +573,29 @@ class ServerAPI(object):
|
|||
set_default_settings_variant
|
||||
)
|
||||
|
||||
def get_sender(self):
|
||||
"""Sender used to send requests.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Sender name or None.
|
||||
"""
|
||||
|
||||
return self._sender
|
||||
|
||||
def set_sender(self, sender):
|
||||
"""Change sender used for requests.
|
||||
|
||||
Args:
|
||||
sender (Union[str, None]): Sender name or None.
|
||||
"""
|
||||
|
||||
if sender == self._sender:
|
||||
return
|
||||
self._sender = sender
|
||||
self._update_session_headers()
|
||||
|
||||
sender = property(get_sender, set_sender)
|
||||
|
||||
def get_default_service_username(self):
|
||||
"""Default username used for callbacks when used with service API key.
|
||||
|
||||
|
|
@ -742,6 +779,7 @@ class ServerAPI(object):
|
|||
("X-as-user", self._as_user_stack.username),
|
||||
("x-ayon-version", self._client_version),
|
||||
("x-ayon-site-id", self._site_id),
|
||||
("x-sender", self._sender),
|
||||
):
|
||||
if value is not None:
|
||||
self._session.headers[key] = value
|
||||
|
|
@ -826,10 +864,36 @@ class ServerAPI(object):
|
|||
self._access_token_is_service = None
|
||||
return None
|
||||
|
||||
def get_users(self):
|
||||
# TODO how to find out if user have permission?
|
||||
users = self.get("users")
|
||||
return users.data
|
||||
def get_users(self, usernames=None, fields=None):
|
||||
"""Get Users.
|
||||
|
||||
Args:
|
||||
usernames (Optional[Iterable[str]]): Filter by usernames.
|
||||
fields (Optional[Iterable[str]]): fields to be queried
|
||||
for users.
|
||||
|
||||
Returns:
|
||||
Generator[dict[str, Any]]: Queried users.
|
||||
"""
|
||||
|
||||
filters = {}
|
||||
if usernames is not None:
|
||||
usernames = set(usernames)
|
||||
if not usernames:
|
||||
return
|
||||
filters["userNames"] = list(usernames)
|
||||
|
||||
if not fields:
|
||||
fields = self.get_default_fields_for_type("user")
|
||||
|
||||
query = users_graphql_query(set(fields))
|
||||
for attr, filter_value in filters.items():
|
||||
query.set_variable_value(attr, filter_value)
|
||||
|
||||
for parsed_data in query.continuous_query(self):
|
||||
for user in parsed_data["users"]:
|
||||
user["roles"] = json.loads(user["roles"])
|
||||
yield user
|
||||
|
||||
def get_user(self, username=None):
|
||||
output = None
|
||||
|
|
@ -859,6 +923,9 @@ class ServerAPI(object):
|
|||
if self._client_version is not None:
|
||||
headers["x-ayon-version"] = self._client_version
|
||||
|
||||
if self._sender is not None:
|
||||
headers["x-sender"] = self._sender
|
||||
|
||||
if self._access_token:
|
||||
if self._access_token_is_service:
|
||||
headers["X-Api-Key"] = self._access_token
|
||||
|
|
@ -900,18 +967,24 @@ class ServerAPI(object):
|
|||
|
||||
self.validate_server_availability()
|
||||
|
||||
response = self.post(
|
||||
"auth/login",
|
||||
name=username,
|
||||
password=password
|
||||
)
|
||||
if response.status_code != 200:
|
||||
_detail = response.data.get("detail")
|
||||
details = ""
|
||||
if _detail:
|
||||
details = " {}".format(_detail)
|
||||
self._token_validation_started = True
|
||||
|
||||
raise AuthenticationError("Login failed {}".format(details))
|
||||
try:
|
||||
response = self.post(
|
||||
"auth/login",
|
||||
name=username,
|
||||
password=password
|
||||
)
|
||||
if response.status_code != 200:
|
||||
_detail = response.data.get("detail")
|
||||
details = ""
|
||||
if _detail:
|
||||
details = " {}".format(_detail)
|
||||
|
||||
raise AuthenticationError("Login failed {}".format(details))
|
||||
|
||||
finally:
|
||||
self._token_validation_started = False
|
||||
|
||||
self._access_token = response["token"]
|
||||
|
||||
|
|
@ -1127,7 +1200,7 @@ class ServerAPI(object):
|
|||
filters["includeLogsFilter"] = include_logs
|
||||
|
||||
if not fields:
|
||||
fields = DEFAULT_EVENT_FIELDS
|
||||
fields = self.get_default_fields_for_type("event")
|
||||
|
||||
query = events_graphql_query(set(fields))
|
||||
for attr, filter_value in filters.items():
|
||||
|
|
@ -1228,7 +1301,8 @@ class ServerAPI(object):
|
|||
target_topic,
|
||||
sender,
|
||||
description=None,
|
||||
sequential=None
|
||||
sequential=None,
|
||||
events_filter=None,
|
||||
):
|
||||
"""Enroll job based on events.
|
||||
|
||||
|
|
@ -1270,6 +1344,8 @@ class ServerAPI(object):
|
|||
in target event.
|
||||
sequential (Optional[bool]): The source topic must be processed
|
||||
in sequence.
|
||||
events_filter (Optional[ayon_server.sqlfilter.Filter]): A dict-like
|
||||
with conditions to filter the source event.
|
||||
|
||||
Returns:
|
||||
Union[None, dict[str, Any]]: None if there is no event matching
|
||||
|
|
@ -1285,6 +1361,8 @@ class ServerAPI(object):
|
|||
kwargs["sequential"] = sequential
|
||||
if description is not None:
|
||||
kwargs["description"] = description
|
||||
if events_filter is not None:
|
||||
kwargs["filter"] = events_filter
|
||||
response = self.post("enroll", **kwargs)
|
||||
if response.status_code == 204:
|
||||
return None
|
||||
|
|
@ -1612,6 +1690,19 @@ class ServerAPI(object):
|
|||
|
||||
return copy.deepcopy(attributes)
|
||||
|
||||
def get_attributes_fields_for_type(self, entity_type):
|
||||
"""Prepare attribute fields for entity type.
|
||||
|
||||
Returns:
|
||||
set[str]: Attributes fields for entity type.
|
||||
"""
|
||||
|
||||
attributes = self.get_attributes_for_type(entity_type)
|
||||
return {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
|
||||
def get_default_fields_for_type(self, entity_type):
|
||||
"""Default fields for entity type.
|
||||
|
||||
|
|
@ -1624,51 +1715,46 @@ class ServerAPI(object):
|
|||
set[str]: Fields that should be queried from server.
|
||||
"""
|
||||
|
||||
attributes = self.get_attributes_for_type(entity_type)
|
||||
# Event does not have attributes
|
||||
if entity_type == "event":
|
||||
return set(DEFAULT_EVENT_FIELDS)
|
||||
|
||||
if entity_type == "project":
|
||||
return DEFAULT_PROJECT_FIELDS | {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
entity_type_defaults = DEFAULT_PROJECT_FIELDS
|
||||
|
||||
if entity_type == "folder":
|
||||
return DEFAULT_FOLDER_FIELDS | {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
elif entity_type == "folder":
|
||||
entity_type_defaults = DEFAULT_FOLDER_FIELDS
|
||||
|
||||
if entity_type == "task":
|
||||
return DEFAULT_TASK_FIELDS | {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
elif entity_type == "task":
|
||||
entity_type_defaults = DEFAULT_TASK_FIELDS
|
||||
|
||||
if entity_type == "product":
|
||||
return DEFAULT_PRODUCT_FIELDS | {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
elif entity_type == "product":
|
||||
entity_type_defaults = DEFAULT_PRODUCT_FIELDS
|
||||
|
||||
if entity_type == "version":
|
||||
return DEFAULT_VERSION_FIELDS | {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
elif entity_type == "version":
|
||||
entity_type_defaults = DEFAULT_VERSION_FIELDS
|
||||
|
||||
if entity_type == "representation":
|
||||
return (
|
||||
elif entity_type == "representation":
|
||||
entity_type_defaults = (
|
||||
DEFAULT_REPRESENTATION_FIELDS
|
||||
| REPRESENTATION_FILES_FIELDS
|
||||
| {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
)
|
||||
|
||||
if entity_type == "productType":
|
||||
return DEFAULT_PRODUCT_TYPE_FIELDS
|
||||
elif entity_type == "productType":
|
||||
entity_type_defaults = DEFAULT_PRODUCT_TYPE_FIELDS
|
||||
|
||||
raise ValueError("Unknown entity type \"{}\"".format(entity_type))
|
||||
elif entity_type == "workfile":
|
||||
entity_type_defaults = DEFAULT_WORKFILE_INFO_FIELDS
|
||||
|
||||
elif entity_type == "user":
|
||||
entity_type_defaults = DEFAULT_USER_FIELDS
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown entity type \"{}\"".format(entity_type))
|
||||
return (
|
||||
entity_type_defaults
|
||||
| self.get_attributes_fields_for_type(entity_type)
|
||||
)
|
||||
|
||||
def get_addons_info(self, details=True):
|
||||
"""Get information about addons available on server.
|
||||
|
|
@ -2926,6 +3012,79 @@ class ServerAPI(object):
|
|||
only_values=only_values
|
||||
)
|
||||
|
||||
def get_secrets(self):
|
||||
"""Get all secrets.
|
||||
|
||||
Example output:
|
||||
[
|
||||
{
|
||||
"name": "secret_1",
|
||||
"value": "secret_value_1",
|
||||
},
|
||||
{
|
||||
"name": "secret_2",
|
||||
"value": "secret_value_2",
|
||||
}
|
||||
]
|
||||
|
||||
Returns:
|
||||
list[dict[str, str]]: List of secret entities.
|
||||
"""
|
||||
|
||||
response = self.get("secrets")
|
||||
response.raise_for_status()
|
||||
return response.data
|
||||
|
||||
def get_secret(self, secret_name):
|
||||
"""Get secret by name.
|
||||
|
||||
Example output:
|
||||
{
|
||||
"name": "secret_name",
|
||||
"value": "secret_value",
|
||||
}
|
||||
|
||||
Args:
|
||||
secret_name (str): Name of secret.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Secret entity data.
|
||||
"""
|
||||
|
||||
response = self.get("secrets/{}".format(secret_name))
|
||||
response.raise_for_status()
|
||||
return response.data
|
||||
|
||||
def save_secret(self, secret_name, secret_value):
|
||||
"""Save secret.
|
||||
|
||||
This endpoint can create and update secret.
|
||||
|
||||
Args:
|
||||
secret_name (str): Name of secret.
|
||||
secret_value (str): Value of secret.
|
||||
"""
|
||||
|
||||
response = self.put(
|
||||
"secrets/{}".format(secret_name),
|
||||
name=secret_name,
|
||||
value=secret_value,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.data
|
||||
|
||||
|
||||
def delete_secret(self, secret_name):
|
||||
"""Delete secret by name.
|
||||
|
||||
Args:
|
||||
secret_name (str): Name of secret to delete.
|
||||
"""
|
||||
|
||||
response = self.delete("secrets/{}".format(secret_name))
|
||||
response.raise_for_status()
|
||||
return response.data
|
||||
|
||||
# Entity getters
|
||||
def get_rest_project(self, project_name):
|
||||
"""Query project by name.
|
||||
|
|
@ -3070,8 +3229,6 @@ class ServerAPI(object):
|
|||
else:
|
||||
use_rest = False
|
||||
fields = set(fields)
|
||||
if own_attributes:
|
||||
fields.add("ownAttrib")
|
||||
for field in fields:
|
||||
if field.startswith("config"):
|
||||
use_rest = True
|
||||
|
|
@ -3084,6 +3241,13 @@ class ServerAPI(object):
|
|||
yield project
|
||||
|
||||
else:
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("project")
|
||||
|
||||
if own_attributes:
|
||||
fields.add("ownAttrib")
|
||||
|
||||
query = projects_graphql_query(fields)
|
||||
for parsed_data in query.continuous_query(self):
|
||||
for project in parsed_data["projects"]:
|
||||
|
|
@ -3124,8 +3288,12 @@ class ServerAPI(object):
|
|||
fill_own_attribs(project)
|
||||
return project
|
||||
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("project")
|
||||
|
||||
if own_attributes:
|
||||
field.add("ownAttrib")
|
||||
fields.add("ownAttrib")
|
||||
query = project_graphql_query(fields)
|
||||
query.set_variable_value("projectName", project_name)
|
||||
|
||||
|
|
@ -3282,10 +3450,13 @@ class ServerAPI(object):
|
|||
|
||||
filters["parentFolderIds"] = list(parent_ids)
|
||||
|
||||
if fields:
|
||||
fields = set(fields)
|
||||
else:
|
||||
if not fields:
|
||||
fields = self.get_default_fields_for_type("folder")
|
||||
else:
|
||||
fields = set(fields)
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("folder")
|
||||
|
||||
use_rest = False
|
||||
if "data" in fields:
|
||||
|
|
@ -3519,8 +3690,11 @@ class ServerAPI(object):
|
|||
|
||||
if not fields:
|
||||
fields = self.get_default_fields_for_type("task")
|
||||
|
||||
fields = set(fields)
|
||||
else:
|
||||
fields = set(fields)
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("task")
|
||||
|
||||
use_rest = False
|
||||
if "data" in fields:
|
||||
|
|
@ -3705,6 +3879,9 @@ class ServerAPI(object):
|
|||
# Convert fields and add minimum required fields
|
||||
if fields:
|
||||
fields = set(fields) | {"id"}
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("folder")
|
||||
else:
|
||||
fields = self.get_default_fields_for_type("product")
|
||||
|
||||
|
|
@ -3961,7 +4138,11 @@ class ServerAPI(object):
|
|||
|
||||
if not fields:
|
||||
fields = self.get_default_fields_for_type("version")
|
||||
fields = set(fields)
|
||||
else:
|
||||
fields = set(fields)
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("version")
|
||||
|
||||
if active is not None:
|
||||
fields.add("active")
|
||||
|
|
@ -4419,7 +4600,11 @@ class ServerAPI(object):
|
|||
|
||||
if not fields:
|
||||
fields = self.get_default_fields_for_type("representation")
|
||||
fields = set(fields)
|
||||
else:
|
||||
fields = set(fields)
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("representation")
|
||||
|
||||
use_rest = False
|
||||
if "data" in fields:
|
||||
|
|
@ -4765,8 +4950,15 @@ class ServerAPI(object):
|
|||
filters["workfileIds"] = list(workfile_ids)
|
||||
|
||||
if not fields:
|
||||
fields = DEFAULT_WORKFILE_INFO_FIELDS
|
||||
fields = self.get_default_fields_for_type("workfile")
|
||||
|
||||
fields = set(fields)
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in self.get_attributes_for_type("workfile")
|
||||
}
|
||||
if own_attributes:
|
||||
fields.add("ownAttrib")
|
||||
|
||||
|
|
@ -4843,18 +5035,61 @@ class ServerAPI(object):
|
|||
return workfile_info
|
||||
return None
|
||||
|
||||
def _prepare_thumbnail_content(self, project_name, response):
|
||||
content = None
|
||||
content_type = response.content_type
|
||||
|
||||
# It is expected the response contains thumbnail id otherwise the
|
||||
# content cannot be cached and filepath returned
|
||||
thumbnail_id = response.headers.get("X-Thumbnail-Id")
|
||||
if thumbnail_id is not None:
|
||||
content = response.content
|
||||
|
||||
return ThumbnailContent(
|
||||
project_name, thumbnail_id, content, content_type
|
||||
)
|
||||
|
||||
def get_thumbnail_by_id(self, project_name, thumbnail_id):
|
||||
"""Get thumbnail from server by id.
|
||||
|
||||
Permissions of thumbnails are related to entities so thumbnails must
|
||||
be queried per entity. So an entity type and entity type is required
|
||||
to be passed.
|
||||
|
||||
Notes:
|
||||
It is recommended to use one of prepared entity type specific
|
||||
methods 'get_folder_thumbnail', 'get_version_thumbnail' or
|
||||
'get_workfile_thumbnail'.
|
||||
We do recommend pass thumbnail id if you have access to it. Each
|
||||
entity that allows thumbnails has 'thumbnailId' field, so it
|
||||
can be queried.
|
||||
|
||||
Args:
|
||||
project_name (str): Project under which the entity is located.
|
||||
thumbnail_id (Optional[str]): DEPRECATED Use
|
||||
'get_thumbnail_by_id'.
|
||||
|
||||
Returns:
|
||||
ThumbnailContent: Thumbnail content wrapper. Does not have to be
|
||||
valid.
|
||||
"""
|
||||
|
||||
response = self.raw_get(
|
||||
"projects/{}/thumbnails/{}".format(
|
||||
project_name,
|
||||
thumbnail_id
|
||||
)
|
||||
)
|
||||
return self._prepare_thumbnail_content(project_name, response)
|
||||
|
||||
def get_thumbnail(
|
||||
self, project_name, entity_type, entity_id, thumbnail_id=None
|
||||
):
|
||||
"""Get thumbnail from server.
|
||||
|
||||
Permissions of thumbnails are related to entities so thumbnails must be
|
||||
queried per entity. So an entity type and entity type is required to
|
||||
be passed.
|
||||
|
||||
If thumbnail id is passed logic can look into locally cached thumbnails
|
||||
before calling server which can enhance loading time. If thumbnail id
|
||||
is not passed the thumbnail is always downloaded even if is available.
|
||||
Permissions of thumbnails are related to entities so thumbnails must
|
||||
be queried per entity. So an entity type and entity type is required
|
||||
to be passed.
|
||||
|
||||
Notes:
|
||||
It is recommended to use one of prepared entity type specific
|
||||
|
|
@ -4868,20 +5103,16 @@ class ServerAPI(object):
|
|||
project_name (str): Project under which the entity is located.
|
||||
entity_type (str): Entity type which passed entity id represents.
|
||||
entity_id (str): Entity id for which thumbnail should be returned.
|
||||
thumbnail_id (Optional[str]): Prepared thumbnail id from entity.
|
||||
Used only to check if thumbnail was already cached.
|
||||
thumbnail_id (Optional[str]): DEPRECATED Use
|
||||
'get_thumbnail_by_id'.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to downloaded thumbnail or none if entity
|
||||
does not have any (or if user does not have permissions).
|
||||
ThumbnailContent: Thumbnail content wrapper. Does not have to be
|
||||
valid.
|
||||
"""
|
||||
|
||||
# Look for thumbnail into cache and return the path if was found
|
||||
filepath = self._thumbnail_cache.get_thumbnail_filepath(
|
||||
project_name, thumbnail_id
|
||||
)
|
||||
if filepath:
|
||||
return filepath
|
||||
if thumbnail_id:
|
||||
return self.get_thumbnail_by_id(project_name, thumbnail_id)
|
||||
|
||||
if entity_type in (
|
||||
"folder",
|
||||
|
|
@ -4890,29 +5121,12 @@ class ServerAPI(object):
|
|||
):
|
||||
entity_type += "s"
|
||||
|
||||
# Receive thumbnail content from server
|
||||
result = self.raw_get("projects/{}/{}/{}/thumbnail".format(
|
||||
response = self.raw_get("projects/{}/{}/{}/thumbnail".format(
|
||||
project_name,
|
||||
entity_type,
|
||||
entity_id
|
||||
))
|
||||
|
||||
if result.content_type is None:
|
||||
return None
|
||||
|
||||
# It is expected the response contains thumbnail id otherwise the
|
||||
# content cannot be cached and filepath returned
|
||||
thumbnail_id = result.headers.get("X-Thumbnail-Id")
|
||||
if thumbnail_id is None:
|
||||
return None
|
||||
|
||||
# Cache thumbnail and return path
|
||||
return self._thumbnail_cache.store_thumbnail(
|
||||
project_name,
|
||||
thumbnail_id,
|
||||
result.content,
|
||||
result.content_type
|
||||
)
|
||||
return self._prepare_thumbnail_content(project_name, response)
|
||||
|
||||
def get_folder_thumbnail(
|
||||
self, project_name, folder_id, thumbnail_id=None
|
||||
|
|
|
|||
219
openpype/vendor/python/common/ayon_api/thumbnails.py
vendored
219
openpype/vendor/python/common/ayon_api/thumbnails.py
vendored
|
|
@ -1,219 +0,0 @@
|
|||
import os
|
||||
import time
|
||||
import collections
|
||||
|
||||
import appdirs
|
||||
|
||||
FileInfo = collections.namedtuple(
|
||||
"FileInfo",
|
||||
("path", "size", "modification_time")
|
||||
)
|
||||
|
||||
|
||||
class ThumbnailCache:
|
||||
"""Cache of thumbnails on local storage.
|
||||
|
||||
Thumbnails are cached to appdirs to predefined directory. Each project has
|
||||
own subfolder with thumbnails -> that's because each project has own
|
||||
thumbnail id validation and file names are thumbnail ids with matching
|
||||
extension. Extensions are predefined (.png and .jpeg).
|
||||
|
||||
Cache has cleanup mechanism which is triggered on initialized by default.
|
||||
|
||||
The cleanup has 2 levels:
|
||||
1. soft cleanup which remove all files that are older then 'days_alive'
|
||||
2. max size cleanup which remove all files until the thumbnails folder
|
||||
contains less then 'max_filesize'
|
||||
- this is time consuming so it's not triggered automatically
|
||||
|
||||
Args:
|
||||
cleanup (bool): Trigger soft cleanup (Cleanup expired thumbnails).
|
||||
"""
|
||||
|
||||
# Lifetime of thumbnails (in seconds)
|
||||
# - default 3 days
|
||||
days_alive = 3 * 24 * 60 * 60
|
||||
# Max size of thumbnail directory (in bytes)
|
||||
# - default 2 Gb
|
||||
max_filesize = 2 * 1024 * 1024 * 1024
|
||||
|
||||
def __init__(self, cleanup=True):
|
||||
self._thumbnails_dir = None
|
||||
if cleanup:
|
||||
self.cleanup()
|
||||
|
||||
def get_thumbnails_dir(self):
|
||||
"""Root directory where thumbnails are stored.
|
||||
|
||||
Returns:
|
||||
str: Path to thumbnails root.
|
||||
"""
|
||||
|
||||
if self._thumbnails_dir is None:
|
||||
directory = appdirs.user_data_dir("ayon", "ynput")
|
||||
self._thumbnails_dir = os.path.join(directory, "thumbnails")
|
||||
return self._thumbnails_dir
|
||||
|
||||
thumbnails_dir = property(get_thumbnails_dir)
|
||||
|
||||
def get_thumbnails_dir_file_info(self):
|
||||
"""Get information about all files in thumbnails directory.
|
||||
|
||||
Returns:
|
||||
List[FileInfo]: List of file information about all files.
|
||||
"""
|
||||
|
||||
thumbnails_dir = self.thumbnails_dir
|
||||
files_info = []
|
||||
if not os.path.exists(thumbnails_dir):
|
||||
return files_info
|
||||
|
||||
for root, _, filenames in os.walk(thumbnails_dir):
|
||||
for filename in filenames:
|
||||
path = os.path.join(root, filename)
|
||||
files_info.append(FileInfo(
|
||||
path, os.path.getsize(path), os.path.getmtime(path)
|
||||
))
|
||||
return files_info
|
||||
|
||||
def get_thumbnails_dir_size(self, files_info=None):
|
||||
"""Got full size of thumbnail directory.
|
||||
|
||||
Args:
|
||||
files_info (List[FileInfo]): Prepared file information about
|
||||
files in thumbnail directory.
|
||||
|
||||
Returns:
|
||||
int: File size of all files in thumbnail directory.
|
||||
"""
|
||||
|
||||
if files_info is None:
|
||||
files_info = self.get_thumbnails_dir_file_info()
|
||||
|
||||
if not files_info:
|
||||
return 0
|
||||
|
||||
return sum(
|
||||
file_info.size
|
||||
for file_info in files_info
|
||||
)
|
||||
|
||||
def cleanup(self, check_max_size=False):
|
||||
"""Cleanup thumbnails directory.
|
||||
|
||||
Args:
|
||||
check_max_size (bool): Also cleanup files to match max size of
|
||||
thumbnails directory.
|
||||
"""
|
||||
|
||||
thumbnails_dir = self.get_thumbnails_dir()
|
||||
# Skip if thumbnails dir does not exists yet
|
||||
if not os.path.exists(thumbnails_dir):
|
||||
return
|
||||
|
||||
self._soft_cleanup(thumbnails_dir)
|
||||
if check_max_size:
|
||||
self._max_size_cleanup(thumbnails_dir)
|
||||
|
||||
def _soft_cleanup(self, thumbnails_dir):
|
||||
current_time = time.time()
|
||||
for root, _, filenames in os.walk(thumbnails_dir):
|
||||
for filename in filenames:
|
||||
path = os.path.join(root, filename)
|
||||
modification_time = os.path.getmtime(path)
|
||||
if current_time - modification_time > self.days_alive:
|
||||
os.remove(path)
|
||||
|
||||
def _max_size_cleanup(self, thumbnails_dir):
|
||||
files_info = self.get_thumbnails_dir_file_info()
|
||||
size = self.get_thumbnails_dir_size(files_info)
|
||||
if size < self.max_filesize:
|
||||
return
|
||||
|
||||
sorted_file_info = collections.deque(
|
||||
sorted(files_info, key=lambda item: item.modification_time)
|
||||
)
|
||||
diff = size - self.max_filesize
|
||||
while diff > 0:
|
||||
if not sorted_file_info:
|
||||
break
|
||||
|
||||
file_info = sorted_file_info.popleft()
|
||||
diff -= file_info.size
|
||||
os.remove(file_info.path)
|
||||
|
||||
def get_thumbnail_filepath(self, project_name, thumbnail_id):
|
||||
"""Get thumbnail by thumbnail id.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project.
|
||||
thumbnail_id (str): Thumbnail id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to thumbnail image or None if thumbnail
|
||||
is not cached yet.
|
||||
"""
|
||||
|
||||
if not thumbnail_id:
|
||||
return None
|
||||
|
||||
for ext in (
|
||||
".png",
|
||||
".jpeg",
|
||||
):
|
||||
filepath = os.path.join(
|
||||
self.thumbnails_dir, project_name, thumbnail_id + ext
|
||||
)
|
||||
if os.path.exists(filepath):
|
||||
return filepath
|
||||
return None
|
||||
|
||||
def get_project_dir(self, project_name):
|
||||
"""Path to root directory for specific project.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project for which root directory path
|
||||
should be returned.
|
||||
|
||||
Returns:
|
||||
str: Path to root of project's thumbnails.
|
||||
"""
|
||||
|
||||
return os.path.join(self.thumbnails_dir, project_name)
|
||||
|
||||
def make_sure_project_dir_exists(self, project_name):
|
||||
project_dir = self.get_project_dir(project_name)
|
||||
if not os.path.exists(project_dir):
|
||||
os.makedirs(project_dir)
|
||||
return project_dir
|
||||
|
||||
def store_thumbnail(self, project_name, thumbnail_id, content, mime_type):
|
||||
"""Store thumbnail to cache folder.
|
||||
|
||||
Args:
|
||||
project_name (str): Project where the thumbnail belong to.
|
||||
thumbnail_id (str): Id of thumbnail.
|
||||
content (bytes): Byte content of thumbnail file.
|
||||
mime_data (str): Type of content.
|
||||
|
||||
Returns:
|
||||
str: Path to cached thumbnail image file.
|
||||
"""
|
||||
|
||||
if mime_type == "image/png":
|
||||
ext = ".png"
|
||||
elif mime_type == "image/jpeg":
|
||||
ext = ".jpeg"
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unknown mime type for thumbnail \"{}\"".format(mime_type))
|
||||
|
||||
project_dir = self.make_sure_project_dir_exists(project_name)
|
||||
thumbnail_path = os.path.join(project_dir, thumbnail_id + ext)
|
||||
with open(thumbnail_path, "wb") as stream:
|
||||
stream.write(content)
|
||||
|
||||
current_time = time.time()
|
||||
os.utime(thumbnail_path, (current_time, current_time))
|
||||
|
||||
return thumbnail_path
|
||||
39
openpype/vendor/python/common/ayon_api/utils.py
vendored
39
openpype/vendor/python/common/ayon_api/utils.py
vendored
|
|
@ -27,6 +27,45 @@ RepresentationParents = collections.namedtuple(
|
|||
)
|
||||
|
||||
|
||||
class ThumbnailContent:
|
||||
"""Wrapper for thumbnail content.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
thumbnail_id (Union[str, None]): Thumbnail id.
|
||||
content_type (Union[str, None]): Content type e.g. 'image/png'.
|
||||
content (Union[bytes, None]): Thumbnail content.
|
||||
"""
|
||||
|
||||
def __init__(self, project_name, thumbnail_id, content, content_type):
|
||||
self.project_name = project_name
|
||||
self.thumbnail_id = thumbnail_id
|
||||
self.content_type = content_type
|
||||
self.content = content or b""
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Wrapper for thumbnail id.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
|
||||
return self.thumbnail_id
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
"""Content of thumbnail is valid.
|
||||
|
||||
Returns:
|
||||
bool: Content is valid and can be used.
|
||||
"""
|
||||
return (
|
||||
self.thumbnail_id is not None
|
||||
and self.content_type is not None
|
||||
)
|
||||
|
||||
|
||||
def prepare_query_string(key_values):
|
||||
"""Prepare data to query string.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
"""Package declaring Python API for Ayon server."""
|
||||
__version__ = "0.3.3"
|
||||
__version__ = "0.3.5"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.16.4-nightly.1"
|
||||
__version__ = "3.16.4-nightly.2"
|
||||
|
|
|
|||
|
|
@ -127,9 +127,7 @@
|
|||
"linux": []
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [
|
||||
"-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms"
|
||||
],
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
|
|
|
|||
17
server_addon/max/server/__init__.py
Normal file
17
server_addon/max/server/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from typing import Type
|
||||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import MaxSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class MaxAddon(BaseServerAddon):
|
||||
name = "max"
|
||||
title = "Max"
|
||||
version = __version__
|
||||
settings_model: Type[MaxSettings] = MaxSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
return settings_model_cls(**DEFAULT_VALUES)
|
||||
10
server_addon/max/server/settings/__init__.py
Normal file
10
server_addon/max/server/settings/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from .main import (
|
||||
MaxSettings,
|
||||
DEFAULT_VALUES,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"MaxSettings",
|
||||
"DEFAULT_VALUES",
|
||||
)
|
||||
48
server_addon/max/server/settings/imageio.py
Normal file
48
server_addon/max/server/settings/imageio.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from pydantic import Field, validator
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
from ayon_server.settings.validators import ensure_unique_names
|
||||
|
||||
|
||||
class ImageIOConfigModel(BaseSettingsModel):
|
||||
override_global_config: bool = Field(
|
||||
False,
|
||||
title="Override global OCIO config"
|
||||
)
|
||||
filepath: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="Config path"
|
||||
)
|
||||
|
||||
|
||||
class ImageIOFileRuleModel(BaseSettingsModel):
|
||||
name: str = Field("", title="Rule name")
|
||||
pattern: str = Field("", title="Regex pattern")
|
||||
colorspace: str = Field("", title="Colorspace name")
|
||||
ext: str = Field("", title="File extension")
|
||||
|
||||
|
||||
class ImageIOFileRulesModel(BaseSettingsModel):
|
||||
activate_host_rules: bool = Field(False)
|
||||
rules: list[ImageIOFileRuleModel] = Field(
|
||||
default_factory=list,
|
||||
title="Rules"
|
||||
)
|
||||
|
||||
@validator("rules")
|
||||
def validate_unique_outputs(cls, value):
|
||||
ensure_unique_names(value)
|
||||
return value
|
||||
|
||||
|
||||
class ImageIOSettings(BaseSettingsModel):
|
||||
activate_host_color_management: bool = Field(
|
||||
True, title="Enable Color Management"
|
||||
)
|
||||
ocio_config: ImageIOConfigModel = Field(
|
||||
default_factory=ImageIOConfigModel,
|
||||
title="OCIO config"
|
||||
)
|
||||
file_rules: ImageIOFileRulesModel = Field(
|
||||
default_factory=ImageIOFileRulesModel,
|
||||
title="File Rules"
|
||||
)
|
||||
60
server_addon/max/server/settings/main.py
Normal file
60
server_addon/max/server/settings/main.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
from pydantic import Field
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
from .imageio import ImageIOSettings
|
||||
from .render_settings import (
|
||||
RenderSettingsModel, DEFAULT_RENDER_SETTINGS
|
||||
)
|
||||
from .publishers import (
|
||||
PublishersModel, DEFAULT_PUBLISH_SETTINGS
|
||||
)
|
||||
|
||||
|
||||
class PRTAttributesModel(BaseSettingsModel):
|
||||
_layout = "compact"
|
||||
name: str = Field(title="Name")
|
||||
value: str = Field(title="Attribute")
|
||||
|
||||
|
||||
class PointCloudSettings(BaseSettingsModel):
|
||||
attribute: list[PRTAttributesModel] = Field(
|
||||
default_factory=list, title="Channel Attribute")
|
||||
|
||||
|
||||
class MaxSettings(BaseSettingsModel):
|
||||
imageio: ImageIOSettings = Field(
|
||||
default_factory=ImageIOSettings,
|
||||
title="Color Management (ImageIO)"
|
||||
)
|
||||
RenderSettings: RenderSettingsModel = Field(
|
||||
default_factory=RenderSettingsModel,
|
||||
title="Render Settings"
|
||||
)
|
||||
PointCloud: PointCloudSettings = Field(
|
||||
default_factory=PointCloudSettings,
|
||||
title="Point Cloud"
|
||||
)
|
||||
publish: PublishersModel = Field(
|
||||
default_factory=PublishersModel,
|
||||
title="Publish Plugins")
|
||||
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
"RenderSettings": DEFAULT_RENDER_SETTINGS,
|
||||
"PointCloud": {
|
||||
"attribute": [
|
||||
{"name": "Age", "value": "age"},
|
||||
{"name": "Radius", "value": "radius"},
|
||||
{"name": "Position", "value": "position"},
|
||||
{"name": "Rotation", "value": "rotation"},
|
||||
{"name": "Scale", "value": "scale"},
|
||||
{"name": "Velocity", "value": "velocity"},
|
||||
{"name": "Color", "value": "color"},
|
||||
{"name": "TextureCoordinate", "value": "texcoord"},
|
||||
{"name": "MaterialID", "value": "matid"},
|
||||
{"name": "custFloats", "value": "custFloats"},
|
||||
{"name": "custVecs", "value": "custVecs"},
|
||||
]
|
||||
},
|
||||
"publish": DEFAULT_PUBLISH_SETTINGS
|
||||
|
||||
}
|
||||
26
server_addon/max/server/settings/publishers.py
Normal file
26
server_addon/max/server/settings/publishers.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from pydantic import Field
|
||||
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
|
||||
|
||||
class BasicValidateModel(BaseSettingsModel):
|
||||
enabled: bool = Field(title="Enabled")
|
||||
optional: bool = Field(title="Optional")
|
||||
active: bool = Field(title="Active")
|
||||
|
||||
|
||||
class PublishersModel(BaseSettingsModel):
|
||||
ValidateFrameRange: BasicValidateModel = Field(
|
||||
default_factory=BasicValidateModel,
|
||||
title="Validate Frame Range",
|
||||
section="Validators"
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_PUBLISH_SETTINGS = {
|
||||
"ValidateFrameRange": {
|
||||
"enabled": True,
|
||||
"optional": True,
|
||||
"active": True
|
||||
}
|
||||
}
|
||||
49
server_addon/max/server/settings/render_settings.py
Normal file
49
server_addon/max/server/settings/render_settings.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from pydantic import Field
|
||||
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
|
||||
|
||||
def aov_separators_enum():
|
||||
return [
|
||||
{"value": "dash", "label": "- (dash)"},
|
||||
{"value": "underscore", "label": "_ (underscore)"},
|
||||
{"value": "dot", "label": ". (dot)"}
|
||||
]
|
||||
|
||||
|
||||
def image_format_enum():
|
||||
"""Return enumerator for image output formats."""
|
||||
return [
|
||||
{"label": "bmp", "value": "bmp"},
|
||||
{"label": "exr", "value": "exr"},
|
||||
{"label": "tif", "value": "tif"},
|
||||
{"label": "tiff", "value": "tiff"},
|
||||
{"label": "jpg", "value": "jpg"},
|
||||
{"label": "png", "value": "png"},
|
||||
{"label": "tga", "value": "tga"},
|
||||
{"label": "dds", "value": "dds"}
|
||||
]
|
||||
|
||||
|
||||
class RenderSettingsModel(BaseSettingsModel):
|
||||
default_render_image_folder: str = Field(
|
||||
title="Default render image folder"
|
||||
)
|
||||
aov_separator: str = Field(
|
||||
"underscore",
|
||||
title="AOV Separator character",
|
||||
enum_resolver=aov_separators_enum
|
||||
)
|
||||
image_format: str = Field(
|
||||
enum_resolver=image_format_enum,
|
||||
title="Output Image Format"
|
||||
)
|
||||
multipass: bool = Field(title="multipass")
|
||||
|
||||
|
||||
DEFAULT_RENDER_SETTINGS = {
|
||||
"default_render_image_folder": "renders/3dsmax",
|
||||
"aov_separator": "underscore",
|
||||
"image_format": "png",
|
||||
"multipass": True
|
||||
}
|
||||
1
server_addon/max/server/version.py
Normal file
1
server_addon/max/server/version.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
__version__ = "0.1.0"
|
||||
Loading…
Add table
Add a link
Reference in a new issue