mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +01:00
Merge pull request #4393 from ynput/feature/simplified_creator_api
General: Simplified CreateContext api
This commit is contained in:
commit
bc161ed5af
3 changed files with 188 additions and 94 deletions
|
|
@ -8,7 +8,7 @@ import inspect
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from openpype.client import get_assets
|
from openpype.client import get_assets, get_asset_by_name
|
||||||
from openpype.settings import (
|
from openpype.settings import (
|
||||||
get_system_settings,
|
get_system_settings,
|
||||||
get_project_settings
|
get_project_settings
|
||||||
|
|
@ -17,13 +17,10 @@ from openpype.lib.attribute_definitions import (
|
||||||
UnknownDef,
|
UnknownDef,
|
||||||
serialize_attr_defs,
|
serialize_attr_defs,
|
||||||
deserialize_attr_defs,
|
deserialize_attr_defs,
|
||||||
|
get_default_values,
|
||||||
)
|
)
|
||||||
from openpype.host import IPublishHost
|
from openpype.host import IPublishHost
|
||||||
from openpype.pipeline import legacy_io
|
from openpype.pipeline import legacy_io
|
||||||
from openpype.pipeline.mongodb import (
|
|
||||||
AvalonMongoDB,
|
|
||||||
session_data_from_environment,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .creator_plugins import (
|
from .creator_plugins import (
|
||||||
Creator,
|
Creator,
|
||||||
|
|
@ -1338,8 +1335,6 @@ class CreateContext:
|
||||||
Args:
|
Args:
|
||||||
host(ModuleType): Host implementation which handles implementation and
|
host(ModuleType): Host implementation which handles implementation and
|
||||||
global metadata.
|
global metadata.
|
||||||
dbcon(AvalonMongoDB): Connection to mongo with context (at least
|
|
||||||
project).
|
|
||||||
headless(bool): Context is created out of UI (Current not used).
|
headless(bool): Context is created out of UI (Current not used).
|
||||||
reset(bool): Reset context on initialization.
|
reset(bool): Reset context on initialization.
|
||||||
discover_publish_plugins(bool): Discover publish plugins during reset
|
discover_publish_plugins(bool): Discover publish plugins during reset
|
||||||
|
|
@ -1347,16 +1342,8 @@ class CreateContext:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, host, dbcon=None, headless=False, reset=True,
|
self, host, headless=False, reset=True, discover_publish_plugins=True
|
||||||
discover_publish_plugins=True
|
|
||||||
):
|
):
|
||||||
# Create conncetion if is not passed
|
|
||||||
if dbcon is None:
|
|
||||||
session = session_data_from_environment(True)
|
|
||||||
dbcon = AvalonMongoDB(session)
|
|
||||||
dbcon.install()
|
|
||||||
|
|
||||||
self.dbcon = dbcon
|
|
||||||
self.host = host
|
self.host = host
|
||||||
|
|
||||||
# Prepare attribute for logger (Created on demand in `log` property)
|
# Prepare attribute for logger (Created on demand in `log` property)
|
||||||
|
|
@ -1380,6 +1367,10 @@ class CreateContext:
|
||||||
" Missing methods: {}"
|
" Missing methods: {}"
|
||||||
).format(joined_methods))
|
).format(joined_methods))
|
||||||
|
|
||||||
|
self._current_project_name = None
|
||||||
|
self._current_asset_name = None
|
||||||
|
self._current_task_name = None
|
||||||
|
|
||||||
self._host_is_valid = host_is_valid
|
self._host_is_valid = host_is_valid
|
||||||
# Currently unused variable
|
# Currently unused variable
|
||||||
self.headless = headless
|
self.headless = headless
|
||||||
|
|
@ -1499,11 +1490,20 @@ class CreateContext:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host_name(self):
|
def host_name(self):
|
||||||
|
if hasattr(self.host, "name"):
|
||||||
|
return self.host.name
|
||||||
return os.environ["AVALON_APP"]
|
return os.environ["AVALON_APP"]
|
||||||
|
|
||||||
@property
|
def get_current_project_name(self):
|
||||||
def project_name(self):
|
return self._current_project_name
|
||||||
return self.dbcon.active_project()
|
|
||||||
|
def get_current_asset_name(self):
|
||||||
|
return self._current_asset_name
|
||||||
|
|
||||||
|
def get_current_task_name(self):
|
||||||
|
return self._current_task_name
|
||||||
|
|
||||||
|
project_name = property(get_current_project_name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log(self):
|
def log(self):
|
||||||
|
|
@ -1520,7 +1520,7 @@ class CreateContext:
|
||||||
|
|
||||||
self.reset_preparation()
|
self.reset_preparation()
|
||||||
|
|
||||||
self.reset_avalon_context()
|
self.reset_current_context()
|
||||||
self.reset_plugins(discover_publish_plugins)
|
self.reset_plugins(discover_publish_plugins)
|
||||||
self.reset_context_data()
|
self.reset_context_data()
|
||||||
|
|
||||||
|
|
@ -1567,14 +1567,22 @@ class CreateContext:
|
||||||
self._collection_shared_data = None
|
self._collection_shared_data = None
|
||||||
self.refresh_thumbnails()
|
self.refresh_thumbnails()
|
||||||
|
|
||||||
def reset_avalon_context(self):
|
def reset_current_context(self):
|
||||||
"""Give ability to reset avalon context.
|
"""Refresh current context.
|
||||||
|
|
||||||
Reset is based on optional host implementation of `get_current_context`
|
Reset is based on optional host implementation of `get_current_context`
|
||||||
function or using `legacy_io.Session`.
|
function or using `legacy_io.Session`.
|
||||||
|
|
||||||
Some hosts have ability to change context file without using workfiles
|
Some hosts have ability to change context file without using workfiles
|
||||||
tool but that change is not propagated to
|
tool but that change is not propagated to 'legacy_io.Session'
|
||||||
|
nor 'os.environ'.
|
||||||
|
|
||||||
|
Todos:
|
||||||
|
UI: Current context should be also checked on save - compare
|
||||||
|
initial values vs. current values.
|
||||||
|
Related to UI checks: Current workfile can be also considered
|
||||||
|
as current context information as that's where the metadata
|
||||||
|
are stored. We should store the workfile (if is available) too.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
project_name = asset_name = task_name = None
|
project_name = asset_name = task_name = None
|
||||||
|
|
@ -1592,12 +1600,9 @@ class CreateContext:
|
||||||
if not task_name:
|
if not task_name:
|
||||||
task_name = legacy_io.Session.get("AVALON_TASK")
|
task_name = legacy_io.Session.get("AVALON_TASK")
|
||||||
|
|
||||||
if project_name:
|
self._current_project_name = project_name
|
||||||
self.dbcon.Session["AVALON_PROJECT"] = project_name
|
self._current_asset_name = asset_name
|
||||||
if asset_name:
|
self._current_task_name = task_name
|
||||||
self.dbcon.Session["AVALON_ASSET"] = asset_name
|
|
||||||
if task_name:
|
|
||||||
self.dbcon.Session["AVALON_TASK"] = task_name
|
|
||||||
|
|
||||||
def reset_plugins(self, discover_publish_plugins=True):
|
def reset_plugins(self, discover_publish_plugins=True):
|
||||||
"""Reload plugins.
|
"""Reload plugins.
|
||||||
|
|
@ -1792,40 +1797,128 @@ class CreateContext:
|
||||||
with self.bulk_instances_collection():
|
with self.bulk_instances_collection():
|
||||||
self._bulk_instances_to_process.append(instance)
|
self._bulk_instances_to_process.append(instance)
|
||||||
|
|
||||||
def create(self, identifier, *args, **kwargs):
|
def _get_creator_in_create(self, identifier):
|
||||||
"""Wrapper for creators to trigger created.
|
"""Creator by identifier with unified error.
|
||||||
|
|
||||||
Different types of creators may expect different arguments thus the
|
Helper method to get creator by identifier with same error when creator
|
||||||
hints for args are blind.
|
is not available.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
identifier (str): Creator's identifier.
|
identifier (str): Identifier of creator plugin.
|
||||||
*args (Tuple[Any]): Arguments for create method.
|
|
||||||
**kwargs (Dict[Any, Any]): Keyword argument for create method.
|
Returns:
|
||||||
|
BaseCreator: Creator found by identifier.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
CreatorError: When identifier is not known.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
error_message = "Failed to run Creator with identifier \"{}\". {}"
|
|
||||||
creator = self.creators.get(identifier)
|
creator = self.creators.get(identifier)
|
||||||
label = getattr(creator, "label", None)
|
# Fake CreatorError (Could be maybe specific exception?)
|
||||||
failed = False
|
if creator is None:
|
||||||
add_traceback = False
|
raise CreatorError(
|
||||||
exc_info = None
|
"Creator {} was not found".format(identifier)
|
||||||
try:
|
)
|
||||||
# Fake CreatorError (Could be maybe specific exception?)
|
return creator
|
||||||
if creator is None:
|
|
||||||
|
def create(
|
||||||
|
self,
|
||||||
|
creator_identifier,
|
||||||
|
variant,
|
||||||
|
asset_doc=None,
|
||||||
|
task_name=None,
|
||||||
|
pre_create_data=None
|
||||||
|
):
|
||||||
|
"""Trigger create of plugins with standartized arguments.
|
||||||
|
|
||||||
|
Arguments 'asset_doc' and 'task_name' use current context as default
|
||||||
|
values. If only 'task_name' is provided it will be overriden by
|
||||||
|
task name from current context. If 'task_name' is not provided
|
||||||
|
when 'asset_doc' is, it is considered that task name is not specified,
|
||||||
|
which can lead to error if subset name template requires task name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
creator_identifier (str): Identifier of creator plugin.
|
||||||
|
variant (str): Variant used for subset name.
|
||||||
|
asset_doc (Dict[str, Any]): Asset document which define context of
|
||||||
|
creation (possible context of created instance/s).
|
||||||
|
task_name (str): Name of task to which is context related.
|
||||||
|
pre_create_data (Dict[str, Any]): Pre-create attribute values.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: Output of triggered creator's 'create' method.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
CreatorError: If creator was not found or asset is empty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
creator = self._get_creator_in_create(creator_identifier)
|
||||||
|
|
||||||
|
project_name = self.project_name
|
||||||
|
if asset_doc is None:
|
||||||
|
asset_name = self.get_current_asset_name()
|
||||||
|
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||||
|
task_name = self.get_current_task_name()
|
||||||
|
if asset_doc is None:
|
||||||
raise CreatorError(
|
raise CreatorError(
|
||||||
"Creator {} was not found".format(identifier)
|
"Asset with name {} was not found".format(asset_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
creator.create(*args, **kwargs)
|
if pre_create_data is None:
|
||||||
|
pre_create_data = {}
|
||||||
|
|
||||||
|
precreate_attr_defs = creator.get_pre_create_attr_defs() or []
|
||||||
|
# Create default values of precreate data
|
||||||
|
_pre_create_data = get_default_values(precreate_attr_defs)
|
||||||
|
# Update passed precreate data to default values
|
||||||
|
# TODO validate types
|
||||||
|
_pre_create_data.update(pre_create_data)
|
||||||
|
|
||||||
|
subset_name = creator.get_subset_name(
|
||||||
|
variant,
|
||||||
|
task_name,
|
||||||
|
asset_doc,
|
||||||
|
project_name,
|
||||||
|
self.host_name
|
||||||
|
)
|
||||||
|
instance_data = {
|
||||||
|
"asset": asset_doc["name"],
|
||||||
|
"task": task_name,
|
||||||
|
"family": creator.family,
|
||||||
|
"variant": variant
|
||||||
|
}
|
||||||
|
return creator.create(
|
||||||
|
subset_name,
|
||||||
|
instance_data,
|
||||||
|
_pre_create_data
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_with_unified_error(
|
||||||
|
self, identifier, creator, *args, **kwargs
|
||||||
|
):
|
||||||
|
error_message = "Failed to run Creator with identifier \"{}\". {}"
|
||||||
|
|
||||||
|
label = None
|
||||||
|
add_traceback = False
|
||||||
|
result = None
|
||||||
|
fail_info = None
|
||||||
|
success = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to get creator and his label
|
||||||
|
if creator is None:
|
||||||
|
creator = self._get_creator_in_create(identifier)
|
||||||
|
label = getattr(creator, "label", label)
|
||||||
|
|
||||||
|
# Run create
|
||||||
|
result = creator.create(*args, **kwargs)
|
||||||
|
success = True
|
||||||
|
|
||||||
except CreatorError:
|
except CreatorError:
|
||||||
failed = True
|
|
||||||
exc_info = sys.exc_info()
|
exc_info = sys.exc_info()
|
||||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||||
|
|
||||||
except:
|
except:
|
||||||
failed = True
|
|
||||||
add_traceback = True
|
add_traceback = True
|
||||||
exc_info = sys.exc_info()
|
exc_info = sys.exc_info()
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
|
|
@ -1833,12 +1926,35 @@ class CreateContext:
|
||||||
exc_info=True
|
exc_info=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if failed:
|
if not success:
|
||||||
raise CreatorsCreateFailed([
|
fail_info = prepare_failed_creator_operation_info(
|
||||||
prepare_failed_creator_operation_info(
|
identifier, label, exc_info, add_traceback
|
||||||
identifier, label, exc_info, add_traceback
|
)
|
||||||
)
|
return result, fail_info
|
||||||
])
|
|
||||||
|
def create_with_unified_error(self, identifier, *args, **kwargs):
|
||||||
|
"""Trigger create but raise only one error if anything fails.
|
||||||
|
|
||||||
|
Added to raise unified exception. Capture any possible issues and
|
||||||
|
reraise it with unified information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
identifier (str): Identifier of creator.
|
||||||
|
*args (Tuple[Any]): Arguments for create method.
|
||||||
|
**kwargs (Dict[Any, Any]): Keyword argument for create method.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
CreatorsCreateFailed: When creation fails due to any possible
|
||||||
|
reason. If anything goes wrong this is only possible exception
|
||||||
|
the method should raise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result, fail_info = self._create_with_unified_error(
|
||||||
|
identifier, None, *args, **kwargs
|
||||||
|
)
|
||||||
|
if fail_info is not None:
|
||||||
|
raise CreatorsCreateFailed([fail_info])
|
||||||
|
return result
|
||||||
|
|
||||||
def _remove_instance(self, instance):
|
def _remove_instance(self, instance):
|
||||||
self._instances_by_id.pop(instance.id, None)
|
self._instances_by_id.pop(instance.id, None)
|
||||||
|
|
@ -1968,38 +2084,12 @@ class CreateContext:
|
||||||
Reset instances if any autocreator executed properly.
|
Reset instances if any autocreator executed properly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
error_message = "Failed to run AutoCreator with identifier \"{}\". {}"
|
|
||||||
failed_info = []
|
failed_info = []
|
||||||
for creator in self.sorted_autocreators:
|
for creator in self.sorted_autocreators:
|
||||||
identifier = creator.identifier
|
identifier = creator.identifier
|
||||||
label = creator.label
|
_, fail_info = self._create_with_unified_error(identifier, creator)
|
||||||
failed = False
|
if fail_info is not None:
|
||||||
add_traceback = False
|
failed_info.append(fail_info)
|
||||||
try:
|
|
||||||
creator.create()
|
|
||||||
|
|
||||||
except CreatorError:
|
|
||||||
failed = True
|
|
||||||
exc_info = sys.exc_info()
|
|
||||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
|
||||||
|
|
||||||
# Use bare except because some hosts raise their exceptions that
|
|
||||||
# do not inherit from python's `BaseException`
|
|
||||||
except:
|
|
||||||
failed = True
|
|
||||||
add_traceback = True
|
|
||||||
exc_info = sys.exc_info()
|
|
||||||
self.log.warning(
|
|
||||||
error_message.format(identifier, ""),
|
|
||||||
exc_info=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if failed:
|
|
||||||
failed_info.append(
|
|
||||||
prepare_failed_creator_operation_info(
|
|
||||||
identifier, label, exc_info, add_traceback
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if failed_info:
|
if failed_info:
|
||||||
raise CreatorsCreateFailed(failed_info)
|
raise CreatorsCreateFailed(failed_info)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
||||||
thumbnail_paths_by_instance_id.get(None)
|
thumbnail_paths_by_instance_id.get(None)
|
||||||
)
|
)
|
||||||
|
|
||||||
project_name = create_context.project_name
|
project_name = create_context.get_current_project_name()
|
||||||
if project_name:
|
if project_name:
|
||||||
context.data["projectName"] = project_name
|
context.data["projectName"] = project_name
|
||||||
|
|
||||||
|
|
@ -53,11 +53,15 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
||||||
context.data.update(create_context.context_data_to_store())
|
context.data.update(create_context.context_data_to_store())
|
||||||
context.data["newPublishing"] = True
|
context.data["newPublishing"] = True
|
||||||
# Update context data
|
# Update context data
|
||||||
for key in ("AVALON_PROJECT", "AVALON_ASSET", "AVALON_TASK"):
|
asset_name = create_context.get_current_asset_name()
|
||||||
value = create_context.dbcon.Session.get(key)
|
task_name = create_context.get_current_task_name()
|
||||||
if value is not None:
|
for key, value in (
|
||||||
legacy_io.Session[key] = value
|
("AVALON_PROJECT", project_name),
|
||||||
os.environ[key] = value
|
("AVALON_ASSET", asset_name),
|
||||||
|
("AVALON_TASK", task_name)
|
||||||
|
):
|
||||||
|
legacy_io.Session[key] = value
|
||||||
|
os.environ[key] = value
|
||||||
|
|
||||||
def create_instance(
|
def create_instance(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -1573,20 +1573,19 @@ class PublisherController(BasePublisherController):
|
||||||
Handle both creation and publishing parts.
|
Handle both creation and publishing parts.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dbcon (AvalonMongoDB): Connection to mongo with context.
|
|
||||||
headless (bool): Headless publishing. ATM not implemented or used.
|
headless (bool): Headless publishing. ATM not implemented or used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_log = None
|
_log = None
|
||||||
|
|
||||||
def __init__(self, dbcon=None, headless=False):
|
def __init__(self, headless=False):
|
||||||
super(PublisherController, self).__init__()
|
super(PublisherController, self).__init__()
|
||||||
|
|
||||||
self._host = registered_host()
|
self._host = registered_host()
|
||||||
self._headless = headless
|
self._headless = headless
|
||||||
|
|
||||||
self._create_context = CreateContext(
|
self._create_context = CreateContext(
|
||||||
self._host, dbcon, headless=headless, reset=False
|
self._host, headless=headless, reset=False
|
||||||
)
|
)
|
||||||
|
|
||||||
self._publish_plugins_proxy = None
|
self._publish_plugins_proxy = None
|
||||||
|
|
@ -1740,7 +1739,7 @@ class PublisherController(BasePublisherController):
|
||||||
self._create_context.reset_preparation()
|
self._create_context.reset_preparation()
|
||||||
|
|
||||||
# Reset avalon context
|
# Reset avalon context
|
||||||
self._create_context.reset_avalon_context()
|
self._create_context.reset_current_context()
|
||||||
|
|
||||||
self._asset_docs_cache.reset()
|
self._asset_docs_cache.reset()
|
||||||
|
|
||||||
|
|
@ -2004,9 +2003,10 @@ class PublisherController(BasePublisherController):
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
try:
|
try:
|
||||||
self._create_context.create(
|
self._create_context.create_with_unified_error(
|
||||||
creator_identifier, subset_name, instance_data, options
|
creator_identifier, subset_name, instance_data, options
|
||||||
)
|
)
|
||||||
|
|
||||||
except CreatorsOperationFailed as exc:
|
except CreatorsOperationFailed as exc:
|
||||||
success = False
|
success = False
|
||||||
self._emit_event(
|
self._emit_event(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue