mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into feature/OP_2395_env_groups
This commit is contained in:
commit
a61e3f70e7
11 changed files with 226 additions and 90 deletions
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -1,6 +1,6 @@
|
|||
# Changelog
|
||||
|
||||
## [3.7.0-nightly.8](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.7.0-nightly.9](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...HEAD)
|
||||
|
||||
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Settings UI: Hyperlinks to settings [\#2420](https://github.com/pypeclub/OpenPype/pull/2420)
|
||||
- Modules: JobQueue module moved one hierarchy level higher [\#2419](https://github.com/pypeclub/OpenPype/pull/2419)
|
||||
- Ftrack: Check existence of object type on recreation [\#2404](https://github.com/pypeclub/OpenPype/pull/2404)
|
||||
- Flame: moving `utility\_scripts` to api folder also with `scripts` [\#2385](https://github.com/pypeclub/OpenPype/pull/2385)
|
||||
- Centos 7 dependency compatibility [\#2384](https://github.com/pypeclub/OpenPype/pull/2384)
|
||||
|
|
@ -29,13 +31,14 @@
|
|||
- General: OpenPype default modules hierarchy [\#2338](https://github.com/pypeclub/OpenPype/pull/2338)
|
||||
- General: FFprobe error exception contain original error message [\#2328](https://github.com/pypeclub/OpenPype/pull/2328)
|
||||
- Resolve: Add experimental button to menu [\#2325](https://github.com/pypeclub/OpenPype/pull/2325)
|
||||
- Input links: Cleanup and unification of differences [\#2322](https://github.com/pypeclub/OpenPype/pull/2322)
|
||||
- General: Don't validate vendor bin with executing them [\#2317](https://github.com/pypeclub/OpenPype/pull/2317)
|
||||
- General: Multilayer EXRs support [\#2315](https://github.com/pypeclub/OpenPype/pull/2315)
|
||||
- General: Reduce vendor imports [\#2305](https://github.com/pypeclub/OpenPype/pull/2305)
|
||||
- Ftrack: Synchronize input links [\#2287](https://github.com/pypeclub/OpenPype/pull/2287)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- PS: Introduced settings for invalid characters to use in ValidateNaming plugin [\#2417](https://github.com/pypeclub/OpenPype/pull/2417)
|
||||
- Settings UI: Breadcrumbs path does not create new entities [\#2416](https://github.com/pypeclub/OpenPype/pull/2416)
|
||||
- AfterEffects: Variant 2022 is in defaults but missing in schemas [\#2412](https://github.com/pypeclub/OpenPype/pull/2412)
|
||||
- General: Fix access to environments from default settings [\#2403](https://github.com/pypeclub/OpenPype/pull/2403)
|
||||
- Fix: Placeholder Input color set fix [\#2399](https://github.com/pypeclub/OpenPype/pull/2399)
|
||||
- Settings: Fix state change of wrapper label [\#2396](https://github.com/pypeclub/OpenPype/pull/2396)
|
||||
|
|
@ -55,8 +58,7 @@
|
|||
- Tools: Use Qt context on tools show [\#2340](https://github.com/pypeclub/OpenPype/pull/2340)
|
||||
- Flame: Fix default argument value in custom dictionary [\#2339](https://github.com/pypeclub/OpenPype/pull/2339)
|
||||
- Timers Manager: Disable auto stop timer on linux platform [\#2334](https://github.com/pypeclub/OpenPype/pull/2334)
|
||||
- nuke: bake preset single input exception [\#2331](https://github.com/pypeclub/OpenPype/pull/2331)
|
||||
- Hiero: fixing multiple templates at a hierarchy parent [\#2330](https://github.com/pypeclub/OpenPype/pull/2330)
|
||||
- Fix - provider icons are pulled from a folder [\#2326](https://github.com/pypeclub/OpenPype/pull/2326)
|
||||
- Royal Render: Fix plugin order and OpenPype auto-detection [\#2291](https://github.com/pypeclub/OpenPype/pull/2291)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
|
@ -66,7 +68,6 @@
|
|||
- Linux : flip updating submodules logic [\#2357](https://github.com/pypeclub/OpenPype/pull/2357)
|
||||
- Update of avalon-core [\#2346](https://github.com/pypeclub/OpenPype/pull/2346)
|
||||
- Maya: configurable model top level validation [\#2321](https://github.com/pypeclub/OpenPype/pull/2321)
|
||||
- Create test publish class for After Effects [\#2270](https://github.com/pypeclub/OpenPype/pull/2270)
|
||||
|
||||
## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23)
|
||||
|
||||
|
|
@ -88,17 +89,6 @@
|
|||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.2-nightly.2...3.6.2)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265)
|
||||
- SceneInventory: Choose loader in asset switcher [\#2262](https://github.com/pypeclub/OpenPype/pull/2262)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Tools: Parenting of tools in Nuke and Hiero [\#2266](https://github.com/pypeclub/OpenPype/pull/2266)
|
||||
- limiting validator to specific editorial hosts [\#2264](https://github.com/pypeclub/OpenPype/pull/2264)
|
||||
- Tools: Select Context dialog attribute fix [\#2261](https://github.com/pypeclub/OpenPype/pull/2261)
|
||||
|
||||
## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.1-nightly.1...3.6.1)
|
||||
|
|
|
|||
|
|
@ -731,6 +731,10 @@ class LaunchHook:
|
|||
def app_name(self):
|
||||
return getattr(self.application, "full_name", None)
|
||||
|
||||
@property
|
||||
def modules_manager(self):
|
||||
return getattr(self.launch_context, "modules_manager", None)
|
||||
|
||||
def validate(self):
|
||||
"""Optional validation of launch hook on initialization.
|
||||
|
||||
|
|
@ -793,9 +797,13 @@ class ApplicationLaunchContext:
|
|||
"""
|
||||
|
||||
def __init__(self, application, executable, env_group=None, **data):
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
# Application object
|
||||
self.application = application
|
||||
|
||||
self.modules_manager = ModulesManager()
|
||||
|
||||
# Logger
|
||||
logger_name = "{}-{}".format(self.__class__.__name__, self.app_name)
|
||||
self.log = PypeLogger.get_logger(logger_name)
|
||||
|
|
@ -908,10 +916,7 @@ class ApplicationLaunchContext:
|
|||
paths.append(path)
|
||||
|
||||
# Load modules paths
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
manager = ModulesManager()
|
||||
paths.extend(manager.collect_launch_hook_paths())
|
||||
paths.extend(self.modules_manager.collect_launch_hook_paths())
|
||||
|
||||
return paths
|
||||
|
||||
|
|
|
|||
|
|
@ -1433,7 +1433,11 @@ def get_creator_by_name(creator_name, case_sensitive=False):
|
|||
|
||||
@with_avalon
|
||||
def change_timer_to_current_context():
|
||||
"""Called after context change to change timers"""
|
||||
"""Called after context change to change timers.
|
||||
|
||||
TODO:
|
||||
- use TimersManager's static method instead of reimplementing it here
|
||||
"""
|
||||
webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL")
|
||||
if not webserver_url:
|
||||
log.warning("Couldn't find webserver url")
|
||||
|
|
@ -1448,8 +1452,7 @@ def change_timer_to_current_context():
|
|||
data = {
|
||||
"project_name": avalon.io.Session["AVALON_PROJECT"],
|
||||
"asset_name": avalon.io.Session["AVALON_ASSET"],
|
||||
"task_name": avalon.io.Session["AVALON_TASK"],
|
||||
"hierarchy": get_hierarchy()
|
||||
"task_name": avalon.io.Session["AVALON_TASK"]
|
||||
}
|
||||
|
||||
requests.post(rest_api_url, json=data)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class PostFtrackHook(PostLaunchHook):
|
|||
)
|
||||
if entity:
|
||||
self.ftrack_status_change(session, entity, project_name)
|
||||
self.start_timer(session, entity, ftrack_api)
|
||||
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Couldn't finish Ftrack procedure.", exc_info=True
|
||||
|
|
@ -160,26 +160,3 @@ class PostFtrackHook(PostLaunchHook):
|
|||
" on Ftrack entity type \"{}\""
|
||||
).format(next_status_name, entity.entity_type)
|
||||
self.log.warning(msg)
|
||||
|
||||
def start_timer(self, session, entity, _ftrack_api):
|
||||
"""Start Ftrack timer on task from context."""
|
||||
self.log.debug("Triggering timer start.")
|
||||
|
||||
user_entity = session.query("User where username is \"{}\"".format(
|
||||
os.environ["FTRACK_API_USER"]
|
||||
)).first()
|
||||
if not user_entity:
|
||||
self.log.warning(
|
||||
"Couldn't find user with username \"{}\" in Ftrack".format(
|
||||
os.environ["FTRACK_API_USER"]
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
user_entity.start_timer(entity, force=True)
|
||||
session.commit()
|
||||
self.log.debug("Timer start triggered successfully.")
|
||||
|
||||
except Exception:
|
||||
self.log.warning("Couldn't trigger Ftrack timer.", exc_info=True)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
class InvalidContextError(ValueError):
|
||||
"""Context for which the timer should be started is invalid."""
|
||||
pass
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from openpype.lib import PostLaunchHook
|
||||
|
||||
|
||||
class PostStartTimerHook(PostLaunchHook):
|
||||
"""Start timer with TimersManager module.
|
||||
|
||||
This module requires enabled TimerManager module.
|
||||
"""
|
||||
order = None
|
||||
|
||||
def execute(self):
|
||||
project_name = self.data.get("project_name")
|
||||
asset_name = self.data.get("asset_name")
|
||||
task_name = self.data.get("task_name")
|
||||
|
||||
missing_context_keys = set()
|
||||
if not project_name:
|
||||
missing_context_keys.add("project_name")
|
||||
if not asset_name:
|
||||
missing_context_keys.add("asset_name")
|
||||
if not task_name:
|
||||
missing_context_keys.add("task_name")
|
||||
|
||||
if missing_context_keys:
|
||||
missing_keys_str = ", ".join([
|
||||
"\"{}\"".format(key) for key in missing_context_keys
|
||||
])
|
||||
self.log.debug("Hook {} skipped. Missing data keys: {}".format(
|
||||
self.__class__.__name__, missing_keys_str
|
||||
))
|
||||
return
|
||||
|
||||
timers_manager = self.modules_manager.modules_by_name.get(
|
||||
"timers_manager"
|
||||
)
|
||||
if not timers_manager or not timers_manager.enabled:
|
||||
self.log.info((
|
||||
"Skipping starting timer because"
|
||||
" TimersManager is not available."
|
||||
))
|
||||
return
|
||||
|
||||
timers_manager.start_timer_with_webserver(
|
||||
project_name, asset_name, task_name, logger=self.log
|
||||
)
|
||||
|
|
@ -39,17 +39,23 @@ class TimersManagerModuleRestApi:
|
|||
async def start_timer(self, request):
|
||||
data = await request.json()
|
||||
try:
|
||||
project_name = data['project_name']
|
||||
asset_name = data['asset_name']
|
||||
task_name = data['task_name']
|
||||
hierarchy = data['hierarchy']
|
||||
project_name = data["project_name"]
|
||||
asset_name = data["asset_name"]
|
||||
task_name = data["task_name"]
|
||||
except KeyError:
|
||||
log.error("Payload must contain fields 'project_name, " +
|
||||
"'asset_name', 'task_name', 'hierarchy'")
|
||||
return Response(status=400)
|
||||
msg = (
|
||||
"Payload must contain fields 'project_name,"
|
||||
" 'asset_name' and 'task_name'"
|
||||
)
|
||||
log.error(msg)
|
||||
return Response(status=400, message=msg)
|
||||
|
||||
self.module.stop_timers()
|
||||
self.module.start_timer(project_name, asset_name, task_name, hierarchy)
|
||||
try:
|
||||
self.module.start_timer(project_name, asset_name, task_name)
|
||||
except Exception as exc:
|
||||
return Response(status=404, message=str(exc))
|
||||
|
||||
return Response(status=200)
|
||||
|
||||
async def stop_timer(self, request):
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import os
|
||||
import platform
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayService
|
||||
|
||||
from avalon.api import AvalonMongoDB
|
||||
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import (
|
||||
ITrayService,
|
||||
ILaunchHookPaths
|
||||
)
|
||||
from .exceptions import InvalidContextError
|
||||
|
||||
|
||||
class ExampleTimersManagerConnector:
|
||||
"""Timers manager can handle timers of multiple modules/addons.
|
||||
|
|
@ -64,7 +70,7 @@ class ExampleTimersManagerConnector:
|
|||
self._timers_manager_module.timer_stopped(self._module.id)
|
||||
|
||||
|
||||
class TimersManager(OpenPypeModule, ITrayService):
|
||||
class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths):
|
||||
""" Handles about Timers.
|
||||
|
||||
Should be able to start/stop all timers at once.
|
||||
|
|
@ -151,47 +157,112 @@ class TimersManager(OpenPypeModule, ITrayService):
|
|||
self._idle_manager.stop()
|
||||
self._idle_manager.wait()
|
||||
|
||||
def start_timer(self, project_name, asset_name, task_name, hierarchy):
|
||||
"""
|
||||
Start timer for 'project_name', 'asset_name' and 'task_name'
|
||||
def get_timer_data_for_path(self, task_path):
|
||||
"""Convert string path to a timer data.
|
||||
|
||||
Called from REST api by hosts.
|
||||
|
||||
Args:
|
||||
project_name (string)
|
||||
asset_name (string)
|
||||
task_name (string)
|
||||
hierarchy (string)
|
||||
It is expected that first item is project name, last item is task name
|
||||
and parent asset name is before task name.
|
||||
"""
|
||||
path_items = task_path.split("/")
|
||||
if len(path_items) < 3:
|
||||
raise InvalidContextError("Invalid path \"{}\"".format(task_path))
|
||||
task_name = path_items.pop(-1)
|
||||
asset_name = path_items.pop(-1)
|
||||
project_name = path_items.pop(0)
|
||||
return self.get_timer_data_for_context(
|
||||
project_name, asset_name, task_name, self.log
|
||||
)
|
||||
|
||||
def get_launch_hook_paths(self):
|
||||
"""Implementation of `ILaunchHookPaths`."""
|
||||
return os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"launch_hooks"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_timer_data_for_context(
|
||||
project_name, asset_name, task_name, logger=None
|
||||
):
|
||||
"""Prepare data for timer related callbacks.
|
||||
|
||||
TODO:
|
||||
- return predefined object that has access to asset document etc.
|
||||
"""
|
||||
if not project_name or not asset_name or not task_name:
|
||||
raise InvalidContextError((
|
||||
"Missing context information got"
|
||||
" Project: \"{}\" Asset: \"{}\" Task: \"{}\""
|
||||
).format(str(project_name), str(asset_name), str(task_name)))
|
||||
|
||||
dbconn = AvalonMongoDB()
|
||||
dbconn.install()
|
||||
dbconn.Session["AVALON_PROJECT"] = project_name
|
||||
|
||||
asset_doc = dbconn.find_one({
|
||||
"type": "asset", "name": asset_name
|
||||
})
|
||||
asset_doc = dbconn.find_one(
|
||||
{
|
||||
"type": "asset",
|
||||
"name": asset_name
|
||||
},
|
||||
{
|
||||
"data.tasks": True,
|
||||
"data.parents": True
|
||||
}
|
||||
)
|
||||
if not asset_doc:
|
||||
raise ValueError("Uknown asset {}".format(asset_name))
|
||||
dbconn.uninstall()
|
||||
raise InvalidContextError((
|
||||
"Asset \"{}\" not found in project \"{}\""
|
||||
).format(asset_name, project_name))
|
||||
|
||||
task_type = ''
|
||||
asset_data = asset_doc.get("data") or {}
|
||||
asset_tasks = asset_data.get("tasks") or {}
|
||||
if task_name not in asset_tasks:
|
||||
dbconn.uninstall()
|
||||
raise InvalidContextError((
|
||||
"Task \"{}\" not found on asset \"{}\" in project \"{}\""
|
||||
).format(task_name, asset_name, project_name))
|
||||
|
||||
task_type = ""
|
||||
try:
|
||||
task_type = asset_doc["data"]["tasks"][task_name]["type"]
|
||||
task_type = asset_tasks[task_name]["type"]
|
||||
except KeyError:
|
||||
self.log.warning("Couldn't find task_type for {}".
|
||||
format(task_name))
|
||||
msg = "Couldn't find task_type for {}".format(task_name)
|
||||
if logger is not None:
|
||||
logger.warning(msg)
|
||||
else:
|
||||
print(msg)
|
||||
|
||||
hierarchy = hierarchy.split("\\")
|
||||
hierarchy.append(asset_name)
|
||||
hierarchy_items = asset_data.get("parents") or []
|
||||
hierarchy_items.append(asset_name)
|
||||
|
||||
data = {
|
||||
dbconn.uninstall()
|
||||
return {
|
||||
"project_name": project_name,
|
||||
"task_name": task_name,
|
||||
"task_type": task_type,
|
||||
"hierarchy": hierarchy
|
||||
"hierarchy": hierarchy_items
|
||||
}
|
||||
|
||||
def start_timer(self, project_name, asset_name, task_name):
|
||||
"""Start timer for passed context.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name
|
||||
asset_name (str): Asset name
|
||||
task_name (str): Task name
|
||||
"""
|
||||
data = self.get_timer_data_for_context(
|
||||
project_name, asset_name, task_name, self.log
|
||||
)
|
||||
self.timer_started(None, data)
|
||||
|
||||
def get_task_time(self, project_name, asset_name, task_name):
|
||||
"""Get total time for passed context.
|
||||
|
||||
TODO:
|
||||
- convert context to timer data
|
||||
"""
|
||||
times = {}
|
||||
for module_id, connector in self._connectors_by_module_id.items():
|
||||
if hasattr(connector, "get_task_time"):
|
||||
|
|
@ -202,6 +273,10 @@ class TimersManager(OpenPypeModule, ITrayService):
|
|||
return times
|
||||
|
||||
def timer_started(self, source_id, data):
|
||||
"""Connector triggered that timer has started.
|
||||
|
||||
New timer has started for context in data.
|
||||
"""
|
||||
for module_id, connector in self._connectors_by_module_id.items():
|
||||
if module_id == source_id:
|
||||
continue
|
||||
|
|
@ -219,6 +294,14 @@ class TimersManager(OpenPypeModule, ITrayService):
|
|||
self.is_running = True
|
||||
|
||||
def timer_stopped(self, source_id):
|
||||
"""Connector triggered that hist timer has stopped.
|
||||
|
||||
Should stop all other timers.
|
||||
|
||||
TODO:
|
||||
- pass context for which timer has stopped to validate if timers are
|
||||
same and valid
|
||||
"""
|
||||
for module_id, connector in self._connectors_by_module_id.items():
|
||||
if module_id == source_id:
|
||||
continue
|
||||
|
|
@ -237,6 +320,7 @@ class TimersManager(OpenPypeModule, ITrayService):
|
|||
self.timer_started(None, self.last_task)
|
||||
|
||||
def stop_timers(self):
|
||||
"""Stop all timers."""
|
||||
if self.is_running is False:
|
||||
return
|
||||
|
||||
|
|
@ -295,18 +379,40 @@ class TimersManager(OpenPypeModule, ITrayService):
|
|||
self, server_manager
|
||||
)
|
||||
|
||||
def change_timer_from_host(self, project_name, asset_name, task_name):
|
||||
"""Prepared method for calling change timers on REST api"""
|
||||
@staticmethod
|
||||
def start_timer_with_webserver(
|
||||
project_name, asset_name, task_name, logger=None
|
||||
):
|
||||
"""Prepared method for calling change timers on REST api.
|
||||
|
||||
Webserver must be active. At the moment is Webserver running only when
|
||||
OpenPype Tray is used.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
asset_name (str): Asset name.
|
||||
task_name (str): Task name.
|
||||
logger (logging.Logger): Logger object. Using 'print' if not
|
||||
passed.
|
||||
"""
|
||||
webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL")
|
||||
if not webserver_url:
|
||||
self.log.warning("Couldn't find webserver url")
|
||||
msg = "Couldn't find webserver url"
|
||||
if logger is not None:
|
||||
logger.warning(msg)
|
||||
else:
|
||||
print(msg)
|
||||
return
|
||||
|
||||
rest_api_url = "{}/timers_manager/start_timer".format(webserver_url)
|
||||
try:
|
||||
import requests
|
||||
except Exception:
|
||||
self.log.warning("Couldn't start timer")
|
||||
msg = "Couldn't start timer ('requests' is not available)"
|
||||
if logger is not None:
|
||||
logger.warning(msg)
|
||||
else:
|
||||
print(msg)
|
||||
return
|
||||
data = {
|
||||
"project_name": project_name,
|
||||
|
|
@ -314,4 +420,4 @@ class TimersManager(OpenPypeModule, ITrayService):
|
|||
"task_name": task_name
|
||||
}
|
||||
|
||||
requests.post(rest_api_url, json=data)
|
||||
return requests.post(rest_api_url, json=data)
|
||||
|
|
|
|||
|
|
@ -359,7 +359,8 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
if frame_start is None:
|
||||
replacement_final = replacement_size = str(MISSING_KEY_VALUE)
|
||||
else:
|
||||
replacement_final = "%{eif:n+" + str(frame_start) + ":d}"
|
||||
replacement_final = "%{eif:n+" + str(frame_start) + ":d:" + \
|
||||
str(len(str(frame_end))) + "}"
|
||||
replacement_size = str(frame_end)
|
||||
|
||||
final_text = final_text.replace(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.7.0-nightly.8"
|
||||
__version__ = "3.7.0-nightly.9"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.7.0-nightly.8" # OpenPype
|
||||
version = "3.7.0-nightly.9" # OpenPype
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue