mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge branch 'develop' into enhancement/web_action_action_to_create_folder_structure
This commit is contained in:
commit
be8d654d89
15 changed files with 135 additions and 44 deletions
3
.github/workflows/pr_linting.yml
vendored
3
.github/workflows/pr_linting.yml
vendored
|
|
@ -21,6 +21,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: astral-sh/ruff-action@v1
|
- uses: astral-sh/ruff-action@v3
|
||||||
with:
|
with:
|
||||||
changed-files: "true"
|
changed-files: "true"
|
||||||
|
version-file: "pyproject.toml"
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -77,6 +77,7 @@ dump.sql
|
||||||
# Poetry
|
# Poetry
|
||||||
########
|
########
|
||||||
.poetry/
|
.poetry/
|
||||||
|
poetry.lock
|
||||||
.python-version
|
.python-version
|
||||||
.editorconfig
|
.editorconfig
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ from .workfile import (
|
||||||
get_workdir,
|
get_workdir,
|
||||||
get_custom_workfile_template_by_string_context,
|
get_custom_workfile_template_by_string_context,
|
||||||
get_workfile_template_key_from_context,
|
get_workfile_template_key_from_context,
|
||||||
get_last_workfile
|
get_last_workfile,
|
||||||
|
MissingWorkdirError,
|
||||||
)
|
)
|
||||||
from . import (
|
from . import (
|
||||||
register_loader_plugin_path,
|
register_loader_plugin_path,
|
||||||
|
|
@ -251,7 +252,7 @@ def uninstall_host():
|
||||||
pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)
|
pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)
|
||||||
deregister_loader_plugin_path(LOAD_PATH)
|
deregister_loader_plugin_path(LOAD_PATH)
|
||||||
deregister_inventory_action_path(INVENTORY_PATH)
|
deregister_inventory_action_path(INVENTORY_PATH)
|
||||||
log.info("Global plug-ins unregistred")
|
log.info("Global plug-ins unregistered")
|
||||||
|
|
||||||
deregister_host()
|
deregister_host()
|
||||||
|
|
||||||
|
|
@ -617,7 +618,18 @@ def version_up_current_workfile():
|
||||||
last_workfile_path = get_last_workfile(
|
last_workfile_path = get_last_workfile(
|
||||||
work_root, file_template, data, extensions, True
|
work_root, file_template, data, extensions, True
|
||||||
)
|
)
|
||||||
new_workfile_path = version_up(last_workfile_path)
|
# `get_last_workfile` will return the first expected file version
|
||||||
|
# if no files exist yet. In that case, if they do not exist we will
|
||||||
|
# want to save v001
|
||||||
|
new_workfile_path = last_workfile_path
|
||||||
if os.path.exists(new_workfile_path):
|
if os.path.exists(new_workfile_path):
|
||||||
new_workfile_path = version_up(new_workfile_path)
|
new_workfile_path = version_up(new_workfile_path)
|
||||||
|
|
||||||
|
# Raise an error if the parent folder doesn't exist as `host.save_workfile`
|
||||||
|
# is not supposed/able to create missing folders.
|
||||||
|
parent_folder = os.path.dirname(new_workfile_path)
|
||||||
|
if not os.path.exists(parent_folder):
|
||||||
|
raise MissingWorkdirError(
|
||||||
|
f"Work area directory '{parent_folder}' does not exist.")
|
||||||
|
|
||||||
host.save_workfile(new_workfile_path)
|
host.save_workfile(new_workfile_path)
|
||||||
|
|
|
||||||
|
|
@ -2303,10 +2303,16 @@ class CreateContext:
|
||||||
for plugin_name, plugin_value in item_changes.pop(
|
for plugin_name, plugin_value in item_changes.pop(
|
||||||
"publish_attributes"
|
"publish_attributes"
|
||||||
).items():
|
).items():
|
||||||
|
if plugin_value is None:
|
||||||
|
current_publish[plugin_name] = None
|
||||||
|
continue
|
||||||
plugin_changes = current_publish.setdefault(
|
plugin_changes = current_publish.setdefault(
|
||||||
plugin_name, {}
|
plugin_name, {}
|
||||||
)
|
)
|
||||||
plugin_changes.update(plugin_value)
|
if plugin_changes is None:
|
||||||
|
current_publish[plugin_name] = plugin_value
|
||||||
|
else:
|
||||||
|
plugin_changes.update(plugin_value)
|
||||||
|
|
||||||
item_values.update(item_changes)
|
item_values.update(item_changes)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,29 +160,26 @@ class AttributeValues:
|
||||||
return self._attr_defs_by_key.get(key, default)
|
return self._attr_defs_by_key.get(key, default)
|
||||||
|
|
||||||
def update(self, value):
|
def update(self, value):
|
||||||
changes = {}
|
changes = self._update(value)
|
||||||
for _key, _value in dict(value).items():
|
|
||||||
if _key in self._data and self._data.get(_key) == _value:
|
|
||||||
continue
|
|
||||||
self._data[_key] = _value
|
|
||||||
changes[_key] = _value
|
|
||||||
|
|
||||||
if changes:
|
if changes:
|
||||||
self._parent.attribute_value_changed(self._key, changes)
|
self._parent.attribute_value_changed(self._key, changes)
|
||||||
|
|
||||||
def pop(self, key, default=None):
|
def pop(self, key, default=None):
|
||||||
has_key = key in self._data
|
value, changes = self._pop(key, default)
|
||||||
value = self._data.pop(key, default)
|
if changes:
|
||||||
# Remove attribute definition if is 'UnknownDef'
|
self._parent.attribute_value_changed(self._key, changes)
|
||||||
# - gives option to get rid of unknown values
|
|
||||||
attr_def = self._attr_defs_by_key.get(key)
|
|
||||||
if isinstance(attr_def, UnknownDef):
|
|
||||||
self._attr_defs_by_key.pop(key)
|
|
||||||
self._attr_defs.remove(attr_def)
|
|
||||||
elif has_key:
|
|
||||||
self._parent.attribute_value_changed(self._key, {key: None})
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def set_value(self, value):
|
||||||
|
pop_keys = set(value.keys()) - set(self._data.keys())
|
||||||
|
changes = self._update(value)
|
||||||
|
for key in pop_keys:
|
||||||
|
_, key_changes = self._pop(key, None)
|
||||||
|
changes.update(key_changes)
|
||||||
|
|
||||||
|
if changes:
|
||||||
|
self._parent.attribute_value_changed(self._key, changes)
|
||||||
|
|
||||||
def reset_values(self):
|
def reset_values(self):
|
||||||
self._data = {}
|
self._data = {}
|
||||||
|
|
||||||
|
|
@ -228,6 +225,29 @@ class AttributeValues:
|
||||||
|
|
||||||
return serialize_attr_defs(self._attr_defs)
|
return serialize_attr_defs(self._attr_defs)
|
||||||
|
|
||||||
|
def _update(self, value):
|
||||||
|
changes = {}
|
||||||
|
for key, value in dict(value).items():
|
||||||
|
if key in self._data and self._data.get(key) == value:
|
||||||
|
continue
|
||||||
|
self._data[key] = value
|
||||||
|
changes[key] = value
|
||||||
|
return changes
|
||||||
|
|
||||||
|
def _pop(self, key, default):
|
||||||
|
has_key = key in self._data
|
||||||
|
value = self._data.pop(key, default)
|
||||||
|
# Remove attribute definition if is 'UnknownDef'
|
||||||
|
# - gives option to get rid of unknown values
|
||||||
|
attr_def = self._attr_defs_by_key.get(key)
|
||||||
|
changes = {}
|
||||||
|
if isinstance(attr_def, UnknownDef):
|
||||||
|
self._attr_defs_by_key.pop(key)
|
||||||
|
self._attr_defs.remove(attr_def)
|
||||||
|
elif has_key:
|
||||||
|
changes[key] = None
|
||||||
|
return value, changes
|
||||||
|
|
||||||
|
|
||||||
class CreatorAttributeValues(AttributeValues):
|
class CreatorAttributeValues(AttributeValues):
|
||||||
"""Creator specific attribute values of an instance."""
|
"""Creator specific attribute values of an instance."""
|
||||||
|
|
@ -270,6 +290,23 @@ class PublishAttributes:
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self._data[key]
|
return self._data[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
"""Set value for plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): Plugin name.
|
||||||
|
value (dict[str, Any]): Value to set.
|
||||||
|
|
||||||
|
"""
|
||||||
|
current_value = self._data.get(key)
|
||||||
|
if isinstance(current_value, PublishAttributeValues):
|
||||||
|
current_value.set_value(value)
|
||||||
|
else:
|
||||||
|
self._data[key] = value
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
self.pop(key)
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
return key in self._data
|
return key in self._data
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from .path_resolving import (
|
||||||
from .utils import (
|
from .utils import (
|
||||||
should_use_last_workfile_on_launch,
|
should_use_last_workfile_on_launch,
|
||||||
should_open_workfiles_tool_on_launch,
|
should_open_workfiles_tool_on_launch,
|
||||||
|
MissingWorkdirError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .build_workfile import BuildWorkfile
|
from .build_workfile import BuildWorkfile
|
||||||
|
|
@ -46,6 +47,7 @@ __all__ = (
|
||||||
|
|
||||||
"should_use_last_workfile_on_launch",
|
"should_use_last_workfile_on_launch",
|
||||||
"should_open_workfiles_tool_on_launch",
|
"should_open_workfiles_tool_on_launch",
|
||||||
|
"MissingWorkdirError",
|
||||||
|
|
||||||
"BuildWorkfile",
|
"BuildWorkfile",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ from ayon_core.lib import filter_profiles
|
||||||
from ayon_core.settings import get_project_settings
|
from ayon_core.settings import get_project_settings
|
||||||
|
|
||||||
|
|
||||||
|
class MissingWorkdirError(Exception):
|
||||||
|
"""Raised when accessing a work directory not found on disk."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def should_use_last_workfile_on_launch(
|
def should_use_last_workfile_on_launch(
|
||||||
project_name,
|
project_name,
|
||||||
host_name,
|
host_name,
|
||||||
|
|
|
||||||
|
|
@ -163,9 +163,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
||||||
# Store new staging to cleanup paths
|
# Store new staging to cleanup paths
|
||||||
instance.context.data["cleanupFullPaths"].append(dst_staging)
|
instance.context.data["cleanupFullPaths"].append(dst_staging)
|
||||||
|
|
||||||
thumbnail_created = False
|
|
||||||
oiio_supported = is_oiio_supported()
|
oiio_supported = is_oiio_supported()
|
||||||
|
repre_thumb_created = False
|
||||||
for repre in filtered_repres:
|
for repre in filtered_repres:
|
||||||
|
# Reset for each iteration to handle cases where multiple
|
||||||
|
# reviewable thumbnails are needed
|
||||||
|
repre_thumb_created = False
|
||||||
repre_files = repre["files"]
|
repre_files = repre["files"]
|
||||||
src_staging = os.path.normpath(repre["stagingDir"])
|
src_staging = os.path.normpath(repre["stagingDir"])
|
||||||
if not isinstance(repre_files, (list, tuple)):
|
if not isinstance(repre_files, (list, tuple)):
|
||||||
|
|
@ -214,7 +217,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
||||||
)
|
)
|
||||||
# If the input can read by OIIO then use OIIO method for
|
# If the input can read by OIIO then use OIIO method for
|
||||||
# conversion otherwise use ffmpeg
|
# conversion otherwise use ffmpeg
|
||||||
thumbnail_created = self._create_thumbnail_oiio(
|
repre_thumb_created = self._create_thumbnail_oiio(
|
||||||
full_input_path,
|
full_input_path,
|
||||||
full_output_path,
|
full_output_path,
|
||||||
colorspace_data
|
colorspace_data
|
||||||
|
|
@ -223,19 +226,19 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
||||||
# Try to use FFMPEG if OIIO is not supported or for cases when
|
# Try to use FFMPEG if OIIO is not supported or for cases when
|
||||||
# oiiotool isn't available or representation is not having
|
# oiiotool isn't available or representation is not having
|
||||||
# colorspace data
|
# colorspace data
|
||||||
if not thumbnail_created:
|
if not repre_thumb_created:
|
||||||
if oiio_supported:
|
if oiio_supported:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"Converting with FFMPEG because input"
|
"Converting with FFMPEG because input"
|
||||||
" can't be read by OIIO."
|
" can't be read by OIIO."
|
||||||
)
|
)
|
||||||
|
|
||||||
thumbnail_created = self._create_thumbnail_ffmpeg(
|
repre_thumb_created = self._create_thumbnail_ffmpeg(
|
||||||
full_input_path, full_output_path
|
full_input_path, full_output_path
|
||||||
)
|
)
|
||||||
|
|
||||||
# Skip representation and try next one if wasn't created
|
# Skip representation and try next one if wasn't created
|
||||||
if not thumbnail_created:
|
if not repre_thumb_created:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(explicit_repres) > 1:
|
if len(explicit_repres) > 1:
|
||||||
|
|
@ -291,7 +294,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
||||||
# There is no need to create more then one thumbnail
|
# There is no need to create more then one thumbnail
|
||||||
break
|
break
|
||||||
|
|
||||||
if not thumbnail_created:
|
if not repre_thumb_created:
|
||||||
self.log.warning("Thumbnail has not been created.")
|
self.log.warning("Thumbnail has not been created.")
|
||||||
|
|
||||||
def _is_review_instance(self, instance):
|
def _is_review_instance(self, instance):
|
||||||
|
|
@ -450,7 +453,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
||||||
# output arguments from presets
|
# output arguments from presets
|
||||||
jpeg_items.extend(ffmpeg_args.get("output") or [])
|
jpeg_items.extend(ffmpeg_args.get("output") or [])
|
||||||
# we just want one frame from movie files
|
# we just want one frame from movie files
|
||||||
jpeg_items.extend(["-vframes", "1"])
|
jpeg_items.extend(["-frames:v", "1"])
|
||||||
|
|
||||||
if resolution_arg:
|
if resolution_arg:
|
||||||
jpeg_items.extend(resolution_arg)
|
jpeg_items.extend(resolution_arg)
|
||||||
|
|
@ -498,7 +501,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
||||||
"-i", video_file_path,
|
"-i", video_file_path,
|
||||||
"-analyzeduration", max_int,
|
"-analyzeduration", max_int,
|
||||||
"-probesize", max_int,
|
"-probesize", max_int,
|
||||||
"-vframes", "1"
|
"-frames:v", "1"
|
||||||
]
|
]
|
||||||
|
|
||||||
# add output file path
|
# add output file path
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
||||||
"-analyzeduration", max_int,
|
"-analyzeduration", max_int,
|
||||||
"-probesize", max_int,
|
"-probesize", max_int,
|
||||||
"-i", src_path,
|
"-i", src_path,
|
||||||
"-vframes", "1",
|
"-frames:v", "1",
|
||||||
dst_path
|
dst_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -619,8 +619,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
||||||
# used for all represe
|
# used for all represe
|
||||||
# from temp to final
|
# from temp to final
|
||||||
original_directory = (
|
original_directory = (
|
||||||
instance.data.get("originalDirname") or instance_stagingdir)
|
instance.data.get("originalDirname") or stagingdir)
|
||||||
|
|
||||||
_rootless = self.get_rootless_path(anatomy, original_directory)
|
_rootless = self.get_rootless_path(anatomy, original_directory)
|
||||||
if _rootless == original_directory:
|
if _rootless == original_directory:
|
||||||
raise KnownPublishError((
|
raise KnownPublishError((
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,10 @@ import collections
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
import ayon_api
|
import ayon_api
|
||||||
|
from ayon_api import RequestTypes
|
||||||
from ayon_api.operations import OperationsSession
|
from ayon_api.operations import OperationsSession
|
||||||
|
|
||||||
|
|
||||||
InstanceFilterResult = collections.namedtuple(
|
InstanceFilterResult = collections.namedtuple(
|
||||||
"InstanceFilterResult",
|
"InstanceFilterResult",
|
||||||
["instance", "thumbnail_path", "version_id"]
|
["instance", "thumbnail_path", "version_id"]
|
||||||
|
|
@ -161,6 +163,30 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin):
|
||||||
return None
|
return None
|
||||||
return os.path.normpath(filled_path)
|
return os.path.normpath(filled_path)
|
||||||
|
|
||||||
|
def _create_thumbnail(self, project_name: str, src_filepath: str) -> str:
|
||||||
|
"""Upload thumbnail to AYON and return its id.
|
||||||
|
|
||||||
|
This is temporary fix of 'create_thumbnail' function in ayon_api to
|
||||||
|
fix jpeg mime type.
|
||||||
|
|
||||||
|
"""
|
||||||
|
mime_type = None
|
||||||
|
with open(src_filepath, "rb") as stream:
|
||||||
|
if b"\xff\xd8\xff" == stream.read(3):
|
||||||
|
mime_type = "image/jpeg"
|
||||||
|
|
||||||
|
if mime_type is None:
|
||||||
|
return ayon_api.create_thumbnail(project_name, src_filepath)
|
||||||
|
|
||||||
|
response = ayon_api.upload_file(
|
||||||
|
f"projects/{project_name}/thumbnails",
|
||||||
|
src_filepath,
|
||||||
|
request_type=RequestTypes.post,
|
||||||
|
headers={"Content-Type": mime_type},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()["id"]
|
||||||
|
|
||||||
def _integrate_thumbnails(
|
def _integrate_thumbnails(
|
||||||
self,
|
self,
|
||||||
filtered_instance_items,
|
filtered_instance_items,
|
||||||
|
|
@ -179,7 +205,7 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin):
|
||||||
).format(instance_label))
|
).format(instance_label))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
thumbnail_id = ayon_api.create_thumbnail(
|
thumbnail_id = self._create_thumbnail(
|
||||||
project_name, thumbnail_path
|
project_name, thumbnail_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,12 @@ class ThumbnailsModel:
|
||||||
entity_type,
|
entity_type,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
):
|
):
|
||||||
thumbnail_paths = set()
|
output = {
|
||||||
|
entity_id: None
|
||||||
|
for entity_id in entity_ids
|
||||||
|
}
|
||||||
if not project_name or not entity_type or not entity_ids:
|
if not project_name or not entity_type or not entity_ids:
|
||||||
return thumbnail_paths
|
return output
|
||||||
|
|
||||||
thumbnail_id_by_entity_id = {}
|
thumbnail_id_by_entity_id = {}
|
||||||
if entity_type == "folder":
|
if entity_type == "folder":
|
||||||
|
|
@ -43,7 +46,7 @@ class ThumbnailsModel:
|
||||||
)
|
)
|
||||||
|
|
||||||
if not thumbnail_id_by_entity_id:
|
if not thumbnail_id_by_entity_id:
|
||||||
return thumbnail_paths
|
return output
|
||||||
|
|
||||||
entity_ids_by_thumbnail_id = collections.defaultdict(set)
|
entity_ids_by_thumbnail_id = collections.defaultdict(set)
|
||||||
for entity_id, thumbnail_id in thumbnail_id_by_entity_id.items():
|
for entity_id, thumbnail_id in thumbnail_id_by_entity_id.items():
|
||||||
|
|
@ -51,10 +54,6 @@ class ThumbnailsModel:
|
||||||
continue
|
continue
|
||||||
entity_ids_by_thumbnail_id[thumbnail_id].add(entity_id)
|
entity_ids_by_thumbnail_id[thumbnail_id].add(entity_id)
|
||||||
|
|
||||||
output = {
|
|
||||||
entity_id: None
|
|
||||||
for entity_id in entity_ids
|
|
||||||
}
|
|
||||||
for thumbnail_id, entity_ids in entity_ids_by_thumbnail_id.items():
|
for thumbnail_id, entity_ids in entity_ids_by_thumbnail_id.items():
|
||||||
thumbnail_path = self._get_thumbnail_path(
|
thumbnail_path = self._get_thumbnail_path(
|
||||||
project_name, entity_type, next(iter(entity_ids)), thumbnail_id
|
project_name, entity_type, next(iter(entity_ids)), thumbnail_id
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Package declaring AYON addon 'core' version."""
|
"""Package declaring AYON addon 'core' version."""
|
||||||
__version__ = "1.1.6+dev"
|
__version__ = "1.1.8+dev"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name = "core"
|
name = "core"
|
||||||
title = "Core"
|
title = "Core"
|
||||||
version = "1.1.6+dev"
|
version = "1.1.8+dev"
|
||||||
|
|
||||||
client_dir = "ayon_core"
|
client_dir = "ayon_core"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ayon-core"
|
name = "ayon-core"
|
||||||
version = "1.1.6+dev"
|
version = "1.1.8+dev"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Ynput Team <team@ynput.io>"]
|
authors = ["Ynput Team <team@ynput.io>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue